From 15cd2f9443e363adcf6c579c2ebe6e9a5544b6e4 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 11 Jul 2024 13:29:33 -0700 Subject: [PATCH 001/591] buildscripts: OS X env should be in macos.sh unix.sh is shared by multiple OSes and environments. Clear JAVA_HOME, since we never want to use that as PATH is more reliable, better supported, and more typical. --- buildscripts/kokoro/macos.sh | 3 +++ buildscripts/kokoro/unix.sh | 5 ----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/buildscripts/kokoro/macos.sh b/buildscripts/kokoro/macos.sh index 97259231ee8..018d15dd2f9 100755 --- a/buildscripts/kokoro/macos.sh +++ b/buildscripts/kokoro/macos.sh @@ -15,4 +15,7 @@ export GRADLE_FLAGS="${GRADLE_FLAGS:-} --max-workers=2" . "$GRPC_JAVA_DIR"/buildscripts/kokoro/kokoro.sh trap spongify_logs EXIT +export -n JAVA_HOME +export PATH="$(/usr/libexec/java_home -v"1.8.0")/bin:${PATH}" + "$GRPC_JAVA_DIR"/buildscripts/kokoro/unix.sh diff --git a/buildscripts/kokoro/unix.sh b/buildscripts/kokoro/unix.sh index 9b1a4054c7e..1b88b56ab40 100755 --- a/buildscripts/kokoro/unix.sh +++ b/buildscripts/kokoro/unix.sh @@ -23,11 +23,6 @@ readonly GRPC_JAVA_DIR="$(cd "$(dirname "$0")"/../.. && pwd)" # cd to the root dir of grpc-java cd $(dirname $0)/../.. -# TODO(zpencer): always make sure we are using Oracle jdk8 -if [[ -f /usr/libexec/java_home ]]; then - JAVA_HOME=$(/usr/libexec/java_home -v"1.8.0") -fi - # ARCH is x86_64 unless otherwise specified. ARCH="${ARCH:-x86_64}" From f3cf7c3c75a2dd80b5e852b42efb6dc41e0d073a Mon Sep 17 00:00:00 2001 From: Vindhya Ningegowda Date: Thu, 12 Sep 2024 15:40:20 -0700 Subject: [PATCH 002/591] xds: Add xDS node ID in few control plane errors (#11519) --- .../java/io/grpc/xds/CdsLoadBalancer2.java | 18 +++++--- .../java/io/grpc/xds/XdsNameResolver.java | 4 +- .../java/io/grpc/xds/XdsServerWrapper.java | 15 +++++-- .../io/grpc/xds/CdsLoadBalancer2Test.java | 44 ++++++++++++++----- 4 files changed, 59 insertions(+), 22 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java index 773fdf20563..3f1eb3e7e4f 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java @@ -206,8 +206,9 @@ private void handleClusterDiscovered() { } loopStatus = Status.UNAVAILABLE.withDescription(String.format( "CDS error: circular aggregate clusters directly under %s for " - + "root cluster %s, named %s", - clusterState.name, root.name, namesCausingLoops)); + + "root cluster %s, named %s, xDS node ID: %s", + clusterState.name, root.name, namesCausingLoops, + xdsClient.getBootstrapInfo().node().getId())); } } } @@ -224,9 +225,9 @@ private void handleClusterDiscovered() { childLb.shutdown(); childLb = null; } - Status unavailable = - Status.UNAVAILABLE.withDescription("CDS error: found 0 leaf (logical DNS or EDS) " - + "clusters for root cluster " + root.name); + Status unavailable = Status.UNAVAILABLE.withDescription(String.format( + "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster %s" + + " xDS node ID: %s", root.name, xdsClient.getBootstrapInfo().node().getId())); helper.updateBalancingState( TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(unavailable))); return; @@ -288,11 +289,14 @@ private void addAncestors(Set ancestors, ClusterState clusterState, } private void handleClusterDiscoveryError(Status error) { + String description = error.getDescription() == null ? "" : error.getDescription() + " "; + Status errorWithNodeId = error.withDescription( + description + "xDS node ID: " + xdsClient.getBootstrapInfo().node().getId()); if (childLb != null) { - childLb.handleNameResolutionError(error); + childLb.handleNameResolutionError(errorWithNodeId); } else { helper.updateBalancingState( - TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error))); + TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(errorWithNodeId))); } } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index f0329387fc9..ca73b7d8451 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -815,10 +815,12 @@ private void cleanUpRoutes(String error) { // the config selector handles the error message itself. Once the LB API allows providing // failure information for addresses yet still providing a service config, the config seector // could be avoided. + String errorWithNodeId = + error + ", xDS node ID: " + xdsClient.getBootstrapInfo().node().getId(); listener.onResult(ResolutionResult.newBuilder() .setAttributes(Attributes.newBuilder() .set(InternalConfigSelector.KEY, - new FailingConfigSelector(Status.UNAVAILABLE.withDescription(error))) + new FailingConfigSelector(Status.UNAVAILABLE.withDescription(errorWithNodeId))) .build()) .setServiceConfig(emptyServiceConfig) .build()); diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index dfb7c4fb7db..bd622a71124 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -425,7 +425,8 @@ public void onResourceDoesNotExist(final String resourceName) { return; } StatusException statusException = Status.UNAVAILABLE.withDescription( - "Listener " + resourceName + " unavailable").asException(); + String.format("Listener %s unavailable, xDS node ID: %s", resourceName, + xdsClient.getBootstrapInfo().node().getId())).asException(); handleConfigNotFound(statusException); } @@ -434,9 +435,12 @@ public void onError(final Status error) { if (stopped) { return; } - logger.log(Level.FINE, "Error from XdsClient", error); + String description = error.getDescription() == null ? "" : error.getDescription() + " "; + Status errorWithNodeId = error.withDescription( + description + "xDS node ID: " + xdsClient.getBootstrapInfo().node().getId()); + logger.log(Level.FINE, "Error from XdsClient", errorWithNodeId); if (!isServing) { - listener.onNotServing(error.asException()); + listener.onNotServing(errorWithNodeId.asException()); } } @@ -664,8 +668,11 @@ public void run() { if (!routeDiscoveryStates.containsKey(resourceName)) { return; } + String description = error.getDescription() == null ? "" : error.getDescription() + " "; + Status errorWithNodeId = error.withDescription( + description + "xDS node ID: " + xdsClient.getBootstrapInfo().node().getId()); logger.log(Level.WARNING, "Error loading RDS resource {0} from XdsClient: {1}.", - new Object[]{resourceName, error}); + new Object[]{resourceName, errorWithNodeId}); maybeUpdateSelector(); } }); diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java index 0884587cd95..da32332a2a5 100644 --- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java +++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java @@ -58,7 +58,9 @@ import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; import io.grpc.xds.XdsClusterResource.CdsUpdate; +import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.Bootstrapper.ServerInfo; +import io.grpc.xds.client.EnvoyProtoData; import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsResourceType; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; @@ -94,6 +96,16 @@ public class CdsLoadBalancer2Test { private static final String DNS_HOST_NAME = "backend-service-dns.googleapis.com:443"; private static final ServerInfo LRS_SERVER_INFO = ServerInfo.create("lrs.googleapis.com", InsecureChannelCredentials.create()); + private static final String SERVER_URI = "trafficdirector.googleapis.com"; + private static final String NODE_ID = + "projects/42/networks/default/nodes/5c85b298-6f5b-4722-b74a-f7d1f0ccf5ad"; + private static final EnvoyProtoData.Node BOOTSTRAP_NODE = + EnvoyProtoData.Node.newBuilder().setId(NODE_ID).build(); + private static final BootstrapInfo BOOTSTRAP_INFO = BootstrapInfo.builder() + .servers(ImmutableList.of( + ServerInfo.create(SERVER_URI, InsecureChannelCredentials.create()))) + .node(BOOTSTRAP_NODE) + .build(); private final UpstreamTlsContext upstreamTlsContext = CommonTlsContextTestsUtil.buildUpstreamTlsContext("google_cloud_private_spiffe", true); private final OutlierDetection outlierDetection = OutlierDetection.create( @@ -211,7 +223,8 @@ public void nonAggregateCluster_resourceNotExist_returnErrorPicker() { verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER); + "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER + + " xDS node ID: " + NODE_ID); assertPicker(pickerCaptor.getValue(), unavailable, null); assertThat(childBalancers).isEmpty(); } @@ -254,7 +267,8 @@ public void nonAggregateCluster_resourceRevoked() { xdsClient.deliverResourceNotExist(CLUSTER); assertThat(childBalancer.shutdown).isTrue(); Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER); + "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER + + " xDS node ID: " + NODE_ID); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); assertPicker(pickerCaptor.getValue(), unavailable, null); @@ -331,7 +345,8 @@ public void aggregateCluster_noNonAggregateClusterExits_returnErrorPicker() { verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER); + "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER + + " xDS node ID: " + NODE_ID); assertPicker(pickerCaptor.getValue(), unavailable, null); assertThat(childBalancers).isEmpty(); } @@ -379,7 +394,8 @@ public void aggregateCluster_descendantClustersRevoked() { verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER); + "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER + + " xDS node ID: " + NODE_ID); assertPicker(pickerCaptor.getValue(), unavailable, null); assertThat(childBalancer.shutdown).isTrue(); assertThat(childBalancers).isEmpty(); @@ -418,7 +434,8 @@ public void aggregateCluster_rootClusterRevoked() { verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER); + "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER + + " xDS node ID: " + NODE_ID); assertPicker(pickerCaptor.getValue(), unavailable, null); assertThat(childBalancer.shutdown).isTrue(); assertThat(childBalancers).isEmpty(); @@ -466,7 +483,8 @@ public void aggregateCluster_intermediateClusterChanges() { verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER); + "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER + + " xDS node ID: " + NODE_ID); assertPicker(pickerCaptor.getValue(), unavailable, null); assertThat(childBalancer.shutdown).isTrue(); assertThat(childBalancers).isEmpty(); @@ -507,7 +525,7 @@ public void aggregateCluster_withLoops() { Status unavailable = Status.UNAVAILABLE.withDescription( "CDS error: circular aggregate clusters directly under cluster-02.googleapis.com for root" + " cluster cluster-foo.googleapis.com, named [cluster-01.googleapis.com," - + " cluster-02.googleapis.com]"); + + " cluster-02.googleapis.com], xDS node ID: " + NODE_ID); assertPicker(pickerCaptor.getValue(), unavailable, null); } @@ -549,7 +567,7 @@ public void aggregateCluster_withLoops_afterEds() { Status unavailable = Status.UNAVAILABLE.withDescription( "CDS error: circular aggregate clusters directly under cluster-02.googleapis.com for root" + " cluster cluster-foo.googleapis.com, named [cluster-01.googleapis.com," - + " cluster-02.googleapis.com]"); + + " cluster-02.googleapis.com], xDS node ID: " + NODE_ID); assertPicker(pickerCaptor.getValue(), unavailable, null); } @@ -617,7 +635,7 @@ public void aggregateCluster_discoveryErrorBeforeChildLbCreated_returnErrorPicke eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); Status expectedError = Status.UNAVAILABLE.withDescription( "Unable to load CDS cluster-foo.googleapis.com. xDS server returned: " - + "RESOURCE_EXHAUSTED: OOM"); + + "RESOURCE_EXHAUSTED: OOM xDS node ID: " + NODE_ID); assertPicker(pickerCaptor.getValue(), expectedError, null); assertThat(childBalancers).isEmpty(); } @@ -647,7 +665,8 @@ public void aggregateCluster_discoveryErrorAfterChildLbCreated_propagateToChildL @Test public void handleNameResolutionErrorFromUpstream_beforeChildLbCreated_returnErrorPicker() { - Status upstreamError = Status.UNAVAILABLE.withDescription("unreachable"); + Status upstreamError = Status.UNAVAILABLE.withDescription( + "unreachable xDS node ID: " + NODE_ID); loadBalancer.handleNameResolutionError(upstreamError); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); @@ -821,6 +840,11 @@ public void cancelXdsResourceWatch(XdsResourceType } } + @Override + public BootstrapInfo getBootstrapInfo() { + return BOOTSTRAP_INFO; + } + private void deliverCdsUpdate(String clusterName, CdsUpdate update) { if (watchers.containsKey(clusterName)) { List> resourceWatchers = From b8c1aa517a0de2746977af856fb2c30b7fb7a26b Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Fri, 13 Sep 2024 17:11:17 -0700 Subject: [PATCH 003/591] s2a: Add gRPC S2A (#11113) --- MODULE.bazel | 1 + buildscripts/sync-protos.sh | 2 +- repositories.bzl | 2 + s2a/BUILD.bazel | 194 +++++++++ s2a/build.gradle | 151 +++++++ .../grpc/s2a/handshaker/S2AServiceGrpc.java | 285 +++++++++++++ .../grpc/s2a/MtlsToS2AChannelCredentials.java | 96 +++++ .../io/grpc/s2a/S2AChannelCredentials.java | 130 ++++++ .../io/grpc/s2a/channel/S2AChannelPool.java | 43 ++ .../grpc/s2a/channel/S2AGrpcChannelPool.java | 109 +++++ .../channel/S2AHandshakerServiceChannel.java | 195 +++++++++ .../handshaker/ConnectionClosedException.java | 27 ++ .../GetAuthenticationMechanisms.java | 56 +++ .../io/grpc/s2a/handshaker/ProtoUtil.java | 96 +++++ .../handshaker/S2AConnectionException.java | 25 ++ .../io/grpc/s2a/handshaker/S2AIdentity.java | 62 +++ .../s2a/handshaker/S2APrivateKeyMethod.java | 143 +++++++ .../S2AProtocolNegotiatorFactory.java | 249 +++++++++++ .../java/io/grpc/s2a/handshaker/S2AStub.java | 221 ++++++++++ .../grpc/s2a/handshaker/S2ATrustManager.java | 152 +++++++ .../s2a/handshaker/SslContextFactory.java | 178 ++++++++ .../tokenmanager/AccessTokenManager.java | 61 +++ .../tokenmanager/SingleTokenFetcher.java | 57 +++ .../handshaker/tokenmanager/TokenFetcher.java | 28 ++ s2a/src/main/proto/grpc/gcp/s2a/common.proto | 82 ++++ s2a/src/main/proto/grpc/gcp/s2a/s2a.proto | 369 +++++++++++++++++ .../main/proto/grpc/gcp/s2a/s2a_context.proto | 62 +++ .../s2a/MtlsToS2AChannelCredentialsTest.java | 135 ++++++ .../grpc/s2a/S2AChannelCredentialsTest.java | 112 +++++ .../s2a/channel/S2AGrpcChannelPoolTest.java | 125 ++++++ .../S2AHandshakerServiceChannelTest.java | 390 ++++++++++++++++++ .../io/grpc/s2a/handshaker/FakeS2AServer.java | 55 +++ .../s2a/handshaker/FakeS2AServerTest.java | 265 ++++++++++++ .../io/grpc/s2a/handshaker/FakeWriter.java | 363 ++++++++++++++++ .../GetAuthenticationMechanismsTest.java | 61 +++ .../grpc/s2a/handshaker/IntegrationTest.java | 320 ++++++++++++++ .../io/grpc/s2a/handshaker/ProtoUtilTest.java | 131 ++++++ .../handshaker/S2APrivateKeyMethodTest.java | 308 ++++++++++++++ .../S2AProtocolNegotiatorFactoryTest.java | 284 +++++++++++++ .../io/grpc/s2a/handshaker/S2AStubTest.java | 260 ++++++++++++ .../s2a/handshaker/S2ATrustManagerTest.java | 262 ++++++++++++ .../s2a/handshaker/SslContextFactoryTest.java | 177 ++++++++ .../SingleTokenAccessTokenManagerTest.java | 71 ++++ s2a/src/test/resources/README.md | 32 ++ s2a/src/test/resources/client.csr | 16 + s2a/src/test/resources/client_cert.pem | 18 + s2a/src/test/resources/client_key.pem | 28 ++ s2a/src/test/resources/config.cnf | 17 + s2a/src/test/resources/root_cert.pem | 22 + s2a/src/test/resources/root_key.pem | 30 ++ s2a/src/test/resources/server.csr | 16 + s2a/src/test/resources/server_cert.pem | 20 + s2a/src/test/resources/server_key.pem | 28 ++ settings.gradle | 2 + 54 files changed, 6623 insertions(+), 1 deletion(-) create mode 100644 s2a/BUILD.bazel create mode 100644 s2a/build.gradle create mode 100644 s2a/src/generated/main/grpc/io/grpc/s2a/handshaker/S2AServiceGrpc.java create mode 100644 s2a/src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java create mode 100644 s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java create mode 100644 s2a/src/main/java/io/grpc/s2a/channel/S2AChannelPool.java create mode 100644 s2a/src/main/java/io/grpc/s2a/channel/S2AGrpcChannelPool.java create mode 100644 s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/ConnectionClosedException.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/GetAuthenticationMechanisms.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/S2AConnectionException.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/S2AIdentity.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/S2APrivateKeyMethod.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/SslContextFactory.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenFetcher.java create mode 100644 s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/TokenFetcher.java create mode 100644 s2a/src/main/proto/grpc/gcp/s2a/common.proto create mode 100644 s2a/src/main/proto/grpc/gcp/s2a/s2a.proto create mode 100644 s2a/src/main/proto/grpc/gcp/s2a/s2a_context.proto create mode 100644 s2a/src/test/java/io/grpc/s2a/MtlsToS2AChannelCredentialsTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/channel/S2AGrpcChannelPoolTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServer.java create mode 100644 s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServerTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/handshaker/FakeWriter.java create mode 100644 s2a/src/test/java/io/grpc/s2a/handshaker/GetAuthenticationMechanismsTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/handshaker/ProtoUtilTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/handshaker/S2APrivateKeyMethodTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/handshaker/S2ATrustManagerTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/handshaker/SslContextFactoryTest.java create mode 100644 s2a/src/test/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java create mode 100644 s2a/src/test/resources/README.md create mode 100644 s2a/src/test/resources/client.csr create mode 100644 s2a/src/test/resources/client_cert.pem create mode 100644 s2a/src/test/resources/client_key.pem create mode 100644 s2a/src/test/resources/config.cnf create mode 100644 s2a/src/test/resources/root_cert.pem create mode 100644 s2a/src/test/resources/root_key.pem create mode 100644 s2a/src/test/resources/server.csr create mode 100644 s2a/src/test/resources/server_cert.pem create mode 100644 s2a/src/test/resources/server_key.pem diff --git a/MODULE.bazel b/MODULE.bazel index b60ea565073..81fa8795e68 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -41,6 +41,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.perfmark:perfmark-api:0.27.0", "junit:junit:4.13.2", "org.apache.tomcat:annotations-api:6.0.53", + "org.checkerframework:checker-qual:3.12.0", "org.codehaus.mojo:animal-sniffer-annotations:1.24", ] # GRPC_DEPS_END diff --git a/buildscripts/sync-protos.sh b/buildscripts/sync-protos.sh index 5f01be2e5c9..628b1688d4c 100755 --- a/buildscripts/sync-protos.sh +++ b/buildscripts/sync-protos.sh @@ -8,7 +8,7 @@ curl -Ls https://github.com/grpc/grpc-proto/archive/master.tar.gz | tar xz -C "$ base="$tmpdir/grpc-proto-master" # Copy protos in 'src/main/proto' from grpc-proto for these projects -for project in alts grpclb services rls interop-testing; do +for project in alts grpclb services s2a rls interop-testing; do while read -r proto; do [ -f "$base/$proto" ] && cp "$base/$proto" "$project/src/main/proto/$proto" echo "$proto" diff --git a/repositories.bzl b/repositories.bzl index 455e9dcf3ca..f8cf77c8190 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -45,6 +45,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.perfmark:perfmark-api:0.27.0", "junit:junit:4.13.2", "org.apache.tomcat:annotations-api:6.0.53", + "org.checkerframework:checker-qual:3.12.0", "org.codehaus.mojo:animal-sniffer-annotations:1.24", ] # GRPC_DEPS_END @@ -80,6 +81,7 @@ IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS = { "io.grpc:grpc-rls": "@io_grpc_grpc_java//rls", "io.grpc:grpc-services": "@io_grpc_grpc_java//services:services_maven", "io.grpc:grpc-stub": "@io_grpc_grpc_java//stub", + "io.grpc:grpc-s2a": "@io_grpc_grpc_java//s2a", "io.grpc:grpc-testing": "@io_grpc_grpc_java//testing", "io.grpc:grpc-xds": "@io_grpc_grpc_java//xds:xds_maven", "io.grpc:grpc-util": "@io_grpc_grpc_java//util", diff --git a/s2a/BUILD.bazel b/s2a/BUILD.bazel new file mode 100644 index 00000000000..5aeaedbe358 --- /dev/null +++ b/s2a/BUILD.bazel @@ -0,0 +1,194 @@ +load("@rules_proto//proto:defs.bzl", "proto_library") +load("//:java_grpc_library.bzl", "java_grpc_library") +load("@rules_jvm_external//:defs.bzl", "artifact") + +java_library( + name = "s2a_channel_pool", + srcs = glob([ + "src/main/java/io/grpc/s2a/channel/*.java", + ]), + deps = [ + "//api", + "//core", + "//core:internal", + "//netty", + artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.errorprone:error_prone_annotations"), + artifact("com.google.guava:guava"), + artifact("org.checkerframework:checker-qual"), + artifact("io.netty:netty-common"), + artifact("io.netty:netty-transport"), + ], +) + +java_library( + name = "s2a_identity", + srcs = ["src/main/java/io/grpc/s2a/handshaker/S2AIdentity.java"], + deps = [ + ":common_java_proto", + artifact("com.google.errorprone:error_prone_annotations"), + artifact("com.google.guava:guava"), + ], +) + +java_library( + name = "token_fetcher", + srcs = ["src/main/java/io/grpc/s2a/handshaker/tokenmanager/TokenFetcher.java"], + deps = [ + ":s2a_identity", + ], +) + +java_library( + name = "access_token_manager", + srcs = [ + "src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java", + ], + deps = [ + ":s2a_identity", + ":token_fetcher", + artifact("com.google.code.findbugs:jsr305"), + ], +) + +java_library( + name = "single_token_fetcher", + srcs = [ + "src/main/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenFetcher.java", + ], + deps = [ + ":s2a_identity", + ":token_fetcher", + artifact("com.google.guava:guava"), + ], +) + +java_library( + name = "s2a_handshaker", + srcs = [ + "src/main/java/io/grpc/s2a/handshaker/ConnectionClosedException.java", + "src/main/java/io/grpc/s2a/handshaker/GetAuthenticationMechanisms.java", + "src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java", + "src/main/java/io/grpc/s2a/handshaker/S2AConnectionException.java", + "src/main/java/io/grpc/s2a/handshaker/S2APrivateKeyMethod.java", + "src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java", + "src/main/java/io/grpc/s2a/handshaker/S2AStub.java", + "src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java", + "src/main/java/io/grpc/s2a/handshaker/SslContextFactory.java", + ], + deps = [ + ":access_token_manager", + ":common_java_proto", + ":s2a_channel_pool", + ":s2a_identity", + ":s2a_java_proto", + ":s2a_java_grpc_proto", + ":single_token_fetcher", + "//api", + "//core:internal", + "//netty", + "//stub", + artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.errorprone:error_prone_annotations"), + artifact("com.google.guava:guava"), + artifact("org.checkerframework:checker-qual"), + "@com_google_protobuf//:protobuf_java", + artifact("io.netty:netty-common"), + artifact("io.netty:netty-handler"), + artifact("io.netty:netty-transport"), + ], +) + +java_library( + name = "s2av2_credentials", + srcs = ["src/main/java/io/grpc/s2a/S2AChannelCredentials.java"], + visibility = ["//visibility:public"], + deps = [ + ":s2a_channel_pool", + ":s2a_handshaker", + ":s2a_identity", + "//api", + "//core:internal", + "//netty", + artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.errorprone:error_prone_annotations"), + artifact("com.google.guava:guava"), + artifact("org.checkerframework:checker-qual"), + ], +) + +java_library( + name = "mtls_to_s2av2_credentials", + srcs = ["src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java"], + visibility = ["//visibility:public"], + deps = [ + ":s2a_channel_pool", + ":s2av2_credentials", + "//api", + "//util", + artifact("com.google.guava:guava"), + ], +) + +# bazel only accepts proto import with absolute path. +genrule( + name = "protobuf_imports", + srcs = glob(["src/main/proto/grpc/gcp/s2a/*.proto"]), + outs = [ + "protobuf_out/grpc/gcp/s2a/s2a.proto", + "protobuf_out/grpc/gcp/s2a/s2a_context.proto", + "protobuf_out/grpc/gcp/s2a/common.proto", + ], + cmd = "for fname in $(SRCS); do " + + "sed 's,import \",import \"s2a/protobuf_out/,g' $$fname > " + + "$(@D)/protobuf_out/grpc/gcp/s2a/$$(basename $$fname); done", +) + +proto_library( + name = "common_proto", + srcs = [ + "protobuf_out/grpc/gcp/s2a/common.proto", + ], +) + +proto_library( + name = "s2a_context_proto", + srcs = [ + "protobuf_out/grpc/gcp/s2a/s2a_context.proto", + ], + deps = [ + ":common_proto", + ], +) + +proto_library( + name = "s2a_proto", + srcs = [ + "protobuf_out/grpc/gcp/s2a/s2a.proto", + ], + deps = [ + ":common_proto", + ":s2a_context_proto", + ], +) + +java_proto_library( + name = "s2a_java_proto", + deps = [":s2a_proto"], +) + +java_proto_library( + name = "s2a_context_java_proto", + deps = [":s2a_context_proto"], +) + +java_proto_library( + name = "common_java_proto", + deps = [":common_proto"], +) + +java_grpc_library( + name = "s2a_java_grpc_proto", + srcs = [":s2a_proto"], + deps = [":s2a_java_proto"], +) diff --git a/s2a/build.gradle b/s2a/build.gradle new file mode 100644 index 00000000000..403ac93552f --- /dev/null +++ b/s2a/build.gradle @@ -0,0 +1,151 @@ +buildscript { + dependencies { + classpath 'com.google.gradle:osdetector-gradle-plugin:1.4.0' + } +} + +plugins { + id "java-library" + id "maven-publish" + + id "com.github.johnrengelman.shadow" + id "com.google.protobuf" + id "ru.vyarus.animalsniffer" +} + +description = "gRPC: S2A" + +apply plugin: "com.google.osdetector" + +dependencies { + + api project(':grpc-api') + implementation project(':grpc-stub'), + project(':grpc-protobuf'), + project(':grpc-core'), + libraries.protobuf.java, + libraries.conscrypt, + libraries.guava.jre // JRE required by protobuf-java-util from grpclb + def nettyDependency = implementation project(':grpc-netty') + compileOnly libraries.javax.annotation + + shadow configurations.implementation.getDependencies().minus(nettyDependency) + shadow project(path: ':grpc-netty-shaded', configuration: 'shadow') + + testImplementation project(':grpc-benchmarks'), + project(':grpc-testing'), + project(':grpc-testing-proto'), + testFixtures(project(':grpc-core')), + libraries.guava, + libraries.junit, + libraries.mockito.core, + libraries.truth, + libraries.conscrypt, + libraries.netty.transport.epoll + + testImplementation 'com.google.truth:truth:1.4.2' + testImplementation 'com.google.truth.extensions:truth-proto-extension:1.4.2' + testImplementation libraries.guava.testlib + + testRuntimeOnly libraries.netty.tcnative, + libraries.netty.tcnative.classes + testRuntimeOnly (libraries.netty.tcnative) { + artifact { + classifier = "linux-x86_64" + } + } + testRuntimeOnly (libraries.netty.tcnative) { + artifact { + classifier = "linux-aarch_64" + } + } + testRuntimeOnly (libraries.netty.tcnative) { + artifact { + classifier = "osx-x86_64" + } + } + testRuntimeOnly (libraries.netty.tcnative) { + artifact { + classifier = "osx-aarch_64" + } + } + testRuntimeOnly (libraries.netty.tcnative) { + artifact { + classifier = "windows-x86_64" + } + } + testRuntimeOnly (libraries.netty.transport.epoll) { + artifact { + classifier = "linux-x86_64" + } + } + + signature libraries.signature.java +} + +tasks.named("compileJava") { + dependsOn(tasks.named("generateProto")) + //dependsOn(tasks.named("syncGeneratedSourcesmain")) +} + + +tasks.named("sourcesJar") { + dependsOn(tasks.named("generateProto")) + //dependsOn(tasks.named("syncGeneratedSourcesmain")) +} + +sourceSets { + main { + //java.srcDirs += "src/generated/main/java" + //java.srcDirs += "src/generated/main/grpc" + } +} +//println sourceSets.main.java.srcDirs +//println sourceSets.test.resources.srcDirs + +configureProtoCompilation() + +tasks.named("javadoc").configure { + exclude 'io/grpc/s2a/**' +} + +tasks.named("jar").configure { + // Must use a different archiveClassifier to avoid conflicting with shadowJar + archiveClassifier = 'original' + manifest { + attributes('Automatic-Module-Name': 'io.grpc.s2a') + } +} + +// We want to use grpc-netty-shaded instead of grpc-netty. But we also want our +// source to work with Bazel, so we rewrite the code as part of the build. +tasks.named("shadowJar").configure { + archiveClassifier = null + dependencies { + exclude(dependency {true}) + } + relocate 'io.grpc.netty', 'io.grpc.netty.shaded.io.grpc.netty' + relocate 'io.netty', 'io.grpc.netty.shaded.io.netty' +} + +publishing { + publications { + maven(MavenPublication) { + // We want this to throw an exception if it isn't working + def originalJar = artifacts.find { dep -> dep.classifier == 'original'} + artifacts.remove(originalJar) + + pom.withXml { + def dependenciesNode = new Node(null, 'dependencies') + project.configurations.shadow.allDependencies.each { dep -> + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', dep.group) + dependencyNode.appendNode('artifactId', dep.name) + dependencyNode.appendNode('version', dep.version) + dependencyNode.appendNode('scope', 'compile') + } + asNode().dependencies[0].replaceNode(dependenciesNode) + } + } + } +} diff --git a/s2a/src/generated/main/grpc/io/grpc/s2a/handshaker/S2AServiceGrpc.java b/s2a/src/generated/main/grpc/io/grpc/s2a/handshaker/S2AServiceGrpc.java new file mode 100644 index 00000000000..b365954b189 --- /dev/null +++ b/s2a/src/generated/main/grpc/io/grpc/s2a/handshaker/S2AServiceGrpc.java @@ -0,0 +1,285 @@ +package io.grpc.s2a.handshaker; + +import static io.grpc.MethodDescriptor.generateFullMethodName; + +/** + */ +@javax.annotation.Generated( + value = "by gRPC proto compiler", + comments = "Source: grpc/gcp/s2a/s2a.proto") +@io.grpc.stub.annotations.GrpcGenerated +public final class S2AServiceGrpc { + + private S2AServiceGrpc() {} + + public static final java.lang.String SERVICE_NAME = "grpc.gcp.s2a.S2AService"; + + // Static method descriptors that strictly reflect the proto. + private static volatile io.grpc.MethodDescriptor getSetUpSessionMethod; + + @io.grpc.stub.annotations.RpcMethod( + fullMethodName = SERVICE_NAME + '/' + "SetUpSession", + requestType = io.grpc.s2a.handshaker.SessionReq.class, + responseType = io.grpc.s2a.handshaker.SessionResp.class, + methodType = io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) + public static io.grpc.MethodDescriptor getSetUpSessionMethod() { + io.grpc.MethodDescriptor getSetUpSessionMethod; + if ((getSetUpSessionMethod = S2AServiceGrpc.getSetUpSessionMethod) == null) { + synchronized (S2AServiceGrpc.class) { + if ((getSetUpSessionMethod = S2AServiceGrpc.getSetUpSessionMethod) == null) { + S2AServiceGrpc.getSetUpSessionMethod = getSetUpSessionMethod = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) + .setFullMethodName(generateFullMethodName(SERVICE_NAME, "SetUpSession")) + .setSampledToLocalTracing(true) + .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + io.grpc.s2a.handshaker.SessionReq.getDefaultInstance())) + .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + io.grpc.s2a.handshaker.SessionResp.getDefaultInstance())) + .setSchemaDescriptor(new S2AServiceMethodDescriptorSupplier("SetUpSession")) + .build(); + } + } + } + return getSetUpSessionMethod; + } + + /** + * Creates a new async stub that supports all call types for the service + */ + public static S2AServiceStub newStub(io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public S2AServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new S2AServiceStub(channel, callOptions); + } + }; + return S2AServiceStub.newStub(factory, channel); + } + + /** + * Creates a new blocking-style stub that supports unary and streaming output calls on the service + */ + public static S2AServiceBlockingStub newBlockingStub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public S2AServiceBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new S2AServiceBlockingStub(channel, callOptions); + } + }; + return S2AServiceBlockingStub.newStub(factory, channel); + } + + /** + * Creates a new ListenableFuture-style stub that supports unary calls on the service + */ + public static S2AServiceFutureStub newFutureStub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public S2AServiceFutureStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new S2AServiceFutureStub(channel, callOptions); + } + }; + return S2AServiceFutureStub.newStub(factory, channel); + } + + /** + */ + public interface AsyncService { + + /** + *
+     * SetUpSession is a bidirectional stream used by applications to offload
+     * operations from the TLS handshake.
+     * 
+ */ + default io.grpc.stub.StreamObserver setUpSession( + io.grpc.stub.StreamObserver responseObserver) { + return io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall(getSetUpSessionMethod(), responseObserver); + } + } + + /** + * Base class for the server implementation of the service S2AService. + */ + public static abstract class S2AServiceImplBase + implements io.grpc.BindableService, AsyncService { + + @java.lang.Override public final io.grpc.ServerServiceDefinition bindService() { + return S2AServiceGrpc.bindService(this); + } + } + + /** + * A stub to allow clients to do asynchronous rpc calls to service S2AService. + */ + public static final class S2AServiceStub + extends io.grpc.stub.AbstractAsyncStub { + private S2AServiceStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected S2AServiceStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new S2AServiceStub(channel, callOptions); + } + + /** + *
+     * SetUpSession is a bidirectional stream used by applications to offload
+     * operations from the TLS handshake.
+     * 
+ */ + public io.grpc.stub.StreamObserver setUpSession( + io.grpc.stub.StreamObserver responseObserver) { + return io.grpc.stub.ClientCalls.asyncBidiStreamingCall( + getChannel().newCall(getSetUpSessionMethod(), getCallOptions()), responseObserver); + } + } + + /** + * A stub to allow clients to do synchronous rpc calls to service S2AService. + */ + public static final class S2AServiceBlockingStub + extends io.grpc.stub.AbstractBlockingStub { + private S2AServiceBlockingStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected S2AServiceBlockingStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new S2AServiceBlockingStub(channel, callOptions); + } + } + + /** + * A stub to allow clients to do ListenableFuture-style rpc calls to service S2AService. + */ + public static final class S2AServiceFutureStub + extends io.grpc.stub.AbstractFutureStub { + private S2AServiceFutureStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected S2AServiceFutureStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new S2AServiceFutureStub(channel, callOptions); + } + } + + private static final int METHODID_SET_UP_SESSION = 0; + + 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 AsyncService serviceImpl; + private final int methodId; + + MethodHandlers(AsyncService 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_SET_UP_SESSION: + return (io.grpc.stub.StreamObserver) serviceImpl.setUpSession( + (io.grpc.stub.StreamObserver) responseObserver); + default: + throw new AssertionError(); + } + } + } + + public static final io.grpc.ServerServiceDefinition bindService(AsyncService service) { + return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor()) + .addMethod( + getSetUpSessionMethod(), + io.grpc.stub.ServerCalls.asyncBidiStreamingCall( + new MethodHandlers< + io.grpc.s2a.handshaker.SessionReq, + io.grpc.s2a.handshaker.SessionResp>( + service, METHODID_SET_UP_SESSION))) + .build(); + } + + private static abstract class S2AServiceBaseDescriptorSupplier + implements io.grpc.protobuf.ProtoFileDescriptorSupplier, io.grpc.protobuf.ProtoServiceDescriptorSupplier { + S2AServiceBaseDescriptorSupplier() {} + + @java.lang.Override + public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() { + return io.grpc.s2a.handshaker.S2AProto.getDescriptor(); + } + + @java.lang.Override + public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() { + return getFileDescriptor().findServiceByName("S2AService"); + } + } + + private static final class S2AServiceFileDescriptorSupplier + extends S2AServiceBaseDescriptorSupplier { + S2AServiceFileDescriptorSupplier() {} + } + + private static final class S2AServiceMethodDescriptorSupplier + extends S2AServiceBaseDescriptorSupplier + implements io.grpc.protobuf.ProtoMethodDescriptorSupplier { + private final java.lang.String methodName; + + S2AServiceMethodDescriptorSupplier(java.lang.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 (S2AServiceGrpc.class) { + result = serviceDescriptor; + if (result == null) { + serviceDescriptor = result = io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME) + .setSchemaDescriptor(new S2AServiceFileDescriptorSupplier()) + .addMethod(getSetUpSessionMethod()) + .build(); + } + } + } + return result; + } +} diff --git a/s2a/src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java b/s2a/src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java new file mode 100644 index 00000000000..56f612502bf --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java @@ -0,0 +1,96 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.isNullOrEmpty; + +import io.grpc.ChannelCredentials; +import io.grpc.TlsChannelCredentials; +import io.grpc.util.AdvancedTlsX509KeyManager; +import io.grpc.util.AdvancedTlsX509TrustManager; +import java.io.File; +import java.io.IOException; +import java.security.GeneralSecurityException; + +/** + * Configures an {@code S2AChannelCredentials.Builder} instance with credentials used to establish a + * connection with the S2A to support talking to the S2A over mTLS. + */ +public final class MtlsToS2AChannelCredentials { + /** + * Creates a {@code S2AChannelCredentials.Builder} builder, that talks to the S2A over mTLS. + * + * @param s2aAddress the address of the S2A server used to secure the connection. + * @param privateKeyPath the path to the private key PEM to use for authenticating to the S2A. + * @param certChainPath the path to the cert chain PEM to use for authenticating to the S2A. + * @param trustBundlePath the path to the trust bundle PEM. + * @return a {@code MtlsToS2AChannelCredentials.Builder} instance. + */ + public static Builder createBuilder( + String s2aAddress, String privateKeyPath, String certChainPath, String trustBundlePath) { + checkArgument(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty."); + checkArgument(!isNullOrEmpty(privateKeyPath), "privateKeyPath must not be null or empty."); + checkArgument(!isNullOrEmpty(certChainPath), "certChainPath must not be null or empty."); + checkArgument(!isNullOrEmpty(trustBundlePath), "trustBundlePath must not be null or empty."); + return new Builder(s2aAddress, privateKeyPath, certChainPath, trustBundlePath); + } + + /** Builds an {@code MtlsToS2AChannelCredentials} instance. */ + public static final class Builder { + private final String s2aAddress; + private final String privateKeyPath; + private final String certChainPath; + private final String trustBundlePath; + + Builder( + String s2aAddress, String privateKeyPath, String certChainPath, String trustBundlePath) { + this.s2aAddress = s2aAddress; + this.privateKeyPath = privateKeyPath; + this.certChainPath = certChainPath; + this.trustBundlePath = trustBundlePath; + } + + public S2AChannelCredentials.Builder build() throws GeneralSecurityException, IOException { + checkState(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty."); + checkState(!isNullOrEmpty(privateKeyPath), "privateKeyPath must not be null or empty."); + checkState(!isNullOrEmpty(certChainPath), "certChainPath must not be null or empty."); + checkState(!isNullOrEmpty(trustBundlePath), "trustBundlePath must not be null or empty."); + File privateKeyFile = new File(privateKeyPath); + File certChainFile = new File(certChainPath); + File trustBundleFile = new File(trustBundlePath); + + AdvancedTlsX509KeyManager keyManager = new AdvancedTlsX509KeyManager(); + keyManager.updateIdentityCredentials(certChainFile, privateKeyFile); + + AdvancedTlsX509TrustManager trustManager = AdvancedTlsX509TrustManager.newBuilder().build(); + trustManager.updateTrustCredentials(trustBundleFile); + + ChannelCredentials channelToS2ACredentials = + TlsChannelCredentials.newBuilder() + .keyManager(keyManager) + .trustManager(trustManager) + .build(); + + return S2AChannelCredentials.createBuilder(s2aAddress) + .setS2AChannelCredentials(channelToS2ACredentials); + } + } + + private MtlsToS2AChannelCredentials() {} +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java new file mode 100644 index 00000000000..8a5f1f51350 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java @@ -0,0 +1,130 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.isNullOrEmpty; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.grpc.Channel; +import io.grpc.ChannelCredentials; +import io.grpc.internal.ObjectPool; +import io.grpc.internal.SharedResourcePool; +import io.grpc.netty.InternalNettyChannelCredentials; +import io.grpc.netty.InternalProtocolNegotiator; +import io.grpc.s2a.channel.S2AHandshakerServiceChannel; +import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.handshaker.S2AProtocolNegotiatorFactory; +import java.util.Optional; +import javax.annotation.concurrent.NotThreadSafe; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Configures gRPC to use S2A for transport security when establishing a secure channel. Only for + * use on the client side of a gRPC connection. + */ +public final class S2AChannelCredentials { + /** + * Creates a channel credentials builder for establishing an S2A-secured connection. + * + * @param s2aAddress the address of the S2A server used to secure the connection. + * @return a {@code S2AChannelCredentials.Builder} instance. + */ + public static Builder createBuilder(String s2aAddress) { + checkArgument(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty."); + return new Builder(s2aAddress); + } + + /** Builds an {@code S2AChannelCredentials} instance. */ + @NotThreadSafe + public static final class Builder { + private final String s2aAddress; + private ObjectPool s2aChannelPool; + private Optional s2aChannelCredentials; + private @Nullable S2AIdentity localIdentity = null; + + Builder(String s2aAddress) { + this.s2aAddress = s2aAddress; + this.s2aChannelPool = null; + this.s2aChannelCredentials = Optional.empty(); + } + + /** + * Sets the local identity of the client in the form of a SPIFFE ID. The client may set at most + * 1 local identity. If no local identity is specified, then the S2A chooses a default local + * identity, if one exists. + */ + @CanIgnoreReturnValue + public Builder setLocalSpiffeId(String localSpiffeId) { + checkNotNull(localSpiffeId); + checkArgument(localIdentity == null, "localIdentity is already set."); + localIdentity = S2AIdentity.fromSpiffeId(localSpiffeId); + return this; + } + + /** + * Sets the local identity of the client in the form of a hostname. The client may set at most 1 + * local identity. If no local identity is specified, then the S2A chooses a default local + * identity, if one exists. + */ + @CanIgnoreReturnValue + public Builder setLocalHostname(String localHostname) { + checkNotNull(localHostname); + checkArgument(localIdentity == null, "localIdentity is already set."); + localIdentity = S2AIdentity.fromHostname(localHostname); + return this; + } + + /** + * Sets the local identity of the client in the form of a UID. The client may set at most 1 + * local identity. If no local identity is specified, then the S2A chooses a default local + * identity, if one exists. + */ + @CanIgnoreReturnValue + public Builder setLocalUid(String localUid) { + checkNotNull(localUid); + checkArgument(localIdentity == null, "localIdentity is already set."); + localIdentity = S2AIdentity.fromUid(localUid); + return this; + } + + /** Sets the credentials to be used when connecting to the S2A. */ + @CanIgnoreReturnValue + public Builder setS2AChannelCredentials(ChannelCredentials s2aChannelCredentials) { + this.s2aChannelCredentials = Optional.of(s2aChannelCredentials); + return this; + } + + public ChannelCredentials build() { + checkState(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty."); + ObjectPool s2aChannelPool = + SharedResourcePool.forResource( + S2AHandshakerServiceChannel.getChannelResource(s2aAddress, s2aChannelCredentials)); + checkNotNull(s2aChannelPool, "s2aChannelPool"); + this.s2aChannelPool = s2aChannelPool; + return InternalNettyChannelCredentials.create(buildProtocolNegotiatorFactory()); + } + + InternalProtocolNegotiator.ClientFactory buildProtocolNegotiatorFactory() { + return S2AProtocolNegotiatorFactory.createClientFactory(localIdentity, s2aChannelPool); + } + } + + private S2AChannelCredentials() {} +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/channel/S2AChannelPool.java b/s2a/src/main/java/io/grpc/s2a/channel/S2AChannelPool.java new file mode 100644 index 00000000000..e5caf5e69bd --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/channel/S2AChannelPool.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.channel; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.grpc.Channel; +import javax.annotation.concurrent.ThreadSafe; + +/** Manages a channel pool to be used for communication with the S2A. */ +@ThreadSafe +public interface S2AChannelPool extends AutoCloseable { + /** + * Retrieves an open channel to the S2A from the channel pool. + * + * @throws IllegalStateException if no channel is available. + */ + @CanIgnoreReturnValue + Channel getChannel(); + + /** Returns a channel to the channel pool. */ + void returnToPool(Channel channel); + + /** + * Returns all channels to the channel pool and closes the pool so that no new channels can be + * retrieved from the pool. + */ + @Override + void close(); +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/channel/S2AGrpcChannelPool.java b/s2a/src/main/java/io/grpc/s2a/channel/S2AGrpcChannelPool.java new file mode 100644 index 00000000000..4794cd9ee49 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/channel/S2AGrpcChannelPool.java @@ -0,0 +1,109 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.channel; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.grpc.Channel; +import io.grpc.internal.ObjectPool; +import javax.annotation.concurrent.ThreadSafe; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Manages a gRPC channel pool and a cached gRPC channel to be used for communication with the S2A. + */ +@ThreadSafe +public final class S2AGrpcChannelPool implements S2AChannelPool { + private static final int MAX_NUMBER_USERS_OF_CACHED_CHANNEL = 100000; + private final ObjectPool channelPool; + + @GuardedBy("this") + private @Nullable Channel cachedChannel; + + @GuardedBy("this") + private int numberOfUsersOfCachedChannel = 0; + + private enum State { + OPEN, + CLOSED, + } + + ; + + @GuardedBy("this") + private State state = State.OPEN; + + public static S2AChannelPool create(ObjectPool channelPool) { + checkNotNull(channelPool, "Channel pool should not be null."); + return new S2AGrpcChannelPool(channelPool); + } + + private S2AGrpcChannelPool(ObjectPool channelPool) { + this.channelPool = channelPool; + } + + /** + * Retrieves a channel from {@code channelPool} if {@code channel} is null, and returns {@code + * channel} otherwise. + * + * @return a {@link Channel} obtained from the channel pool. + */ + @Override + public synchronized Channel getChannel() { + checkState(state.equals(State.OPEN), "Channel pool is not open."); + checkState( + numberOfUsersOfCachedChannel < MAX_NUMBER_USERS_OF_CACHED_CHANNEL, + "Max number of channels have been retrieved from the channel pool."); + if (cachedChannel == null) { + cachedChannel = channelPool.getObject(); + } + numberOfUsersOfCachedChannel += 1; + return cachedChannel; + } + + /** + * Returns {@code channel} to {@code channelPool}. + * + *

The caller must ensure that {@code channel} was retrieved from this channel pool. + */ + @Override + public synchronized void returnToPool(Channel channel) { + checkState(state.equals(State.OPEN), "Channel pool is not open."); + checkArgument( + cachedChannel != null && numberOfUsersOfCachedChannel > 0 && cachedChannel.equals(channel), + "Cannot return the channel to channel pool because the channel was not obtained from" + + " channel pool."); + numberOfUsersOfCachedChannel -= 1; + if (numberOfUsersOfCachedChannel == 0) { + channelPool.returnObject(channel); + cachedChannel = null; + } + } + + @Override + public synchronized void close() { + state = State.CLOSED; + numberOfUsersOfCachedChannel = 0; + if (cachedChannel != null) { + channelPool.returnObject(cachedChannel); + cachedChannel = null; + } + } +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java b/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java new file mode 100644 index 00000000000..75ec7347bb5 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java @@ -0,0 +1,195 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.channel; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Maps; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ChannelCredentials; +import io.grpc.ClientCall; +import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor; +import io.grpc.internal.SharedResourceHolder.Resource; +import io.grpc.netty.NettyChannelBuilder; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.concurrent.DefaultThreadFactory; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.ConcurrentMap; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Provides APIs for managing gRPC channels to S2A servers. Each channel is local and plaintext. If + * credentials are provided, they are used to secure the channel. + * + *

This is done as follows: for each S2A server, provides an implementation of gRPC's {@link + * SharedResourceHolder.Resource} interface called a {@code Resource}. A {@code + * Resource} is a factory for creating gRPC channels to the S2A server at a given address, + * and a channel must be returned to the {@code Resource} when it is no longer needed. + * + *

Typical usage pattern is below: + * + *

{@code
+ * Resource resource = S2AHandshakerServiceChannel.getChannelResource("localhost:1234",
+ * creds);
+ * Channel channel = resource.create();
+ * // Send an RPC over the channel to the S2A server running at localhost:1234.
+ * resource.close(channel);
+ * }
+ */ +@ThreadSafe +public final class S2AHandshakerServiceChannel { + private static final ConcurrentMap> SHARED_RESOURCE_CHANNELS = + Maps.newConcurrentMap(); + private static final Duration DELEGATE_TERMINATION_TIMEOUT = Duration.ofSeconds(2); + private static final Duration CHANNEL_SHUTDOWN_TIMEOUT = Duration.ofSeconds(10); + + /** + * Returns a {@link SharedResourceHolder.Resource} instance for managing channels to an S2A server + * running at {@code s2aAddress}. + * + * @param s2aAddress the address of the S2A, typically in the format {@code host:port}. + * @param s2aChannelCredentials the credentials to use when establishing a connection to the S2A. + * @return a {@link ChannelResource} instance that manages a {@link Channel} to the S2A server + * running at {@code s2aAddress}. + */ + public static Resource getChannelResource( + String s2aAddress, Optional s2aChannelCredentials) { + checkNotNull(s2aAddress); + return SHARED_RESOURCE_CHANNELS.computeIfAbsent( + s2aAddress, channelResource -> new ChannelResource(s2aAddress, s2aChannelCredentials)); + } + + /** + * Defines how to create and destroy a {@link Channel} instance that uses shared resources. A + * channel created by {@code ChannelResource} is a plaintext, local channel to the service running + * at {@code targetAddress}. + */ + private static class ChannelResource implements Resource { + private final String targetAddress; + private final Optional channelCredentials; + + public ChannelResource(String targetAddress, Optional channelCredentials) { + this.targetAddress = targetAddress; + this.channelCredentials = channelCredentials; + } + + /** + * Creates a {@code EventLoopHoldingChannel} instance to the service running at {@code + * targetAddress}. This channel uses a dedicated thread pool for its {@code EventLoopGroup} + * instance to avoid blocking. + */ + @Override + public Channel create() { + EventLoopGroup eventLoopGroup = + new NioEventLoopGroup(1, new DefaultThreadFactory("S2A channel pool", true)); + ManagedChannel channel = null; + if (channelCredentials.isPresent()) { + // Create a secure channel. + channel = + NettyChannelBuilder.forTarget(targetAddress, channelCredentials.get()) + .channelType(NioSocketChannel.class) + .directExecutor() + .eventLoopGroup(eventLoopGroup) + .build(); + } else { + // Create a plaintext channel. + channel = + NettyChannelBuilder.forTarget(targetAddress) + .channelType(NioSocketChannel.class) + .directExecutor() + .eventLoopGroup(eventLoopGroup) + .usePlaintext() + .build(); + } + return EventLoopHoldingChannel.create(channel, eventLoopGroup); + } + + /** Destroys a {@code EventLoopHoldingChannel} instance. */ + @Override + public void close(Channel instanceChannel) { + checkNotNull(instanceChannel); + EventLoopHoldingChannel channel = (EventLoopHoldingChannel) instanceChannel; + channel.close(); + } + + @Override + public String toString() { + return "grpc-s2a-channel"; + } + } + + /** + * Manages a channel using a {@link ManagedChannel} instance that belong to the {@code + * EventLoopGroup} thread pool. + */ + @VisibleForTesting + static class EventLoopHoldingChannel extends Channel { + private final ManagedChannel delegate; + private final EventLoopGroup eventLoopGroup; + + static EventLoopHoldingChannel create(ManagedChannel delegate, EventLoopGroup eventLoopGroup) { + checkNotNull(delegate); + checkNotNull(eventLoopGroup); + return new EventLoopHoldingChannel(delegate, eventLoopGroup); + } + + private EventLoopHoldingChannel(ManagedChannel delegate, EventLoopGroup eventLoopGroup) { + this.delegate = delegate; + this.eventLoopGroup = eventLoopGroup; + } + + /** + * Returns the address of the service to which the {@code delegate} channel connects, which is + * typically of the form {@code host:port}. + */ + @Override + public String authority() { + return delegate.authority(); + } + + /** Creates a {@link ClientCall} that invokes the operations in {@link MethodDescriptor}. */ + @Override + public ClientCall newCall( + MethodDescriptor methodDescriptor, CallOptions options) { + return delegate.newCall(methodDescriptor, options); + } + + @SuppressWarnings("FutureReturnValueIgnored") + public void close() { + delegate.shutdownNow(); + boolean isDelegateTerminated; + try { + isDelegateTerminated = + delegate.awaitTermination(DELEGATE_TERMINATION_TIMEOUT.getSeconds(), SECONDS); + } catch (InterruptedException e) { + isDelegateTerminated = false; + } + long quietPeriodSeconds = isDelegateTerminated ? 0 : 1; + eventLoopGroup.shutdownGracefully( + quietPeriodSeconds, CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS); + } + } + + private S2AHandshakerServiceChannel() {} +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/ConnectionClosedException.java b/s2a/src/main/java/io/grpc/s2a/handshaker/ConnectionClosedException.java new file mode 100644 index 00000000000..1a7f86bda91 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/ConnectionClosedException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import java.io.IOException; + +/** Indicates that a connection has been closed. */ +@SuppressWarnings("serial") // This class is never serialized. +final class ConnectionClosedException extends IOException { + public ConnectionClosedException(String errorMessage) { + super(errorMessage); + } +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/GetAuthenticationMechanisms.java b/s2a/src/main/java/io/grpc/s2a/handshaker/GetAuthenticationMechanisms.java new file mode 100644 index 00000000000..56d74a9b766 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/GetAuthenticationMechanisms.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import com.google.errorprone.annotations.Immutable; +import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.handshaker.tokenmanager.AccessTokenManager; +import java.util.Optional; + +/** Retrieves the authentication mechanism for a given local identity. */ +@Immutable +final class GetAuthenticationMechanisms { + private static final Optional TOKEN_MANAGER = AccessTokenManager.create(); + + /** + * Retrieves the authentication mechanism for a given local identity. + * + * @param localIdentity the identity for which to fetch a token. + * @return an {@link AuthenticationMechanism} for the given local identity. + */ + static Optional getAuthMechanism(Optional localIdentity) { + if (!TOKEN_MANAGER.isPresent()) { + return Optional.empty(); + } + AccessTokenManager manager = TOKEN_MANAGER.get(); + // If no identity is provided, fetch the default access token and DO NOT attach an identity + // to the request. + if (!localIdentity.isPresent()) { + return Optional.of( + AuthenticationMechanism.newBuilder().setToken(manager.getDefaultToken()).build()); + } else { + // Fetch an access token for the provided identity. + return Optional.of( + AuthenticationMechanism.newBuilder() + .setIdentity(localIdentity.get().getIdentity()) + .setToken(manager.getToken(localIdentity.get())) + .build()); + } + } + + private GetAuthenticationMechanisms() {} +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java b/s2a/src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java new file mode 100644 index 00000000000..59e3931d9e6 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java @@ -0,0 +1,96 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import com.google.common.collect.ImmutableSet; + +/** Converts proto messages to Netty strings. */ +final class ProtoUtil { + /** + * Converts {@link Ciphersuite} to its {@link String} representation. + * + * @param ciphersuite the {@link Ciphersuite} to be converted. + * @return a {@link String} representing the ciphersuite. + * @throws AssertionError if the {@link Ciphersuite} is not one of the supported ciphersuites. + */ + static String convertCiphersuite(Ciphersuite ciphersuite) { + switch (ciphersuite) { + case CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: + return "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"; + case CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: + return "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"; + case CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: + return "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"; + case CIPHERSUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256: + return "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; + case CIPHERSUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384: + return "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"; + case CIPHERSUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: + return "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"; + default: + throw new AssertionError( + String.format("Ciphersuite %d is not supported.", ciphersuite.getNumber())); + } + } + + /** + * Converts a {@link TLSVersion} object to its {@link String} representation. + * + * @param tlsVersion the {@link TLSVersion} object to be converted. + * @return a {@link String} representation of the TLS version. + * @throws AssertionError if the {@code tlsVersion} is not one of the supported TLS versions. + */ + static String convertTlsProtocolVersion(TLSVersion tlsVersion) { + switch (tlsVersion) { + case TLS_VERSION_1_3: + return "TLSv1.3"; + case TLS_VERSION_1_2: + return "TLSv1.2"; + case TLS_VERSION_1_1: + return "TLSv1.1"; + case TLS_VERSION_1_0: + return "TLSv1"; + default: + throw new AssertionError( + String.format("TLS version %d is not supported.", tlsVersion.getNumber())); + } + } + + /** + * Builds a set of strings representing all {@link TLSVersion}s between {@code minTlsVersion} and + * {@code maxTlsVersion}. + */ + static ImmutableSet buildTlsProtocolVersionSet( + TLSVersion minTlsVersion, TLSVersion maxTlsVersion) { + ImmutableSet.Builder tlsVersions = ImmutableSet.builder(); + for (TLSVersion tlsVersion : TLSVersion.values()) { + int versionNumber; + try { + versionNumber = tlsVersion.getNumber(); + } catch (IllegalArgumentException e) { + continue; + } + if (versionNumber >= minTlsVersion.getNumber() + && versionNumber <= maxTlsVersion.getNumber()) { + tlsVersions.add(convertTlsProtocolVersion(tlsVersion)); + } + } + return tlsVersions.build(); + } + + private ProtoUtil() {} +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AConnectionException.java b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AConnectionException.java new file mode 100644 index 00000000000..d976308ad22 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AConnectionException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +/** Exception that denotes a runtime error that was encountered when talking to the S2A server. */ +@SuppressWarnings("serial") // This class is never serialized. +public class S2AConnectionException extends RuntimeException { + S2AConnectionException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AIdentity.java b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AIdentity.java new file mode 100644 index 00000000000..c4fed7377ac --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AIdentity.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.errorprone.annotations.ThreadSafe; + +/** + * Stores an identity in such a way that it can be sent to the S2A handshaker service. The identity + * may be formatted as a SPIFFE ID or as a hostname. + */ +@ThreadSafe +public final class S2AIdentity { + private final Identity identity; + + /** Returns an {@link S2AIdentity} instance with SPIFFE ID set to {@code spiffeId}. */ + public static S2AIdentity fromSpiffeId(String spiffeId) { + checkNotNull(spiffeId); + return new S2AIdentity(Identity.newBuilder().setSpiffeId(spiffeId).build()); + } + + /** Returns an {@link S2AIdentity} instance with hostname set to {@code hostname}. */ + public static S2AIdentity fromHostname(String hostname) { + checkNotNull(hostname); + return new S2AIdentity(Identity.newBuilder().setHostname(hostname).build()); + } + + /** Returns an {@link S2AIdentity} instance with UID set to {@code uid}. */ + public static S2AIdentity fromUid(String uid) { + checkNotNull(uid); + return new S2AIdentity(Identity.newBuilder().setUid(uid).build()); + } + + /** Returns an {@link S2AIdentity} instance with {@code identity} set. */ + public static S2AIdentity fromIdentity(Identity identity) { + return new S2AIdentity(identity == null ? Identity.getDefaultInstance() : identity); + } + + private S2AIdentity(Identity identity) { + this.identity = identity; + } + + /** Returns the proto {@link Identity} representation of this identity instance. */ + public Identity getIdentity() { + return identity; + } +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2APrivateKeyMethod.java b/s2a/src/main/java/io/grpc/s2a/handshaker/S2APrivateKeyMethod.java new file mode 100644 index 00000000000..fb6d5761355 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/S2APrivateKeyMethod.java @@ -0,0 +1,143 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.ByteString; +import io.grpc.s2a.handshaker.S2AIdentity; +import io.netty.handler.ssl.OpenSslPrivateKeyMethod; +import java.io.IOException; +import java.util.Optional; +import javax.annotation.concurrent.NotThreadSafe; +import javax.net.ssl.SSLEngine; + +/** + * Handles requests on signing bytes with a private key designated by {@code stub}. + * + *

This is done by sending the to-be-signed bytes to an S2A server (designated by {@code stub}) + * and read the signature from the server. + * + *

OpenSSL libraries must be appropriately initialized before using this class. One possible way + * to initialize OpenSSL library is to call {@code + * GrpcSslContexts.configure(SslContextBuilder.forClient());}. + */ +@NotThreadSafe +final class S2APrivateKeyMethod implements OpenSslPrivateKeyMethod { + private final S2AStub stub; + private final Optional localIdentity; + private static final ImmutableMap + OPENSSL_TO_S2A_SIGNATURE_ALGORITHM_MAP = + ImmutableMap.of( + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256, + SignatureAlgorithm.S2A_SSL_SIGN_RSA_PKCS1_SHA256, + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA384, + SignatureAlgorithm.S2A_SSL_SIGN_RSA_PKCS1_SHA384, + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA512, + SignatureAlgorithm.S2A_SSL_SIGN_RSA_PKCS1_SHA512, + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256, + SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP256R1_SHA256, + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP384R1_SHA384, + SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP384R1_SHA384, + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP521R1_SHA512, + SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP521R1_SHA512, + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256, + SignatureAlgorithm.S2A_SSL_SIGN_RSA_PSS_RSAE_SHA256, + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA384, + SignatureAlgorithm.S2A_SSL_SIGN_RSA_PSS_RSAE_SHA384, + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA512, + SignatureAlgorithm.S2A_SSL_SIGN_RSA_PSS_RSAE_SHA512); + + public static S2APrivateKeyMethod create(S2AStub stub, Optional localIdentity) { + checkNotNull(stub); + return new S2APrivateKeyMethod(stub, localIdentity); + } + + private S2APrivateKeyMethod(S2AStub stub, Optional localIdentity) { + this.stub = stub; + this.localIdentity = localIdentity; + } + + /** + * Converts the signature algorithm to an enum understood by S2A. + * + * @param signatureAlgorithm the int representation of the signature algorithm define by {@code + * OpenSslPrivateKeyMethod}. + * @return the signature algorithm enum defined by S2A proto. + * @throws UnsupportedOperationException if the algorithm is not supported by S2A. + */ + @VisibleForTesting + static SignatureAlgorithm convertOpenSslSignAlgToS2ASignAlg(int signatureAlgorithm) { + SignatureAlgorithm sig = OPENSSL_TO_S2A_SIGNATURE_ALGORITHM_MAP.get(signatureAlgorithm); + if (sig == null) { + throw new UnsupportedOperationException( + String.format("Signature Algorithm %d is not supported.", signatureAlgorithm)); + } + return sig; + } + + /** + * Signs the input bytes by sending the request to the S2A srever. + * + * @param engine not used. + * @param signatureAlgorithm the {@link OpenSslPrivateKeyMethod}'s signature algorithm + * representation + * @param input the bytes to be signed. + * @return the signature of the {@code input}. + * @throws IOException if the connection to the S2A server is corrupted. + * @throws InterruptedException if the connection to the S2A server is interrupted. + * @throws S2AConnectionException if the response from the S2A server does not contain valid data. + */ + @Override + public byte[] sign(SSLEngine engine, int signatureAlgorithm, byte[] input) + throws IOException, InterruptedException { + checkArgument(input.length > 0, "No bytes to sign."); + SignatureAlgorithm s2aSignatureAlgorithm = + convertOpenSslSignAlgToS2ASignAlg(signatureAlgorithm); + SessionReq.Builder reqBuilder = + SessionReq.newBuilder() + .setOffloadPrivateKeyOperationReq( + OffloadPrivateKeyOperationReq.newBuilder() + .setOperation(OffloadPrivateKeyOperationReq.PrivateKeyOperation.SIGN) + .setSignatureAlgorithm(s2aSignatureAlgorithm) + .setRawBytes(ByteString.copyFrom(input))); + if (localIdentity.isPresent()) { + reqBuilder.setLocalIdentity(localIdentity.get().getIdentity()); + } + + SessionResp resp = stub.send(reqBuilder.build()); + + if (resp.hasStatus() && resp.getStatus().getCode() != 0) { + throw new S2AConnectionException( + String.format( + "Error occurred in response from S2A, error code: %d, error message: \"%s\".", + resp.getStatus().getCode(), resp.getStatus().getDetails())); + } + if (!resp.hasOffloadPrivateKeyOperationResp()) { + throw new S2AConnectionException("No valid response received from S2A."); + } + return resp.getOffloadPrivateKeyOperationResp().getOutBytes().toByteArray(); + } + + @Override + public byte[] decrypt(SSLEngine engine, byte[] input) { + throw new UnsupportedOperationException("decrypt is not supported."); + } +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java new file mode 100644 index 00000000000..25d1e325ea8 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java @@ -0,0 +1,249 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HostAndPort; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.errorprone.annotations.ThreadSafe; +import io.grpc.Channel; +import io.grpc.internal.ObjectPool; +import io.grpc.netty.GrpcHttp2ConnectionHandler; +import io.grpc.netty.InternalProtocolNegotiator; +import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; +import io.grpc.netty.InternalProtocolNegotiators; +import io.grpc.netty.InternalProtocolNegotiators.ProtocolNegotiationHandler; +import io.grpc.s2a.channel.S2AChannelPool; +import io.grpc.s2a.channel.S2AGrpcChannelPool; +import io.grpc.s2a.handshaker.S2AIdentity; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.ssl.SslContext; +import io.netty.util.AsciiString; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executors; +import javax.annotation.Nullable; + +/** Factory for performing negotiation of a secure channel using the S2A. */ +@ThreadSafe +public final class S2AProtocolNegotiatorFactory { + @VisibleForTesting static final int DEFAULT_PORT = 443; + private static final AsciiString SCHEME = AsciiString.of("https"); + + /** + * Creates a {@code S2AProtocolNegotiatorFactory} configured for a client to establish secure + * connections using the S2A. + * + * @param localIdentity the identity of the client; if none is provided, the S2A will use the + * client's default identity. + * @param s2aChannelPool a pool of shared channels that can be used to connect to the S2A. + * @return a factory for creating a client-side protocol negotiator. + */ + public static InternalProtocolNegotiator.ClientFactory createClientFactory( + @Nullable S2AIdentity localIdentity, ObjectPool s2aChannelPool) { + checkNotNull(s2aChannelPool, "S2A channel pool should not be null."); + S2AChannelPool channelPool = S2AGrpcChannelPool.create(s2aChannelPool); + return new S2AClientProtocolNegotiatorFactory(localIdentity, channelPool); + } + + static final class S2AClientProtocolNegotiatorFactory + implements InternalProtocolNegotiator.ClientFactory { + private final @Nullable S2AIdentity localIdentity; + private final S2AChannelPool channelPool; + + S2AClientProtocolNegotiatorFactory( + @Nullable S2AIdentity localIdentity, S2AChannelPool channelPool) { + this.localIdentity = localIdentity; + this.channelPool = channelPool; + } + + @Override + public ProtocolNegotiator newNegotiator() { + return S2AProtocolNegotiator.createForClient(channelPool, localIdentity); + } + + @Override + public int getDefaultPort() { + return DEFAULT_PORT; + } + } + + /** Negotiates the TLS handshake using S2A. */ + @VisibleForTesting + static final class S2AProtocolNegotiator implements ProtocolNegotiator { + + private final S2AChannelPool channelPool; + private final Optional localIdentity; + private final ListeningExecutorService service = + MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)); + + static S2AProtocolNegotiator createForClient( + S2AChannelPool channelPool, @Nullable S2AIdentity localIdentity) { + checkNotNull(channelPool, "Channel pool should not be null."); + if (localIdentity == null) { + return new S2AProtocolNegotiator(channelPool, Optional.empty()); + } else { + return new S2AProtocolNegotiator(channelPool, Optional.of(localIdentity)); + } + } + + @VisibleForTesting + static @Nullable String getHostNameFromAuthority(@Nullable String authority) { + if (authority == null) { + return null; + } + return HostAndPort.fromString(authority).getHost(); + } + + private S2AProtocolNegotiator(S2AChannelPool channelPool, Optional localIdentity) { + this.channelPool = channelPool; + this.localIdentity = localIdentity; + } + + @Override + public AsciiString scheme() { + return SCHEME; + } + + @Override + public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { + checkNotNull(grpcHandler, "grpcHandler should not be null."); + String hostname = getHostNameFromAuthority(grpcHandler.getAuthority()); + checkArgument(!isNullOrEmpty(hostname), "hostname should not be null or empty."); + return new S2AProtocolNegotiationHandler( + grpcHandler, channelPool, localIdentity, hostname, service); + } + + @Override + public void close() { + service.shutdown(); + channelPool.close(); + } + } + + @VisibleForTesting + static class BufferReadsHandler extends ChannelInboundHandlerAdapter { + private final List reads = new ArrayList<>(); + private boolean readComplete; + + public List getReads() { + return reads; + } + + @Override + public void channelRead(ChannelHandlerContext unused, Object msg) { + reads.add(msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext unused) { + readComplete = true; + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + for (Object msg : reads) { + super.channelRead(ctx, msg); + } + if (readComplete) { + super.channelReadComplete(ctx); + } + } + } + + private static final class S2AProtocolNegotiationHandler extends ProtocolNegotiationHandler { + private final S2AChannelPool channelPool; + private final Optional localIdentity; + private final String hostname; + private final GrpcHttp2ConnectionHandler grpcHandler; + private final ListeningExecutorService service; + + private S2AProtocolNegotiationHandler( + GrpcHttp2ConnectionHandler grpcHandler, + S2AChannelPool channelPool, + Optional localIdentity, + String hostname, + ListeningExecutorService service) { + super( + // superclass (InternalProtocolNegotiators.ProtocolNegotiationHandler) expects 'next' + // handler but we don't have a next handler _yet_. So we "disable" superclass's behavior + // here and then manually add 'next' when we call fireProtocolNegotiationEvent() + new ChannelHandlerAdapter() { + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + ctx.pipeline().remove(this); + } + }, + grpcHandler.getNegotiationLogger()); + this.grpcHandler = grpcHandler; + this.channelPool = channelPool; + this.localIdentity = localIdentity; + this.hostname = hostname; + checkNotNull(service, "service should not be null."); + this.service = service; + } + + @Override + protected void handlerAdded0(ChannelHandlerContext ctx) { + // Buffer all reads until the TLS Handler is added. + BufferReadsHandler bufferReads = new BufferReadsHandler(); + ctx.pipeline().addBefore(ctx.name(), /* name= */ null, bufferReads); + + Channel ch = channelPool.getChannel(); + S2AServiceGrpc.S2AServiceStub stub = S2AServiceGrpc.newStub(ch); + S2AStub s2aStub = S2AStub.newInstance(stub); + + ListenableFuture sslContextFuture = + service.submit(() -> SslContextFactory.createForClient(s2aStub, hostname, localIdentity)); + Futures.addCallback( + sslContextFuture, + new FutureCallback() { + @Override + public void onSuccess(SslContext sslContext) { + ChannelHandler handler = + InternalProtocolNegotiators.tls(sslContext).newHandler(grpcHandler); + + // Remove the bufferReads handler and delegate the rest of the handshake to the TLS + // handler. + ctx.pipeline().addAfter(ctx.name(), /* name= */ null, handler); + fireProtocolNegotiationEvent(ctx); + ctx.pipeline().remove(bufferReads); + } + + @Override + public void onFailure(Throwable t) { + ctx.fireExceptionCaught(t); + } + }, + service); + } + } + + private S2AProtocolNegotiatorFactory() {} +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java new file mode 100644 index 00000000000..8249ca59d09 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java @@ -0,0 +1,221 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Verify.verify; +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.google.common.annotations.VisibleForTesting; +import io.grpc.stub.StreamObserver; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.concurrent.NotThreadSafe; + +/** Reads and writes messages to and from the S2A. */ +@NotThreadSafe +class S2AStub implements AutoCloseable { + private static final Logger logger = Logger.getLogger(S2AStub.class.getName()); + private static final long HANDSHAKE_RPC_DEADLINE_SECS = 20; + private final StreamObserver reader = new Reader(); + private final BlockingQueue responses = new ArrayBlockingQueue<>(10); + private S2AServiceGrpc.S2AServiceStub serviceStub; + private StreamObserver writer; + private boolean doneReading = false; + private boolean doneWriting = false; + + static S2AStub newInstance(S2AServiceGrpc.S2AServiceStub serviceStub) { + checkNotNull(serviceStub); + return new S2AStub(serviceStub); + } + + @VisibleForTesting + static S2AStub newInstanceForTesting(StreamObserver writer) { + checkNotNull(writer); + return new S2AStub(writer); + } + + private S2AStub(S2AServiceGrpc.S2AServiceStub serviceStub) { + this.serviceStub = serviceStub; + } + + private S2AStub(StreamObserver writer) { + this.writer = writer; + } + + @VisibleForTesting + StreamObserver getReader() { + return reader; + } + + @VisibleForTesting + BlockingQueue getResponses() { + return responses; + } + + /** + * Sends a request and returns the response. Caller must wait until this method executes prior to + * calling it again. If this method throws {@code ConnectionClosedException}, then it should not + * be called again, and both {@code reader} and {@code writer} are closed. + * + * @param req the {@code SessionReq} message to be sent to the S2A server. + * @return the {@code SessionResp} message received from the S2A server. + * @throws ConnectionClosedException if {@code reader} or {@code writer} calls their {@code + * onCompleted} method. + * @throws IOException if an unexpected response is received, or if the {@code reader} or {@code + * writer} calls their {@code onError} method. + */ + public SessionResp send(SessionReq req) throws IOException, InterruptedException { + if (doneWriting && doneReading) { + logger.log(Level.INFO, "Stream to the S2A is closed."); + throw new ConnectionClosedException("Stream to the S2A is closed."); + } + createWriterIfNull(); + if (!responses.isEmpty()) { + IOException exception = null; + SessionResp resp = null; + try { + resp = responses.take().getResultOrThrow(); + } catch (IOException e) { + exception = e; + } + responses.clear(); + if (exception != null) { + throw new IOException( + "Received an unexpected response from a host at the S2A's address. The S2A might be" + + " unavailable." + + exception.getMessage()); + } + return resp; + } + try { + writer.onNext(req); + } catch (RuntimeException e) { + writer.onError(e); + responses.offer(Result.createWithThrowable(e)); + } + try { + return responses.take().getResultOrThrow(); + } catch (ConnectionClosedException e) { + // A ConnectionClosedException is thrown by getResultOrThrow when reader calls its + // onCompleted method. The close method is called to also close the writer, and then the + // ConnectionClosedException is re-thrown in order to indicate to the caller that send + // should not be called again. + close(); + throw e; + } + } + + @Override + public void close() { + if (doneWriting && doneReading) { + return; + } + verify(!doneWriting); + doneReading = true; + doneWriting = true; + if (writer != null) { + writer.onCompleted(); + } + } + + /** Create a new writer if the writer is null. */ + private void createWriterIfNull() { + if (writer == null) { + writer = + serviceStub + .withWaitForReady() + .withDeadlineAfter(HANDSHAKE_RPC_DEADLINE_SECS, SECONDS) + .setUpSession(reader); + } + } + + private class Reader implements StreamObserver { + /** + * Places a {@code SessionResp} message in the {@code responses} queue, or an {@code + * IOException} if reading is complete. + * + * @param resp the {@code SessionResp} message received from the S2A handshaker module. + */ + @Override + public void onNext(SessionResp resp) { + verify(!doneReading); + responses.offer(Result.createWithResponse(resp)); + } + + /** + * Places a {@code Throwable} in the {@code responses} queue. + * + * @param t the {@code Throwable} caught when reading the stream to the S2A handshaker module. + */ + @Override + public void onError(Throwable t) { + responses.offer(Result.createWithThrowable(t)); + } + + /** + * Sets {@code doneReading} to true, and places a {@code ConnectionClosedException} in the + * {@code responses} queue. + */ + @Override + public void onCompleted() { + logger.log(Level.INFO, "Reading from the S2A is complete."); + doneReading = true; + responses.offer( + Result.createWithThrowable( + new ConnectionClosedException("Reading from the S2A is complete."))); + } + } + + private static final class Result { + private final Optional response; + private final Optional throwable; + + static Result createWithResponse(SessionResp response) { + return new Result(Optional.of(response), Optional.empty()); + } + + static Result createWithThrowable(Throwable throwable) { + return new Result(Optional.empty(), Optional.of(throwable)); + } + + private Result(Optional response, Optional throwable) { + checkArgument(response.isPresent() != throwable.isPresent()); + this.response = response; + this.throwable = throwable; + } + + /** Throws {@code throwable} if present, and returns {@code response} otherwise. */ + SessionResp getResultOrThrow() throws IOException { + if (throwable.isPresent()) { + if (throwable.get() instanceof ConnectionClosedException) { + ConnectionClosedException exception = (ConnectionClosedException) throwable.get(); + throw exception; + } else { + throw new IOException(throwable.get()); + } + } + verify(response.isPresent()); + return response.get(); + } + } +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java b/s2a/src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java new file mode 100644 index 00000000000..fb113bb29cc --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java @@ -0,0 +1,152 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.protobuf.ByteString; +import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.handshaker.ValidatePeerCertificateChainReq.VerificationMode; +import java.io.IOException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Optional; +import javax.annotation.concurrent.NotThreadSafe; +import javax.net.ssl.X509TrustManager; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Offloads verification of the peer certificate chain to S2A. */ +@NotThreadSafe +final class S2ATrustManager implements X509TrustManager { + private final Optional localIdentity; + private final S2AStub stub; + private final String hostname; + + static S2ATrustManager createForClient( + S2AStub stub, String hostname, Optional localIdentity) { + checkNotNull(stub); + checkNotNull(hostname); + return new S2ATrustManager(stub, hostname, localIdentity); + } + + private S2ATrustManager(S2AStub stub, String hostname, Optional localIdentity) { + this.stub = stub; + this.hostname = hostname; + this.localIdentity = localIdentity; + } + + /** + * Validates the given certificate chain provided by the peer. + * + * @param chain the peer certificate chain + * @param authType the authentication type based on the client certificate + * @throws IllegalArgumentException if null or zero-length chain is passed in for the chain + * parameter. + * @throws CertificateException if the certificate chain is not trusted by this TrustManager. + */ + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + checkPeerTrusted(chain, /* isCheckingClientCertificateChain= */ true); + } + + /** + * Validates the given certificate chain provided by the peer. + * + * @param chain the peer certificate chain + * @param authType the authentication type based on the client certificate + * @throws IllegalArgumentException if null or zero-length chain is passed in for the chain + * parameter. + * @throws CertificateException if the certificate chain is not trusted by this TrustManager. + */ + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + checkPeerTrusted(chain, /* isCheckingClientCertificateChain= */ false); + } + + /** + * Returns null because the accepted issuers are held in S2A and this class receives decision made + * from S2A on the fly about which to use to verify a given chain. + * + * @return null. + */ + @Override + public X509Certificate @Nullable [] getAcceptedIssuers() { + return null; + } + + private void checkPeerTrusted(X509Certificate[] chain, boolean isCheckingClientCertificateChain) + throws CertificateException { + checkNotNull(chain); + checkArgument(chain.length > 0, "Certificate chain has zero certificates."); + + ValidatePeerCertificateChainReq.Builder validatePeerCertificateChainReq = + ValidatePeerCertificateChainReq.newBuilder().setMode(VerificationMode.UNSPECIFIED); + if (isCheckingClientCertificateChain) { + validatePeerCertificateChainReq.setClientPeer( + ValidatePeerCertificateChainReq.ClientPeer.newBuilder() + .addAllCertificateChain(certificateChainToDerChain(chain))); + } else { + validatePeerCertificateChainReq.setServerPeer( + ValidatePeerCertificateChainReq.ServerPeer.newBuilder() + .addAllCertificateChain(certificateChainToDerChain(chain)) + .setServerHostname(hostname)); + } + + SessionReq.Builder reqBuilder = + SessionReq.newBuilder().setValidatePeerCertificateChainReq(validatePeerCertificateChainReq); + if (localIdentity.isPresent()) { + reqBuilder.setLocalIdentity(localIdentity.get().getIdentity()); + } + + SessionResp resp; + try { + resp = stub.send(reqBuilder.build()); + } catch (IOException | InterruptedException e) { + throw new CertificateException("Failed to send request to S2A.", e); + } + if (resp.hasStatus() && resp.getStatus().getCode() != 0) { + throw new CertificateException( + String.format( + "Error occurred in response from S2A, error code: %d, error message: %s.", + resp.getStatus().getCode(), resp.getStatus().getDetails())); + } + + if (!resp.hasValidatePeerCertificateChainResp()) { + throw new CertificateException("No valid response received from S2A."); + } + + ValidatePeerCertificateChainResp validationResult = resp.getValidatePeerCertificateChainResp(); + if (validationResult.getValidationResult() + != ValidatePeerCertificateChainResp.ValidationResult.SUCCESS) { + throw new CertificateException(validationResult.getValidationDetails()); + } + } + + private static ImmutableList certificateChainToDerChain(X509Certificate[] chain) + throws CertificateEncodingException { + ImmutableList.Builder derChain = ImmutableList.builder(); + for (X509Certificate certificate : chain) { + derChain.add(ByteString.copyFrom(certificate.getEncoded())); + } + return derChain.build(); + } +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/SslContextFactory.java b/s2a/src/main/java/io/grpc/s2a/handshaker/SslContextFactory.java new file mode 100644 index 00000000000..1ac5887ebc4 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/SslContextFactory.java @@ -0,0 +1,178 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableSet; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.s2a.handshaker.S2AIdentity; +import io.netty.handler.ssl.OpenSslContextOption; +import io.netty.handler.ssl.OpenSslSessionContext; +import io.netty.handler.ssl.OpenSslX509KeyManagerFactory; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Optional; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLSessionContext; + +/** Creates {@link SslContext} objects with TLS configurations from S2A server. */ +final class SslContextFactory { + + /** + * Creates {@link SslContext} objects for client with TLS configurations from S2A server. + * + * @param stub the {@link S2AStub} to talk to the S2A server. + * @param targetName the {@link String} of the server that this client makes connection to. + * @param localIdentity the {@link S2AIdentity} that should be used when talking to S2A server. + * Will use default identity if empty. + * @return a {@link SslContext} object. + * @throws NullPointerException if either {@code stub} or {@code targetName} is null. + * @throws IOException if an unexpected response from S2A server is received. + * @throws InterruptedException if {@code stub} is closed. + */ + static SslContext createForClient( + S2AStub stub, String targetName, Optional localIdentity) + throws IOException, + InterruptedException, + CertificateException, + KeyStoreException, + NoSuchAlgorithmException, + UnrecoverableKeyException, + GeneralSecurityException { + checkNotNull(stub, "stub should not be null."); + checkNotNull(targetName, "targetName should not be null on client side."); + GetTlsConfigurationResp.ClientTlsConfiguration clientTlsConfiguration; + try { + clientTlsConfiguration = getClientTlsConfigurationFromS2A(stub, localIdentity); + } catch (IOException | InterruptedException e) { + throw new GeneralSecurityException("Failed to get client TLS configuration from S2A.", e); + } + + // Use the default value for timeout. + // Use the smallest possible value for cache size. + // The Provider is by default OPENSSL. No need to manually set it. + SslContextBuilder sslContextBuilder = + GrpcSslContexts.configure(SslContextBuilder.forClient()) + .sessionCacheSize(1) + .sessionTimeout(0); + + configureSslContextWithClientTlsConfiguration(clientTlsConfiguration, sslContextBuilder); + sslContextBuilder.trustManager( + S2ATrustManager.createForClient(stub, targetName, localIdentity)); + sslContextBuilder.option( + OpenSslContextOption.PRIVATE_KEY_METHOD, S2APrivateKeyMethod.create(stub, localIdentity)); + + SslContext sslContext = sslContextBuilder.build(); + SSLSessionContext sslSessionContext = sslContext.sessionContext(); + if (sslSessionContext instanceof OpenSslSessionContext) { + OpenSslSessionContext openSslSessionContext = (OpenSslSessionContext) sslSessionContext; + openSslSessionContext.setSessionCacheEnabled(false); + } + + return sslContext; + } + + private static GetTlsConfigurationResp.ClientTlsConfiguration getClientTlsConfigurationFromS2A( + S2AStub stub, Optional localIdentity) throws IOException, InterruptedException { + checkNotNull(stub, "stub should not be null."); + SessionReq.Builder reqBuilder = SessionReq.newBuilder(); + if (localIdentity.isPresent()) { + reqBuilder.setLocalIdentity(localIdentity.get().getIdentity()); + } + Optional authMechanism = + GetAuthenticationMechanisms.getAuthMechanism(localIdentity); + if (authMechanism.isPresent()) { + reqBuilder.addAuthenticationMechanisms(authMechanism.get()); + } + SessionResp resp = + stub.send( + reqBuilder + .setGetTlsConfigurationReq( + GetTlsConfigurationReq.newBuilder() + .setConnectionSide(ConnectionSide.CONNECTION_SIDE_CLIENT)) + .build()); + if (resp.hasStatus() && resp.getStatus().getCode() != 0) { + throw new S2AConnectionException( + String.format( + "response from S2A server has ean error %d with error message %s.", + resp.getStatus().getCode(), resp.getStatus().getDetails())); + } + if (!resp.getGetTlsConfigurationResp().hasClientTlsConfiguration()) { + throw new S2AConnectionException( + "Response from S2A server does NOT contain ClientTlsConfiguration."); + } + return resp.getGetTlsConfigurationResp().getClientTlsConfiguration(); + } + + private static void configureSslContextWithClientTlsConfiguration( + GetTlsConfigurationResp.ClientTlsConfiguration clientTlsConfiguration, + SslContextBuilder sslContextBuilder) + throws CertificateException, + IOException, + KeyStoreException, + NoSuchAlgorithmException, + UnrecoverableKeyException { + sslContextBuilder.keyManager(createKeylessManager(clientTlsConfiguration)); + ImmutableSet tlsVersions = + ProtoUtil.buildTlsProtocolVersionSet( + clientTlsConfiguration.getMinTlsVersion(), clientTlsConfiguration.getMaxTlsVersion()); + if (tlsVersions.isEmpty()) { + throw new S2AConnectionException("Set of TLS versions received from S2A server is empty."); + } + sslContextBuilder.protocols(tlsVersions); + } + + private static KeyManager createKeylessManager( + GetTlsConfigurationResp.ClientTlsConfiguration clientTlsConfiguration) + throws CertificateException, + IOException, + KeyStoreException, + NoSuchAlgorithmException, + UnrecoverableKeyException { + X509Certificate[] certificates = + new X509Certificate[clientTlsConfiguration.getCertificateChainCount()]; + for (int i = 0; i < clientTlsConfiguration.getCertificateChainCount(); ++i) { + certificates[i] = convertStringToX509Cert(clientTlsConfiguration.getCertificateChain(i)); + } + KeyManager[] keyManagers = + OpenSslX509KeyManagerFactory.newKeyless(certificates).getKeyManagers(); + if (keyManagers == null || keyManagers.length == 0) { + throw new IllegalStateException("No key managers created."); + } + return keyManagers[0]; + } + + private static X509Certificate convertStringToX509Cert(String certificate) + throws CertificateException { + return (X509Certificate) + CertificateFactory.getInstance("X509") + .generateCertificate(new ByteArrayInputStream(certificate.getBytes(UTF_8))); + } + + private SslContextFactory() {} +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java b/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java new file mode 100644 index 00000000000..94549d11c87 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker.tokenmanager; + +import io.grpc.s2a.handshaker.S2AIdentity; +import java.lang.reflect.Method; +import java.util.Optional; +import javax.annotation.concurrent.ThreadSafe; + +/** Manages access tokens for authenticating to the S2A. */ +@ThreadSafe +public final class AccessTokenManager { + private final TokenFetcher tokenFetcher; + + /** Creates an {@code AccessTokenManager} based on the environment where the application runs. */ + @SuppressWarnings("RethrowReflectiveOperationExceptionAsLinkageError") + public static Optional create() { + Optional tokenFetcher; + try { + Class singleTokenFetcherClass = + Class.forName("io.grpc.s2a.handshaker.tokenmanager.SingleTokenFetcher"); + Method createTokenFetcher = singleTokenFetcherClass.getMethod("create"); + tokenFetcher = (Optional) createTokenFetcher.invoke(null); + } catch (ClassNotFoundException e) { + tokenFetcher = Optional.empty(); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + return tokenFetcher.isPresent() + ? Optional.of(new AccessTokenManager((TokenFetcher) tokenFetcher.get())) + : Optional.empty(); + } + + private AccessTokenManager(TokenFetcher tokenFetcher) { + this.tokenFetcher = tokenFetcher; + } + + /** Returns an access token when no identity is specified. */ + public String getDefaultToken() { + return tokenFetcher.getDefaultToken(); + } + + /** Returns an access token for the given identity. */ + public String getToken(S2AIdentity identity) { + return tokenFetcher.getToken(identity); + } +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenFetcher.java b/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenFetcher.java new file mode 100644 index 00000000000..c3dffd2b715 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenFetcher.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker.tokenmanager; + +import com.google.common.annotations.VisibleForTesting; +import io.grpc.s2a.handshaker.S2AIdentity; +import java.util.Optional; + +/** Fetches a single access token via an environment variable. */ +@SuppressWarnings("NonFinalStaticField") +public final class SingleTokenFetcher implements TokenFetcher { + private static final String ENVIRONMENT_VARIABLE = "S2A_ACCESS_TOKEN"; + private static String accessToken = System.getenv(ENVIRONMENT_VARIABLE); + + private final String token; + + /** + * Creates a {@code SingleTokenFetcher} from {@code ENVIRONMENT_VARIABLE}, and returns an empty + * {@code Optional} instance if the token could not be fetched. + */ + public static Optional create() { + return Optional.ofNullable(accessToken).map(SingleTokenFetcher::new); + } + + @VisibleForTesting + public static void setAccessToken(String token) { + accessToken = token; + } + + private SingleTokenFetcher(String token) { + this.token = token; + } + + @Override + public String getDefaultToken() { + return token; + } + + @Override + public String getToken(S2AIdentity identity) { + return token; + } +} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/TokenFetcher.java b/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/TokenFetcher.java new file mode 100644 index 00000000000..9eeddaad844 --- /dev/null +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/TokenFetcher.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker.tokenmanager; + +import io.grpc.s2a.handshaker.S2AIdentity; + +/** Fetches tokens used to authenticate to S2A. */ +interface TokenFetcher { + /** Returns an access token when no identity is specified. */ + String getDefaultToken(); + + /** Returns an access token for the given identity. */ + String getToken(S2AIdentity identity); +} \ No newline at end of file diff --git a/s2a/src/main/proto/grpc/gcp/s2a/common.proto b/s2a/src/main/proto/grpc/gcp/s2a/common.proto new file mode 100644 index 00000000000..749739553dd --- /dev/null +++ b/s2a/src/main/proto/grpc/gcp/s2a/common.proto @@ -0,0 +1,82 @@ +// Copyright 2024 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/s2a/common.proto + +syntax = "proto3"; + +package grpc.gcp.s2a; + +option java_multiple_files = true; +option java_outer_classname = "CommonProto"; +option java_package = "io.grpc.s2a.handshaker"; + +// The TLS 1.0-1.2 ciphersuites that the application can negotiate when using +// S2A. +enum Ciphersuite { + CIPHERSUITE_UNSPECIFIED = 0; + CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 1; + CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 2; + CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 3; + CIPHERSUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 4; + CIPHERSUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 5; + CIPHERSUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 6; +} + +// The TLS versions supported by S2A's handshaker module. +enum TLSVersion { + TLS_VERSION_UNSPECIFIED = 0; + TLS_VERSION_1_0 = 1; + TLS_VERSION_1_1 = 2; + TLS_VERSION_1_2 = 3; + TLS_VERSION_1_3 = 4; +} + +// The side in the TLS connection. +enum ConnectionSide { + CONNECTION_SIDE_UNSPECIFIED = 0; + CONNECTION_SIDE_CLIENT = 1; + CONNECTION_SIDE_SERVER = 2; +} + +// The ALPN protocols that the application can negotiate during a TLS handshake. +enum AlpnProtocol { + ALPN_PROTOCOL_UNSPECIFIED = 0; + ALPN_PROTOCOL_GRPC = 1; + ALPN_PROTOCOL_HTTP2 = 2; + ALPN_PROTOCOL_HTTP1_1 = 3; +} + +message Identity { + oneof identity_oneof { + // The SPIFFE ID of a connection endpoint. + string spiffe_id = 1; + + // The hostname of a connection endpoint. + string hostname = 2; + + // The UID of a connection endpoint. + string uid = 4; + + // The username of a connection endpoint. + string username = 5; + + // The GCP ID of a connection endpoint. + string gcp_id = 6; + } + + // Additional identity-specific attributes. + map attributes = 3; +} diff --git a/s2a/src/main/proto/grpc/gcp/s2a/s2a.proto b/s2a/src/main/proto/grpc/gcp/s2a/s2a.proto new file mode 100644 index 00000000000..8a85e348c24 --- /dev/null +++ b/s2a/src/main/proto/grpc/gcp/s2a/s2a.proto @@ -0,0 +1,369 @@ +// Copyright 2024 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/s2a/s2a.proto + +syntax = "proto3"; + +package grpc.gcp.s2a; + +import "grpc/gcp/s2a/common.proto"; +import "grpc/gcp/s2a/s2a_context.proto"; + +option java_multiple_files = true; +option java_outer_classname = "S2AProto"; +option java_package = "io.grpc.s2a.handshaker"; + +enum SignatureAlgorithm { + S2A_SSL_SIGN_UNSPECIFIED = 0; + // RSA Public-Key Cryptography Standards #1. + S2A_SSL_SIGN_RSA_PKCS1_SHA256 = 1; + S2A_SSL_SIGN_RSA_PKCS1_SHA384 = 2; + S2A_SSL_SIGN_RSA_PKCS1_SHA512 = 3; + // ECDSA. + S2A_SSL_SIGN_ECDSA_SECP256R1_SHA256 = 4; + S2A_SSL_SIGN_ECDSA_SECP384R1_SHA384 = 5; + S2A_SSL_SIGN_ECDSA_SECP521R1_SHA512 = 6; + // RSA Probabilistic Signature Scheme. + S2A_SSL_SIGN_RSA_PSS_RSAE_SHA256 = 7; + S2A_SSL_SIGN_RSA_PSS_RSAE_SHA384 = 8; + S2A_SSL_SIGN_RSA_PSS_RSAE_SHA512 = 9; + // ED25519. + S2A_SSL_SIGN_ED25519 = 10; +} + +message AlpnPolicy { + // If true, the application MUST perform ALPN negotiation. + bool enable_alpn_negotiation = 1; + + // The ordered list of ALPN protocols that specify how the application SHOULD + // negotiate ALPN during the TLS handshake. + // + // The application MAY ignore any ALPN protocols in this list that are not + // supported by the application. + repeated AlpnProtocol alpn_protocols = 2; +} + +message AuthenticationMechanism { + // Applications may specify an identity associated to an authentication + // mechanism. Otherwise, S2A assumes that the authentication mechanism is + // associated with the default identity. If the default identity cannot be + // determined, the request is rejected. + Identity identity = 1; + + oneof mechanism_oneof { + // A token that the application uses to authenticate itself to S2A. + string token = 2; + } +} + +message Status { + // The status code that is specific to the application and the implementation + // of S2A, e.g., gRPC status code. + uint32 code = 1; + + // The status details. + string details = 2; +} + +message GetTlsConfigurationReq { + // The role of the application in the TLS connection. + ConnectionSide connection_side = 1; + + // The server name indication (SNI) extension, which MAY be populated when a + // server is offloading to S2A. The SNI is used to determine the server + // identity if the local identity in the request is empty. + string sni = 2; +} + +message GetTlsConfigurationResp { + // Next ID: 8 + message ClientTlsConfiguration { + reserved 4, 5; + + // The certificate chain that the client MUST use for the TLS handshake. + // It's a list of PEM-encoded certificates, ordered from leaf to root, + // excluding the root. + repeated string certificate_chain = 1; + + // The minimum TLS version number that the client MUST use for the TLS + // handshake. If this field is not provided, the client MUST use the default + // minimum version of the client's TLS library. + TLSVersion min_tls_version = 2; + + // The maximum TLS version number that the client MUST use for the TLS + // handshake. If this field is not provided, the client MUST use the default + // maximum version of the client's TLS library. + TLSVersion max_tls_version = 3; + + // The ordered list of TLS 1.0-1.2 ciphersuites that the client MAY offer to + // negotiate in the TLS handshake. + repeated Ciphersuite ciphersuites = 6; + + // The policy that dictates how the client negotiates ALPN during the TLS + // handshake. + AlpnPolicy alpn_policy = 7; + } + + // Next ID: 12 + message ServerTlsConfiguration { + reserved 4, 5; + + enum RequestClientCertificate { + UNSPECIFIED = 0; + DONT_REQUEST_CLIENT_CERTIFICATE = 1; + REQUEST_CLIENT_CERTIFICATE_BUT_DONT_VERIFY = 2; + REQUEST_CLIENT_CERTIFICATE_AND_VERIFY = 3; + REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_BUT_DONT_VERIFY = 4; + REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY = 5; + } + + // The certificate chain that the server MUST use for the TLS handshake. + // It's a list of PEM-encoded certificates, ordered from leaf to root, + // excluding the root. + repeated string certificate_chain = 1; + + // The minimum TLS version number that the server MUST use for the TLS + // handshake. If this field is not provided, the server MUST use the default + // minimum version of the server's TLS library. + TLSVersion min_tls_version = 2; + + // The maximum TLS version number that the server MUST use for the TLS + // handshake. If this field is not provided, the server MUST use the default + // maximum version of the server's TLS library. + TLSVersion max_tls_version = 3; + + // The ordered list of TLS 1.0-1.2 ciphersuites that the server MAY offer to + // negotiate in the TLS handshake. + repeated Ciphersuite ciphersuites = 10; + + // Whether to enable TLS resumption. + bool tls_resumption_enabled = 6; + + // Whether the server MUST request a client certificate (i.e. to negotiate + // TLS vs. mTLS). + RequestClientCertificate request_client_certificate = 7; + + // Returns the maximum number of extra bytes that + // |OffloadResumptionKeyOperation| can add to the number of unencrypted + // bytes to form the encrypted bytes. + uint32 max_overhead_of_ticket_aead = 9; + + // The policy that dictates how the server negotiates ALPN during the TLS + // handshake. + AlpnPolicy alpn_policy = 11; + } + + oneof tls_configuration { + ClientTlsConfiguration client_tls_configuration = 1; + ServerTlsConfiguration server_tls_configuration = 2; + } +} + +message OffloadPrivateKeyOperationReq { + enum PrivateKeyOperation { + UNSPECIFIED = 0; + // When performing a TLS 1.2 or 1.3 handshake, the (partial) transcript of + // the TLS handshake must be signed to prove possession of the private key. + // + // See https://www.rfc-editor.org/rfc/rfc8446.html#section-4.4.3. + SIGN = 1; + // When performing a TLS 1.2 handshake using an RSA algorithm, the key + // exchange algorithm involves the client generating a premaster secret, + // encrypting it using the server's public key, and sending this encrypted + // blob to the server in a ClientKeyExchange message. + // + // See https://www.rfc-editor.org/rfc/rfc4346#section-7.4.7.1. + DECRYPT = 2; + } + + // The operation the private key is used for. + PrivateKeyOperation operation = 1; + + // The signature algorithm to be used for signing operations. + SignatureAlgorithm signature_algorithm = 2; + + // The input bytes to be signed or decrypted. + oneof in_bytes { + // Raw bytes to be hashed and signed, or decrypted. + bytes raw_bytes = 4; + // A SHA256 hash to be signed. Must be 32 bytes. + bytes sha256_digest = 5; + // A SHA384 hash to be signed. Must be 48 bytes. + bytes sha384_digest = 6; + // A SHA512 hash to be signed. Must be 64 bytes. + bytes sha512_digest = 7; + } +} + +message OffloadPrivateKeyOperationResp { + // The signed or decrypted output bytes. + bytes out_bytes = 1; +} + +message OffloadResumptionKeyOperationReq { + enum ResumptionKeyOperation { + UNSPECIFIED = 0; + ENCRYPT = 1; + DECRYPT = 2; + } + + // The operation the resumption key is used for. + ResumptionKeyOperation operation = 1; + + // The bytes to be encrypted or decrypted. + bytes in_bytes = 2; +} + +message OffloadResumptionKeyOperationResp { + // The encrypted or decrypted bytes. + bytes out_bytes = 1; +} + +message ValidatePeerCertificateChainReq { + enum VerificationMode { + // The default verification mode supported by S2A. + UNSPECIFIED = 0; + // The SPIFFE verification mode selects the set of trusted certificates to + // use for path building based on the SPIFFE trust domain in the peer's leaf + // certificate. + SPIFFE = 1; + // The connect-to-Google verification mode uses the trust bundle for + // connecting to Google, e.g. *.mtls.googleapis.com endpoints. + CONNECT_TO_GOOGLE = 2; + } + + message ClientPeer { + // The certificate chain to be verified. The chain MUST be a list of + // DER-encoded certificates, ordered from leaf to root, excluding the root. + repeated bytes certificate_chain = 1; + } + + message ServerPeer { + // The certificate chain to be verified. The chain MUST be a list of + // DER-encoded certificates, ordered from leaf to root, excluding the root. + repeated bytes certificate_chain = 1; + + // The expected hostname of the server. + string server_hostname = 2; + + // The UnrestrictedClientPolicy specified by the user. + bytes serialized_unrestricted_client_policy = 3; + } + + // The verification mode that S2A MUST use to validate the peer certificate + // chain. + VerificationMode mode = 1; + + oneof peer_oneof { + ClientPeer client_peer = 2; + ServerPeer server_peer = 3; + } +} + +message ValidatePeerCertificateChainResp { + enum ValidationResult { + UNSPECIFIED = 0; + SUCCESS = 1; + FAILURE = 2; + } + + // The result of validating the peer certificate chain. + ValidationResult validation_result = 1; + + // The validation details. This field is only populated when the validation + // result is NOT SUCCESS. + string validation_details = 2; + + // The S2A context contains information from the peer certificate chain. + // + // The S2A context MAY be populated even if validation of the peer certificate + // chain fails. + S2AContext context = 3; +} + +message SessionReq { + // The identity corresponding to the TLS configurations that MUST be used for + // the TLS handshake. + // + // If a managed identity already exists, the local identity and authentication + // mechanisms are ignored. If a managed identity doesn't exist and the local + // identity is not populated, S2A will try to deduce the managed identity to + // use from the SNI extension. If that also fails, S2A uses the default + // identity (if one exists). + Identity local_identity = 1; + + // The authentication mechanisms that the application wishes to use to + // authenticate to S2A, ordered by preference. S2A will always use the first + // authentication mechanism that matches the managed identity. + repeated AuthenticationMechanism authentication_mechanisms = 2; + + oneof req_oneof { + // Requests the certificate chain and TLS configuration corresponding to the + // local identity, which the application MUST use to negotiate the TLS + // handshake. + GetTlsConfigurationReq get_tls_configuration_req = 3; + + // Signs or decrypts the input bytes using a private key corresponding to + // the local identity in the request. + // + // WARNING: More than one OffloadPrivateKeyOperationReq may be sent to the + // S2Av2 by a server during a TLS 1.2 handshake. + OffloadPrivateKeyOperationReq offload_private_key_operation_req = 4; + + // Encrypts or decrypts the input bytes using a resumption key corresponding + // to the local identity in the request. + OffloadResumptionKeyOperationReq offload_resumption_key_operation_req = 5; + + // Verifies the peer's certificate chain using + // (a) trust bundles corresponding to the local identity in the request, and + // (b) the verification mode in the request. + ValidatePeerCertificateChainReq validate_peer_certificate_chain_req = 6; + } +} + +message SessionResp { + // Status of the session response. + // + // The status field is populated so that if an error occurs when making an + // individual request, then communication with the S2A may continue. If an + // error is returned directly (e.g. at the gRPC layer), then it may result + // that the bidirectional stream being closed. + Status status = 1; + + oneof resp_oneof { + // Contains the certificate chain and TLS configurations corresponding to + // the local identity. + GetTlsConfigurationResp get_tls_configuration_resp = 2; + + // Contains the signed or encrypted output bytes using the private key + // corresponding to the local identity. + OffloadPrivateKeyOperationResp offload_private_key_operation_resp = 3; + + // Contains the encrypted or decrypted output bytes using the resumption key + // corresponding to the local identity. + OffloadResumptionKeyOperationResp offload_resumption_key_operation_resp = 4; + + // Contains the validation result, peer identity and fingerprints of peer + // certificates. + ValidatePeerCertificateChainResp validate_peer_certificate_chain_resp = 5; + } +} + +service S2AService { + // SetUpSession is a bidirectional stream used by applications to offload + // operations from the TLS handshake. + rpc SetUpSession(stream SessionReq) returns (stream SessionResp) {} +} diff --git a/s2a/src/main/proto/grpc/gcp/s2a/s2a_context.proto b/s2a/src/main/proto/grpc/gcp/s2a/s2a_context.proto new file mode 100644 index 00000000000..edaeaf22669 --- /dev/null +++ b/s2a/src/main/proto/grpc/gcp/s2a/s2a_context.proto @@ -0,0 +1,62 @@ +// Copyright 2024 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/s2a/s2a_context.proto + +syntax = "proto3"; + +package grpc.gcp.s2a; + +import "grpc/gcp/s2a/common.proto"; + +option java_multiple_files = true; +option java_outer_classname = "S2AContextProto"; +option java_package = "io.grpc.s2a.handshaker"; + +message S2AContext { + // The SPIFFE ID from the peer leaf certificate, if present. + // + // This field is only populated if the leaf certificate is a valid SPIFFE + // SVID; in particular, there is a unique URI SAN and this URI SAN is a valid + // SPIFFE ID. + string leaf_cert_spiffe_id = 1; + + // The URIs that are present in the SubjectAltName extension of the peer leaf + // certificate. + // + // Note that the extracted URIs are not validated and may not be properly + // formatted. + repeated string leaf_cert_uris = 2; + + // The DNSNames that are present in the SubjectAltName extension of the peer + // leaf certificate. + repeated string leaf_cert_dnsnames = 3; + + // The (ordered) list of fingerprints in the certificate chain used to verify + // the given leaf certificate. The order MUST be from leaf certificate + // fingerprint to root certificate fingerprint. + // + // A fingerprint is the base-64 encoding of the SHA256 hash of the + // DER-encoding of a certificate. The list MAY be populated even if the peer + // certificate chain was NOT validated successfully. + repeated string peer_certificate_chain_fingerprints = 4; + + // The local identity used during session setup. + Identity local_identity = 5; + + // The SHA256 hash of the DER-encoding of the local leaf certificate used in + // the handshake. + bytes local_leaf_cert_fingerprint = 6; +} diff --git a/s2a/src/test/java/io/grpc/s2a/MtlsToS2AChannelCredentialsTest.java b/s2a/src/test/java/io/grpc/s2a/MtlsToS2AChannelCredentialsTest.java new file mode 100644 index 00000000000..5ccc522292e --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/MtlsToS2AChannelCredentialsTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class MtlsToS2AChannelCredentialsTest { + @Test + public void createBuilder_nullAddress_throwsException() throws Exception { + assertThrows( + IllegalArgumentException.class, + () -> + MtlsToS2AChannelCredentials.createBuilder( + /* s2aAddress= */ null, + /* privateKeyPath= */ "src/test/resources/client_key.pem", + /* certChainPath= */ "src/test/resources/client_cert.pem", + /* trustBundlePath= */ "src/test/resources/root_cert.pem")); + } + + @Test + public void createBuilder_nullPrivateKeyPath_throwsException() throws Exception { + assertThrows( + IllegalArgumentException.class, + () -> + MtlsToS2AChannelCredentials.createBuilder( + /* s2aAddress= */ "s2a_address", + /* privateKeyPath= */ null, + /* certChainPath= */ "src/test/resources/client_cert.pem", + /* trustBundlePath= */ "src/test/resources/root_cert.pem")); + } + + @Test + public void createBuilder_nullCertChainPath_throwsException() throws Exception { + assertThrows( + IllegalArgumentException.class, + () -> + MtlsToS2AChannelCredentials.createBuilder( + /* s2aAddress= */ "s2a_address", + /* privateKeyPath= */ "src/test/resources/client_key.pem", + /* certChainPath= */ null, + /* trustBundlePath= */ "src/test/resources/root_cert.pem")); + } + + @Test + public void createBuilder_nullTrustBundlePath_throwsException() throws Exception { + assertThrows( + IllegalArgumentException.class, + () -> + MtlsToS2AChannelCredentials.createBuilder( + /* s2aAddress= */ "s2a_address", + /* privateKeyPath= */ "src/test/resources/client_key.pem", + /* certChainPath= */ "src/test/resources/client_cert.pem", + /* trustBundlePath= */ null)); + } + + @Test + public void createBuilder_emptyAddress_throwsException() throws Exception { + assertThrows( + IllegalArgumentException.class, + () -> + MtlsToS2AChannelCredentials.createBuilder( + /* s2aAddress= */ "", + /* privateKeyPath= */ "src/test/resources/client_key.pem", + /* certChainPath= */ "src/test/resources/client_cert.pem", + /* trustBundlePath= */ "src/test/resources/root_cert.pem")); + } + + @Test + public void createBuilder_emptyPrivateKeyPath_throwsException() throws Exception { + assertThrows( + IllegalArgumentException.class, + () -> + MtlsToS2AChannelCredentials.createBuilder( + /* s2aAddress= */ "s2a_address", + /* privateKeyPath= */ "", + /* certChainPath= */ "src/test/resources/client_cert.pem", + /* trustBundlePath= */ "src/test/resources/root_cert.pem")); + } + + @Test + public void createBuilder_emptyCertChainPath_throwsException() throws Exception { + assertThrows( + IllegalArgumentException.class, + () -> + MtlsToS2AChannelCredentials.createBuilder( + /* s2aAddress= */ "s2a_address", + /* privateKeyPath= */ "src/test/resources/client_key.pem", + /* certChainPath= */ "", + /* trustBundlePath= */ "src/test/resources/root_cert.pem")); + } + + @Test + public void createBuilder_emptyTrustBundlePath_throwsException() throws Exception { + assertThrows( + IllegalArgumentException.class, + () -> + MtlsToS2AChannelCredentials.createBuilder( + /* s2aAddress= */ "s2a_address", + /* privateKeyPath= */ "src/test/resources/client_key.pem", + /* certChainPath= */ "src/test/resources/client_cert.pem", + /* trustBundlePath= */ "")); + } + + @Test + public void build_s2AChannelCredentials_success() throws Exception { + assertThat( + MtlsToS2AChannelCredentials.createBuilder( + /* s2aAddress= */ "s2a_address", + /* privateKeyPath= */ "src/test/resources/client_key.pem", + /* certChainPath= */ "src/test/resources/client_cert.pem", + /* trustBundlePath= */ "src/test/resources/root_cert.pem") + .build()) + .isInstanceOf(S2AChannelCredentials.Builder.class); + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java b/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java new file mode 100644 index 00000000000..a6133ed0af8 --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import io.grpc.ChannelCredentials; +import io.grpc.TlsChannelCredentials; +import java.io.File; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@code S2AChannelCredentials}. */ +@RunWith(JUnit4.class) +public final class S2AChannelCredentialsTest { + @Test + public void createBuilder_nullArgument_throwsException() throws Exception { + assertThrows(IllegalArgumentException.class, () -> S2AChannelCredentials.createBuilder(null)); + } + + @Test + public void createBuilder_emptyAddress_throwsException() throws Exception { + assertThrows(IllegalArgumentException.class, () -> S2AChannelCredentials.createBuilder("")); + } + + @Test + public void setLocalSpiffeId_nullArgument_throwsException() throws Exception { + assertThrows( + NullPointerException.class, + () -> S2AChannelCredentials.createBuilder("s2a_address").setLocalSpiffeId(null)); + } + + @Test + public void setLocalHostname_nullArgument_throwsException() throws Exception { + assertThrows( + NullPointerException.class, + () -> S2AChannelCredentials.createBuilder("s2a_address").setLocalHostname(null)); + } + + @Test + public void setLocalUid_nullArgument_throwsException() throws Exception { + assertThrows( + NullPointerException.class, + () -> S2AChannelCredentials.createBuilder("s2a_address").setLocalUid(null)); + } + + @Test + public void build_withLocalSpiffeId_succeeds() throws Exception { + assertThat( + S2AChannelCredentials.createBuilder("s2a_address") + .setLocalSpiffeId("spiffe://test") + .build()) + .isNotNull(); + } + + @Test + public void build_withLocalHostname_succeeds() throws Exception { + assertThat( + S2AChannelCredentials.createBuilder("s2a_address") + .setLocalHostname("local_hostname") + .build()) + .isNotNull(); + } + + @Test + public void build_withLocalUid_succeeds() throws Exception { + assertThat(S2AChannelCredentials.createBuilder("s2a_address").setLocalUid("local_uid").build()) + .isNotNull(); + } + + @Test + public void build_withNoLocalIdentity_succeeds() throws Exception { + assertThat(S2AChannelCredentials.createBuilder("s2a_address").build()) + .isNotNull(); + } + + @Test + public void build_withTlsChannelCredentials_succeeds() throws Exception { + assertThat( + S2AChannelCredentials.createBuilder("s2a_address") + .setLocalSpiffeId("spiffe://test") + .setS2AChannelCredentials(getTlsChannelCredentials()) + .build()) + .isNotNull(); + } + + private static ChannelCredentials getTlsChannelCredentials() throws Exception { + File clientCert = new File("src/test/resources/client_cert.pem"); + File clientKey = new File("src/test/resources/client_key.pem"); + File rootCert = new File("src/test/resources/root_cert.pem"); + return TlsChannelCredentials.newBuilder() + .keyManager(clientCert, clientKey) + .trustManager(rootCert) + .build(); + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/channel/S2AGrpcChannelPoolTest.java b/s2a/src/test/java/io/grpc/s2a/channel/S2AGrpcChannelPoolTest.java new file mode 100644 index 00000000000..260129f8f56 --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/channel/S2AGrpcChannelPoolTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.channel; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; + +import io.grpc.Channel; +import io.grpc.internal.ObjectPool; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link S2AGrpcChannelPool}. */ +@RunWith(JUnit4.class) +public final class S2AGrpcChannelPoolTest { + @Test + public void getChannel_success() throws Exception { + FakeChannelPool fakeChannelPool = new FakeChannelPool(); + S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(fakeChannelPool); + + Channel channel = s2aChannelPool.getChannel(); + + assertThat(channel).isNotNull(); + assertThat(fakeChannelPool.isChannelCached()).isTrue(); + assertThat(s2aChannelPool.getChannel()).isEqualTo(channel); + } + + @Test + public void returnToPool_success() throws Exception { + FakeChannelPool fakeChannelPool = new FakeChannelPool(); + S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(fakeChannelPool); + + s2aChannelPool.returnToPool(s2aChannelPool.getChannel()); + + assertThat(fakeChannelPool.isChannelCached()).isFalse(); + } + + @Test + public void returnToPool_channelStillCachedBecauseMultipleChannelsRetrieved() throws Exception { + FakeChannelPool fakeChannelPool = new FakeChannelPool(); + S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(fakeChannelPool); + + s2aChannelPool.getChannel(); + s2aChannelPool.returnToPool(s2aChannelPool.getChannel()); + + assertThat(fakeChannelPool.isChannelCached()).isTrue(); + } + + @Test + public void returnToPool_failureBecauseChannelWasNotFromPool() throws Exception { + S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(new FakeChannelPool()); + + IllegalArgumentException expected = + assertThrows( + IllegalArgumentException.class, + () -> s2aChannelPool.returnToPool(mock(Channel.class))); + assertThat(expected) + .hasMessageThat() + .isEqualTo( + "Cannot return the channel to channel pool because the channel was not obtained from" + + " channel pool."); + } + + @Test + public void close_success() throws Exception { + FakeChannelPool fakeChannelPool = new FakeChannelPool(); + try (S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(fakeChannelPool)) { + s2aChannelPool.getChannel(); + } + + assertThat(fakeChannelPool.isChannelCached()).isFalse(); + } + + @Test + public void close_poolIsUnusable() throws Exception { + S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(new FakeChannelPool()); + s2aChannelPool.close(); + + IllegalStateException expected = + assertThrows(IllegalStateException.class, s2aChannelPool::getChannel); + + assertThat(expected).hasMessageThat().isEqualTo("Channel pool is not open."); + } + + private static class FakeChannelPool implements ObjectPool { + private final Channel mockChannel = mock(Channel.class); + private @Nullable Channel cachedChannel = null; + + @Override + public Channel getObject() { + if (cachedChannel == null) { + cachedChannel = mockChannel; + } + return cachedChannel; + } + + @Override + public Channel returnObject(Object object) { + assertThat(object).isSameInstanceAs(mockChannel); + cachedChannel = null; + return null; + } + + public boolean isChannelCached() { + return (cachedChannel != null); + } + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java b/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java new file mode 100644 index 00000000000..57288be1b6f --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java @@ -0,0 +1,390 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.channel; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ChannelCredentials; +import io.grpc.ClientCall; +import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.ServerCredentials; +import io.grpc.StatusRuntimeException; +import io.grpc.TlsChannelCredentials; +import io.grpc.TlsServerCredentials; +import io.grpc.benchmarks.Utils; +import io.grpc.internal.SharedResourceHolder.Resource; +import io.grpc.netty.NettyServerBuilder; +import io.grpc.s2a.channel.S2AHandshakerServiceChannel.EventLoopHoldingChannel; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; +import io.grpc.testing.protobuf.SimpleRequest; +import io.grpc.testing.protobuf.SimpleResponse; +import io.grpc.testing.protobuf.SimpleServiceGrpc; +import io.netty.channel.EventLoopGroup; +import java.io.File; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link S2AHandshakerServiceChannel}. */ +@RunWith(JUnit4.class) +public final class S2AHandshakerServiceChannelTest { + @ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private static final Duration CHANNEL_SHUTDOWN_TIMEOUT = Duration.ofSeconds(10); + private final EventLoopGroup mockEventLoopGroup = mock(EventLoopGroup.class); + private Server mtlsServer; + private Server plaintextServer; + + @Before + public void setUp() throws Exception { + mtlsServer = createMtlsServer(); + plaintextServer = createPlaintextServer(); + mtlsServer.start(); + plaintextServer.start(); + } + + /** + * Creates a {@code Resource} and verifies that it produces a {@code ChannelResource} + * instance by using its {@code toString()} method. + */ + @Test + public void getChannelResource_success() { + Resource resource = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + plaintextServer.getPort(), + /* s2aChannelCredentials= */ Optional.empty()); + assertThat(resource.toString()).isEqualTo("grpc-s2a-channel"); + } + + /** Same as getChannelResource_success, but use mTLS. */ + @Test + public void getChannelResource_mtlsSuccess() throws Exception { + Resource resource = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + mtlsServer.getPort(), getTlsChannelCredentials()); + assertThat(resource.toString()).isEqualTo("grpc-s2a-channel"); + } + + /** + * Creates two {@code Resoure}s for the same target address and verifies that they are + * equal. + */ + @Test + public void getChannelResource_twoEqualChannels() { + Resource resource = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + plaintextServer.getPort(), + /* s2aChannelCredentials= */ Optional.empty()); + Resource resourceTwo = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + plaintextServer.getPort(), + /* s2aChannelCredentials= */ Optional.empty()); + assertThat(resource).isEqualTo(resourceTwo); + } + + /** Same as getChannelResource_twoEqualChannels, but use mTLS. */ + @Test + public void getChannelResource_mtlsTwoEqualChannels() throws Exception { + Resource resource = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + mtlsServer.getPort(), getTlsChannelCredentials()); + Resource resourceTwo = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + mtlsServer.getPort(), getTlsChannelCredentials()); + assertThat(resource).isEqualTo(resourceTwo); + } + + /** + * Creates two {@code Resoure}s for different target addresses and verifies that they are + * distinct. + */ + @Test + public void getChannelResource_twoDistinctChannels() { + Resource resource = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + plaintextServer.getPort(), + /* s2aChannelCredentials= */ Optional.empty()); + Resource resourceTwo = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + Utils.pickUnusedPort(), /* s2aChannelCredentials= */ Optional.empty()); + assertThat(resourceTwo).isNotEqualTo(resource); + } + + /** Same as getChannelResource_twoDistinctChannels, but use mTLS. */ + @Test + public void getChannelResource_mtlsTwoDistinctChannels() throws Exception { + Resource resource = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + mtlsServer.getPort(), getTlsChannelCredentials()); + Resource resourceTwo = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + Utils.pickUnusedPort(), getTlsChannelCredentials()); + assertThat(resourceTwo).isNotEqualTo(resource); + } + + /** + * Uses a {@code Resource} to create a channel, closes the channel, and verifies that the + * channel is closed by attempting to make a simple RPC. + */ + @Test + public void close_success() { + Resource resource = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + plaintextServer.getPort(), + /* s2aChannelCredentials= */ Optional.empty()); + Channel channel = resource.create(); + resource.close(channel); + StatusRuntimeException expected = + assertThrows( + StatusRuntimeException.class, + () -> + SimpleServiceGrpc.newBlockingStub(channel) + .unaryRpc(SimpleRequest.getDefaultInstance())); + assertThat(expected).hasMessageThat().isEqualTo("UNAVAILABLE: Channel shutdown invoked"); + } + + /** Same as close_success, but use mTLS. */ + @Test + public void close_mtlsSuccess() throws Exception { + Resource resource = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + mtlsServer.getPort(), getTlsChannelCredentials()); + Channel channel = resource.create(); + resource.close(channel); + StatusRuntimeException expected = + assertThrows( + StatusRuntimeException.class, + () -> + SimpleServiceGrpc.newBlockingStub(channel) + .unaryRpc(SimpleRequest.getDefaultInstance())); + assertThat(expected).hasMessageThat().isEqualTo("UNAVAILABLE: Channel shutdown invoked"); + } + + /** + * Verifies that an {@code EventLoopHoldingChannel}'s {@code newCall} method can be used to + * perform a simple RPC. + */ + @Test + public void newCall_performSimpleRpcSuccess() { + Resource resource = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + plaintextServer.getPort(), + /* s2aChannelCredentials= */ Optional.empty()); + Channel channel = resource.create(); + assertThat(channel).isInstanceOf(EventLoopHoldingChannel.class); + assertThat( + SimpleServiceGrpc.newBlockingStub(channel).unaryRpc(SimpleRequest.getDefaultInstance())) + .isEqualToDefaultInstance(); + } + + /** Same as newCall_performSimpleRpcSuccess, but use mTLS. */ + @Test + public void newCall_mtlsPerformSimpleRpcSuccess() throws Exception { + Resource resource = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + mtlsServer.getPort(), getTlsChannelCredentials()); + Channel channel = resource.create(); + assertThat(channel).isInstanceOf(EventLoopHoldingChannel.class); + assertThat( + SimpleServiceGrpc.newBlockingStub(channel).unaryRpc(SimpleRequest.getDefaultInstance())) + .isEqualToDefaultInstance(); + } + + /** Creates a {@code EventLoopHoldingChannel} instance and verifies its authority. */ + @Test + public void authority_success() throws Exception { + ManagedChannel channel = new FakeManagedChannel(true); + EventLoopHoldingChannel eventLoopHoldingChannel = + EventLoopHoldingChannel.create(channel, mockEventLoopGroup); + assertThat(eventLoopHoldingChannel.authority()).isEqualTo("FakeManagedChannel"); + } + + /** + * Creates and closes a {@code EventLoopHoldingChannel} when its {@code ManagedChannel} terminates + * successfully. + */ + @Test + public void close_withDelegateTerminatedSuccess() throws Exception { + ManagedChannel channel = new FakeManagedChannel(true); + EventLoopHoldingChannel eventLoopHoldingChannel = + EventLoopHoldingChannel.create(channel, mockEventLoopGroup); + eventLoopHoldingChannel.close(); + assertThat(channel.isShutdown()).isTrue(); + verify(mockEventLoopGroup, times(1)) + .shutdownGracefully(0, CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS); + } + + /** + * Creates and closes a {@code EventLoopHoldingChannel} when its {@code ManagedChannel} does not + * terminate successfully. + */ + @Test + public void close_withDelegateTerminatedFailure() throws Exception { + ManagedChannel channel = new FakeManagedChannel(false); + EventLoopHoldingChannel eventLoopHoldingChannel = + EventLoopHoldingChannel.create(channel, mockEventLoopGroup); + eventLoopHoldingChannel.close(); + assertThat(channel.isShutdown()).isTrue(); + verify(mockEventLoopGroup, times(1)) + .shutdownGracefully(1, CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS); + } + + /** + * Creates and closes a {@code EventLoopHoldingChannel}, creates a new channel from the same + * resource, and verifies that this second channel is useable. + */ + @Test + public void create_succeedsAfterCloseIsCalledOnce() throws Exception { + Resource resource = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + plaintextServer.getPort(), + /* s2aChannelCredentials= */ Optional.empty()); + Channel channelOne = resource.create(); + resource.close(channelOne); + + Channel channelTwo = resource.create(); + assertThat(channelTwo).isInstanceOf(EventLoopHoldingChannel.class); + assertThat( + SimpleServiceGrpc.newBlockingStub(channelTwo) + .unaryRpc(SimpleRequest.getDefaultInstance())) + .isEqualToDefaultInstance(); + resource.close(channelTwo); + } + + /** Same as create_succeedsAfterCloseIsCalledOnce, but use mTLS. */ + @Test + public void create_mtlsSucceedsAfterCloseIsCalledOnce() throws Exception { + Resource resource = + S2AHandshakerServiceChannel.getChannelResource( + "localhost:" + mtlsServer.getPort(), getTlsChannelCredentials()); + Channel channelOne = resource.create(); + resource.close(channelOne); + + Channel channelTwo = resource.create(); + assertThat(channelTwo).isInstanceOf(EventLoopHoldingChannel.class); + assertThat( + SimpleServiceGrpc.newBlockingStub(channelTwo) + .unaryRpc(SimpleRequest.getDefaultInstance())) + .isEqualToDefaultInstance(); + resource.close(channelTwo); + } + + private static Server createMtlsServer() throws Exception { + SimpleServiceImpl service = new SimpleServiceImpl(); + File serverCert = new File("src/test/resources/server_cert.pem"); + File serverKey = new File("src/test/resources/server_key.pem"); + File rootCert = new File("src/test/resources/root_cert.pem"); + ServerCredentials creds = + TlsServerCredentials.newBuilder() + .keyManager(serverCert, serverKey) + .trustManager(rootCert) + .clientAuth(TlsServerCredentials.ClientAuth.REQUIRE) + .build(); + return grpcCleanup.register( + NettyServerBuilder.forPort(Utils.pickUnusedPort(), creds).addService(service).build()); + } + + private static Server createPlaintextServer() { + SimpleServiceImpl service = new SimpleServiceImpl(); + return grpcCleanup.register( + ServerBuilder.forPort(Utils.pickUnusedPort()).addService(service).build()); + } + + private static Optional getTlsChannelCredentials() throws Exception { + File clientCert = new File("src/test/resources/client_cert.pem"); + File clientKey = new File("src/test/resources/client_key.pem"); + File rootCert = new File("src/test/resources/root_cert.pem"); + return Optional.of( + TlsChannelCredentials.newBuilder() + .keyManager(clientCert, clientKey) + .trustManager(rootCert) + .build()); + } + + private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase { + @Override + public void unaryRpc(SimpleRequest request, StreamObserver streamObserver) { + streamObserver.onNext(SimpleResponse.getDefaultInstance()); + streamObserver.onCompleted(); + } + } + + private static class FakeManagedChannel extends ManagedChannel { + private final boolean isDelegateTerminatedSuccess; + private boolean isShutdown = false; + + FakeManagedChannel(boolean isDelegateTerminatedSuccess) { + this.isDelegateTerminatedSuccess = isDelegateTerminatedSuccess; + } + + @Override + public String authority() { + return "FakeManagedChannel"; + } + + @Override + public ClientCall newCall( + MethodDescriptor methodDescriptor, CallOptions options) { + throw new UnsupportedOperationException("This method should not be called."); + } + + @Override + public ManagedChannel shutdown() { + throw new UnsupportedOperationException("This method should not be called."); + } + + @Override + public boolean isShutdown() { + return isShutdown; + } + + @Override + public boolean isTerminated() { + throw new UnsupportedOperationException("This method should not be called."); + } + + @Override + public ManagedChannel shutdownNow() { + isShutdown = true; + return null; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + if (isDelegateTerminatedSuccess) { + return true; + } + throw new InterruptedException("Await termination was interrupted."); + } + } +} diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServer.java b/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServer.java new file mode 100644 index 00000000000..66f636ada22 --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import io.grpc.stub.StreamObserver; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.logging.Logger; + +/** A fake S2Av2 server that should be used for testing only. */ +public final class FakeS2AServer extends S2AServiceGrpc.S2AServiceImplBase { + private static final Logger logger = Logger.getLogger(FakeS2AServer.class.getName()); + + private final FakeWriter writer; + + public FakeS2AServer() throws InvalidKeySpecException, NoSuchAlgorithmException { + this.writer = new FakeWriter(); + this.writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS).initializePrivateKey(); + } + + @Override + public StreamObserver setUpSession(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(SessionReq req) { + logger.info("Received a request from client."); + responseObserver.onNext(writer.handleResponse(req)); + } + + @Override + public void onError(Throwable t) { + responseObserver.onError(t); + } + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServerTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServerTest.java new file mode 100644 index 00000000000..e200d119867 --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServerTest.java @@ -0,0 +1,265 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.google.common.collect.ImmutableList; +import com.google.protobuf.ByteString; +import io.grpc.Grpc; +import io.grpc.InsecureChannelCredentials; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.benchmarks.Utils; +import io.grpc.s2a.handshaker.ValidatePeerCertificateChainReq.VerificationMode; +import io.grpc.stub.StreamObserver; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Logger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link FakeS2AServer}. */ +@RunWith(JUnit4.class) +public final class FakeS2AServerTest { + private static final Logger logger = Logger.getLogger(FakeS2AServerTest.class.getName()); + + private static final ImmutableList FAKE_CERT_DER_CHAIN = + ImmutableList.of( + ByteString.copyFrom( + new byte[] {'f', 'a', 'k', 'e', '-', 'd', 'e', 'r', '-', 'c', 'h', 'a', 'i', 'n'})); + private int port; + private String serverAddress; + private SessionResp response = null; + private Server fakeS2AServer; + + @Before + public void setUp() throws Exception { + port = Utils.pickUnusedPort(); + fakeS2AServer = ServerBuilder.forPort(port).addService(new FakeS2AServer()).build(); + fakeS2AServer.start(); + serverAddress = String.format("localhost:%d", port); + } + + @After + public void tearDown() { + fakeS2AServer.shutdown(); + } + + @Test + public void callS2AServerOnce_getTlsConfiguration_returnsValidResult() + throws InterruptedException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + logger.info("Client connecting to: " + serverAddress); + ManagedChannel channel = + Grpc.newChannelBuilder(serverAddress, InsecureChannelCredentials.create()) + .executor(executor) + .build(); + + try { + S2AServiceGrpc.S2AServiceStub asyncStub = S2AServiceGrpc.newStub(channel); + StreamObserver requestObserver = + asyncStub.setUpSession( + new StreamObserver() { + @Override + public void onNext(SessionResp resp) { + response = resp; + } + + @Override + public void onError(Throwable t) { + throw new RuntimeException(t); + } + + @Override + public void onCompleted() {} + }); + try { + requestObserver.onNext( + SessionReq.newBuilder() + .setGetTlsConfigurationReq( + GetTlsConfigurationReq.newBuilder() + .setConnectionSide(ConnectionSide.CONNECTION_SIDE_CLIENT)) + .build()); + } catch (RuntimeException e) { + // Cancel the RPC. + requestObserver.onError(e); + throw e; + } + // Mark the end of requests. + requestObserver.onCompleted(); + // Wait for receiving to happen. + } finally { + channel.shutdown(); + channel.awaitTermination(1, SECONDS); + executor.shutdown(); + executor.awaitTermination(1, SECONDS); + } + + SessionResp expected = + SessionResp.newBuilder() + .setGetTlsConfigurationResp( + GetTlsConfigurationResp.newBuilder() + .setClientTlsConfiguration( + GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() + .addCertificateChain(FakeWriter.LEAF_CERT) + .addCertificateChain(FakeWriter.INTERMEDIATE_CERT_2) + .addCertificateChain(FakeWriter.INTERMEDIATE_CERT_1) + .setMinTlsVersion(TLSVersion.TLS_VERSION_1_3) + .setMaxTlsVersion(TLSVersion.TLS_VERSION_1_3) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256))) + .build(); + assertThat(response).ignoringRepeatedFieldOrder().isEqualTo(expected); + } + + @Test + public void callS2AServerOnce_validatePeerCertifiate_returnsValidResult() + throws InterruptedException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + logger.info("Client connecting to: " + serverAddress); + ManagedChannel channel = + Grpc.newChannelBuilder(serverAddress, InsecureChannelCredentials.create()) + .executor(executor) + .build(); + + try { + S2AServiceGrpc.S2AServiceStub asyncStub = S2AServiceGrpc.newStub(channel); + StreamObserver requestObserver = + asyncStub.setUpSession( + new StreamObserver() { + @Override + public void onNext(SessionResp resp) { + response = resp; + } + + @Override + public void onError(Throwable t) { + throw new RuntimeException(t); + } + + @Override + public void onCompleted() {} + }); + try { + requestObserver.onNext( + SessionReq.newBuilder() + .setValidatePeerCertificateChainReq( + ValidatePeerCertificateChainReq.newBuilder() + .setMode(VerificationMode.UNSPECIFIED) + .setClientPeer( + ValidatePeerCertificateChainReq.ClientPeer.newBuilder() + .addAllCertificateChain(FAKE_CERT_DER_CHAIN))) + .build()); + } catch (RuntimeException e) { + // Cancel the RPC. + requestObserver.onError(e); + throw e; + } + // Mark the end of requests. + requestObserver.onCompleted(); + // Wait for receiving to happen. + } finally { + channel.shutdown(); + channel.awaitTermination(1, SECONDS); + executor.shutdown(); + executor.awaitTermination(1, SECONDS); + } + + SessionResp expected = + SessionResp.newBuilder() + .setValidatePeerCertificateChainResp( + ValidatePeerCertificateChainResp.newBuilder() + .setValidationResult(ValidatePeerCertificateChainResp.ValidationResult.SUCCESS)) + .build(); + assertThat(response).ignoringRepeatedFieldOrder().isEqualTo(expected); + } + + @Test + public void callS2AServerRepeatedly_returnsValidResult() throws InterruptedException { + final int numberOfRequests = 10; + ExecutorService executor = Executors.newSingleThreadExecutor(); + logger.info("Client connecting to: " + serverAddress); + ManagedChannel channel = + Grpc.newChannelBuilder(serverAddress, InsecureChannelCredentials.create()) + .executor(executor) + .build(); + + try { + S2AServiceGrpc.S2AServiceStub asyncStub = S2AServiceGrpc.newStub(channel); + CountDownLatch finishLatch = new CountDownLatch(1); + StreamObserver requestObserver = + asyncStub.setUpSession( + new StreamObserver() { + private int expectedNumberOfReplies = numberOfRequests; + + @Override + public void onNext(SessionResp reply) { + System.out.println("Received a message from the S2AService service."); + expectedNumberOfReplies -= 1; + } + + @Override + public void onError(Throwable t) { + finishLatch.countDown(); + if (expectedNumberOfReplies != 0) { + throw new RuntimeException(t); + } + } + + @Override + public void onCompleted() { + finishLatch.countDown(); + if (expectedNumberOfReplies != 0) { + throw new RuntimeException(); + } + } + }); + try { + for (int i = 0; i < numberOfRequests; i++) { + requestObserver.onNext(SessionReq.getDefaultInstance()); + } + } catch (RuntimeException e) { + // Cancel the RPC. + requestObserver.onError(e); + throw e; + } + // Mark the end of requests. + requestObserver.onCompleted(); + // Wait for receiving to happen. + if (!finishLatch.await(10, SECONDS)) { + throw new RuntimeException(); + } + } finally { + channel.shutdown(); + channel.awaitTermination(1, SECONDS); + executor.shutdown(); + executor.awaitTermination(1, SECONDS); + } + } + +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeWriter.java b/s2a/src/test/java/io/grpc/s2a/handshaker/FakeWriter.java new file mode 100644 index 00000000000..45961b81b7b --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/FakeWriter.java @@ -0,0 +1,363 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static io.grpc.s2a.handshaker.TLSVersion.TLS_VERSION_1_2; +import static io.grpc.s2a.handshaker.TLSVersion.TLS_VERSION_1_3; + +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.protobuf.ByteString; +import io.grpc.stub.StreamObserver; +import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +/** A fake Writer Class to mock the behavior of S2A server. */ +final class FakeWriter implements StreamObserver { + /** Fake behavior of S2A service. */ + enum Behavior { + OK_STATUS, + EMPTY_RESPONSE, + ERROR_STATUS, + ERROR_RESPONSE, + COMPLETE_STATUS, + BAD_TLS_VERSION_RESPONSE, + } + + enum VerificationResult { + UNSPECIFIED, + SUCCESS, + FAILURE + } + + public static final String LEAF_CERT = + "-----BEGIN CERTIFICATE-----\n" + + "MIICkDCCAjagAwIBAgIUSAtcrPhNNs1zxv51lIfGOVtkw6QwCgYIKoZIzj0EAwIw\n" + + "QTEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xEDAOBgNVBAsMB2NvbnRleHQxFDAS\n" + + "BgorBgEEAdZ5AggBDAQyMDIyMCAXDTIzMDcxNDIyMzYwNFoYDzIwNTAxMTI5MjIz\n" + + "NjA0WjARMQ8wDQYDVQQDDAZ1bnVzZWQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\n" + + "AAQGFlJpLxJMh4HuUm0DKjnUF7larH3tJvroQ12xpk+pPKQepn4ILoq9lZ8Xd3jz\n" + + "U98eDRXG5f4VjnX98DDHE4Ido4IBODCCATQwDgYDVR0PAQH/BAQDAgeAMCAGA1Ud\n" + + "JQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMIGxBgNV\n" + + "HREBAf8EgaYwgaOGSnNwaWZmZTovL3NpZ25lci1yb2xlLmNvbnRleHQuc2VjdXJp\n" + + "dHktcmVhbG0ucHJvZC5nb29nbGUuY29tL3JvbGUvbGVhZi1yb2xlgjNzaWduZXIt\n" + + "cm9sZS5jb250ZXh0LnNlY3VyaXR5LXJlYWxtLnByb2Quc3BpZmZlLmdvb2eCIGZx\n" + + "ZG4tb2YtdGhlLW5vZGUucHJvZC5nb29nbGUuY29tMB0GA1UdDgQWBBSWSd5Fw6dI\n" + + "TGpt0m1Uxwf0iKqebzAfBgNVHSMEGDAWgBRm5agVVdpWfRZKM7u6OMuzHhqPcDAK\n" + + "BggqhkjOPQQDAgNIADBFAiB0sjRPSYy2eFq8Y0vQ8QN4AZ2NMajskvxnlifu7O4U\n" + + "RwIhANTh5Fkyx2nMYFfyl+W45dY8ODTw3HnlZ4b51hTAdkWl\n" + + "-----END CERTIFICATE-----"; + public static final String INTERMEDIATE_CERT_2 = + "-----BEGIN CERTIFICATE-----\n" + + "MIICQjCCAeigAwIBAgIUKxXRDlnWXefNV5lj5CwhDuXEq7MwCgYIKoZIzj0EAwIw\n" + + "OzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xEDAOBgNVBAsMB2NvbnRleHQxDjAM\n" + + "BgNVBAMMBTEyMzQ1MCAXDTIzMDcxNDIyMzYwNFoYDzIwNTAxMTI5MjIzNjA0WjBB\n" + + "MRcwFQYDVQQKDA5zZWN1cml0eS1yZWFsbTEQMA4GA1UECwwHY29udGV4dDEUMBIG\n" + + "CisGAQQB1nkCCAEMBDIwMjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT/Zu7x\n" + + "UYVyg+T/vg2H+y4I6t36Kc4qxD0eqqZjRLYBVKkUQHxBqc14t0DpoROMYQCNd4DF\n" + + "pcxv/9m6DaJbRk6Ao4HBMIG+MA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAG\n" + + "AQH/AgEBMFgGA1UdHgEB/wROMEygSjA1gjNzaWduZXItcm9sZS5jb250ZXh0LnNl\n" + + "Y3VyaXR5LXJlYWxtLnByb2Quc3BpZmZlLmdvb2cwEYIPcHJvZC5nb29nbGUuY29t\n" + + "MB0GA1UdDgQWBBRm5agVVdpWfRZKM7u6OMuzHhqPcDAfBgNVHSMEGDAWgBQcjNAh\n" + + "SCHTj+BW8KrzSSLo2ASEgjAKBggqhkjOPQQDAgNIADBFAiEA6KyGd9VxXDZceMZG\n" + + "IsbC40rtunFjLYI0mjZw9RcRWx8CIHCIiIHxafnDaCi+VB99NZfzAdu37g6pJptB\n" + + "gjIY71MO\n" + + "-----END CERTIFICATE-----"; + public static final String INTERMEDIATE_CERT_1 = + "-----BEGIN CERTIFICATE-----\n" + + "MIICODCCAd6gAwIBAgIUXtZECORWRSKnS9rRTJYkiALUXswwCgYIKoZIzj0EAwIw\n" + + "NzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xDTALBgNVBAsMBHJvb3QxDTALBgNV\n" + + "BAMMBDEyMzQwIBcNMjMwNzE0MjIzNjA0WhgPMjA1MDExMjkyMjM2MDRaMDsxFzAV\n" + + "BgNVBAoMDnNlY3VyaXR5LXJlYWxtMRAwDgYDVQQLDAdjb250ZXh0MQ4wDAYDVQQD\n" + + "DAUxMjM0NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAycVTZrjockbpD59f1a\n" + + "4l1SNL7nSyXz66Guz4eDveQqLmaMBg7vpACfO4CtiAGnolHEffuRtSkdM434m5En\n" + + "bXCjgcEwgb4wDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQIwWAYD\n" + + "VR0eAQH/BE4wTKBKMDWCM3NpZ25lci1yb2xlLmNvbnRleHQuc2VjdXJpdHktcmVh\n" + + "bG0ucHJvZC5zcGlmZmUuZ29vZzARgg9wcm9kLmdvb2dsZS5jb20wHQYDVR0OBBYE\n" + + "FByM0CFIIdOP4FbwqvNJIujYBISCMB8GA1UdIwQYMBaAFMX+vebuj/lYfYEC23IA\n" + + "8HoIW0HsMAoGCCqGSM49BAMCA0gAMEUCIQCfxeXEBd7UPmeImT16SseCRu/6cHxl\n" + + "kTDsq9sKZ+eXBAIgA+oViAVOUhUQO1/6Mjlczg8NmMy2vNtG4V/7g9dMMVU=\n" + + "-----END CERTIFICATE-----"; + + private static final String PRIVATE_KEY = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgqA2U0ld1OOHLMXWf" + + "uyN4GSaqhhudEIaKkll3rdIq0M+hRANCAAQGFlJpLxJMh4HuUm0DKjnUF7larH3t" + + "JvroQ12xpk+pPKQepn4ILoq9lZ8Xd3jzU98eDRXG5f4VjnX98DDHE4Id"; + private static final ImmutableMap + ALGORITHM_TO_SIGNATURE_INSTANCE_IDENTIFIER = + ImmutableMap.of( + SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP256R1_SHA256, + "SHA256withECDSA", + SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP384R1_SHA384, + "SHA384withECDSA", + SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP521R1_SHA512, + "SHA512withECDSA"); + + private boolean fakeWriterClosed = false; + private Behavior behavior = Behavior.OK_STATUS; + private StreamObserver reader; + private VerificationResult verificationResult = VerificationResult.UNSPECIFIED; + private String failureReason; + private PrivateKey privateKey; + + @CanIgnoreReturnValue + FakeWriter setReader(StreamObserver reader) { + this.reader = reader; + return this; + } + + @CanIgnoreReturnValue + FakeWriter setBehavior(Behavior behavior) { + this.behavior = behavior; + return this; + } + + @CanIgnoreReturnValue + FakeWriter setVerificationResult(VerificationResult verificationResult) { + this.verificationResult = verificationResult; + return this; + } + + @CanIgnoreReturnValue + FakeWriter setFailureReason(String failureReason) { + this.failureReason = failureReason; + return this; + } + + @CanIgnoreReturnValue + FakeWriter initializePrivateKey() throws InvalidKeySpecException, NoSuchAlgorithmException { + privateKey = + KeyFactory.getInstance("EC") + .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIVATE_KEY))); + return this; + } + + @CanIgnoreReturnValue + FakeWriter resetPrivateKey() { + privateKey = null; + return this; + } + + void sendUnexpectedResponse() { + reader.onNext(SessionResp.getDefaultInstance()); + } + + void sendIoError() { + reader.onError(new IOException("Intended ERROR from FakeWriter.")); + } + + void sendGetTlsConfigResp() { + reader.onNext( + SessionResp.newBuilder() + .setGetTlsConfigurationResp( + GetTlsConfigurationResp.newBuilder() + .setClientTlsConfiguration( + GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() + .addCertificateChain(LEAF_CERT) + .addCertificateChain(INTERMEDIATE_CERT_2) + .addCertificateChain(INTERMEDIATE_CERT_1) + .setMinTlsVersion(TLS_VERSION_1_3) + .setMaxTlsVersion(TLS_VERSION_1_3) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256))) + .build()); + } + + boolean isFakeWriterClosed() { + return fakeWriterClosed; + } + + @Override + public void onNext(SessionReq sessionReq) { + switch (behavior) { + case OK_STATUS: + reader.onNext(handleResponse(sessionReq)); + break; + case EMPTY_RESPONSE: + reader.onNext(SessionResp.getDefaultInstance()); + break; + case ERROR_STATUS: + reader.onNext( + SessionResp.newBuilder() + .setStatus( + Status.newBuilder() + .setCode(1) + .setDetails("Intended ERROR Status from FakeWriter.")) + .build()); + break; + case ERROR_RESPONSE: + reader.onError(new S2AConnectionException("Intended ERROR from FakeWriter.")); + break; + case COMPLETE_STATUS: + reader.onCompleted(); + break; + case BAD_TLS_VERSION_RESPONSE: + reader.onNext( + SessionResp.newBuilder() + .setGetTlsConfigurationResp( + GetTlsConfigurationResp.newBuilder() + .setClientTlsConfiguration( + GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() + .addCertificateChain(LEAF_CERT) + .addCertificateChain(INTERMEDIATE_CERT_2) + .addCertificateChain(INTERMEDIATE_CERT_1) + .setMinTlsVersion(TLS_VERSION_1_3) + .setMaxTlsVersion(TLS_VERSION_1_2))) + .build()); + break; + default: + reader.onNext(handleResponse(sessionReq)); + } + } + + SessionResp handleResponse(SessionReq sessionReq) { + if (sessionReq.hasGetTlsConfigurationReq()) { + return handleGetTlsConfigurationReq(sessionReq.getGetTlsConfigurationReq()); + } + + if (sessionReq.hasValidatePeerCertificateChainReq()) { + return handleValidatePeerCertificateChainReq(sessionReq.getValidatePeerCertificateChainReq()); + } + + if (sessionReq.hasOffloadPrivateKeyOperationReq()) { + return handleOffloadPrivateKeyOperationReq(sessionReq.getOffloadPrivateKeyOperationReq()); + } + + return SessionResp.newBuilder() + .setStatus( + Status.newBuilder().setCode(255).setDetails("No supported operation designated.")) + .build(); + } + + private SessionResp handleGetTlsConfigurationReq(GetTlsConfigurationReq req) { + if (!req.getConnectionSide().equals(ConnectionSide.CONNECTION_SIDE_CLIENT)) { + return SessionResp.newBuilder() + .setStatus( + Status.newBuilder() + .setCode(255) + .setDetails("No TLS configuration for the server side.")) + .build(); + } + return SessionResp.newBuilder() + .setGetTlsConfigurationResp( + GetTlsConfigurationResp.newBuilder() + .setClientTlsConfiguration( + GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() + .addCertificateChain(LEAF_CERT) + .addCertificateChain(INTERMEDIATE_CERT_2) + .addCertificateChain(INTERMEDIATE_CERT_1) + .setMinTlsVersion(TLS_VERSION_1_3) + .setMaxTlsVersion(TLS_VERSION_1_3) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256))) + .build(); + } + + private SessionResp handleValidatePeerCertificateChainReq(ValidatePeerCertificateChainReq req) { + if (verifyValidatePeerCertificateChainReq(req) + && verificationResult == VerificationResult.SUCCESS) { + return SessionResp.newBuilder() + .setValidatePeerCertificateChainResp( + ValidatePeerCertificateChainResp.newBuilder() + .setValidationResult(ValidatePeerCertificateChainResp.ValidationResult.SUCCESS)) + .build(); + } + return SessionResp.newBuilder() + .setValidatePeerCertificateChainResp( + ValidatePeerCertificateChainResp.newBuilder() + .setValidationResult( + verificationResult == VerificationResult.FAILURE + ? ValidatePeerCertificateChainResp.ValidationResult.FAILURE + : ValidatePeerCertificateChainResp.ValidationResult.UNSPECIFIED) + .setValidationDetails(failureReason)) + .build(); + } + + private boolean verifyValidatePeerCertificateChainReq(ValidatePeerCertificateChainReq req) { + if (req.getMode() != ValidatePeerCertificateChainReq.VerificationMode.UNSPECIFIED) { + return false; + } + if (req.getClientPeer().getCertificateChainCount() > 0) { + return true; + } + if (req.getServerPeer().getCertificateChainCount() > 0 + && !req.getServerPeer().getServerHostname().isEmpty()) { + return true; + } + return false; + } + + private SessionResp handleOffloadPrivateKeyOperationReq(OffloadPrivateKeyOperationReq req) { + if (privateKey == null) { + return SessionResp.newBuilder() + .setStatus(Status.newBuilder().setCode(255).setDetails("No Private Key available.")) + .build(); + } + String signatureIdentifier = + ALGORITHM_TO_SIGNATURE_INSTANCE_IDENTIFIER.get(req.getSignatureAlgorithm()); + if (signatureIdentifier == null) { + return SessionResp.newBuilder() + .setStatus( + Status.newBuilder() + .setCode(255) + .setDetails("Only ECDSA key algorithms are supported.")) + .build(); + } + + byte[] signature; + try { + Signature sig = Signature.getInstance(signatureIdentifier); + sig.initSign(privateKey); + sig.update(req.getRawBytes().toByteArray()); + signature = sig.sign(); + } catch (Exception e) { + return SessionResp.newBuilder() + .setStatus(Status.newBuilder().setCode(255).setDetails(e.getMessage())) + .build(); + } + + return SessionResp.newBuilder() + .setOffloadPrivateKeyOperationResp( + OffloadPrivateKeyOperationResp.newBuilder().setOutBytes(ByteString.copyFrom(signature))) + .build(); + } + + @Override + public void onError(Throwable t) { + throw new UnsupportedOperationException("onError is not supported by FakeWriter."); + } + + @Override + public void onCompleted() { + fakeWriterClosed = true; + reader.onCompleted(); + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/GetAuthenticationMechanismsTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/GetAuthenticationMechanismsTest.java new file mode 100644 index 00000000000..884e1ec88eb --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/GetAuthenticationMechanismsTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import com.google.common.truth.Expect; +import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.handshaker.tokenmanager.SingleTokenFetcher; +import java.util.Optional; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link GetAuthenticationMechanisms}. */ +@RunWith(JUnit4.class) +public final class GetAuthenticationMechanismsTest { + @Rule public final Expect expect = Expect.create(); + private static final String TOKEN = "access_token"; + + @BeforeClass + public static void setUpClass() { + // Set the token that the client will use to authenticate to the S2A. + SingleTokenFetcher.setAccessToken(TOKEN); + } + + @Test + public void getAuthMechanisms_emptyIdentity_success() { + expect + .that(GetAuthenticationMechanisms.getAuthMechanism(Optional.empty())) + .isEqualTo( + Optional.of(AuthenticationMechanism.newBuilder().setToken("access_token").build())); + } + + @Test + public void getAuthMechanisms_nonEmptyIdentity_success() { + S2AIdentity fakeIdentity = S2AIdentity.fromSpiffeId("fake-spiffe-id"); + expect + .that(GetAuthenticationMechanisms.getAuthMechanism(Optional.of(fakeIdentity))) + .isEqualTo( + Optional.of( + AuthenticationMechanism.newBuilder() + .setIdentity(fakeIdentity.getIdentity()) + .setToken("access_token") + .build())); + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java new file mode 100644 index 00000000000..859771a4afa --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java @@ -0,0 +1,320 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.concurrent.TimeUnit.SECONDS; + +import io.grpc.ChannelCredentials; +import io.grpc.Grpc; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.ServerCredentials; +import io.grpc.TlsServerCredentials; +import io.grpc.benchmarks.Utils; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyServerBuilder; +import io.grpc.s2a.MtlsToS2AChannelCredentials; +import io.grpc.s2a.S2AChannelCredentials; +import io.grpc.s2a.handshaker.FakeS2AServer; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.protobuf.SimpleRequest; +import io.grpc.testing.protobuf.SimpleResponse; +import io.grpc.testing.protobuf.SimpleServiceGrpc; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.OpenSslSessionContext; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSessionContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class IntegrationTest { + private static final Logger logger = Logger.getLogger(FakeS2AServer.class.getName()); + + private static final String CERT_CHAIN = + "-----BEGIN CERTIFICATE-----\n" + + "MIICkDCCAjagAwIBAgIUSAtcrPhNNs1zxv51lIfGOVtkw6QwCgYIKoZIzj0EAwIw\n" + + "QTEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xEDAOBgNVBAsMB2NvbnRleHQxFDAS\n" + + "BgorBgEEAdZ5AggBDAQyMDIyMCAXDTIzMDcxNDIyMzYwNFoYDzIwNTAxMTI5MjIz\n" + + "NjA0WjARMQ8wDQYDVQQDDAZ1bnVzZWQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\n" + + "AAQGFlJpLxJMh4HuUm0DKjnUF7larH3tJvroQ12xpk+pPKQepn4ILoq9lZ8Xd3jz\n" + + "U98eDRXG5f4VjnX98DDHE4Ido4IBODCCATQwDgYDVR0PAQH/BAQDAgeAMCAGA1Ud\n" + + "JQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMIGxBgNV\n" + + "HREBAf8EgaYwgaOGSnNwaWZmZTovL3NpZ25lci1yb2xlLmNvbnRleHQuc2VjdXJp\n" + + "dHktcmVhbG0ucHJvZC5nb29nbGUuY29tL3JvbGUvbGVhZi1yb2xlgjNzaWduZXIt\n" + + "cm9sZS5jb250ZXh0LnNlY3VyaXR5LXJlYWxtLnByb2Quc3BpZmZlLmdvb2eCIGZx\n" + + "ZG4tb2YtdGhlLW5vZGUucHJvZC5nb29nbGUuY29tMB0GA1UdDgQWBBSWSd5Fw6dI\n" + + "TGpt0m1Uxwf0iKqebzAfBgNVHSMEGDAWgBRm5agVVdpWfRZKM7u6OMuzHhqPcDAK\n" + + "BggqhkjOPQQDAgNIADBFAiB0sjRPSYy2eFq8Y0vQ8QN4AZ2NMajskvxnlifu7O4U\n" + + "RwIhANTh5Fkyx2nMYFfyl+W45dY8ODTw3HnlZ4b51hTAdkWl\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIICQjCCAeigAwIBAgIUKxXRDlnWXefNV5lj5CwhDuXEq7MwCgYIKoZIzj0EAwIw\n" + + "OzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xEDAOBgNVBAsMB2NvbnRleHQxDjAM\n" + + "BgNVBAMMBTEyMzQ1MCAXDTIzMDcxNDIyMzYwNFoYDzIwNTAxMTI5MjIzNjA0WjBB\n" + + "MRcwFQYDVQQKDA5zZWN1cml0eS1yZWFsbTEQMA4GA1UECwwHY29udGV4dDEUMBIG\n" + + "CisGAQQB1nkCCAEMBDIwMjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT/Zu7x\n" + + "UYVyg+T/vg2H+y4I6t36Kc4qxD0eqqZjRLYBVKkUQHxBqc14t0DpoROMYQCNd4DF\n" + + "pcxv/9m6DaJbRk6Ao4HBMIG+MA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAG\n" + + "AQH/AgEBMFgGA1UdHgEB/wROMEygSjA1gjNzaWduZXItcm9sZS5jb250ZXh0LnNl\n" + + "Y3VyaXR5LXJlYWxtLnByb2Quc3BpZmZlLmdvb2cwEYIPcHJvZC5nb29nbGUuY29t\n" + + "MB0GA1UdDgQWBBRm5agVVdpWfRZKM7u6OMuzHhqPcDAfBgNVHSMEGDAWgBQcjNAh\n" + + "SCHTj+BW8KrzSSLo2ASEgjAKBggqhkjOPQQDAgNIADBFAiEA6KyGd9VxXDZceMZG\n" + + "IsbC40rtunFjLYI0mjZw9RcRWx8CIHCIiIHxafnDaCi+VB99NZfzAdu37g6pJptB\n" + + "gjIY71MO\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIICODCCAd6gAwIBAgIUXtZECORWRSKnS9rRTJYkiALUXswwCgYIKoZIzj0EAwIw\n" + + "NzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xDTALBgNVBAsMBHJvb3QxDTALBgNV\n" + + "BAMMBDEyMzQwIBcNMjMwNzE0MjIzNjA0WhgPMjA1MDExMjkyMjM2MDRaMDsxFzAV\n" + + "BgNVBAoMDnNlY3VyaXR5LXJlYWxtMRAwDgYDVQQLDAdjb250ZXh0MQ4wDAYDVQQD\n" + + "DAUxMjM0NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAycVTZrjockbpD59f1a\n" + + "4l1SNL7nSyXz66Guz4eDveQqLmaMBg7vpACfO4CtiAGnolHEffuRtSkdM434m5En\n" + + "bXCjgcEwgb4wDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQIwWAYD\n" + + "VR0eAQH/BE4wTKBKMDWCM3NpZ25lci1yb2xlLmNvbnRleHQuc2VjdXJpdHktcmVh\n" + + "bG0ucHJvZC5zcGlmZmUuZ29vZzARgg9wcm9kLmdvb2dsZS5jb20wHQYDVR0OBBYE\n" + + "FByM0CFIIdOP4FbwqvNJIujYBISCMB8GA1UdIwQYMBaAFMX+vebuj/lYfYEC23IA\n" + + "8HoIW0HsMAoGCCqGSM49BAMCA0gAMEUCIQCfxeXEBd7UPmeImT16SseCRu/6cHxl\n" + + "kTDsq9sKZ+eXBAIgA+oViAVOUhUQO1/6Mjlczg8NmMy2vNtG4V/7g9dMMVU=\n" + + "-----END CERTIFICATE-----"; + private static final String ROOT_PEM = + "-----BEGIN CERTIFICATE-----\n" + + "MIIBtTCCAVqgAwIBAgIUbAe+8OocndQXRBCElLBxBSdfdV8wCgYIKoZIzj0EAwIw\n" + + "NzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xDTALBgNVBAsMBHJvb3QxDTALBgNV\n" + + "BAMMBDEyMzQwIBcNMjMwNzE0MjIzNjA0WhgPMjA1MDExMjkyMjM2MDRaMDcxFzAV\n" + + "BgNVBAoMDnNlY3VyaXR5LXJlYWxtMQ0wCwYDVQQLDARyb290MQ0wCwYDVQQDDAQx\n" + + "MjM0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaMY2tBW5r1t0+vhayz0ZoGMF\n" + + "boX/ZmmCmIh0iTWg4madvwNOh74CMVVvDUlXZcuVqZ3vVIX/a7PTFVqUwQlKW6NC\n" + + "MEAwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMX+\n" + + "vebuj/lYfYEC23IA8HoIW0HsMAoGCCqGSM49BAMCA0kAMEYCIQDETd27nsUTXKWY\n" + + "CiOno78O09gK95NoTkPU5e2chJYMqAIhALYFAyh7PU5xgFQsN9hiqgsHUc5/pmBG\n" + + "BGjJ1iz8rWGJ\n" + + "-----END CERTIFICATE-----"; + private static final String PRIVATE_KEY = + "-----BEGIN PRIVATE KEY-----\n" + + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgqA2U0ld1OOHLMXWf\n" + + "uyN4GSaqhhudEIaKkll3rdIq0M+hRANCAAQGFlJpLxJMh4HuUm0DKjnUF7larH3t\n" + + "JvroQ12xpk+pPKQepn4ILoq9lZ8Xd3jzU98eDRXG5f4VjnX98DDHE4Id\n" + + "-----END PRIVATE KEY-----"; + + private String s2aAddress; + private int s2aPort; + private Server s2aServer; + private String s2aDelayAddress; + private int s2aDelayPort; + private Server s2aDelayServer; + private String mtlsS2AAddress; + private int mtlsS2APort; + private Server mtlsS2AServer; + private int serverPort; + private String serverAddress; + private Server server; + + @Before + public void setUp() throws Exception { + s2aPort = Utils.pickUnusedPort(); + s2aAddress = "localhost:" + s2aPort; + s2aServer = ServerBuilder.forPort(s2aPort).addService(new FakeS2AServer()).build(); + logger.info("S2A service listening on localhost:" + s2aPort); + s2aServer.start(); + + mtlsS2APort = Utils.pickUnusedPort(); + mtlsS2AAddress = "localhost:" + mtlsS2APort; + File s2aCert = new File("src/test/resources/server_cert.pem"); + File s2aKey = new File("src/test/resources/server_key.pem"); + File rootCert = new File("src/test/resources/root_cert.pem"); + ServerCredentials s2aCreds = + TlsServerCredentials.newBuilder() + .keyManager(s2aCert, s2aKey) + .trustManager(rootCert) + .clientAuth(TlsServerCredentials.ClientAuth.REQUIRE) + .build(); + mtlsS2AServer = + NettyServerBuilder.forPort(mtlsS2APort, s2aCreds).addService(new FakeS2AServer()).build(); + logger.info("mTLS S2A service listening on localhost:" + mtlsS2APort); + mtlsS2AServer.start(); + + s2aDelayPort = Utils.pickUnusedPort(); + s2aDelayAddress = "localhost:" + s2aDelayPort; + s2aDelayServer = ServerBuilder.forPort(s2aDelayPort).addService(new FakeS2AServer()).build(); + + serverPort = Utils.pickUnusedPort(); + serverAddress = "localhost:" + serverPort; + server = + NettyServerBuilder.forPort(serverPort) + .addService(new SimpleServiceImpl()) + .sslContext(buildSslContext()) + .build(); + logger.info("Simple Service listening on localhost:" + serverPort); + server.start(); + } + + @After + public void tearDown() throws Exception { + server.awaitTermination(10, SECONDS); + server.shutdown(); + s2aServer.awaitTermination(10, SECONDS); + s2aServer.shutdown(); + s2aDelayServer.awaitTermination(10, SECONDS); + s2aDelayServer.shutdown(); + mtlsS2AServer.awaitTermination(10, SECONDS); + mtlsS2AServer.shutdown(); + } + + @Test + public void clientCommunicateUsingS2ACredentials_succeeds() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + ChannelCredentials credentials = + S2AChannelCredentials.createBuilder(s2aAddress).setLocalSpiffeId("test-spiffe-id").build(); + ManagedChannel channel = + Grpc.newChannelBuilder(serverAddress, credentials).executor(executor).build(); + + assertThat(doUnaryRpc(executor, channel)).isTrue(); + } + + @Test + public void clientCommunicateUsingS2ACredentialsNoLocalIdentity_succeeds() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + ChannelCredentials credentials = S2AChannelCredentials.createBuilder(s2aAddress).build(); + ManagedChannel channel = + Grpc.newChannelBuilder(serverAddress, credentials).executor(executor).build(); + + assertThat(doUnaryRpc(executor, channel)).isTrue(); + } + + @Test + public void clientCommunicateUsingMtlsToS2ACredentials_succeeds() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + ChannelCredentials credentials = + MtlsToS2AChannelCredentials.createBuilder( + /* s2aAddress= */ mtlsS2AAddress, + /* privateKeyPath= */ "src/test/resources/client_key.pem", + /* certChainPath= */ "src/test/resources/client_cert.pem", + /* trustBundlePath= */ "src/test/resources/root_cert.pem") + .build() + .setLocalSpiffeId("test-spiffe-id") + .build(); + ManagedChannel channel = + Grpc.newChannelBuilder(serverAddress, credentials).executor(executor).build(); + + assertThat(doUnaryRpc(executor, channel)).isTrue(); + } + + @Test + public void clientCommunicateUsingS2ACredentials_s2AdelayStart_succeeds() throws Exception { + DoUnaryRpc doUnaryRpc = new DoUnaryRpc(); + doUnaryRpc.start(); + Thread.sleep(2000); + s2aDelayServer.start(); + doUnaryRpc.join(); + } + + private class DoUnaryRpc extends Thread { + @Override + public void run() { + ExecutorService executor = Executors.newSingleThreadExecutor(); + ChannelCredentials credentials = S2AChannelCredentials.createBuilder(s2aDelayAddress).build(); + ManagedChannel channel = + Grpc.newChannelBuilder(serverAddress, credentials).executor(executor).build(); + boolean result = false; + try { + result = doUnaryRpc(executor, channel); + } catch (InterruptedException e) { + logger.log(Level.SEVERE, "Failed to do unary rpc", e); + result = false; + } + assertThat(result).isTrue(); + } + } + + public static boolean doUnaryRpc(ExecutorService executor, ManagedChannel channel) + throws InterruptedException { + try { + SimpleServiceGrpc.SimpleServiceBlockingStub stub = + SimpleServiceGrpc.newBlockingStub(channel); + SimpleResponse resp = stub.unaryRpc(SimpleRequest.newBuilder() + .setRequestMessage("S2A team") + .build()); + if (!resp.getResponseMessage().equals("Hello, S2A team!")) { + logger.info( + "Received unexpected message from the Simple Service: " + resp.getResponseMessage()); + throw new RuntimeException(); + } else { + System.out.println( + "We received this message from the Simple Service: " + resp.getResponseMessage()); + return true; + } + } finally { + channel.shutdown(); + channel.awaitTermination(1, SECONDS); + executor.shutdown(); + executor.awaitTermination(1, SECONDS); + } + } + + private static SslContext buildSslContext() throws SSLException { + SslContextBuilder sslServerContextBuilder = + SslContextBuilder.forServer( + new ByteArrayInputStream(CERT_CHAIN.getBytes(UTF_8)), + new ByteArrayInputStream(PRIVATE_KEY.getBytes(UTF_8))); + SslContext sslServerContext = + GrpcSslContexts.configure(sslServerContextBuilder, SslProvider.OPENSSL) + .protocols("TLSv1.3", "TLSv1.2") + .trustManager(new ByteArrayInputStream(ROOT_PEM.getBytes(UTF_8))) + .clientAuth(ClientAuth.REQUIRE) + .build(); + + // Enable TLS resumption. This requires using the OpenSSL provider, since the JDK provider does + // not allow a server to send session tickets. + SSLSessionContext sslSessionContext = sslServerContext.sessionContext(); + if (!(sslSessionContext instanceof OpenSslSessionContext)) { + throw new SSLException("sslSessionContext does not use OpenSSL."); + } + OpenSslSessionContext openSslSessionContext = (OpenSslSessionContext) sslSessionContext; + // Calling {@code setTicketKeys} without specifying any keys means that the SSL libraries will + // handle the generation of the resumption master secret. + openSslSessionContext.setTicketKeys(); + + return sslServerContext; + } + + public static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase { + @Override + public void unaryRpc(SimpleRequest request, StreamObserver observer) { + observer.onNext( + SimpleResponse.newBuilder() + .setResponseMessage("Hello, " + request.getRequestMessage() + "!") + .build()); + observer.onCompleted(); + } + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/ProtoUtilTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/ProtoUtilTest.java new file mode 100644 index 00000000000..6d134b43f7a --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/ProtoUtilTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableSet; +import com.google.common.truth.Expect; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ProtoUtil}. */ +@RunWith(JUnit4.class) +public final class ProtoUtilTest { + @Rule public final Expect expect = Expect.create(); + + @Test + public void convertCiphersuite_success() { + expect + .that( + ProtoUtil.convertCiphersuite( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)) + .isEqualTo("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"); + expect + .that( + ProtoUtil.convertCiphersuite( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384)) + .isEqualTo("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"); + expect + .that( + ProtoUtil.convertCiphersuite( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256)) + .isEqualTo("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"); + expect + .that( + ProtoUtil.convertCiphersuite(Ciphersuite.CIPHERSUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256)) + .isEqualTo("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); + expect + .that( + ProtoUtil.convertCiphersuite(Ciphersuite.CIPHERSUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384)) + .isEqualTo("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"); + expect + .that( + ProtoUtil.convertCiphersuite( + Ciphersuite.CIPHERSUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256)) + .isEqualTo("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"); + } + + @Test + public void convertCiphersuite_withUnspecifiedCiphersuite_fails() { + AssertionError expected = + assertThrows( + AssertionError.class, + () -> ProtoUtil.convertCiphersuite(Ciphersuite.CIPHERSUITE_UNSPECIFIED)); + expect.that(expected).hasMessageThat().isEqualTo("Ciphersuite 0 is not supported."); + } + + @Test + public void convertTlsProtocolVersion_success() { + expect + .that(ProtoUtil.convertTlsProtocolVersion(TLSVersion.TLS_VERSION_1_3)) + .isEqualTo("TLSv1.3"); + expect + .that(ProtoUtil.convertTlsProtocolVersion(TLSVersion.TLS_VERSION_1_2)) + .isEqualTo("TLSv1.2"); + expect + .that(ProtoUtil.convertTlsProtocolVersion(TLSVersion.TLS_VERSION_1_1)) + .isEqualTo("TLSv1.1"); + expect.that(ProtoUtil.convertTlsProtocolVersion(TLSVersion.TLS_VERSION_1_0)).isEqualTo("TLSv1"); + } + + @Test + public void convertTlsProtocolVersion_withUnknownTlsVersion_fails() { + AssertionError expected = + assertThrows( + AssertionError.class, + () -> ProtoUtil.convertTlsProtocolVersion(TLSVersion.TLS_VERSION_UNSPECIFIED)); + expect.that(expected).hasMessageThat().isEqualTo("TLS version 0 is not supported."); + } + + @Test + public void buildTlsProtocolVersionSet_success() { + expect + .that( + ProtoUtil.buildTlsProtocolVersionSet( + TLSVersion.TLS_VERSION_1_0, TLSVersion.TLS_VERSION_1_3)) + .isEqualTo(ImmutableSet.of("TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3")); + expect + .that( + ProtoUtil.buildTlsProtocolVersionSet( + TLSVersion.TLS_VERSION_1_2, TLSVersion.TLS_VERSION_1_2)) + .isEqualTo(ImmutableSet.of("TLSv1.2")); + expect + .that( + ProtoUtil.buildTlsProtocolVersionSet( + TLSVersion.TLS_VERSION_1_3, TLSVersion.TLS_VERSION_1_3)) + .isEqualTo(ImmutableSet.of("TLSv1.3")); + expect + .that( + ProtoUtil.buildTlsProtocolVersionSet( + TLSVersion.TLS_VERSION_1_3, TLSVersion.TLS_VERSION_1_2)) + .isEmpty(); + } + + @Test + public void buildTlsProtocolVersionSet_failure() { + AssertionError expected = + assertThrows( + AssertionError.class, + () -> + ProtoUtil.buildTlsProtocolVersionSet( + TLSVersion.TLS_VERSION_UNSPECIFIED, TLSVersion.TLS_VERSION_1_3)); + expect.that(expected).hasMessageThat().isEqualTo("TLS version 0 is not supported."); + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2APrivateKeyMethodTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/S2APrivateKeyMethodTest.java new file mode 100644 index 00000000000..8252aa245d7 --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/S2APrivateKeyMethodTest.java @@ -0,0 +1,308 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.truth.Expect; +import com.google.protobuf.ByteString; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.s2a.handshaker.S2AIdentity; +import io.netty.handler.ssl.OpenSslPrivateKeyMethod; +import io.netty.handler.ssl.SslContextBuilder; +import java.io.ByteArrayInputStream; +import java.security.PublicKey; +import java.security.Signature; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Optional; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class S2APrivateKeyMethodTest { + @Rule public final Expect expect = Expect.create(); + private static final byte[] DATA_TO_SIGN = "random bytes for signing.".getBytes(UTF_8); + + private S2AStub stub; + private FakeWriter writer; + private S2APrivateKeyMethod keyMethod; + + private static PublicKey extractPublicKeyFromPem(String pem) throws Exception { + X509Certificate cert = + (X509Certificate) + CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(pem.getBytes(UTF_8))); + return cert.getPublicKey(); + } + + private static boolean verifySignature( + byte[] dataToSign, byte[] signature, String signatureAlgorithm) throws Exception { + Signature sig = Signature.getInstance(signatureAlgorithm); + sig.initVerify(extractPublicKeyFromPem(FakeWriter.LEAF_CERT)); + sig.update(dataToSign); + return sig.verify(signature); + } + + @Before + public void setUp() { + // This is line is to ensure that JNI correctly links the necessary objects. Without this, we + // get `java.lang.UnsatisfiedLinkError` on + // `io.netty.internal.tcnative.NativeStaticallyReferencedJniMethods.sslSignRsaPkcsSha1()` + GrpcSslContexts.configure(SslContextBuilder.forClient()); + + writer = new FakeWriter(); + stub = S2AStub.newInstanceForTesting(writer); + writer.setReader(stub.getReader()); + keyMethod = S2APrivateKeyMethod.create(stub, /* localIdentity= */ Optional.empty()); + } + + @Test + public void signatureAlgorithmConversion_success() { + expect + .that( + S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg( + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256)) + .isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_RSA_PKCS1_SHA256); + expect + .that( + S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg( + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA384)) + .isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_RSA_PKCS1_SHA384); + expect + .that( + S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg( + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA512)) + .isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_RSA_PKCS1_SHA512); + expect + .that( + S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg( + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256)) + .isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP256R1_SHA256); + expect + .that( + S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg( + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP384R1_SHA384)) + .isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP384R1_SHA384); + expect + .that( + S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg( + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP521R1_SHA512)) + .isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP521R1_SHA512); + expect + .that( + S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg( + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256)) + .isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_RSA_PSS_RSAE_SHA256); + expect + .that( + S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg( + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA384)) + .isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_RSA_PSS_RSAE_SHA384); + expect + .that( + S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg( + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA512)) + .isEqualTo(SignatureAlgorithm.S2A_SSL_SIGN_RSA_PSS_RSAE_SHA512); + } + + @Test + public void signatureAlgorithmConversion_unsupportedOperation() { + UnsupportedOperationException e = + assertThrows( + UnsupportedOperationException.class, + () -> S2APrivateKeyMethod.convertOpenSslSignAlgToS2ASignAlg(-1)); + + assertThat(e).hasMessageThat().contains("Signature Algorithm -1 is not supported."); + } + + @Test + public void createOnNullStub_returnsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> S2APrivateKeyMethod.create(/* stub= */ null, /* localIdentity= */ Optional.empty())); + } + + @Test + public void decrypt_unsupportedOperation() { + UnsupportedOperationException e = + assertThrows( + UnsupportedOperationException.class, + () -> keyMethod.decrypt(/* engine= */ null, DATA_TO_SIGN)); + + assertThat(e).hasMessageThat().contains("decrypt is not supported."); + } + + @Test + public void fakelocalIdentity_signWithSha256_success() throws Exception { + S2AIdentity fakeIdentity = S2AIdentity.fromSpiffeId("fake-spiffe-id"); + S2AStub mockStub = mock(S2AStub.class); + OpenSslPrivateKeyMethod keyMethodWithFakeIdentity = + S2APrivateKeyMethod.create(mockStub, Optional.of(fakeIdentity)); + SessionReq req = + SessionReq.newBuilder() + .setLocalIdentity(fakeIdentity.getIdentity()) + .setOffloadPrivateKeyOperationReq( + OffloadPrivateKeyOperationReq.newBuilder() + .setOperation(OffloadPrivateKeyOperationReq.PrivateKeyOperation.SIGN) + .setSignatureAlgorithm(SignatureAlgorithm.S2A_SSL_SIGN_ECDSA_SECP256R1_SHA256) + .setRawBytes(ByteString.copyFrom(DATA_TO_SIGN))) + .build(); + byte[] expectedOutbytes = "fake out bytes".getBytes(UTF_8); + when(mockStub.send(req)) + .thenReturn( + SessionResp.newBuilder() + .setOffloadPrivateKeyOperationResp( + OffloadPrivateKeyOperationResp.newBuilder() + .setOutBytes(ByteString.copyFrom(expectedOutbytes))) + .build()); + + byte[] signature = + keyMethodWithFakeIdentity.sign( + /* engine= */ null, + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256, + DATA_TO_SIGN); + verify(mockStub).send(req); + assertThat(signature).isEqualTo(expectedOutbytes); + } + + @Test + public void signWithSha256_success() throws Exception { + writer.initializePrivateKey().setBehavior(FakeWriter.Behavior.OK_STATUS); + + byte[] signature = + keyMethod.sign( + /* engine= */ null, + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256, + DATA_TO_SIGN); + + assertThat(signature).isNotEmpty(); + assertThat(verifySignature(DATA_TO_SIGN, signature, "SHA256withECDSA")).isTrue(); + } + + @Test + public void signWithSha384_success() throws Exception { + writer.initializePrivateKey().setBehavior(FakeWriter.Behavior.OK_STATUS); + + byte[] signature = + keyMethod.sign( + /* engine= */ null, + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP384R1_SHA384, + DATA_TO_SIGN); + + assertThat(signature).isNotEmpty(); + assertThat(verifySignature(DATA_TO_SIGN, signature, "SHA384withECDSA")).isTrue(); + } + + @Test + public void signWithSha512_success() throws Exception { + writer.initializePrivateKey().setBehavior(FakeWriter.Behavior.OK_STATUS); + + byte[] signature = + keyMethod.sign( + /* engine= */ null, + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP521R1_SHA512, + DATA_TO_SIGN); + + assertThat(signature).isNotEmpty(); + assertThat(verifySignature(DATA_TO_SIGN, signature, "SHA512withECDSA")).isTrue(); + } + + @Test + public void sign_noKeyAvailable() throws Exception { + writer.resetPrivateKey().setBehavior(FakeWriter.Behavior.OK_STATUS); + + S2AConnectionException e = + assertThrows( + S2AConnectionException.class, + () -> + keyMethod.sign( + /* engine= */ null, + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256, + DATA_TO_SIGN)); + + assertThat(e) + .hasMessageThat() + .contains( + "Error occurred in response from S2A, error code: 255, error message: \"No Private Key" + + " available.\"."); + } + + @Test + public void sign_algorithmNotSupported() throws Exception { + writer.initializePrivateKey().setBehavior(FakeWriter.Behavior.OK_STATUS); + + S2AConnectionException e = + assertThrows( + S2AConnectionException.class, + () -> + keyMethod.sign( + /* engine= */ null, + OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256, + DATA_TO_SIGN)); + + assertThat(e) + .hasMessageThat() + .contains( + "Error occurred in response from S2A, error code: 255, error message: \"Only ECDSA key" + + " algorithms are supported.\"."); + } + + @Test + public void sign_getsErrorResponse() throws Exception { + writer.initializePrivateKey().setBehavior(FakeWriter.Behavior.ERROR_STATUS); + + S2AConnectionException e = + assertThrows( + S2AConnectionException.class, + () -> + keyMethod.sign( + /* engine= */ null, + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256, + DATA_TO_SIGN)); + + assertThat(e) + .hasMessageThat() + .contains( + "Error occurred in response from S2A, error code: 1, error message: \"Intended ERROR" + + " Status from FakeWriter.\"."); + } + + @Test + public void sign_getsEmptyResponse() throws Exception { + writer.initializePrivateKey().setBehavior(FakeWriter.Behavior.EMPTY_RESPONSE); + + S2AConnectionException e = + assertThrows( + S2AConnectionException.class, + () -> + keyMethod.sign( + /* engine= */ null, + OpenSslPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256, + DATA_TO_SIGN)); + + assertThat(e).hasMessageThat().contains("No valid response received from S2A."); + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java new file mode 100644 index 00000000000..f130e52aac7 --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java @@ -0,0 +1,284 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.google.common.testing.NullPointerTester; +import com.google.common.testing.NullPointerTester.Visibility; +import io.grpc.Channel; +import io.grpc.Grpc; +import io.grpc.InsecureChannelCredentials; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.benchmarks.Utils; +import io.grpc.internal.ObjectPool; +import io.grpc.internal.SharedResourcePool; +import io.grpc.internal.TestUtils.NoopChannelLogger; +import io.grpc.netty.GrpcHttp2ConnectionHandler; +import io.grpc.netty.InternalProtocolNegotiator; +import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; +import io.grpc.s2a.channel.S2AChannelPool; +import io.grpc.s2a.channel.S2AGrpcChannelPool; +import io.grpc.s2a.channel.S2AHandshakerServiceChannel; +import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.handshaker.S2AProtocolNegotiatorFactory.S2AProtocolNegotiator; +import io.grpc.stub.StreamObserver; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http2.Http2ConnectionDecoder; +import io.netty.handler.codec.http2.Http2ConnectionEncoder; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.util.AsciiString; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link S2AProtocolNegotiatorFactory}. */ +@RunWith(JUnit4.class) +public class S2AProtocolNegotiatorFactoryTest { + private static final S2AIdentity LOCAL_IDENTITY = S2AIdentity.fromSpiffeId("local identity"); + private final ChannelHandlerContext mockChannelHandlerContext = mock(ChannelHandlerContext.class); + private GrpcHttp2ConnectionHandler fakeConnectionHandler; + private String authority; + private int port; + private Server fakeS2AServer; + private ObjectPool channelPool; + + @Before + public void setUp() throws Exception { + port = Utils.pickUnusedPort(); + fakeS2AServer = ServerBuilder.forPort(port).addService(new S2AServiceImpl()).build(); + fakeS2AServer.start(); + channelPool = new FakeChannelPool(); + authority = "localhost:" + port; + fakeConnectionHandler = FakeConnectionHandler.create(authority); + } + + @After + public void tearDown() { + fakeS2AServer.shutdown(); + } + + @Test + public void handlerRemoved_success() throws Exception { + S2AProtocolNegotiatorFactory.BufferReadsHandler handler1 = + new S2AProtocolNegotiatorFactory.BufferReadsHandler(); + S2AProtocolNegotiatorFactory.BufferReadsHandler handler2 = + new S2AProtocolNegotiatorFactory.BufferReadsHandler(); + EmbeddedChannel channel = new EmbeddedChannel(handler1, handler2); + channel.writeInbound("message1"); + channel.writeInbound("message2"); + channel.writeInbound("message3"); + assertThat(handler1.getReads()).hasSize(3); + assertThat(handler2.getReads()).isEmpty(); + channel.pipeline().remove(handler1); + assertThat(handler2.getReads()).hasSize(3); + } + + @Test + public void createProtocolNegotiatorFactory_nullArgument() throws Exception { + NullPointerTester tester = new NullPointerTester().setDefault(Optional.class, Optional.empty()); + + tester.testStaticMethods(S2AProtocolNegotiatorFactory.class, Visibility.PUBLIC); + } + + @Test + public void createProtocolNegotiator_nullArgument() throws Exception { + S2AChannelPool pool = + S2AGrpcChannelPool.create( + SharedResourcePool.forResource( + S2AHandshakerServiceChannel.getChannelResource( + "localhost:8080", /* s2aChannelCredentials= */ Optional.empty()))); + + NullPointerTester tester = + new NullPointerTester() + .setDefault(S2AChannelPool.class, pool) + .setDefault(Optional.class, Optional.empty()); + + tester.testStaticMethods(S2AProtocolNegotiator.class, Visibility.PACKAGE); + } + + @Test + public void createProtocolNegotiatorFactory_getsDefaultPort_succeeds() throws Exception { + InternalProtocolNegotiator.ClientFactory clientFactory = + S2AProtocolNegotiatorFactory.createClientFactory(LOCAL_IDENTITY, channelPool); + + assertThat(clientFactory.getDefaultPort()).isEqualTo(S2AProtocolNegotiatorFactory.DEFAULT_PORT); + } + + @Test + public void s2aProtocolNegotiator_getHostNameOnNull_returnsNull() throws Exception { + assertThat(S2AProtocolNegotiatorFactory.S2AProtocolNegotiator.getHostNameFromAuthority(null)) + .isNull(); + } + + @Test + public void s2aProtocolNegotiator_getHostNameOnValidAuthority_returnsValidHostname() + throws Exception { + assertThat( + S2AProtocolNegotiatorFactory.S2AProtocolNegotiator.getHostNameFromAuthority( + "hostname:80")) + .isEqualTo("hostname"); + } + + @Test + public void createProtocolNegotiatorFactory_buildsAnS2AProtocolNegotiatorOnClientSide_succeeds() + throws Exception { + InternalProtocolNegotiator.ClientFactory clientFactory = + S2AProtocolNegotiatorFactory.createClientFactory(LOCAL_IDENTITY, channelPool); + + ProtocolNegotiator clientNegotiator = clientFactory.newNegotiator(); + + assertThat(clientNegotiator).isInstanceOf(S2AProtocolNegotiator.class); + assertThat(clientNegotiator.scheme()).isEqualTo(AsciiString.of("https")); + } + + @Test + public void closeProtocolNegotiator_verifyProtocolNegotiatorIsClosedOnClientSide() + throws Exception { + InternalProtocolNegotiator.ClientFactory clientFactory = + S2AProtocolNegotiatorFactory.createClientFactory(LOCAL_IDENTITY, channelPool); + ProtocolNegotiator clientNegotiator = clientFactory.newNegotiator(); + + clientNegotiator.close(); + + assertThat(((FakeChannelPool) channelPool).isChannelCached()).isFalse(); + } + + @Test + public void createChannelHandler_addHandlerToMockContext() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + ManagedChannel channel = + Grpc.newChannelBuilder(authority, InsecureChannelCredentials.create()) + .executor(executor) + .build(); + FakeS2AChannelPool fakeChannelPool = new FakeS2AChannelPool(channel); + ProtocolNegotiator clientNegotiator = + S2AProtocolNegotiatorFactory.S2AProtocolNegotiator.createForClient( + fakeChannelPool, LOCAL_IDENTITY); + + ChannelHandler channelHandler = clientNegotiator.newHandler(fakeConnectionHandler); + + ((ChannelDuplexHandler) channelHandler).userEventTriggered(mockChannelHandlerContext, "event"); + verify(mockChannelHandlerContext).fireUserEventTriggered("event"); + } + + /** A {@link S2AChannelPool} that returns the given channel. */ + private static class FakeS2AChannelPool implements S2AChannelPool { + private final Channel channel; + + FakeS2AChannelPool(Channel channel) { + this.channel = channel; + } + + @Override + public Channel getChannel() { + return channel; + } + + @Override + public void returnToPool(Channel channel) {} + + @Override + public void close() {} + } + + /** A {@code GrpcHttp2ConnectionHandler} that does nothing. */ + private static class FakeConnectionHandler extends GrpcHttp2ConnectionHandler { + private static final Http2ConnectionDecoder DECODER = mock(Http2ConnectionDecoder.class); + private static final Http2ConnectionEncoder ENCODER = mock(Http2ConnectionEncoder.class); + private static final Http2Settings SETTINGS = new Http2Settings(); + private final String authority; + + static FakeConnectionHandler create(String authority) { + return new FakeConnectionHandler(null, DECODER, ENCODER, SETTINGS, authority); + } + + private FakeConnectionHandler( + ChannelPromise channelUnused, + Http2ConnectionDecoder decoder, + Http2ConnectionEncoder encoder, + Http2Settings initialSettings, + String authority) { + super(channelUnused, decoder, encoder, initialSettings, new NoopChannelLogger()); + this.authority = authority; + } + + @Override + public String getAuthority() { + return authority; + } + } + + /** An S2A server that handles GetTlsConfiguration request. */ + private static class S2AServiceImpl extends S2AServiceGrpc.S2AServiceImplBase { + static final FakeWriter writer = new FakeWriter(); + + @Override + public StreamObserver setUpSession(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(SessionReq req) { + responseObserver.onNext(writer.handleResponse(req)); + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() {} + }; + } + } + + private static class FakeChannelPool implements ObjectPool { + private final Channel mockChannel = mock(Channel.class); + private @Nullable Channel cachedChannel = null; + + @Override + public Channel getObject() { + if (cachedChannel == null) { + cachedChannel = mockChannel; + } + return cachedChannel; + } + + @Override + public Channel returnObject(Object object) { + assertThat(object).isSameInstanceAs(mockChannel); + cachedChannel = null; + return null; + } + + public boolean isChannelCached() { + return (cachedChannel != null); + } + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java new file mode 100644 index 00000000000..bb90be12b6a --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java @@ -0,0 +1,260 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.truth.Expect; +import io.grpc.internal.SharedResourcePool; +import io.grpc.s2a.channel.S2AChannelPool; +import io.grpc.s2a.channel.S2AGrpcChannelPool; +import io.grpc.s2a.channel.S2AHandshakerServiceChannel; +import io.grpc.stub.StreamObserver; +import java.io.IOException; +import java.util.Optional; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link S2AStub}. */ +@RunWith(JUnit4.class) +public class S2AStubTest { + @Rule public final Expect expect = Expect.create(); + private static final String S2A_ADDRESS = "localhost:8080"; + private S2AStub stub; + private FakeWriter writer; + + @Before + public void setUp() { + writer = new FakeWriter(); + stub = S2AStub.newInstanceForTesting(writer); + writer.setReader(stub.getReader()); + } + + @Test + public void send_receiveOkStatus() throws Exception { + S2AChannelPool channelPool = + S2AGrpcChannelPool.create( + SharedResourcePool.forResource( + S2AHandshakerServiceChannel.getChannelResource( + S2A_ADDRESS, /* s2aChannelCredentials= */ Optional.empty()))); + S2AServiceGrpc.S2AServiceStub serviceStub = S2AServiceGrpc.newStub(channelPool.getChannel()); + S2AStub newStub = S2AStub.newInstance(serviceStub); + + IOException expected = + assertThrows(IOException.class, () -> newStub.send(SessionReq.getDefaultInstance())); + + assertThat(expected).hasMessageThat().contains("DEADLINE_EXCEEDED"); + } + + @Test + public void send_clientTlsConfiguration_receiveOkStatus() throws Exception { + SessionReq req = + SessionReq.newBuilder() + .setGetTlsConfigurationReq( + GetTlsConfigurationReq.newBuilder() + .setConnectionSide(ConnectionSide.CONNECTION_SIDE_CLIENT)) + .build(); + + SessionResp resp = stub.send(req); + + SessionResp expected = + SessionResp.newBuilder() + .setGetTlsConfigurationResp( + GetTlsConfigurationResp.newBuilder() + .setClientTlsConfiguration( + GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() + .addCertificateChain(FakeWriter.LEAF_CERT) + .addCertificateChain(FakeWriter.INTERMEDIATE_CERT_2) + .addCertificateChain(FakeWriter.INTERMEDIATE_CERT_1) + .setMinTlsVersion(TLSVersion.TLS_VERSION_1_3) + .setMaxTlsVersion(TLSVersion.TLS_VERSION_1_3) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256))) + .build(); + assertThat(resp).ignoringRepeatedFieldOrder().isEqualTo(expected); + } + + @Test + public void send_serverTlsConfiguration_receiveErrorStatus() throws Exception { + SessionReq req = + SessionReq.newBuilder() + .setGetTlsConfigurationReq( + GetTlsConfigurationReq.newBuilder() + .setConnectionSide(ConnectionSide.CONNECTION_SIDE_SERVER)) + .build(); + + SessionResp resp = stub.send(req); + + SessionResp expected = + SessionResp.newBuilder() + .setStatus( + Status.newBuilder() + .setCode(255) + .setDetails("No TLS configuration for the server side.")) + .build(); + assertThat(resp).isEqualTo(expected); + } + + @Test + public void send_receiveErrorStatus() throws Exception { + writer.setBehavior(FakeWriter.Behavior.ERROR_STATUS); + + SessionResp resp = stub.send(SessionReq.getDefaultInstance()); + + SessionResp expected = + SessionResp.newBuilder() + .setStatus( + Status.newBuilder().setCode(1).setDetails("Intended ERROR Status from FakeWriter.")) + .build(); + assertThat(resp).isEqualTo(expected); + } + + @Test + public void send_receiveErrorResponse() throws InterruptedException { + writer.setBehavior(FakeWriter.Behavior.ERROR_RESPONSE); + + IOException expected = + assertThrows(IOException.class, () -> stub.send(SessionReq.getDefaultInstance())); + + expect.that(expected).hasCauseThat().isInstanceOf(RuntimeException.class); + expect.that(expected).hasMessageThat().contains("Intended ERROR from FakeWriter."); + } + + @Test + public void send_receiveCompleteStatus() throws Exception { + writer.setBehavior(FakeWriter.Behavior.COMPLETE_STATUS); + + ConnectionClosedException expected = + assertThrows( + ConnectionClosedException.class, () -> stub.send(SessionReq.getDefaultInstance())); + + assertThat(expected).hasMessageThat().contains("Reading from the S2A is complete."); + } + + @Test + public void send_receiveUnexpectedResponse() throws Exception { + writer.sendIoError(); + + IOException expected = + assertThrows(IOException.class, () -> stub.send(SessionReq.getDefaultInstance())); + + assertThat(expected) + .hasMessageThat() + .contains( + "Received an unexpected response from a host at the S2A's address. The S2A might be" + + " unavailable."); + } + + @Test + public void send_receiveManyUnexpectedResponse_expectResponsesEmpty() throws Exception { + writer.sendIoError(); + writer.sendIoError(); + writer.sendIoError(); + + IOException expected = + assertThrows(IOException.class, () -> stub.send(SessionReq.getDefaultInstance())); + + assertThat(expected) + .hasMessageThat() + .contains( + "Received an unexpected response from a host at the S2A's address. The S2A might be" + + " unavailable."); + + assertThat(stub.getResponses()).isEmpty(); + } + + @Test + public void send_receiveDelayedResponse() throws Exception { + writer.sendGetTlsConfigResp(); + SessionResp resp = stub.send(SessionReq.getDefaultInstance()); + SessionResp expected = + SessionResp.newBuilder() + .setGetTlsConfigurationResp( + GetTlsConfigurationResp.newBuilder() + .setClientTlsConfiguration( + GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() + .addCertificateChain(FakeWriter.LEAF_CERT) + .addCertificateChain(FakeWriter.INTERMEDIATE_CERT_2) + .addCertificateChain(FakeWriter.INTERMEDIATE_CERT_1) + .setMinTlsVersion(TLSVersion.TLS_VERSION_1_3) + .setMaxTlsVersion(TLSVersion.TLS_VERSION_1_3) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256))) + .build(); + assertThat(resp).ignoringRepeatedFieldOrder().isEqualTo(expected); + } + + @Test + public void send_afterEarlyClose_receivesClosedException() throws InterruptedException { + stub.close(); + expect.that(writer.isFakeWriterClosed()).isTrue(); + + ConnectionClosedException expected = + assertThrows( + ConnectionClosedException.class, () -> stub.send(SessionReq.getDefaultInstance())); + + assertThat(expected).hasMessageThat().contains("Stream to the S2A is closed."); + } + + @Test + public void send_failToWrite() throws Exception { + FailWriter failWriter = new FailWriter(); + stub = S2AStub.newInstanceForTesting(failWriter); + + IOException expected = + assertThrows(IOException.class, () -> stub.send(SessionReq.getDefaultInstance())); + + expect.that(expected).hasCauseThat().isInstanceOf(S2AConnectionException.class); + expect + .that(expected) + .hasCauseThat() + .hasMessageThat() + .isEqualTo("Could not send request to S2A."); + } + + /** Fails whenever a write is attempted. */ + private static class FailWriter implements StreamObserver { + @Override + public void onNext(SessionReq req) { + assertThat(req).isNotNull(); + throw new S2AConnectionException("Could not send request to S2A."); + } + + @Override + public void onError(Throwable t) { + assertThat(t).isInstanceOf(S2AConnectionException.class); + } + + @Override + public void onCompleted() { + throw new UnsupportedOperationException(); + } + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2ATrustManagerTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/S2ATrustManagerTest.java new file mode 100644 index 00000000000..384e1aba5cc --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/S2ATrustManagerTest.java @@ -0,0 +1,262 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import io.grpc.s2a.handshaker.S2AIdentity; +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class S2ATrustManagerTest { + private S2AStub stub; + private FakeWriter writer; + private static final String FAKE_HOSTNAME = "Fake-Hostname"; + private static final String CLIENT_CERT_PEM = + "MIICKjCCAc+gAwIBAgIUC2GShcVO+5Zkml+7VO3OQ+B2c7EwCgYIKoZIzj0EAwIw" + + "HzEdMBsGA1UEAwwUcm9vdGNlcnQuZXhhbXBsZS5jb20wIBcNMjMwMTI2MTk0OTUx" + + "WhgPMjA1MDA2MTMxOTQ5NTFaMB8xHTAbBgNVBAMMFGxlYWZjZXJ0LmV4YW1wbGUu" + + "Y29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeciYZgFAZjxyzTrklCRIWpad" + + "8wkyCZQzJSf0IfNn9NKtfzL2V/blteULO0o9Da8e2Avaj+XCKfFTc7salMo/waOB" + + "5jCB4zAOBgNVHQ8BAf8EBAMCB4AwIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwIGCCsG" + + "AQUFBwMBMAwGA1UdEwEB/wQCMAAwYQYDVR0RBFowWIYic3BpZmZlOi8vZm9vLnBy" + + "b2QuZ29vZ2xlLmNvbS9wMS9wMoIUZm9vLnByb2Quc3BpZmZlLmdvb2eCHG1hY2hp" + + "bmUtbmFtZS5wcm9kLmdvb2dsZS5jb20wHQYDVR0OBBYEFETY6Cu/aW924nfvUrOs" + + "yXCC1hrpMB8GA1UdIwQYMBaAFJLkXGlTYKISiGd+K/Ijh4IOEpHBMAoGCCqGSM49" + + "BAMCA0kAMEYCIQCZDW472c1/4jEOHES/88X7NTqsYnLtIpTjp5PZ62z3sAIhAN1J" + + "vxvbxt9ySdFO+cW7oLBEkCwUicBhxJi5VfQeQypT"; + + @Before + public void setUp() { + writer = new FakeWriter(); + stub = S2AStub.newInstanceForTesting(writer); + writer.setReader(stub.getReader()); + } + + @Test + public void createForClient_withNullStub_throwsError() { + NullPointerException expected = + assertThrows( + NullPointerException.class, + () -> + S2ATrustManager.createForClient( + /* stub= */ null, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty())); + + assertThat(expected).hasMessageThat().isNull(); + } + + @Test + public void createForClient_withNullHostname_throwsError() { + NullPointerException expected = + assertThrows( + NullPointerException.class, + () -> + S2ATrustManager.createForClient( + stub, /* hostname= */ null, /* localIdentity= */ Optional.empty())); + + assertThat(expected).hasMessageThat().isNull(); + } + + @Test + public void getAcceptedIssuers_returnsExpectedNullResult() { + S2ATrustManager trustManager = + S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty()); + + assertThat(trustManager.getAcceptedIssuers()).isNull(); + } + + @Test + public void checkClientTrusted_withEmptyCertificateChain_throwsException() + throws CertificateException { + writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS); + S2ATrustManager trustManager = + S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty()); + + IllegalArgumentException expected = + assertThrows( + IllegalArgumentException.class, + () -> trustManager.checkClientTrusted(new X509Certificate[] {}, /* authType= */ "")); + + assertThat(expected).hasMessageThat().contains("Certificate chain has zero certificates."); + } + + @Test + public void checkServerTrusted_withEmptyCertificateChain_throwsException() + throws CertificateException { + writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS); + S2ATrustManager trustManager = + S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty()); + + IllegalArgumentException expected = + assertThrows( + IllegalArgumentException.class, + () -> trustManager.checkServerTrusted(new X509Certificate[] {}, /* authType= */ "")); + + assertThat(expected).hasMessageThat().contains("Certificate chain has zero certificates."); + } + + @Test + public void checkClientTrusted_getsSuccessResponse() throws CertificateException { + writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS); + S2ATrustManager trustManager = + S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty()); + + // Expect no exception. + trustManager.checkClientTrusted(getCerts(), /* authType= */ ""); + } + + @Test + public void checkClientTrusted_withLocalIdentity_getsSuccessResponse() + throws CertificateException { + writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS); + S2ATrustManager trustManager = + S2ATrustManager.createForClient( + stub, FAKE_HOSTNAME, Optional.of(S2AIdentity.fromSpiffeId("fake-spiffe-id"))); + + // Expect no exception. + trustManager.checkClientTrusted(getCerts(), /* authType= */ ""); + } + + @Test + public void checkServerTrusted_getsSuccessResponse() throws CertificateException { + writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS); + S2ATrustManager trustManager = + S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty()); + + // Expect no exception. + trustManager.checkServerTrusted(getCerts(), /* authType= */ ""); + } + + @Test + public void checkServerTrusted_withLocalIdentity_getsSuccessResponse() + throws CertificateException { + writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS); + S2ATrustManager trustManager = + S2ATrustManager.createForClient( + stub, FAKE_HOSTNAME, Optional.of(S2AIdentity.fromSpiffeId("fake-spiffe-id"))); + + // Expect no exception. + trustManager.checkServerTrusted(getCerts(), /* authType= */ ""); + } + + @Test + public void checkClientTrusted_getsIntendedFailureResponse() throws CertificateException { + writer + .setVerificationResult(FakeWriter.VerificationResult.FAILURE) + .setFailureReason("Intended failure."); + S2ATrustManager trustManager = + S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty()); + + CertificateException expected = + assertThrows( + CertificateException.class, + () -> trustManager.checkClientTrusted(getCerts(), /* authType= */ "")); + + assertThat(expected).hasMessageThat().contains("Intended failure."); + } + + @Test + public void checkClientTrusted_getsIntendedFailureStatusInResponse() throws CertificateException { + writer.setBehavior(FakeWriter.Behavior.ERROR_STATUS); + S2ATrustManager trustManager = + S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty()); + + CertificateException expected = + assertThrows( + CertificateException.class, + () -> trustManager.checkClientTrusted(getCerts(), /* authType= */ "")); + + assertThat(expected).hasMessageThat().contains("Error occurred in response from S2A"); + } + + @Test + public void checkClientTrusted_getsIntendedFailureFromServer() throws CertificateException { + writer.setBehavior(FakeWriter.Behavior.ERROR_RESPONSE); + S2ATrustManager trustManager = + S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty()); + + CertificateException expected = + assertThrows( + CertificateException.class, + () -> trustManager.checkClientTrusted(getCerts(), /* authType= */ "")); + + assertThat(expected).hasMessageThat().isEqualTo("Failed to send request to S2A."); + } + + @Test + public void checkServerTrusted_getsIntendedFailureResponse() throws CertificateException { + writer + .setVerificationResult(FakeWriter.VerificationResult.FAILURE) + .setFailureReason("Intended failure."); + S2ATrustManager trustManager = + S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty()); + + CertificateException expected = + assertThrows( + CertificateException.class, + () -> trustManager.checkServerTrusted(getCerts(), /* authType= */ "")); + + assertThat(expected).hasMessageThat().contains("Intended failure."); + } + + @Test + public void checkServerTrusted_getsIntendedFailureStatusInResponse() throws CertificateException { + writer.setBehavior(FakeWriter.Behavior.ERROR_STATUS); + S2ATrustManager trustManager = + S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty()); + + CertificateException expected = + assertThrows( + CertificateException.class, + () -> trustManager.checkServerTrusted(getCerts(), /* authType= */ "")); + + assertThat(expected).hasMessageThat().contains("Error occurred in response from S2A"); + } + + @Test + public void checkServerTrusted_getsIntendedFailureFromServer() throws CertificateException { + writer.setBehavior(FakeWriter.Behavior.ERROR_RESPONSE); + S2ATrustManager trustManager = + S2ATrustManager.createForClient(stub, FAKE_HOSTNAME, /* localIdentity= */ Optional.empty()); + + CertificateException expected = + assertThrows( + CertificateException.class, + () -> trustManager.checkServerTrusted(getCerts(), /* authType= */ "")); + + assertThat(expected).hasMessageThat().isEqualTo("Failed to send request to S2A."); + } + + private X509Certificate[] getCerts() throws CertificateException { + byte[] decoded = Base64.getDecoder().decode(CLIENT_CERT_PEM); + return new X509Certificate[] { + (X509Certificate) + CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(decoded)) + }; + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/SslContextFactoryTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/SslContextFactoryTest.java new file mode 100644 index 00000000000..a2a66a7b563 --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/SslContextFactoryTest.java @@ -0,0 +1,177 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.truth.Expect; +import io.grpc.s2a.handshaker.S2AIdentity; +import io.netty.handler.ssl.OpenSslSessionContext; +import io.netty.handler.ssl.SslContext; +import java.security.GeneralSecurityException; +import java.util.Optional; +import javax.net.ssl.SSLSessionContext; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link SslContextFactory}. */ +@RunWith(JUnit4.class) +public final class SslContextFactoryTest { + @Rule public final Expect expect = Expect.create(); + private static final String FAKE_TARGET_NAME = "fake_target_name"; + private S2AStub stub; + private FakeWriter writer; + + @Before + public void setUp() { + writer = new FakeWriter(); + stub = S2AStub.newInstanceForTesting(writer); + writer.setReader(stub.getReader()); + } + + @Test + public void createForClient_returnsValidSslContext() throws Exception { + SslContext sslContext = + SslContextFactory.createForClient( + stub, FAKE_TARGET_NAME, /* localIdentity= */ Optional.empty()); + + expect.that(sslContext).isNotNull(); + expect.that(sslContext.sessionCacheSize()).isEqualTo(1); + expect.that(sslContext.sessionTimeout()).isEqualTo(300); + expect.that(sslContext.isClient()).isTrue(); + expect.that(sslContext.applicationProtocolNegotiator().protocols()).containsExactly("h2"); + SSLSessionContext sslSessionContext = sslContext.sessionContext(); + if (sslSessionContext instanceof OpenSslSessionContext) { + OpenSslSessionContext openSslSessionContext = (OpenSslSessionContext) sslSessionContext; + expect.that(openSslSessionContext.isSessionCacheEnabled()).isFalse(); + } + } + + @Test + public void createForClient_withLocalIdentity_returnsValidSslContext() throws Exception { + SslContext sslContext = + SslContextFactory.createForClient( + stub, FAKE_TARGET_NAME, Optional.of(S2AIdentity.fromSpiffeId("fake-spiffe-id"))); + + expect.that(sslContext).isNotNull(); + expect.that(sslContext.sessionCacheSize()).isEqualTo(1); + expect.that(sslContext.sessionTimeout()).isEqualTo(300); + expect.that(sslContext.isClient()).isTrue(); + expect.that(sslContext.applicationProtocolNegotiator().protocols()).containsExactly("h2"); + SSLSessionContext sslSessionContext = sslContext.sessionContext(); + if (sslSessionContext instanceof OpenSslSessionContext) { + OpenSslSessionContext openSslSessionContext = (OpenSslSessionContext) sslSessionContext; + expect.that(openSslSessionContext.isSessionCacheEnabled()).isFalse(); + } + } + + @Test + public void createForClient_returnsEmptyResponse_error() throws Exception { + writer.setBehavior(FakeWriter.Behavior.EMPTY_RESPONSE); + + S2AConnectionException expected = + assertThrows( + S2AConnectionException.class, + () -> + SslContextFactory.createForClient( + stub, FAKE_TARGET_NAME, /* localIdentity= */ Optional.empty())); + + assertThat(expected) + .hasMessageThat() + .contains("Response from S2A server does NOT contain ClientTlsConfiguration."); + } + + @Test + public void createForClient_returnsErrorStatus_error() throws Exception { + writer.setBehavior(FakeWriter.Behavior.ERROR_STATUS); + + S2AConnectionException expected = + assertThrows( + S2AConnectionException.class, + () -> + SslContextFactory.createForClient( + stub, FAKE_TARGET_NAME, /* localIdentity= */ Optional.empty())); + + assertThat(expected).hasMessageThat().contains("Intended ERROR Status from FakeWriter."); + } + + @Test + public void createForClient_getsErrorFromServer_throwsError() throws Exception { + writer.sendIoError(); + + GeneralSecurityException expected = + assertThrows( + GeneralSecurityException.class, + () -> + SslContextFactory.createForClient( + stub, FAKE_TARGET_NAME, /* localIdentity= */ Optional.empty())); + + assertThat(expected) + .hasMessageThat() + .contains("Failed to get client TLS configuration from S2A."); + } + + @Test + public void createForClient_getsBadTlsVersionsFromServer_throwsError() throws Exception { + writer.setBehavior(FakeWriter.Behavior.BAD_TLS_VERSION_RESPONSE); + + S2AConnectionException expected = + assertThrows( + S2AConnectionException.class, + () -> + SslContextFactory.createForClient( + stub, FAKE_TARGET_NAME, /* localIdentity= */ Optional.empty())); + + assertThat(expected) + .hasMessageThat() + .contains("Set of TLS versions received from S2A server is empty."); + } + + @Test + public void createForClient_nullStub_throwsError() throws Exception { + writer.sendUnexpectedResponse(); + + NullPointerException expected = + assertThrows( + NullPointerException.class, + () -> + SslContextFactory.createForClient( + /* stub= */ null, FAKE_TARGET_NAME, /* localIdentity= */ Optional.empty())); + + assertThat(expected).hasMessageThat().isEqualTo("stub should not be null."); + } + + @Test + public void createForClient_nullTargetName_throwsError() throws Exception { + writer.sendUnexpectedResponse(); + + NullPointerException expected = + assertThrows( + NullPointerException.class, + () -> + SslContextFactory.createForClient( + stub, /* targetName= */ null, /* localIdentity= */ Optional.empty())); + + assertThat(expected) + .hasMessageThat() + .isEqualTo("targetName should not be null on client side."); + } +} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java new file mode 100644 index 00000000000..80adba07f20 --- /dev/null +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.s2a.handshaker.tokenmanager; + +import static com.google.common.truth.Truth.assertThat; + +import io.grpc.s2a.handshaker.S2AIdentity; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class SingleTokenAccessTokenManagerTest { + private static final S2AIdentity IDENTITY = S2AIdentity.fromSpiffeId("spiffe_id"); + private static final String TOKEN = "token"; + + @Before + public void setUp() { + SingleTokenFetcher.setAccessToken(null); + } + + @Test + public void getDefaultToken_success() throws Exception { + SingleTokenFetcher.setAccessToken(TOKEN); + Optional manager = AccessTokenManager.create(); + assertThat(manager).isPresent(); + assertThat(manager.get().getDefaultToken()).isEqualTo(TOKEN); + } + + @Test + public void getToken_success() throws Exception { + SingleTokenFetcher.setAccessToken(TOKEN); + Optional manager = AccessTokenManager.create(); + assertThat(manager).isPresent(); + assertThat(manager.get().getToken(IDENTITY)).isEqualTo(TOKEN); + } + + @Test + public void getToken_noEnvironmentVariable() throws Exception { + assertThat(SingleTokenFetcher.create()).isEmpty(); + } + + @Test + public void create_success() throws Exception { + SingleTokenFetcher.setAccessToken(TOKEN); + Optional manager = AccessTokenManager.create(); + assertThat(manager).isPresent(); + assertThat(manager.get().getToken(IDENTITY)).isEqualTo(TOKEN); + } + + @Test + public void create_noEnvironmentVariable() throws Exception { + assertThat(AccessTokenManager.create()).isEmpty(); + } +} \ No newline at end of file diff --git a/s2a/src/test/resources/README.md b/s2a/src/test/resources/README.md new file mode 100644 index 00000000000..726b921a615 --- /dev/null +++ b/s2a/src/test/resources/README.md @@ -0,0 +1,32 @@ +# Generating certificates and keys for testing mTLS-S2A + +Content from: https://github.com/google/s2a-go/blob/main/testdata/README.md + +Create root CA + +``` +openssl req -x509 -sha256 -days 7305 -newkey rsa:2048 -keyout root_key.pem -out +root_cert.pem +``` + +Generate private keys for server and client + +``` +openssl genrsa -out server_key.pem 2048 +openssl genrsa -out client_key.pem 2048 +``` + +Generate CSRs for server and client (set Common Name to localhost, leave all +other fields blank) + +``` +openssl req -key server_key.pem -new -out server.csr -config config.cnf +openssl req -key client_key.pem -new -out client.csr -config config.cnf +``` + +Sign CSRs for server and client + +``` +openssl x509 -req -CA root_cert.pem -CAkey root_key.pem -in server.csr -out server_cert.pem -days 7305 -extfile config.cnf -extensions req_ext +openssl x509 -req -CA root_cert.pem -CAkey root_key.pem -in client.csr -out client_cert.pem -days 7305 +``` \ No newline at end of file diff --git a/s2a/src/test/resources/client.csr b/s2a/src/test/resources/client.csr new file mode 100644 index 00000000000..664f5a4cf86 --- /dev/null +++ b/s2a/src/test/resources/client.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIChzCCAW8CAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAoSS3KtFgiXX4vAUNscFGIB/r2OOMgiZMKHz72dN0 +5kSxwdpQxpMIhwEoe0lhHNfOiuE7/r6VbGG9RGGIcQcoSonc3InPRfpnzfj9KohJ +i8pYkLL9EwElAEl9sWnvVKTza8jTApDP2Z/fntBEsWAMsLPpuRZT6tgN1sXe4vNG +4wufJSxuImyCVAx1fkZjRkYEKOtm1osnEDng4R0WXZ6S+q5lYzYPk1wxgbjdZu2U +fWxP6V63SphV0NFXTx0E401j2h258cIqTVj8lRX6dfl9gO0d43Rd+hSU7R4iXGEw +arixuH9g5H745AFf9H52twHPcNP9cEKBljBpSV5z3MvTkQIDAQABoC4wLAYJKoZI +hvcNAQkOMR8wHTAbBgNVHREEFDAShxAAAAAAAAAAAAAAAAAAAAAAMA0GCSqGSIb3 +DQEBCwUAA4IBAQCQHim3aIpGJs5u6JhEA07Rwm8YKyVALDEklhsHILlFhdNr2uV7 +S+3bHV79mDGjxNWvFcgK5h5ENkT60tXbhbie1gYmFT0RMCYHDsL09NGTh8G9Bbdl +UKeA9DMhRSYzE7Ks3Lo1dJvX7OAEI0qV77dGpQknufYpmHiBXuqtB9I0SpYi1c4O +9IUn/NY0yiYFPsIEsVRz/1dK97wazusLnijaMwNNhUc9bJwTyujhlr+b8ioPyADG +e+GDF97d0nQ8806DOJF4GTRKwaXD+R5zN5t4ULhZ7ERqLNeE9EnWRe4CvSGvBoNA +hIVeYaLd761Z9ZKvOnsgCr8qvMDilDFY6OfB +-----END CERTIFICATE REQUEST----- \ No newline at end of file diff --git a/s2a/src/test/resources/client_cert.pem b/s2a/src/test/resources/client_cert.pem new file mode 100644 index 00000000000..b72f6991c91 --- /dev/null +++ b/s2a/src/test/resources/client_cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC9DCCAdwCFB+cDXee2sIHjdlBhdNpTo+G2XAjMA0GCSqGSIb3DQEBCwUAMFkx +CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl +cm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzEw +MTcyMzA5MDNaFw00MzEwMTcyMzA5MDNaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKEktyrRYIl1+LwFDbHBRiAf +69jjjIImTCh8+9nTdOZEscHaUMaTCIcBKHtJYRzXzorhO/6+lWxhvURhiHEHKEqJ +3NyJz0X6Z834/SqISYvKWJCy/RMBJQBJfbFp71Sk82vI0wKQz9mf357QRLFgDLCz +6bkWU+rYDdbF3uLzRuMLnyUsbiJsglQMdX5GY0ZGBCjrZtaLJxA54OEdFl2ekvqu +ZWM2D5NcMYG43WbtlH1sT+let0qYVdDRV08dBONNY9odufHCKk1Y/JUV+nX5fYDt +HeN0XfoUlO0eIlxhMGq4sbh/YOR++OQBX/R+drcBz3DT/XBCgZYwaUlec9zL05EC +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEARorc1t2OJnwm1lxhf2KpTpNvNOI9FJak +iSHz/MxhMdu4BG/dQHkKkWoVC6W2Kaimx4OImBwRlGEmGf4P0bXOLSTOumk2k1np +ZUbw7Z2cJzvBmT2BLoHRXcBvbFIBW5DJUSHR37eXEKP57BeD+Og4/3XhNzehSpTX +DRd2Ix/D39JjYA462nqPHQP8HDMf6+0BFmvf9ZRYmFucccYQRCUCKDqb8+wGf9W6 +tKNRE6qPG2jpAQ9qkgO7XuucbLvpywt5xj+yDRbOIq43l40mHaz4lRp697oaxjP8 +HSVcMydW3cluoW3AVInNIaqbM1dr6931MllK62DKipFtmCycq/56XA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/client_key.pem b/s2a/src/test/resources/client_key.pem new file mode 100644 index 00000000000..dd3e2ff78f1 --- /dev/null +++ b/s2a/src/test/resources/client_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQChJLcq0WCJdfi8 +BQ2xwUYgH+vY44yCJkwofPvZ03TmRLHB2lDGkwiHASh7SWEc186K4Tv+vpVsYb1E +YYhxByhKidzcic9F+mfN+P0qiEmLyliQsv0TASUASX2xae9UpPNryNMCkM/Zn9+e +0ESxYAyws+m5FlPq2A3Wxd7i80bjC58lLG4ibIJUDHV+RmNGRgQo62bWiycQOeDh +HRZdnpL6rmVjNg+TXDGBuN1m7ZR9bE/pXrdKmFXQ0VdPHQTjTWPaHbnxwipNWPyV +Ffp1+X2A7R3jdF36FJTtHiJcYTBquLG4f2DkfvjkAV/0fna3Ac9w0/1wQoGWMGlJ +XnPcy9ORAgMBAAECggEALAUqoGDIHWUDyOEch5WDwZzWwc4PgTJTFbBm4G96fLkB +UjKAZG6gIrk3RM6b39Q4UQoMaJ/Jk+zzVi3Kpw3MfOhCVGC1JamtF8BP8IGAjdZ9 +8TFkHv/uCrEIzCFjRt00vhoDQq0qiom4/dppGYdikBbl3zDxRbM1vJkbNSY+FCGW +dA0uJ5XdMLR6lPeB5odqjUggnfUgPCOLdV/F+HkSM9NP1bzmHLiKznzwFsfat139 +7LdzJwNN5IX4Io6cxsxNlrX/NNvPkKdGv07Z6FYxWROyKCunjh48xFcQg0ltoRuq +R9P8/LwS8GYrcc1uC/uBc0e6VgM9D9fsvh+8SQtf3QKBgQDXX+z2GnsFoEs7xv9U +qN0HEX4jOkihZvFu43layUmeCeE8wlEctJ0TsM5Bd7FMoUG6e5/btwhsAIYW89Xn +l/R8OzxR6Kh952Dce4DAULuIeopiw7ASJwTZtO9lWhxw0hjM1hxXTG+xxOqQvsRX +c+d+vtvdIqyJ4ELfzg9kUtkdpwKBgQC/ig3cmej7dQdRAMn0YAwgwhuLkCqVFh4y +WIlqyPPejKf8RXubqgtaSYx/T7apP87SMMSfSLaUdrYAGjST6k+tG5cmwutPIbw/ +osL7U3hcIhjX3hfHgI69Ojcpplbd5yqTxZHpxIs6iAQCEqNuasLXIDMouqNhGF1D +YssD6qxcBwKBgQCdZqWvVrsB6ZwSG+UO4jpmqAofhMD/9FQOToCqMOF0dpP966WL +7RO/CEA06FzTPCblOuQhlyq4g8l7jMiPcSZkhIYY9oftO+Q2Pqxh4J6tp6DrfUh4 +e7u3v9wVnj2a1nD5gqFDy8D1kow7LLAhmbtdje7xNh4SxasaFWZ6U3IJkQKBgGS1 +F5i3q9IatCAZBBZjMb0/kfANevYsTPA3sPjec6q91c1EUzuDarisFx0RMn9Gt124 +mokNWEIzMHpZTO/AsOfZq92LeuF+YVYsI8y1FIGMw/csJOCWbXZ812gkt2OxGafc +p118I6BAx6q3VgrGQ2+M1JlDmIeCofa+SPPkPX+dAoGBAJrOgEJ+oyEaX/YR1g+f +33pWoPQbRCG7T4+Y0oetCCWIcMg1/IUvGUCGmRDxj5dMqB+a0vJtviQN9rjpSuNS +0EVw79AJkIjHhi6KDOfAuyBvzGhxpqxGufnQ2GU0QL65NxQfd290xkxikN0ZGtuB +SDgZoJxMOGYwf8EX5i9h27Db +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/s2a/src/test/resources/config.cnf b/s2a/src/test/resources/config.cnf new file mode 100644 index 00000000000..38d9a9ccdb0 --- /dev/null +++ b/s2a/src/test/resources/config.cnf @@ -0,0 +1,17 @@ +[req] +distinguished_name = req_distinguished_name +req_extensions = req_ext + +[req_distinguished_name] +countryName = Country Name (2 letter code) +stateOrProvinceName = State or Province Name (full name) +localityName = Locality Name (eg, city) +organizationalUnitName = Organizational Unit Name (eg, section) +commonName = Common Name (eg, your name or your server\'s hostname) +emailAddress = Email Address + +[req_ext] +subjectAltName = @alt_names + +[alt_names] +IP.1 = :: \ No newline at end of file diff --git a/s2a/src/test/resources/root_cert.pem b/s2a/src/test/resources/root_cert.pem new file mode 100644 index 00000000000..737e601691c --- /dev/null +++ b/s2a/src/test/resources/root_cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDkzCCAnugAwIBAgIUb7RsINwsFgKf0Q0RuzfOgp48j6UwDQYJKoZIhvcNAQEL +BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X +DTIzMTAxNzIzMDczOFoXDTQzMTAxNzIzMDczOFowWTELMAkGA1UEBhMCQVUxEzAR +BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 +IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAkIFnQLuhzYnm3rvmi/U7zMgEP2Tqgb3VC00frSXEV6olZcLgyC9g +0DAGdt9l9lP90DQTG5KCOtoW2BTqM/aaVpR0OaDFOCy90FIj6YyZLZ9w2PQxQcxS +GQHyEvWszTkNxeDyG1mPTj+Go8JLKqdvLg/9GUgPg6stxyAZwYhyUTGuEM4bv0sn +b3vmHRmIGJ/w6aLtd7nK8LkNHa3WVrbvRGHrzdMHfpzF/M/5fAk8GfRYugo39knf +VLKGyQCXNI8Y1iHGEmPqQZIFPTjBL6caIlbEV0VHlxoSOGB6JVxcllxAEvd6abqX +RJVJPQzzGfEnMNYp9SiZQ9bvDRUsUkWyYwIDAQABo1MwUTAdBgNVHQ4EFgQUAZMN +F9JAGHbA3jGOeu6bWFvSdWkwHwYDVR0jBBgwFoAUAZMNF9JAGHbA3jGOeu6bWFvS +dWkwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAicBli36ISJFu +lrJqOHVqTeNP6go0I35VGnP44nEEP5cBvRD3XntBFEk5D3mSNNOGt+2ncxom8VR9 +FsLuTfHAipXePJI6MSxFuBPea8V/YPBs3npk5f1FRvJ5vEgtzFvBjsKmp1dS9hH0 +KUWtWcsAkO2Anc/LVc0xxSidL8NjzYoEFqiki0TNNwCJjmd9XwnBLHW38sEb/pgy +KTyRpOyG3Zg2UDjBHiXPBrmIvVFLB6+LrPNvfr1k4HjIgVY539ZXUvVMDKytMrDY +h63EMDn4kkPpxXlufgWGybjN5D51OylyWBZLe+L1DQyWEg0Vd7GwPzb6p7bmI7MP +pooqbgbDpQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/root_key.pem b/s2a/src/test/resources/root_key.pem new file mode 100644 index 00000000000..aae992426d7 --- /dev/null +++ b/s2a/src/test/resources/root_key.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQInmQVkXP3TFcCAggA +MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECGeCAVH1pefxBIIEyD3Nj1Dy19oy +fogU+z8YBLXuSCx8s3zncYPF9nYlegGSSo0ace/WxfPu8AEPus1P2MxlxfcCQ1A+ +5+vMihtEpgpTg9R4RlLAWs45jz4AduGiwqW05+W5zgDn6g7p7HIL0+M5FxKRkAW0 +KEH4Jy8Vc1XQxkhOm1Q4NLI8PT94rcBDE9Od03sdrW/hQgaOFz5AWOlT5jF1uUOz +glF1RQQxfJygTB6qlPTC3BAaiAnWij3NOg5L5vvUhjLa7iOZOhRQBRkf4YtHsM+2 +rFy8Z7MeHOvrqFf8LXosNy3JreQW036rLGR0Xh5myATkNrEwA8df37AgLUmwqyfz +hjZefPW77LgMAXlaN8s345AGikOX8yQKEFzPV/Nag32p6t4oiRRcUUfdB4wzKi6T +mzZ6lKcGR3qqL4V6lJSV3I2fmgkYZnUwymolyu+1+CVYDLuE53TBi5dRXwgOghi7 +npw7PqqQCian8yxHF9c1rYukD0ov0/y8ratjOu9XoJG2/wWQJNvDkAyc3mSJf+3y +6Wtu1qhLszU8pZOGW0fK6bGyHSp+wkoah/vRzB0+yFjvuMIG6py2ZDQeqhqS3ZV2 +nZHHjj0tZ45Wbdf4k17ujEK34pFXluPH//zADnd6ym2W0t6x+jtqR5tYu3poORQg +jFgpudkn2RUSq8N/gIiHDwblYBxU2dmyzEVudv1zNgVSHyetGLxsFoNB7Prn89rJ +u24a/xtuCyC2pshWo3KiL74hkkCsC8rLbEAAbADheb35b+Ca3JnMwgyUHbHL6Hqf +EiVIgm14lB/1uz651X58Boo6tDFkgrxEtGDUIZm8yk2n0tGflp7BtYbMCw+7gqhb +XN4hlhFDcCJm8peXcyCtGajOnBuNO9JJDNYor6QjptaIpQBFb7/0rc7kyO12BIUv +F9mrCHF18Hd/9AtUO93+tyDAnL64Jqq9tUv8dOVtIfbcHXZSYHf24l0XAiKByb8y +9NQLUZkIuF4aUZVHV8ZBDdHNqjzqVglKQlGHdw1XBexSal5pC9HvknOmWBgl0aza +flzeTRPX7TPrMJDE5lgSy58czGpvZzhFYwOp6cwpfjNsiqdzD78Zs0xsRbNg519s +d+cLmbiU3plWCoYCuDb68eZRRzT+o41+QJG2PoMCpzPw5wMLl6HuW7HXMRFpZKJc +tPKpeTIzb8hjhA+TwVIVpTPHvvQehtTUQD2mRujdvNM6PF8tnuC3F3sB3PTjeeJg +uzfEfs3BynRTIj/gX6y87gzwsrwWIEN6U0cCbQ6J1EcgdQCiH8vbhIgfd4DkLgLN +Kkif+fI/HgBOqaiwSw3sHmWgB6PllVQOKH6qAiejTHR/UUvJTPvgKJFLunmBiF12 +N1bRge1sSXE1eLKVdi+dP1j0o6RxhaRrbX7ie3y/wYHwCJnb8h08DEprgCqoswFs +SuNKmvlibBHAsnOdhyCTOd9I5n8XzAUUp6mT+C5WDfl7qfYvh6IHFlSrhZ9aS9b6 +RY873cnphKbqU5d7Cr8Ufx4b4SgS+hEnuP8y5IToLQ3BONGQH2lu7nmd89wjW0uo +IMRXybwf/5FnKhEy8Aw+pD6AxiXC3DZVTKl3SHmjkYBDvNElsJVgygVTKgbOa1Z+ +ovIK/D7QV7Nv3uVortH8XA== +-----END ENCRYPTED PRIVATE KEY----- \ No newline at end of file diff --git a/s2a/src/test/resources/server.csr b/s2a/src/test/resources/server.csr new file mode 100644 index 00000000000..1657b191133 --- /dev/null +++ b/s2a/src/test/resources/server.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIChzCCAW8CAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAlPThqu8tfJ4hQKRiUw/vNPfo2L2LQU8NlrRL7rvV +71E345LGK1h/hM3MHp5VgEvaaIibb0hSNv/TYz3HVCQyNuPlcmkHZTJ9mB0icilU +rYWdM0LPIg46iThmIQVhMiNfpMKQLDLQ7o3Jktjm32OxnQdtYSV+7NFnw8/0pB4j +iaiBYfZIMeGzEJIOFG8GSNJG0pfCI71DyLRonIcb2XzfeDPHeWSF7lbIoMGAuKIE +2mXpwHmAjTMJzIShSgLqCvmbz7wR3ZeVMknXcgcqMmagGphy8SjizIWC5KRbrnRq +F22Ouxdat6scIevRXGp5nYawFYdpK9qo+82gEouVX3dtSQIDAQABoC4wLAYJKoZI +hvcNAQkOMR8wHTAbBgNVHREEFDAShxAAAAAAAAAAAAAAAAAAAAAAMA0GCSqGSIb3 +DQEBCwUAA4IBAQB2qU354OlNVunhZhiOFNwabovxLcgKoQz+GtJ2EzsMEza+NPvV +dttPxXzqL/U+gDghvGzSYGuh2yMfTTPO+XtZKpvMUmIWonN5jItbFwSTaWcoE8Qs +zFZokRuFJ9dy017u642mpdf6neUzjbfCjWs8+3jyFzWlkrMF3RlSTxPuksWjhXsX +dxxLNu8YWcsYRB3fODHqrlBNuDn+9kb9z8to+yq76MA0HtdDkjd/dfgghiTDJhqm +IcwhBXufwQUrOP4YiuiwM0mo7Xlhw65gnSmRcwR9ha98SV2zG5kiRYE+m+94bDbd +kGBRfhpQSzh1w09cVzmLgzkfxRShEB+bb9Ss +-----END CERTIFICATE REQUEST----- \ No newline at end of file diff --git a/s2a/src/test/resources/server_cert.pem b/s2a/src/test/resources/server_cert.pem new file mode 100644 index 00000000000..10a98cf5c21 --- /dev/null +++ b/s2a/src/test/resources/server_cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIUMZkgD5gtoa39H9jdI/ijVkyxC/swDQYJKoZIhvcNAQEL +BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X +DTIzMTAxNzIzMDg1M1oXDTQzMTAxNzIzMDg1M1owFDESMBAGA1UEAwwJbG9jYWxo +b3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlPThqu8tfJ4hQKRi +Uw/vNPfo2L2LQU8NlrRL7rvV71E345LGK1h/hM3MHp5VgEvaaIibb0hSNv/TYz3H +VCQyNuPlcmkHZTJ9mB0icilUrYWdM0LPIg46iThmIQVhMiNfpMKQLDLQ7o3Jktjm +32OxnQdtYSV+7NFnw8/0pB4jiaiBYfZIMeGzEJIOFG8GSNJG0pfCI71DyLRonIcb +2XzfeDPHeWSF7lbIoMGAuKIE2mXpwHmAjTMJzIShSgLqCvmbz7wR3ZeVMknXcgcq +MmagGphy8SjizIWC5KRbrnRqF22Ouxdat6scIevRXGp5nYawFYdpK9qo+82gEouV +X3dtSQIDAQABo18wXTAbBgNVHREEFDAShxAAAAAAAAAAAAAAAAAAAAAAMB0GA1Ud +DgQWBBTKJU+NK7Q6ZPccSigRCMBCBgjkaDAfBgNVHSMEGDAWgBQBkw0X0kAYdsDe +MY567ptYW9J1aTANBgkqhkiG9w0BAQsFAAOCAQEAXuCs6MGVoND8TaJ6qaDmqtpy +wKEW2hsGclI9yv5cMS0XCVTkmKYnIoijtqv6Pdh8PfhIx5oJqJC8Ml16w4Iou4+6 +kKF0DdzdQyiM0OlNCgLYPiR4rh0ZCAFFCvOsDum1g+b9JTFZGooK4TMd9thwms4D +SqpP5v1NWf/ZLH5TYnp2CkPzBxDlnMJZphuWtPHL+78TbgQuQaKu2nMLBGBJqtFi +HDOGxckgZuwBsy0c+aC/ZwaV7FdMP42kxUZduCEx8+BDSGwPoEpz6pwVIkjiyYAm +3O8FUeEPzYzwpkANIbbEIDWV6FVH9IahKRRkE+bL3BqoQkv8SMciEA5zWsPrbA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/server_key.pem b/s2a/src/test/resources/server_key.pem new file mode 100644 index 00000000000..44f087dee94 --- /dev/null +++ b/s2a/src/test/resources/server_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCU9OGq7y18niFA +pGJTD+809+jYvYtBTw2WtEvuu9XvUTfjksYrWH+EzcwenlWAS9poiJtvSFI2/9Nj +PcdUJDI24+VyaQdlMn2YHSJyKVSthZ0zQs8iDjqJOGYhBWEyI1+kwpAsMtDujcmS +2ObfY7GdB21hJX7s0WfDz/SkHiOJqIFh9kgx4bMQkg4UbwZI0kbSl8IjvUPItGic +hxvZfN94M8d5ZIXuVsigwYC4ogTaZenAeYCNMwnMhKFKAuoK+ZvPvBHdl5UySddy +ByoyZqAamHLxKOLMhYLkpFuudGoXbY67F1q3qxwh69FcanmdhrAVh2kr2qj7zaAS +i5Vfd21JAgMBAAECggEACTBuN4hXywdKT92UP0GNZTwh/jT7QUUqNnDa+lhWI1Rk +WUK1vPjRrRSxEfZ8mdSUHbzHsf7JK6FungGyqUsuWdqHTh6SmTibLOYnONm54paK +kx38/0HXdJ2pF0Jos5ohDV3/XOqpnv3aQJfm7kMNMv3BTqvsf5mPiDHtCq7dTGGj +rGiLc0zirKZq79C6YSB1UMB01BsDl2ScflK8b3osT18uYx/BOdjLT4yZWQsU/nbB +OeF+ziWTTUAVjodGeTf+NYG7cFN/9N9PdSnAwuw8Nche3xZKbHTh2I578Zd4bsDX +H+hoMN862nzOXEvD6KyLB8xDdnEZ+p+njeDROJVmgQKBgQDQhzQEl/co1LYc5IDO +mynhCOtKJeRWBLhYEPIuaSY3qF+lrOWzqyOUNppWDx+HeKOq70X1Q+ETeSXtbaL1 +qHBkNcApQ2lStcpkR9whcVbr9NIWC8y8UQxyerEK3x3l0bZ99dfJ/z6lbxdS7prc +Hhxy6pUj8Q8AgpTZA8HfQUF1EQKBgQC23ek24kTVvWeWX2C/82H1Yfia6ITL7WHz +3aEJaZaO5JD3KmOSZgY88Ob3pkDTRYjFZND5zSB7PnM68gpo/OEDla6ZYtfwBWCX +q4QhFtv2obehobmDk+URVfvlOcBikoEP1i8oy7WdZ5CgC4gNKkkD15l68W+g5IIG +2ZOA97yUuQKBgDAzoI2TRxmUGciR9UhMy6Bt/F12ZtKPYsFQoXqi6aeh7wIP9kTS +wXWoLYLJGiOpekOv7X7lQujKbz7zweCBIAG5/wJKx9TLms4VYkgEt+/w9oMMFTZO +kc8Al14I9xNBp6p0In5Z1vRMupp79yX8e90AZpsZRLt8c8W6PZ1Kq0PRAoGBAKmD +7LzD46t/eJccs0M9CoG94Ac5pGCmHTdDLBTdnIO5vehhkwwTJ5U2e+T2aQFwY+kY +G+B1FrconQj3dk78nFoGV2Q5DJOjaHcwt7s0xZNLNj7O/HnMj3wSiP9lGcJGrP1R +P0ZCEIlph9fU2LnbiPPW2J/vT9uF+EMBTosvG9GBAoGAEVaDLLXOHj+oh1i6YY7s +0qokN2CdeKY4gG7iKjuDFb0r/l6R9uFvpUwJMhLEkF5SPQMyrzKFdnTpw3n/jnRa +AWG6GoV+D7LES+lHP5TXKKijbnHJdFjW8PtfDXHCJ6uGG91vH0TMMp1LqhcvGfTv +lcNGXkk6gUNSecxBC1uJfKE= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 61972e30b6c..03eca809226 100644 --- a/settings.gradle +++ b/settings.gradle @@ -65,6 +65,7 @@ include ":grpc-benchmarks" include ":grpc-services" include ":grpc-servlet" include ":grpc-servlet-jakarta" +include ":grpc-s2a" include ":grpc-xds" include ":grpc-bom" include ":grpc-rls" @@ -100,6 +101,7 @@ project(':grpc-benchmarks').projectDir = "$rootDir/benchmarks" as File project(':grpc-services').projectDir = "$rootDir/services" as File project(':grpc-servlet').projectDir = "$rootDir/servlet" as File project(':grpc-servlet-jakarta').projectDir = "$rootDir/servlet/jakarta" as File +project(':grpc-s2a').projectDir = "$rootDir/s2a" as File project(':grpc-xds').projectDir = "$rootDir/xds" as File project(':grpc-bom').projectDir = "$rootDir/bom" as File project(':grpc-rls').projectDir = "$rootDir/rls" as File From 3a6be9ca1e9c8efed109b97546730852b2ab134b Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Mon, 16 Sep 2024 16:32:52 +0530 Subject: [PATCH 004/591] Detect transport executors with no remaining threads (#11503) Detect misconfigured transport executors with too few threads that could further throttle the transport. Fixes #11271 --- .../io/grpc/okhttp/OkHttpClientTransport.java | 33 +++++++++++++++++++ .../okhttp/OkHttpClientTransportTest.java | 22 +++++++++++++ 2 files changed, 55 insertions(+) diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java index 29d3dbc1cdf..2f6b836dc3a 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java @@ -83,9 +83,13 @@ import java.util.Locale; import java.util.Map; import java.util.Random; +import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -499,8 +503,15 @@ public Runnable start(Listener listener) { outboundFlow = new OutboundFlowController(this, frameWriter); } final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch latchForExtraThread = new CountDownLatch(1); + // The transport needs up to two threads to function once started, + // but only needs one during handshaking. Start another thread during handshaking + // to make sure there's still a free thread available. If the number of threads is exhausted, + // it is better to kill the transport than for all the transports to hang unable to send. + CyclicBarrier barrier = new CyclicBarrier(2); // Connecting in the serializingExecutor, so that some stream operations like synStream // will be executed after connected. + serializingExecutor.execute(new Runnable() { @Override public void run() { @@ -510,8 +521,14 @@ public void run() { // initial preface. try { latch.await(); + barrier.await(1000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); + } catch (TimeoutException | BrokenBarrierException e) { + startGoAway(0, ErrorCode.INTERNAL_ERROR, Status.UNAVAILABLE + .withDescription("Timed out waiting for second handshake thread. " + + "The transport executor pool may have run out of threads")); + return; } // Use closed source on failure so that the reader immediately shuts down. BufferedSource source = Okio.buffer(new Source() { @@ -575,6 +592,7 @@ sslSocketFactory, hostnameVerifier, sock, getOverridenHost(), getOverridenPort() return; } finally { clientFrameHandler = new ClientFrameHandler(variant.newReader(source, true)); + latchForExtraThread.countDown(); } synchronized (lock) { socket = Preconditions.checkNotNull(sock, "socket"); @@ -584,6 +602,21 @@ sslSocketFactory, hostnameVerifier, sock, getOverridenHost(), getOverridenPort() } } }); + + executor.execute(new Runnable() { + @Override + public void run() { + try { + barrier.await(1000, TimeUnit.MILLISECONDS); + latchForExtraThread.await(); + } catch (BrokenBarrierException | TimeoutException e) { + // Something bad happened, maybe too few threads available! + // This will be handled in the handshake thread. + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); // Schedule to send connection preface & settings before any other write. try { sendConnectionPrefaceAndSettings(); diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java index 987cc09203e..daf5073992e 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java @@ -247,6 +247,28 @@ public void testToString() throws Exception { assertTrue("Unexpected: " + s, s.contains(address.toString())); } + @Test + public void testTransportExecutorWithTooFewThreads() throws Exception { + ExecutorService fixedPoolExecutor = Executors.newFixedThreadPool(1); + channelBuilder.transportExecutor(fixedPoolExecutor); + InetSocketAddress address = InetSocketAddress.createUnresolved("hostname", 31415); + clientTransport = new OkHttpClientTransport( + channelBuilder.buildTransportFactory(), + address, + "hostname", + null, + EAG_ATTRS, + NO_PROXY, + tooManyPingsRunnable); + clientTransport.start(transportListener); + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); + verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(statusCaptor.capture()); + Status capturedStatus = statusCaptor.getValue(); + assertEquals("Timed out waiting for second handshake thread. " + + "The transport executor pool may have run out of threads", + capturedStatus.getDescription()); + } + /** * Test logging is functioning correctly for client received Http/2 frames. Not intended to test * actual frame content being logged. From 5bec9096a2318f7b29022b8c91ea7168f0ddc177 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Mon, 16 Sep 2024 14:43:27 -0700 Subject: [PATCH 005/591] Otel server context interceptor (#11500) Add opentelemetry tracing API, guarded by environmental variable(disabled by default). Use server interceptor to explicitly propagate span to the application thread. --- opentelemetry/build.gradle | 6 +- .../grpc/opentelemetry/GrpcOpenTelemetry.java | 26 +++ .../InternalGrpcOpenTelemetry.java | 4 + .../OpenTelemetryTracingModule.java | 93 ++++++++- .../opentelemetry/GrpcOpenTelemetryTest.java | 55 ++++++ .../OpenTelemetryTracingModuleTest.java | 181 +++++++++++++++++- 6 files changed, 357 insertions(+), 8 deletions(-) diff --git a/opentelemetry/build.gradle b/opentelemetry/build.gradle index 509960e5dbc..00d913c280d 100644 --- a/opentelemetry/build.gradle +++ b/opentelemetry/build.gradle @@ -14,8 +14,10 @@ dependencies { libraries.opentelemetry.api, libraries.auto.value.annotations - testImplementation testFixtures(project(':grpc-core')), - project(':grpc-testing'), + testImplementation project(':grpc-testing'), + project(':grpc-inprocess'), + testFixtures(project(':grpc-core')), + testFixtures(project(':grpc-api')), libraries.opentelemetry.sdk.testing, libraries.assertj.core // opentelemetry.sdk.testing uses compileOnly for assertj diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java index 03183ef4920..6b257a18a6c 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java @@ -33,10 +33,12 @@ import io.grpc.ManagedChannelBuilder; import io.grpc.MetricSink; import io.grpc.ServerBuilder; +import io.grpc.internal.GrpcUtil; import io.grpc.opentelemetry.internal.OpenTelemetryConstants; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.trace.Tracer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -61,6 +63,10 @@ public Stopwatch get() { } }; + @VisibleForTesting + static boolean ENABLE_OTEL_TRACING = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_ENABLE_OTEL_TRACING", + false); + private final OpenTelemetry openTelemetrySdk; private final MeterProvider meterProvider; private final Meter meter; @@ -68,6 +74,7 @@ public Stopwatch get() { private final boolean disableDefault; private final OpenTelemetryMetricsResource resource; private final OpenTelemetryMetricsModule openTelemetryMetricsModule; + private final OpenTelemetryTracingModule openTelemetryTracingModule; private final List optionalLabels; private final MetricSink sink; @@ -88,6 +95,7 @@ private GrpcOpenTelemetry(Builder builder) { this.optionalLabels = ImmutableList.copyOf(builder.optionalLabels); this.openTelemetryMetricsModule = new OpenTelemetryMetricsModule( STOPWATCH_SUPPLIER, resource, optionalLabels, builder.plugins); + this.openTelemetryTracingModule = new OpenTelemetryTracingModule(openTelemetrySdk); this.sink = new OpenTelemetryMetricSink(meter, enableMetrics, disableDefault, optionalLabels); } @@ -125,6 +133,11 @@ MetricSink getSink() { return sink; } + @VisibleForTesting + Tracer getTracer() { + return this.openTelemetryTracingModule.getTracer(); + } + /** * Registers GrpcOpenTelemetry globally, applying its configuration to all subsequently created * gRPC channels and servers. @@ -152,6 +165,9 @@ public void configureChannelBuilder(ManagedChannelBuilder builder) { InternalManagedChannelBuilder.addMetricSink(builder, sink); InternalManagedChannelBuilder.interceptWithTarget( builder, openTelemetryMetricsModule::getClientInterceptor); + if (ENABLE_OTEL_TRACING) { + builder.intercept(openTelemetryTracingModule.getClientInterceptor()); + } } /** @@ -161,6 +177,11 @@ public void configureChannelBuilder(ManagedChannelBuilder builder) { */ public void configureServerBuilder(ServerBuilder serverBuilder) { serverBuilder.addStreamTracerFactory(openTelemetryMetricsModule.getServerTracerFactory()); + if (ENABLE_OTEL_TRACING) { + serverBuilder.addStreamTracerFactory( + openTelemetryTracingModule.getServerTracerFactory()); + serverBuilder.intercept(openTelemetryTracingModule.getServerSpanPropagationInterceptor()); + } } @VisibleForTesting @@ -342,6 +363,11 @@ public Builder disableAllMetrics() { return this; } + Builder enableTracing(boolean enable) { + ENABLE_OTEL_TRACING = enable; + return this; + } + /** * Returns a new {@link GrpcOpenTelemetry} built with the configuration of this {@link * Builder}. diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/InternalGrpcOpenTelemetry.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/InternalGrpcOpenTelemetry.java index 5d5543dddda..ea1e7ab803f 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/InternalGrpcOpenTelemetry.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/InternalGrpcOpenTelemetry.java @@ -29,4 +29,8 @@ public static void builderPlugin( GrpcOpenTelemetry.Builder builder, InternalOpenTelemetryPlugin plugin) { builder.plugin(plugin); } + + public static void enableTracing(GrpcOpenTelemetry.Builder builder, boolean enable) { + builder.enableTracing(enable); + } } diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java index 11659c87708..6f2d3268ae0 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.ClientStreamTracer.NAME_RESOLUTION_DELAYED; +import static io.grpc.internal.GrpcUtil.IMPLEMENTATION_VERSION; import com.google.common.annotations.VisibleForTesting; import io.grpc.Attributes; @@ -28,15 +29,21 @@ import io.grpc.ClientStreamTracer; import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; +import io.grpc.ForwardingServerCallListener; import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; import io.grpc.ServerStreamTracer; +import io.grpc.opentelemetry.internal.OpenTelemetryConstants; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.ContextPropagators; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.logging.Level; @@ -50,7 +57,7 @@ final class OpenTelemetryTracingModule { private static final Logger logger = Logger.getLogger(OpenTelemetryTracingModule.class.getName()); @VisibleForTesting - static final String OTEL_TRACING_SCOPE_NAME = "grpc-java"; + final io.grpc.Context.Key otelSpan = io.grpc.Context.key("opentelemetry-span-key"); @Nullable private static final AtomicIntegerFieldUpdater callEndedUpdater; @Nullable @@ -83,13 +90,23 @@ final class OpenTelemetryTracingModule { private final MetadataGetter metadataGetter = MetadataGetter.getInstance(); private final MetadataSetter metadataSetter = MetadataSetter.getInstance(); private final TracingClientInterceptor clientInterceptor = new TracingClientInterceptor(); + private final ServerInterceptor serverSpanPropagationInterceptor = + new TracingServerSpanPropagationInterceptor(); private final ServerTracerFactory serverTracerFactory = new ServerTracerFactory(); OpenTelemetryTracingModule(OpenTelemetry openTelemetry) { - this.otelTracer = checkNotNull(openTelemetry.getTracer(OTEL_TRACING_SCOPE_NAME), "otelTracer"); + this.otelTracer = checkNotNull(openTelemetry.getTracerProvider(), "tracerProvider") + .tracerBuilder(OpenTelemetryConstants.INSTRUMENTATION_SCOPE) + .setInstrumentationVersion(IMPLEMENTATION_VERSION) + .build(); this.contextPropagators = checkNotNull(openTelemetry.getPropagators(), "contextPropagators"); } + @VisibleForTesting + Tracer getTracer() { + return otelTracer; + } + /** * Creates a {@link CallAttemptsTracerFactory} for a new call. */ @@ -112,6 +129,10 @@ ClientInterceptor getClientInterceptor() { return clientInterceptor; } + ServerInterceptor getServerSpanPropagationInterceptor() { + return serverSpanPropagationInterceptor; + } + @VisibleForTesting final class CallAttemptsTracerFactory extends ClientStreamTracer.Factory { volatile int callEnded; @@ -252,6 +273,11 @@ public void streamClosed(io.grpc.Status status) { endSpanWithStatus(span, status); } + @Override + public io.grpc.Context filterContext(io.grpc.Context context) { + return context.withValue(otelSpan, span); + } + @Override public void outboundMessageSent( int seqNo, long optionalWireSize, long optionalUncompressedSize) { @@ -293,6 +319,69 @@ public ServerStreamTracer newServerStreamTracer(String fullMethodName, Metadata } } + @VisibleForTesting + final class TracingServerSpanPropagationInterceptor implements ServerInterceptor { + @Override + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, ServerCallHandler next) { + Span span = otelSpan.get(io.grpc.Context.current()); + if (span == null) { + logger.log(Level.FINE, "Server span not found. ServerTracerFactory for server " + + "tracing must be set."); + return next.startCall(call, headers); + } + Context serverCallContext = Context.current().with(span); + try (Scope scope = serverCallContext.makeCurrent()) { + return new ContextServerCallListener<>(next.startCall(call, headers), serverCallContext); + } + } + } + + private static class ContextServerCallListener extends + ForwardingServerCallListener.SimpleForwardingServerCallListener { + private final Context context; + + protected ContextServerCallListener(ServerCall.Listener delegate, Context context) { + super(delegate); + this.context = checkNotNull(context, "context"); + } + + @Override + public void onMessage(ReqT message) { + try (Scope scope = context.makeCurrent()) { + delegate().onMessage(message); + } + } + + @Override + public void onHalfClose() { + try (Scope scope = context.makeCurrent()) { + delegate().onHalfClose(); + } + } + + @Override + public void onCancel() { + try (Scope scope = context.makeCurrent()) { + delegate().onCancel(); + } + } + + @Override + public void onComplete() { + try (Scope scope = context.makeCurrent()) { + delegate().onComplete(); + } + } + + @Override + public void onReady() { + try (Scope scope = context.makeCurrent()) { + delegate().onReady(); + } + } + } + @VisibleForTesting final class TracingClientInterceptor implements ClientInterceptor { diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/GrpcOpenTelemetryTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/GrpcOpenTelemetryTest.java index e4a0fa46e8b..1ae7b755a48 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/GrpcOpenTelemetryTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/GrpcOpenTelemetryTest.java @@ -17,15 +17,26 @@ package io.grpc.opentelemetry; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import com.google.common.collect.ImmutableList; +import io.grpc.ClientInterceptor; +import io.grpc.ManagedChannelBuilder; import io.grpc.MetricSink; +import io.grpc.ServerBuilder; import io.grpc.internal.GrpcUtil; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import io.opentelemetry.sdk.trace.SdkTracerProvider; import java.util.Arrays; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -35,7 +46,19 @@ public class GrpcOpenTelemetryTest { private final InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create(); private final SdkMeterProvider meterProvider = SdkMeterProvider.builder().registerMetricReader(inMemoryMetricReader).build(); + private final SdkTracerProvider tracerProvider = SdkTracerProvider.builder().build(); private final OpenTelemetry noopOpenTelemetry = OpenTelemetry.noop(); + private boolean originalEnableOtelTracing; + + @Before + public void setup() { + originalEnableOtelTracing = GrpcOpenTelemetry.ENABLE_OTEL_TRACING; + } + + @After + public void tearDown() { + GrpcOpenTelemetry.ENABLE_OTEL_TRACING = originalEnableOtelTracing; + } @Test public void build() { @@ -56,6 +79,31 @@ public void build() { assertThat(openTelemetryModule.getOptionalLabels()).isEqualTo(ImmutableList.of("version")); } + @Test + public void buildTracer() { + OpenTelemetrySdk sdk = + OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build(); + + GrpcOpenTelemetry grpcOpenTelemetry = GrpcOpenTelemetry.newBuilder() + .enableTracing(true) + .sdk(sdk).build(); + + assertThat(grpcOpenTelemetry.getOpenTelemetryInstance()).isSameInstanceAs(sdk); + assertThat(grpcOpenTelemetry.getTracer()).isSameInstanceAs( + tracerProvider.tracerBuilder("grpc-java") + .setInstrumentationVersion(GrpcUtil.IMPLEMENTATION_VERSION) + .build()); + ServerBuilder mockServerBuiler = mock(ServerBuilder.class); + grpcOpenTelemetry.configureServerBuilder(mockServerBuiler); + verify(mockServerBuiler, times(2)).addStreamTracerFactory(any()); + verify(mockServerBuiler).intercept(any()); + verifyNoMoreInteractions(mockServerBuiler); + + ManagedChannelBuilder mockChannelBuilder = mock(ManagedChannelBuilder.class); + grpcOpenTelemetry.configureChannelBuilder(mockChannelBuilder); + verify(mockChannelBuilder).intercept(any(ClientInterceptor.class)); + } + @Test public void builderDefaults() { GrpcOpenTelemetry module = GrpcOpenTelemetry.newBuilder().build(); @@ -73,6 +121,13 @@ public void builderDefaults() { assertThat(module.getEnableMetrics()).isEmpty(); assertThat(module.getOptionalLabels()).isEmpty(); assertThat(module.getSink()).isInstanceOf(MetricSink.class); + + assertThat(module.getTracer()).isSameInstanceAs(noopOpenTelemetry + .getTracerProvider() + .tracerBuilder("grpc-java") + .setInstrumentationVersion(GrpcUtil.IMPLEMENTATION_VERSION) + .build() + ); } @Test diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java index 68cba17e802..89e79d55d25 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java @@ -17,13 +17,14 @@ package io.grpc.opentelemetry; import static io.grpc.ClientStreamTracer.NAME_RESOLUTION_DELAYED; -import static io.grpc.opentelemetry.OpenTelemetryTracingModule.OTEL_TRACING_SCOPE_NAME; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -38,14 +39,23 @@ import io.grpc.ClientInterceptor; import io.grpc.ClientInterceptors; import io.grpc.ClientStreamTracer; +import io.grpc.ManagedChannel; import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import io.grpc.NoopServerCall; +import io.grpc.Server; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerInterceptors; import io.grpc.ServerServiceDefinition; import io.grpc.ServerStreamTracer; import io.grpc.Status; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.opentelemetry.OpenTelemetryTracingModule.CallAttemptsTracerFactory; +import io.grpc.opentelemetry.internal.OpenTelemetryConstants; +import io.grpc.testing.GrpcCleanupRule; import io.grpc.testing.GrpcServerRule; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; @@ -54,6 +64,8 @@ import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.TraceId; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerBuilder; +import io.opentelemetry.api.trace.TracerProvider; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.ContextPropagators; @@ -130,6 +142,8 @@ public String parse(InputStream stream) { public final OpenTelemetryRule openTelemetryRule = OpenTelemetryRule.create(); @Rule public final GrpcServerRule grpcServerRule = new GrpcServerRule().directExecutor(); + @Rule + public final GrpcCleanupRule grpcCleanupRule = new GrpcCleanupRule(); private Tracer tracerRule; @Mock private Tracer mockTracer; @@ -156,8 +170,15 @@ public String parse(InputStream stream) { @Before public void setUp() { - tracerRule = openTelemetryRule.getOpenTelemetry().getTracer(OTEL_TRACING_SCOPE_NAME); - when(mockOpenTelemetry.getTracer(OTEL_TRACING_SCOPE_NAME)).thenReturn(mockTracer); + tracerRule = openTelemetryRule.getOpenTelemetry().getTracer( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE); + TracerProvider mockTracerProvider = mock(TracerProvider.class); + when(mockOpenTelemetry.getTracerProvider()).thenReturn(mockTracerProvider); + TracerBuilder mockTracerBuilder = mock(TracerBuilder.class); + when(mockTracerProvider.tracerBuilder(OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .thenReturn(mockTracerBuilder); + when(mockTracerBuilder.setInstrumentationVersion(any())).thenReturn(mockTracerBuilder); + when(mockTracerBuilder.build()).thenReturn(mockTracer); when(mockOpenTelemetry.getPropagators()).thenReturn(ContextPropagators.create(mockPropagator)); when(mockSpanBuilder.startSpan()).thenReturn(mockAttemptSpan); when(mockSpanBuilder.setParent(any())).thenReturn(mockSpanBuilder); @@ -451,7 +472,8 @@ public ClientCall interceptCall( @Test public void clientStreamNeverCreatedStillRecordTracing() { - OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule(mockOpenTelemetry); + OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule( + openTelemetryRule.getOpenTelemetry()); CallAttemptsTracerFactory callTracer = tracingModule.newClientCallTracer(mockClientSpan, method); @@ -570,6 +592,157 @@ public void grpcTraceBinPropagator() { Span.fromContext(contextArgumentCaptor.getValue()).getSpanContext()); } + @Test + public void testServerParentSpanPropagation() throws Exception { + final AtomicReference applicationSpan = new AtomicReference<>(); + OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule( + openTelemetryRule.getOpenTelemetry()); + ServerServiceDefinition serviceDefinition = + ServerServiceDefinition.builder("package1.service2").addMethod( + method, new ServerCallHandler() { + @Override + public ServerCall.Listener startCall( + ServerCall call, Metadata headers) { + applicationSpan.set(Span.fromContext(Context.current())); + call.sendHeaders(new Metadata()); + call.sendMessage("Hello"); + call.close( + Status.PERMISSION_DENIED.withDescription("No you don't"), new Metadata()); + return mockServerCallListener; + } + }).build(); + + Server server = InProcessServerBuilder.forName("test-server-span") + .addService( + ServerInterceptors.intercept(serviceDefinition, + tracingModule.getServerSpanPropagationInterceptor())) + .addStreamTracerFactory(tracingModule.getServerTracerFactory()) + .directExecutor().build().start(); + grpcCleanupRule.register(server); + + ManagedChannel channel = InProcessChannelBuilder.forName("test-server-span") + .directExecutor().build(); + grpcCleanupRule.register(channel); + + Span parentSpan = tracerRule.spanBuilder("test-parent-span").startSpan(); + try (Scope scope = Context.current().with(parentSpan).makeCurrent()) { + Channel interceptedChannel = + ClientInterceptors.intercept( + channel, tracingModule.getClientInterceptor()); + ClientCall call = interceptedChannel.newCall(method, CALL_OPTIONS); + Metadata headers = new Metadata(); + call.start(mockClientCallListener, headers); + + // End the call + call.halfClose(); + call.request(1); + parentSpan.end(); + } + + verify(mockClientCallListener).onClose(statusCaptor.capture(), any(Metadata.class)); + Status rpcStatus = statusCaptor.getValue(); + assertEquals(rpcStatus.getCode(), Status.Code.PERMISSION_DENIED); + assertEquals(rpcStatus.getDescription(), "No you don't"); + assertEquals(applicationSpan.get().getSpanContext().getTraceId(), + parentSpan.getSpanContext().getTraceId()); + + List spans = openTelemetryRule.getSpans(); + assertEquals(spans.size(), 4); + SpanData clientSpan = spans.get(2); + SpanData attemptSpan = spans.get(1); + + assertEquals(clientSpan.getName(), "Sent.package1.service2.method3"); + assertTrue(clientSpan.hasEnded()); + assertEquals(clientSpan.getStatus().getStatusCode(), StatusCode.ERROR); + assertEquals(clientSpan.getStatus().getDescription(), "PERMISSION_DENIED: No you don't"); + + assertEquals(attemptSpan.getName(), "Attempt.package1.service2.method3"); + assertTrue(attemptSpan.hasEnded()); + assertEquals(attemptSpan.getStatus().getStatusCode(), StatusCode.ERROR); + assertEquals(attemptSpan.getStatus().getDescription(), "PERMISSION_DENIED: No you don't"); + + SpanData serverSpan = spans.get(0); + assertEquals(serverSpan.getName(), "Recv.package1.service2.method3"); + assertTrue(serverSpan.hasEnded()); + assertEquals(serverSpan.getStatus().getStatusCode(), StatusCode.ERROR); + assertEquals(serverSpan.getStatus().getDescription(), "PERMISSION_DENIED: No you don't"); + } + + @Test + public void serverSpanPropagationInterceptor() throws Exception { + OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule( + openTelemetryRule.getOpenTelemetry()); + Server server = InProcessServerBuilder.forName("test-span-propagation-interceptor") + .directExecutor().build().start(); + grpcCleanupRule.register(server); + final AtomicReference callbackSpan = new AtomicReference<>(); + ServerCall.Listener getContextListener = new ServerCall.Listener() { + @Override + public void onMessage(Integer message) { + callbackSpan.set(Span.fromContext(Context.current())); + } + + @Override + public void onHalfClose() { + callbackSpan.set(Span.fromContext(Context.current())); + } + + @Override + public void onCancel() { + callbackSpan.set(Span.fromContext(Context.current())); + } + + @Override + public void onComplete() { + callbackSpan.set(Span.fromContext(Context.current())); + } + }; + ServerInterceptor interceptor = tracingModule.getServerSpanPropagationInterceptor(); + @SuppressWarnings("unchecked") + ServerCallHandler handler = mock(ServerCallHandler.class); + when(handler.startCall(any(), any())).thenReturn(getContextListener); + ServerCall call = new NoopServerCall<>(); + Metadata metadata = new Metadata(); + ServerCall.Listener listener = interceptor.interceptCall(call, metadata, handler); + verify(handler).startCall(same(call), same(metadata)); + listener.onMessage(1); + assertEquals(callbackSpan.get(), Span.getInvalid()); + listener.onReady(); + assertEquals(callbackSpan.get(), Span.getInvalid()); + listener.onCancel(); + assertEquals(callbackSpan.get(), Span.getInvalid()); + listener.onHalfClose(); + assertEquals(callbackSpan.get(), Span.getInvalid()); + listener.onComplete(); + assertEquals(callbackSpan.get(), Span.getInvalid()); + + Span parentSpan = tracerRule.spanBuilder("parent-span").startSpan(); + io.grpc.Context context = io.grpc.Context.current().withValue( + tracingModule.otelSpan, parentSpan); + io.grpc.Context previous = context.attach(); + try { + listener = interceptor.interceptCall(call, metadata, handler); + verify(handler, times(2)).startCall(same(call), same(metadata)); + listener.onMessage(1); + assertEquals(callbackSpan.get().getSpanContext().getTraceId(), + parentSpan.getSpanContext().getTraceId()); + listener.onReady(); + assertEquals(callbackSpan.get().getSpanContext().getTraceId(), + parentSpan.getSpanContext().getTraceId()); + listener.onCancel(); + assertEquals(callbackSpan.get().getSpanContext().getTraceId(), + parentSpan.getSpanContext().getTraceId()); + listener.onHalfClose(); + assertEquals(callbackSpan.get().getSpanContext().getTraceId(), + parentSpan.getSpanContext().getTraceId()); + listener.onComplete(); + assertEquals(callbackSpan.get().getSpanContext().getTraceId(), + parentSpan.getSpanContext().getTraceId()); + } finally { + context.detach(previous); + } + } + @Test public void generateTraceSpanName() { assertEquals( From ce33df4a6f646621ef03f4ba6a6d76bd0bae67e6 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 16 Sep 2024 14:54:08 -0700 Subject: [PATCH 006/591] s2a: Use new-style syntax for plugins and remove unused deps There may be more unused deps, but #11527 makes it far too painful for me to bother to clean it up more. --- s2a/build.gradle | 42 ++---------------------------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/s2a/build.gradle b/s2a/build.gradle index 403ac93552f..234f983fd5c 100644 --- a/s2a/build.gradle +++ b/s2a/build.gradle @@ -1,22 +1,15 @@ -buildscript { - dependencies { - classpath 'com.google.gradle:osdetector-gradle-plugin:1.4.0' - } -} - plugins { id "java-library" id "maven-publish" id "com.github.johnrengelman.shadow" + id "com.google.osdetector" id "com.google.protobuf" id "ru.vyarus.animalsniffer" } description = "gRPC: S2A" -apply plugin: "com.google.osdetector" - dependencies { api project(':grpc-api') @@ -24,7 +17,6 @@ dependencies { project(':grpc-protobuf'), project(':grpc-core'), libraries.protobuf.java, - libraries.conscrypt, libraries.guava.jre // JRE required by protobuf-java-util from grpclb def nettyDependency = implementation project(':grpc-netty') compileOnly libraries.javax.annotation @@ -36,12 +28,7 @@ dependencies { project(':grpc-testing'), project(':grpc-testing-proto'), testFixtures(project(':grpc-core')), - libraries.guava, - libraries.junit, - libraries.mockito.core, - libraries.truth, - libraries.conscrypt, - libraries.netty.transport.epoll + libraries.guava testImplementation 'com.google.truth:truth:1.4.2' testImplementation 'com.google.truth.extensions:truth-proto-extension:1.4.2' @@ -74,35 +61,10 @@ dependencies { classifier = "windows-x86_64" } } - testRuntimeOnly (libraries.netty.transport.epoll) { - artifact { - classifier = "linux-x86_64" - } - } signature libraries.signature.java } -tasks.named("compileJava") { - dependsOn(tasks.named("generateProto")) - //dependsOn(tasks.named("syncGeneratedSourcesmain")) -} - - -tasks.named("sourcesJar") { - dependsOn(tasks.named("generateProto")) - //dependsOn(tasks.named("syncGeneratedSourcesmain")) -} - -sourceSets { - main { - //java.srcDirs += "src/generated/main/java" - //java.srcDirs += "src/generated/main/grpc" - } -} -//println sourceSets.main.java.srcDirs -//println sourceSets.test.resources.srcDirs - configureProtoCompilation() tasks.named("javadoc").configure { From bdc0530e1db16d98702da348796ef4e71c301b13 Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Tue, 17 Sep 2024 11:12:27 -0700 Subject: [PATCH 007/591] Fix slow tests that took 40 seconds to get through tearDown. (#11530) --- .../java/io/grpc/s2a/handshaker/IntegrationTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java index 859771a4afa..d9224f69b91 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java @@ -180,14 +180,15 @@ public void setUp() throws Exception { @After public void tearDown() throws Exception { - server.awaitTermination(10, SECONDS); server.shutdown(); - s2aServer.awaitTermination(10, SECONDS); s2aServer.shutdown(); - s2aDelayServer.awaitTermination(10, SECONDS); s2aDelayServer.shutdown(); - mtlsS2AServer.awaitTermination(10, SECONDS); mtlsS2AServer.shutdown(); + + server.awaitTermination(10, SECONDS); + s2aServer.awaitTermination(10, SECONDS); + s2aDelayServer.awaitTermination(10, SECONDS); + mtlsS2AServer.awaitTermination(10, SECONDS); } @Test From 9b0c19e698f0aa28b10f1c6513b5fb51b3e21954 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 16 Sep 2024 18:16:07 -0700 Subject: [PATCH 008/591] s2a: Cleanups to IntegrationTest Move unused and unimportant fields to local variables. pickUnusedPort() is inherently racy, so avoid using it when unnecessary. The channel's default executor is fine to use, but if you don't like it directExecutor() would be an option too. But blocking stub doesn't even use the executor for unary RPCs. Thread.join() does not propagate exceptions from the Thread; it just waits for the thread to exit. --- .../grpc/s2a/handshaker/IntegrationTest.java | 86 ++++++------------- 1 file changed, 27 insertions(+), 59 deletions(-) diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java index d9224f69b91..19dda7a19e4 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java @@ -44,9 +44,7 @@ import io.netty.handler.ssl.SslProvider; import java.io.ByteArrayInputStream; import java.io.File; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.logging.Level; +import java.util.concurrent.FutureTask; import java.util.logging.Logger; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSessionContext; @@ -127,28 +125,21 @@ public final class IntegrationTest { + "-----END PRIVATE KEY-----"; private String s2aAddress; - private int s2aPort; private Server s2aServer; private String s2aDelayAddress; - private int s2aDelayPort; private Server s2aDelayServer; private String mtlsS2AAddress; - private int mtlsS2APort; private Server mtlsS2AServer; - private int serverPort; private String serverAddress; private Server server; @Before public void setUp() throws Exception { - s2aPort = Utils.pickUnusedPort(); + s2aServer = ServerBuilder.forPort(0).addService(new FakeS2AServer()).build().start(); + int s2aPort = s2aServer.getPort(); s2aAddress = "localhost:" + s2aPort; - s2aServer = ServerBuilder.forPort(s2aPort).addService(new FakeS2AServer()).build(); logger.info("S2A service listening on localhost:" + s2aPort); - s2aServer.start(); - mtlsS2APort = Utils.pickUnusedPort(); - mtlsS2AAddress = "localhost:" + mtlsS2APort; File s2aCert = new File("src/test/resources/server_cert.pem"); File s2aKey = new File("src/test/resources/server_key.pem"); File rootCert = new File("src/test/resources/root_cert.pem"); @@ -158,24 +149,25 @@ public void setUp() throws Exception { .trustManager(rootCert) .clientAuth(TlsServerCredentials.ClientAuth.REQUIRE) .build(); - mtlsS2AServer = - NettyServerBuilder.forPort(mtlsS2APort, s2aCreds).addService(new FakeS2AServer()).build(); - logger.info("mTLS S2A service listening on localhost:" + mtlsS2APort); + mtlsS2AServer = NettyServerBuilder.forPort(0, s2aCreds).addService(new FakeS2AServer()).build(); mtlsS2AServer.start(); + int mtlsS2APort = mtlsS2AServer.getPort(); + mtlsS2AAddress = "localhost:" + mtlsS2APort; + logger.info("mTLS S2A service listening on localhost:" + mtlsS2APort); - s2aDelayPort = Utils.pickUnusedPort(); + int s2aDelayPort = Utils.pickUnusedPort(); s2aDelayAddress = "localhost:" + s2aDelayPort; s2aDelayServer = ServerBuilder.forPort(s2aDelayPort).addService(new FakeS2AServer()).build(); - serverPort = Utils.pickUnusedPort(); - serverAddress = "localhost:" + serverPort; server = - NettyServerBuilder.forPort(serverPort) + NettyServerBuilder.forPort(0) .addService(new SimpleServiceImpl()) .sslContext(buildSslContext()) - .build(); + .build() + .start(); + int serverPort = server.getPort(); + serverAddress = "localhost:" + serverPort; logger.info("Simple Service listening on localhost:" + serverPort); - server.start(); } @After @@ -193,28 +185,23 @@ public void tearDown() throws Exception { @Test public void clientCommunicateUsingS2ACredentials_succeeds() throws Exception { - ExecutorService executor = Executors.newSingleThreadExecutor(); ChannelCredentials credentials = S2AChannelCredentials.createBuilder(s2aAddress).setLocalSpiffeId("test-spiffe-id").build(); - ManagedChannel channel = - Grpc.newChannelBuilder(serverAddress, credentials).executor(executor).build(); + ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, credentials).build(); - assertThat(doUnaryRpc(executor, channel)).isTrue(); + assertThat(doUnaryRpc(channel)).isTrue(); } @Test public void clientCommunicateUsingS2ACredentialsNoLocalIdentity_succeeds() throws Exception { - ExecutorService executor = Executors.newSingleThreadExecutor(); ChannelCredentials credentials = S2AChannelCredentials.createBuilder(s2aAddress).build(); - ManagedChannel channel = - Grpc.newChannelBuilder(serverAddress, credentials).executor(executor).build(); + ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, credentials).build(); - assertThat(doUnaryRpc(executor, channel)).isTrue(); + assertThat(doUnaryRpc(channel)).isTrue(); } @Test public void clientCommunicateUsingMtlsToS2ACredentials_succeeds() throws Exception { - ExecutorService executor = Executors.newSingleThreadExecutor(); ChannelCredentials credentials = MtlsToS2AChannelCredentials.createBuilder( /* s2aAddress= */ mtlsS2AAddress, @@ -224,41 +211,24 @@ public void clientCommunicateUsingMtlsToS2ACredentials_succeeds() throws Excepti .build() .setLocalSpiffeId("test-spiffe-id") .build(); - ManagedChannel channel = - Grpc.newChannelBuilder(serverAddress, credentials).executor(executor).build(); + ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, credentials).build(); - assertThat(doUnaryRpc(executor, channel)).isTrue(); + assertThat(doUnaryRpc(channel)).isTrue(); } @Test public void clientCommunicateUsingS2ACredentials_s2AdelayStart_succeeds() throws Exception { - DoUnaryRpc doUnaryRpc = new DoUnaryRpc(); - doUnaryRpc.start(); + ChannelCredentials credentials = S2AChannelCredentials.createBuilder(s2aDelayAddress).build(); + ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, credentials).build(); + + FutureTask rpc = new FutureTask<>(() -> doUnaryRpc(channel)); + new Thread(rpc).start(); Thread.sleep(2000); s2aDelayServer.start(); - doUnaryRpc.join(); - } - - private class DoUnaryRpc extends Thread { - @Override - public void run() { - ExecutorService executor = Executors.newSingleThreadExecutor(); - ChannelCredentials credentials = S2AChannelCredentials.createBuilder(s2aDelayAddress).build(); - ManagedChannel channel = - Grpc.newChannelBuilder(serverAddress, credentials).executor(executor).build(); - boolean result = false; - try { - result = doUnaryRpc(executor, channel); - } catch (InterruptedException e) { - logger.log(Level.SEVERE, "Failed to do unary rpc", e); - result = false; - } - assertThat(result).isTrue(); - } + assertThat(rpc.get()).isTrue(); } - public static boolean doUnaryRpc(ExecutorService executor, ManagedChannel channel) - throws InterruptedException { + public static boolean doUnaryRpc(ManagedChannel channel) throws InterruptedException { try { SimpleServiceGrpc.SimpleServiceBlockingStub stub = SimpleServiceGrpc.newBlockingStub(channel); @@ -277,8 +247,6 @@ public static boolean doUnaryRpc(ExecutorService executor, ManagedChannel channe } finally { channel.shutdown(); channel.awaitTermination(1, SECONDS); - executor.shutdown(); - executor.awaitTermination(1, SECONDS); } } @@ -318,4 +286,4 @@ public void unaryRpc(SimpleRequest request, StreamObserver obser observer.onCompleted(); } } -} \ No newline at end of file +} From 782a44ad62f84d19291b0771b56b431e6e723752 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Thu, 19 Sep 2024 09:52:38 -0700 Subject: [PATCH 009/591] Implement ContextStorageOverride for opentelemetry context bridge (#11523) --- contextstorage/build.gradle | 28 ++++ .../grpc/override/ContextStorageOverride.java | 46 ++++++ .../override/OpenTelemetryContextStorage.java | 72 +++++++++ .../OpenTelemetryContextStorageTest.java | 144 ++++++++++++++++++ settings.gradle | 2 + 5 files changed, 292 insertions(+) create mode 100644 contextstorage/build.gradle create mode 100644 contextstorage/src/main/java/io/grpc/override/ContextStorageOverride.java create mode 100644 contextstorage/src/main/java/io/grpc/override/OpenTelemetryContextStorage.java create mode 100644 contextstorage/src/test/java/io/grpc/override/OpenTelemetryContextStorageTest.java diff --git a/contextstorage/build.gradle b/contextstorage/build.gradle new file mode 100644 index 00000000000..10c7f3a3a86 --- /dev/null +++ b/contextstorage/build.gradle @@ -0,0 +1,28 @@ +plugins { + id "java-library" + // until we are confident we like the name + //id "maven-publish" + + id "ru.vyarus.animalsniffer" +} + +description = 'gRPC: ContextStorageOverride' + +dependencies { + api project(':grpc-api') + implementation libraries.opentelemetry.api + + testImplementation libraries.junit, + libraries.opentelemetry.sdk.testing, + libraries.assertj.core + testImplementation 'junit:junit:4.13.1'// opentelemetry.sdk.testing uses compileOnly for assertj + + signature libraries.signature.java + signature libraries.signature.android +} + +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.override') + } +} diff --git a/contextstorage/src/main/java/io/grpc/override/ContextStorageOverride.java b/contextstorage/src/main/java/io/grpc/override/ContextStorageOverride.java new file mode 100644 index 00000000000..41b24765de0 --- /dev/null +++ b/contextstorage/src/main/java/io/grpc/override/ContextStorageOverride.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.override; + +import io.grpc.Context; + +/** + * Including this class in your dependencies will override the default gRPC context storage using + * reflection. It is a bridge between {@link io.grpc.Context} and + * {@link io.opentelemetry.context.Context}, i.e. propagating io.grpc.context.Context also + * propagates io.opentelemetry.context, and propagating io.opentelemetry.context will also propagate + * io.grpc.context. + */ +public final class ContextStorageOverride extends Context.Storage { + + private final Context.Storage delegate = new OpenTelemetryContextStorage(); + + @Override + public Context doAttach(Context toAttach) { + return delegate.doAttach(toAttach); + } + + @Override + public void detach(Context toDetach, Context toRestore) { + delegate.detach(toDetach, toRestore); + } + + @Override + public Context current() { + return delegate.current(); + } +} diff --git a/contextstorage/src/main/java/io/grpc/override/OpenTelemetryContextStorage.java b/contextstorage/src/main/java/io/grpc/override/OpenTelemetryContextStorage.java new file mode 100644 index 00000000000..01356e9f406 --- /dev/null +++ b/contextstorage/src/main/java/io/grpc/override/OpenTelemetryContextStorage.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.override; + +import io.grpc.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.Scope; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A Context.Storage implementation that attaches io.grpc.context to OpenTelemetry's context and + * io.opentelemetry.context is also saved in the io.grpc.context. + * Bridge between {@link io.grpc.Context} and {@link io.opentelemetry.context.Context}. + */ +final class OpenTelemetryContextStorage extends Context.Storage { + private static final Logger logger = Logger.getLogger( + OpenTelemetryContextStorage.class.getName()); + + private static final io.grpc.Context.Key OTEL_CONTEXT_OVER_GRPC + = io.grpc.Context.key("otel-context-over-grpc"); + private static final Context.Key OTEL_SCOPE = Context.key("otel-scope"); + private static final ContextKey GRPC_CONTEXT_OVER_OTEL = + ContextKey.named("grpc-context-over-otel"); + + @Override + @SuppressWarnings("MustBeClosedChecker") + public Context doAttach(Context toAttach) { + io.grpc.Context previous = current(); + io.opentelemetry.context.Context otelContext = OTEL_CONTEXT_OVER_GRPC.get(toAttach); + if (otelContext == null) { + otelContext = io.opentelemetry.context.Context.current(); + } + Scope scope = otelContext.with(GRPC_CONTEXT_OVER_OTEL, toAttach).makeCurrent(); + return previous.withValue(OTEL_SCOPE, scope); + } + + @Override + public void detach(Context toDetach, Context toRestore) { + Scope scope = OTEL_SCOPE.get(toRestore); + if (scope == null) { + logger.log( + Level.SEVERE, "Detaching context which was not attached."); + } else { + scope.close(); + } + } + + @Override + public Context current() { + io.opentelemetry.context.Context otelCurrent = io.opentelemetry.context.Context.current(); + io.grpc.Context grpcCurrent = otelCurrent.get(GRPC_CONTEXT_OVER_OTEL); + if (grpcCurrent == null) { + grpcCurrent = Context.ROOT; + } + return grpcCurrent.withValue(OTEL_CONTEXT_OVER_GRPC, otelCurrent); + } +} diff --git a/contextstorage/src/test/java/io/grpc/override/OpenTelemetryContextStorageTest.java b/contextstorage/src/test/java/io/grpc/override/OpenTelemetryContextStorageTest.java new file mode 100644 index 00000000000..3c628964342 --- /dev/null +++ b/contextstorage/src/test/java/io/grpc/override/OpenTelemetryContextStorageTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.override; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import com.google.common.util.concurrent.SettableFuture; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class OpenTelemetryContextStorageTest { + @Rule + public final OpenTelemetryRule openTelemetryRule = OpenTelemetryRule.create(); + private Tracer tracerRule = openTelemetryRule.getOpenTelemetry().getTracer( + "context-storage-test"); + private final io.grpc.Context.Key username = io.grpc.Context.key("username"); + private final ContextKey password = ContextKey.named("password"); + + @Test + public void grpcContextPropagation() throws Exception { + final Span parentSpan = tracerRule.spanBuilder("test-context").startSpan(); + final SettableFuture spanPropagated = SettableFuture.create(); + final SettableFuture grpcContextPropagated = SettableFuture.create(); + final SettableFuture spanDetached = SettableFuture.create(); + final SettableFuture grpcContextDetached = SettableFuture.create(); + + io.grpc.Context grpcContext; + try (Scope scope = Context.current().with(parentSpan).makeCurrent()) { + grpcContext = io.grpc.Context.current().withValue(username, "jeff"); + } + new Thread(new Runnable() { + @Override + public void run() { + io.grpc.Context previous = grpcContext.attach(); + try { + grpcContextPropagated.set(username.get(io.grpc.Context.current())); + spanPropagated.set(Span.fromContext(io.opentelemetry.context.Context.current())); + } finally { + grpcContext.detach(previous); + spanDetached.set(Span.fromContext(io.opentelemetry.context.Context.current())); + grpcContextDetached.set(username.get(io.grpc.Context.current())); + } + } + }).start(); + Assert.assertEquals(spanPropagated.get(5, TimeUnit.SECONDS), parentSpan); + Assert.assertEquals(grpcContextPropagated.get(5, TimeUnit.SECONDS), "jeff"); + Assert.assertEquals(spanDetached.get(5, TimeUnit.SECONDS), Span.getInvalid()); + Assert.assertNull(grpcContextDetached.get(5, TimeUnit.SECONDS)); + } + + @Test + public void otelContextPropagation() throws Exception { + final SettableFuture grpcPropagated = SettableFuture.create(); + final AtomicReference otelPropagation = new AtomicReference<>(); + + io.grpc.Context grpcContext = io.grpc.Context.current().withValue(username, "jeff"); + io.grpc.Context previous = grpcContext.attach(); + Context original = Context.current().with(password, "valentine"); + try { + new Thread( + () -> { + try (Scope scope = original.makeCurrent()) { + otelPropagation.set(Context.current().get(password)); + grpcPropagated.set(username.get(io.grpc.Context.current())); + } + } + ).start(); + } finally { + grpcContext.detach(previous); + } + Assert.assertEquals(grpcPropagated.get(5, TimeUnit.SECONDS), "jeff"); + Assert.assertEquals(otelPropagation.get(), "valentine"); + } + + @Test + public void grpcOtelMix() { + io.grpc.Context grpcContext = io.grpc.Context.current().withValue(username, "jeff"); + Context otelContext = Context.current().with(password, "valentine"); + Assert.assertNull(username.get(io.grpc.Context.current())); + Assert.assertNull(Context.current().get(password)); + io.grpc.Context previous = grpcContext.attach(); + try { + assertEquals(username.get(io.grpc.Context.current()), "jeff"); + try (Scope scope = otelContext.makeCurrent()) { + Assert.assertEquals(Context.current().get(password), "valentine"); + assertNull(username.get(io.grpc.Context.current())); + + io.grpc.Context grpcContext2 = io.grpc.Context.current().withValue(username, "frank"); + io.grpc.Context previous2 = grpcContext2.attach(); + try { + assertEquals(username.get(io.grpc.Context.current()), "frank"); + Assert.assertEquals(Context.current().get(password), "valentine"); + } finally { + grpcContext2.detach(previous2); + } + assertNull(username.get(io.grpc.Context.current())); + Assert.assertEquals(Context.current().get(password), "valentine"); + } + } finally { + grpcContext.detach(previous); + } + Assert.assertNull(username.get(io.grpc.Context.current())); + Assert.assertNull(Context.current().get(password)); + } + + @Test + public void grpcContextDetachError() { + io.grpc.Context grpcContext = io.grpc.Context.current().withValue(username, "jeff"); + io.grpc.Context previous = grpcContext.attach(); + try { + previous.detach(grpcContext); + assertEquals(username.get(io.grpc.Context.current()), "jeff"); + } finally { + grpcContext.detach(previous); + } + } +} diff --git a/settings.gradle b/settings.gradle index 03eca809226..b661e0f52db 100644 --- a/settings.gradle +++ b/settings.gradle @@ -77,6 +77,7 @@ include ":grpc-istio-interop-testing" include ":grpc-inprocess" include ":grpc-util" include ":grpc-opentelemetry" +include ":grpc-opentelemetry-context-storage-override" project(':grpc-api').projectDir = "$rootDir/api" as File project(':grpc-core').projectDir = "$rootDir/core" as File @@ -113,6 +114,7 @@ project(':grpc-istio-interop-testing').projectDir = "$rootDir/istio-interop-test project(':grpc-inprocess').projectDir = "$rootDir/inprocess" as File project(':grpc-util').projectDir = "$rootDir/util" as File project(':grpc-opentelemetry').projectDir = "$rootDir/opentelemetry" as File +project(':grpc-opentelemetry-context-storage-override').projectDir = "$rootDir/contextstorage" as File if (settings.hasProperty('skipCodegen') && skipCodegen.toBoolean()) { println '*** Skipping the build of codegen and compilation of proto files because skipCodegen=true' From e75a044107bc8701b2e328b7ec633c982fb3a844 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Fri, 20 Sep 2024 12:32:54 -0700 Subject: [PATCH 010/591] s2a,netty: S2AHandshakerServiceChannel doesn't use custom event loop. (#11539) * S2AHandshakerServiceChannel doesn't use custom event loop. * use executorPool. * log when channel not shutdown. * use a cached threadpool. * update non-executor version. --- .../netty/InternalProtocolNegotiators.java | 18 ++++++- .../channel/S2AHandshakerServiceChannel.java | 50 +++++++------------ .../S2AProtocolNegotiatorFactory.java | 7 ++- .../S2AHandshakerServiceChannelTest.java | 46 +++++++---------- 4 files changed, 56 insertions(+), 65 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java index 0d309828c6d..b9c6a77982a 100644 --- a/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java @@ -17,12 +17,14 @@ package io.grpc.netty; import io.grpc.ChannelLogger; +import io.grpc.internal.ObjectPool; import io.grpc.netty.ProtocolNegotiators.ClientTlsHandler; import io.grpc.netty.ProtocolNegotiators.GrpcNegotiationHandler; import io.grpc.netty.ProtocolNegotiators.WaitUntilActiveHandler; import io.netty.channel.ChannelHandler; import io.netty.handler.ssl.SslContext; import io.netty.util.AsciiString; +import java.util.concurrent.Executor; /** * Internal accessor for {@link ProtocolNegotiators}. @@ -35,9 +37,12 @@ private InternalProtocolNegotiators() {} * Returns a {@link ProtocolNegotiator} that ensures the pipeline is set up so that TLS will * be negotiated, the {@code handler} is added and writes to the {@link io.netty.channel.Channel} * may happen immediately, even before the TLS Handshake is complete. + * @param executorPool a dedicated {@link Executor} pool for time-consuming TLS tasks */ - public static InternalProtocolNegotiator.ProtocolNegotiator tls(SslContext sslContext) { - final io.grpc.netty.ProtocolNegotiator negotiator = ProtocolNegotiators.tls(sslContext); + public static InternalProtocolNegotiator.ProtocolNegotiator tls(SslContext sslContext, + ObjectPool executorPool) { + final io.grpc.netty.ProtocolNegotiator negotiator = ProtocolNegotiators.tls(sslContext, + executorPool); final class TlsNegotiator implements InternalProtocolNegotiator.ProtocolNegotiator { @Override @@ -58,6 +63,15 @@ public void close() { return new TlsNegotiator(); } + + /** + * Returns a {@link ProtocolNegotiator} that ensures the pipeline is set up so that TLS will + * be negotiated, the {@code handler} is added and writes to the {@link io.netty.channel.Channel} + * may happen immediately, even before the TLS Handshake is complete. + */ + public static InternalProtocolNegotiator.ProtocolNegotiator tls(SslContext sslContext) { + return tls(sslContext, null); + } /** * Returns a {@link ProtocolNegotiator} that ensures the pipeline is set up so that TLS will be diff --git a/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java b/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java index 75ec7347bb5..90956907bfe 100644 --- a/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java +++ b/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java @@ -29,13 +29,11 @@ import io.grpc.MethodDescriptor; import io.grpc.internal.SharedResourceHolder.Resource; import io.grpc.netty.NettyChannelBuilder; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.util.concurrent.DefaultThreadFactory; import java.time.Duration; import java.util.Optional; import java.util.concurrent.ConcurrentMap; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.concurrent.ThreadSafe; /** @@ -61,7 +59,6 @@ public final class S2AHandshakerServiceChannel { private static final ConcurrentMap> SHARED_RESOURCE_CHANNELS = Maps.newConcurrentMap(); - private static final Duration DELEGATE_TERMINATION_TIMEOUT = Duration.ofSeconds(2); private static final Duration CHANNEL_SHUTDOWN_TIMEOUT = Duration.ofSeconds(10); /** @@ -95,41 +92,34 @@ public ChannelResource(String targetAddress, Optional channe } /** - * Creates a {@code EventLoopHoldingChannel} instance to the service running at {@code - * targetAddress}. This channel uses a dedicated thread pool for its {@code EventLoopGroup} - * instance to avoid blocking. + * Creates a {@code HandshakerServiceChannel} instance to the service running at {@code + * targetAddress}. */ @Override public Channel create() { - EventLoopGroup eventLoopGroup = - new NioEventLoopGroup(1, new DefaultThreadFactory("S2A channel pool", true)); ManagedChannel channel = null; if (channelCredentials.isPresent()) { // Create a secure channel. channel = NettyChannelBuilder.forTarget(targetAddress, channelCredentials.get()) - .channelType(NioSocketChannel.class) .directExecutor() - .eventLoopGroup(eventLoopGroup) .build(); } else { // Create a plaintext channel. channel = NettyChannelBuilder.forTarget(targetAddress) - .channelType(NioSocketChannel.class) .directExecutor() - .eventLoopGroup(eventLoopGroup) .usePlaintext() .build(); } - return EventLoopHoldingChannel.create(channel, eventLoopGroup); + return HandshakerServiceChannel.create(channel); } - /** Destroys a {@code EventLoopHoldingChannel} instance. */ + /** Destroys a {@code HandshakerServiceChannel} instance. */ @Override public void close(Channel instanceChannel) { checkNotNull(instanceChannel); - EventLoopHoldingChannel channel = (EventLoopHoldingChannel) instanceChannel; + HandshakerServiceChannel channel = (HandshakerServiceChannel) instanceChannel; channel.close(); } @@ -140,23 +130,21 @@ public String toString() { } /** - * Manages a channel using a {@link ManagedChannel} instance that belong to the {@code - * EventLoopGroup} thread pool. + * Manages a channel using a {@link ManagedChannel} instance. */ @VisibleForTesting - static class EventLoopHoldingChannel extends Channel { + static class HandshakerServiceChannel extends Channel { + private static final Logger logger = + Logger.getLogger(S2AHandshakerServiceChannel.class.getName()); private final ManagedChannel delegate; - private final EventLoopGroup eventLoopGroup; - static EventLoopHoldingChannel create(ManagedChannel delegate, EventLoopGroup eventLoopGroup) { + static HandshakerServiceChannel create(ManagedChannel delegate) { checkNotNull(delegate); - checkNotNull(eventLoopGroup); - return new EventLoopHoldingChannel(delegate, eventLoopGroup); + return new HandshakerServiceChannel(delegate); } - private EventLoopHoldingChannel(ManagedChannel delegate, EventLoopGroup eventLoopGroup) { + private HandshakerServiceChannel(ManagedChannel delegate) { this.delegate = delegate; - this.eventLoopGroup = eventLoopGroup; } /** @@ -178,16 +166,12 @@ public ClientCall newCall( @SuppressWarnings("FutureReturnValueIgnored") public void close() { delegate.shutdownNow(); - boolean isDelegateTerminated; try { - isDelegateTerminated = - delegate.awaitTermination(DELEGATE_TERMINATION_TIMEOUT.getSeconds(), SECONDS); + delegate.awaitTermination(CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS); } catch (InterruptedException e) { - isDelegateTerminated = false; + Thread.currentThread().interrupt(); + logger.log(Level.WARNING, "Channel to S2A was not shutdown."); } - long quietPeriodSeconds = isDelegateTerminated ? 0 : 1; - eventLoopGroup.shutdownGracefully( - quietPeriodSeconds, CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS); } } diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java index 25d1e325ea8..14bdc05238d 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java @@ -29,7 +29,9 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.errorprone.annotations.ThreadSafe; import io.grpc.Channel; +import io.grpc.internal.GrpcUtil; import io.grpc.internal.ObjectPool; +import io.grpc.internal.SharedResourcePool; import io.grpc.netty.GrpcHttp2ConnectionHandler; import io.grpc.netty.InternalProtocolNegotiator; import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; @@ -227,7 +229,10 @@ protected void handlerAdded0(ChannelHandlerContext ctx) { @Override public void onSuccess(SslContext sslContext) { ChannelHandler handler = - InternalProtocolNegotiators.tls(sslContext).newHandler(grpcHandler); + InternalProtocolNegotiators.tls( + sslContext, + SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR)) + .newHandler(grpcHandler); // Remove the bufferReads handler and delegate the rest of the handshake to the TLS // handler. diff --git a/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java b/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java index 57288be1b6f..dc5909442bf 100644 --- a/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java +++ b/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java @@ -18,11 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; -import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import io.grpc.CallOptions; import io.grpc.Channel; @@ -39,15 +35,13 @@ import io.grpc.benchmarks.Utils; import io.grpc.internal.SharedResourceHolder.Resource; import io.grpc.netty.NettyServerBuilder; -import io.grpc.s2a.channel.S2AHandshakerServiceChannel.EventLoopHoldingChannel; +import io.grpc.s2a.channel.S2AHandshakerServiceChannel.HandshakerServiceChannel; import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; import io.grpc.testing.protobuf.SimpleRequest; import io.grpc.testing.protobuf.SimpleResponse; import io.grpc.testing.protobuf.SimpleServiceGrpc; -import io.netty.channel.EventLoopGroup; import java.io.File; -import java.time.Duration; import java.util.Optional; import java.util.concurrent.TimeUnit; import org.junit.Before; @@ -60,8 +54,6 @@ @RunWith(JUnit4.class) public final class S2AHandshakerServiceChannelTest { @ClassRule public static final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); - private static final Duration CHANNEL_SHUTDOWN_TIMEOUT = Duration.ofSeconds(10); - private final EventLoopGroup mockEventLoopGroup = mock(EventLoopGroup.class); private Server mtlsServer; private Server plaintextServer; @@ -191,7 +183,7 @@ public void close_mtlsSuccess() throws Exception { } /** - * Verifies that an {@code EventLoopHoldingChannel}'s {@code newCall} method can be used to + * Verifies that an {@code HandshakerServiceChannel}'s {@code newCall} method can be used to * perform a simple RPC. */ @Test @@ -201,7 +193,7 @@ public void newCall_performSimpleRpcSuccess() { "localhost:" + plaintextServer.getPort(), /* s2aChannelCredentials= */ Optional.empty()); Channel channel = resource.create(); - assertThat(channel).isInstanceOf(EventLoopHoldingChannel.class); + assertThat(channel).isInstanceOf(HandshakerServiceChannel.class); assertThat( SimpleServiceGrpc.newBlockingStub(channel).unaryRpc(SimpleRequest.getDefaultInstance())) .isEqualToDefaultInstance(); @@ -214,53 +206,49 @@ public void newCall_mtlsPerformSimpleRpcSuccess() throws Exception { S2AHandshakerServiceChannel.getChannelResource( "localhost:" + mtlsServer.getPort(), getTlsChannelCredentials()); Channel channel = resource.create(); - assertThat(channel).isInstanceOf(EventLoopHoldingChannel.class); + assertThat(channel).isInstanceOf(HandshakerServiceChannel.class); assertThat( SimpleServiceGrpc.newBlockingStub(channel).unaryRpc(SimpleRequest.getDefaultInstance())) .isEqualToDefaultInstance(); } - /** Creates a {@code EventLoopHoldingChannel} instance and verifies its authority. */ + /** Creates a {@code HandshakerServiceChannel} instance and verifies its authority. */ @Test public void authority_success() throws Exception { ManagedChannel channel = new FakeManagedChannel(true); - EventLoopHoldingChannel eventLoopHoldingChannel = - EventLoopHoldingChannel.create(channel, mockEventLoopGroup); + HandshakerServiceChannel eventLoopHoldingChannel = + HandshakerServiceChannel.create(channel); assertThat(eventLoopHoldingChannel.authority()).isEqualTo("FakeManagedChannel"); } /** - * Creates and closes a {@code EventLoopHoldingChannel} when its {@code ManagedChannel} terminates - * successfully. + * Creates and closes a {@code HandshakerServiceChannel} when its {@code ManagedChannel} + * terminates successfully. */ @Test public void close_withDelegateTerminatedSuccess() throws Exception { ManagedChannel channel = new FakeManagedChannel(true); - EventLoopHoldingChannel eventLoopHoldingChannel = - EventLoopHoldingChannel.create(channel, mockEventLoopGroup); + HandshakerServiceChannel eventLoopHoldingChannel = + HandshakerServiceChannel.create(channel); eventLoopHoldingChannel.close(); assertThat(channel.isShutdown()).isTrue(); - verify(mockEventLoopGroup, times(1)) - .shutdownGracefully(0, CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS); } /** - * Creates and closes a {@code EventLoopHoldingChannel} when its {@code ManagedChannel} does not + * Creates and closes a {@code HandshakerServiceChannel} when its {@code ManagedChannel} does not * terminate successfully. */ @Test public void close_withDelegateTerminatedFailure() throws Exception { ManagedChannel channel = new FakeManagedChannel(false); - EventLoopHoldingChannel eventLoopHoldingChannel = - EventLoopHoldingChannel.create(channel, mockEventLoopGroup); + HandshakerServiceChannel eventLoopHoldingChannel = + HandshakerServiceChannel.create(channel); eventLoopHoldingChannel.close(); assertThat(channel.isShutdown()).isTrue(); - verify(mockEventLoopGroup, times(1)) - .shutdownGracefully(1, CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS); } /** - * Creates and closes a {@code EventLoopHoldingChannel}, creates a new channel from the same + * Creates and closes a {@code HandshakerServiceChannel}, creates a new channel from the same * resource, and verifies that this second channel is useable. */ @Test @@ -273,7 +261,7 @@ public void create_succeedsAfterCloseIsCalledOnce() throws Exception { resource.close(channelOne); Channel channelTwo = resource.create(); - assertThat(channelTwo).isInstanceOf(EventLoopHoldingChannel.class); + assertThat(channelTwo).isInstanceOf(HandshakerServiceChannel.class); assertThat( SimpleServiceGrpc.newBlockingStub(channelTwo) .unaryRpc(SimpleRequest.getDefaultInstance())) @@ -291,7 +279,7 @@ public void create_mtlsSucceedsAfterCloseIsCalledOnce() throws Exception { resource.close(channelOne); Channel channelTwo = resource.create(); - assertThat(channelTwo).isInstanceOf(EventLoopHoldingChannel.class); + assertThat(channelTwo).isInstanceOf(HandshakerServiceChannel.class); assertThat( SimpleServiceGrpc.newBlockingStub(channelTwo) .unaryRpc(SimpleRequest.getDefaultInstance())) From d8f73e04566fa588889ca1a422e276d71724643c Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:53:14 -0700 Subject: [PATCH 011/591] s2a: Address comments on PR#11113 (#11534) * Mark S2A public APIs as experimental. * Rename S2AChannelCredentials createBuilder API to newBuilder. * Remove usage of AdvancedTls. * Use InsecureChannelCredentials.create instead of Optional. * Invoke Thread.currentThread().interrupt() in a InterruptedException block. --- .../grpc/s2a/MtlsToS2AChannelCredentials.java | 21 ++++-------- .../io/grpc/s2a/S2AChannelCredentials.java | 12 ++++--- .../channel/S2AHandshakerServiceChannel.java | 27 +++++---------- .../grpc/s2a/handshaker/S2ATrustManager.java | 3 ++ .../s2a/MtlsToS2AChannelCredentialsTest.java | 34 +++++++++---------- .../grpc/s2a/S2AChannelCredentialsTest.java | 24 ++++++------- .../S2AHandshakerServiceChannelTest.java | 25 +++++++------- .../grpc/s2a/handshaker/IntegrationTest.java | 8 ++--- .../S2AProtocolNegotiatorFactoryTest.java | 2 +- .../io/grpc/s2a/handshaker/S2AStubTest.java | 4 +-- 10 files changed, 73 insertions(+), 87 deletions(-) diff --git a/s2a/src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java b/s2a/src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java index 56f612502bf..e8eb01628ed 100644 --- a/s2a/src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java +++ b/s2a/src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java @@ -21,17 +21,16 @@ import static com.google.common.base.Strings.isNullOrEmpty; import io.grpc.ChannelCredentials; +import io.grpc.ExperimentalApi; import io.grpc.TlsChannelCredentials; -import io.grpc.util.AdvancedTlsX509KeyManager; -import io.grpc.util.AdvancedTlsX509TrustManager; import java.io.File; import java.io.IOException; -import java.security.GeneralSecurityException; /** * Configures an {@code S2AChannelCredentials.Builder} instance with credentials used to establish a * connection with the S2A to support talking to the S2A over mTLS. */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/11533") public final class MtlsToS2AChannelCredentials { /** * Creates a {@code S2AChannelCredentials.Builder} builder, that talks to the S2A over mTLS. @@ -42,7 +41,7 @@ public final class MtlsToS2AChannelCredentials { * @param trustBundlePath the path to the trust bundle PEM. * @return a {@code MtlsToS2AChannelCredentials.Builder} instance. */ - public static Builder createBuilder( + public static Builder newBuilder( String s2aAddress, String privateKeyPath, String certChainPath, String trustBundlePath) { checkArgument(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty."); checkArgument(!isNullOrEmpty(privateKeyPath), "privateKeyPath must not be null or empty."); @@ -66,7 +65,7 @@ public static final class Builder { this.trustBundlePath = trustBundlePath; } - public S2AChannelCredentials.Builder build() throws GeneralSecurityException, IOException { + public S2AChannelCredentials.Builder build() throws IOException { checkState(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty."); checkState(!isNullOrEmpty(privateKeyPath), "privateKeyPath must not be null or empty."); checkState(!isNullOrEmpty(certChainPath), "certChainPath must not be null or empty."); @@ -75,19 +74,13 @@ public S2AChannelCredentials.Builder build() throws GeneralSecurityException, IO File certChainFile = new File(certChainPath); File trustBundleFile = new File(trustBundlePath); - AdvancedTlsX509KeyManager keyManager = new AdvancedTlsX509KeyManager(); - keyManager.updateIdentityCredentials(certChainFile, privateKeyFile); - - AdvancedTlsX509TrustManager trustManager = AdvancedTlsX509TrustManager.newBuilder().build(); - trustManager.updateTrustCredentials(trustBundleFile); - ChannelCredentials channelToS2ACredentials = TlsChannelCredentials.newBuilder() - .keyManager(keyManager) - .trustManager(trustManager) + .keyManager(certChainFile, privateKeyFile) + .trustManager(trustBundleFile) .build(); - return S2AChannelCredentials.createBuilder(s2aAddress) + return S2AChannelCredentials.newBuilder(s2aAddress) .setS2AChannelCredentials(channelToS2ACredentials); } } diff --git a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java index 8a5f1f51350..ba0f6d72de1 100644 --- a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java +++ b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java @@ -24,6 +24,8 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.grpc.Channel; import io.grpc.ChannelCredentials; +import io.grpc.ExperimentalApi; +import io.grpc.InsecureChannelCredentials; import io.grpc.internal.ObjectPool; import io.grpc.internal.SharedResourcePool; import io.grpc.netty.InternalNettyChannelCredentials; @@ -31,7 +33,6 @@ import io.grpc.s2a.channel.S2AHandshakerServiceChannel; import io.grpc.s2a.handshaker.S2AIdentity; import io.grpc.s2a.handshaker.S2AProtocolNegotiatorFactory; -import java.util.Optional; import javax.annotation.concurrent.NotThreadSafe; import org.checkerframework.checker.nullness.qual.Nullable; @@ -39,6 +40,7 @@ * Configures gRPC to use S2A for transport security when establishing a secure channel. Only for * use on the client side of a gRPC connection. */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/11533") public final class S2AChannelCredentials { /** * Creates a channel credentials builder for establishing an S2A-secured connection. @@ -46,7 +48,7 @@ public final class S2AChannelCredentials { * @param s2aAddress the address of the S2A server used to secure the connection. * @return a {@code S2AChannelCredentials.Builder} instance. */ - public static Builder createBuilder(String s2aAddress) { + public static Builder newBuilder(String s2aAddress) { checkArgument(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty."); return new Builder(s2aAddress); } @@ -56,13 +58,13 @@ public static Builder createBuilder(String s2aAddress) { public static final class Builder { private final String s2aAddress; private ObjectPool s2aChannelPool; - private Optional s2aChannelCredentials; + private ChannelCredentials s2aChannelCredentials; private @Nullable S2AIdentity localIdentity = null; Builder(String s2aAddress) { this.s2aAddress = s2aAddress; this.s2aChannelPool = null; - this.s2aChannelCredentials = Optional.empty(); + this.s2aChannelCredentials = InsecureChannelCredentials.create(); } /** @@ -107,7 +109,7 @@ public Builder setLocalUid(String localUid) { /** Sets the credentials to be used when connecting to the S2A. */ @CanIgnoreReturnValue public Builder setS2AChannelCredentials(ChannelCredentials s2aChannelCredentials) { - this.s2aChannelCredentials = Optional.of(s2aChannelCredentials); + this.s2aChannelCredentials = s2aChannelCredentials; return this; } diff --git a/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java b/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java index 90956907bfe..443ea553e52 100644 --- a/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java +++ b/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java @@ -30,7 +30,6 @@ import io.grpc.internal.SharedResourceHolder.Resource; import io.grpc.netty.NettyChannelBuilder; import java.time.Duration; -import java.util.Optional; import java.util.concurrent.ConcurrentMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -71,8 +70,9 @@ public final class S2AHandshakerServiceChannel { * running at {@code s2aAddress}. */ public static Resource getChannelResource( - String s2aAddress, Optional s2aChannelCredentials) { + String s2aAddress, ChannelCredentials s2aChannelCredentials) { checkNotNull(s2aAddress); + checkNotNull(s2aChannelCredentials); return SHARED_RESOURCE_CHANNELS.computeIfAbsent( s2aAddress, channelResource -> new ChannelResource(s2aAddress, s2aChannelCredentials)); } @@ -84,9 +84,9 @@ public static Resource getChannelResource( */ private static class ChannelResource implements Resource { private final String targetAddress; - private final Optional channelCredentials; + private final ChannelCredentials channelCredentials; - public ChannelResource(String targetAddress, Optional channelCredentials) { + public ChannelResource(String targetAddress, ChannelCredentials channelCredentials) { this.targetAddress = targetAddress; this.channelCredentials = channelCredentials; } @@ -97,21 +97,10 @@ public ChannelResource(String targetAddress, Optional channe */ @Override public Channel create() { - ManagedChannel channel = null; - if (channelCredentials.isPresent()) { - // Create a secure channel. - channel = - NettyChannelBuilder.forTarget(targetAddress, channelCredentials.get()) - .directExecutor() - .build(); - } else { - // Create a plaintext channel. - channel = - NettyChannelBuilder.forTarget(targetAddress) - .directExecutor() - .usePlaintext() - .build(); - } + ManagedChannel channel = + NettyChannelBuilder.forTarget(targetAddress, channelCredentials) + .directExecutor() + .build(); return HandshakerServiceChannel.create(channel); } diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java b/s2a/src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java index fb113bb29cc..aafbb94c047 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java @@ -121,6 +121,9 @@ private void checkPeerTrusted(X509Certificate[] chain, boolean isCheckingClientC try { resp = stub.send(reqBuilder.build()); } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } throw new CertificateException("Failed to send request to S2A.", e); } if (resp.hasStatus() && resp.getStatus().getCode() != 0) { diff --git a/s2a/src/test/java/io/grpc/s2a/MtlsToS2AChannelCredentialsTest.java b/s2a/src/test/java/io/grpc/s2a/MtlsToS2AChannelCredentialsTest.java index 5ccc522292e..0fc4ecb3268 100644 --- a/s2a/src/test/java/io/grpc/s2a/MtlsToS2AChannelCredentialsTest.java +++ b/s2a/src/test/java/io/grpc/s2a/MtlsToS2AChannelCredentialsTest.java @@ -26,11 +26,11 @@ @RunWith(JUnit4.class) public final class MtlsToS2AChannelCredentialsTest { @Test - public void createBuilder_nullAddress_throwsException() throws Exception { + public void newBuilder_nullAddress_throwsException() throws Exception { assertThrows( IllegalArgumentException.class, () -> - MtlsToS2AChannelCredentials.createBuilder( + MtlsToS2AChannelCredentials.newBuilder( /* s2aAddress= */ null, /* privateKeyPath= */ "src/test/resources/client_key.pem", /* certChainPath= */ "src/test/resources/client_cert.pem", @@ -38,11 +38,11 @@ public void createBuilder_nullAddress_throwsException() throws Exception { } @Test - public void createBuilder_nullPrivateKeyPath_throwsException() throws Exception { + public void newBuilder_nullPrivateKeyPath_throwsException() throws Exception { assertThrows( IllegalArgumentException.class, () -> - MtlsToS2AChannelCredentials.createBuilder( + MtlsToS2AChannelCredentials.newBuilder( /* s2aAddress= */ "s2a_address", /* privateKeyPath= */ null, /* certChainPath= */ "src/test/resources/client_cert.pem", @@ -50,11 +50,11 @@ public void createBuilder_nullPrivateKeyPath_throwsException() throws Exception } @Test - public void createBuilder_nullCertChainPath_throwsException() throws Exception { + public void newBuilder_nullCertChainPath_throwsException() throws Exception { assertThrows( IllegalArgumentException.class, () -> - MtlsToS2AChannelCredentials.createBuilder( + MtlsToS2AChannelCredentials.newBuilder( /* s2aAddress= */ "s2a_address", /* privateKeyPath= */ "src/test/resources/client_key.pem", /* certChainPath= */ null, @@ -62,11 +62,11 @@ public void createBuilder_nullCertChainPath_throwsException() throws Exception { } @Test - public void createBuilder_nullTrustBundlePath_throwsException() throws Exception { + public void newBuilder_nullTrustBundlePath_throwsException() throws Exception { assertThrows( IllegalArgumentException.class, () -> - MtlsToS2AChannelCredentials.createBuilder( + MtlsToS2AChannelCredentials.newBuilder( /* s2aAddress= */ "s2a_address", /* privateKeyPath= */ "src/test/resources/client_key.pem", /* certChainPath= */ "src/test/resources/client_cert.pem", @@ -74,11 +74,11 @@ public void createBuilder_nullTrustBundlePath_throwsException() throws Exception } @Test - public void createBuilder_emptyAddress_throwsException() throws Exception { + public void newBuilder_emptyAddress_throwsException() throws Exception { assertThrows( IllegalArgumentException.class, () -> - MtlsToS2AChannelCredentials.createBuilder( + MtlsToS2AChannelCredentials.newBuilder( /* s2aAddress= */ "", /* privateKeyPath= */ "src/test/resources/client_key.pem", /* certChainPath= */ "src/test/resources/client_cert.pem", @@ -86,11 +86,11 @@ public void createBuilder_emptyAddress_throwsException() throws Exception { } @Test - public void createBuilder_emptyPrivateKeyPath_throwsException() throws Exception { + public void newBuilder_emptyPrivateKeyPath_throwsException() throws Exception { assertThrows( IllegalArgumentException.class, () -> - MtlsToS2AChannelCredentials.createBuilder( + MtlsToS2AChannelCredentials.newBuilder( /* s2aAddress= */ "s2a_address", /* privateKeyPath= */ "", /* certChainPath= */ "src/test/resources/client_cert.pem", @@ -98,11 +98,11 @@ public void createBuilder_emptyPrivateKeyPath_throwsException() throws Exception } @Test - public void createBuilder_emptyCertChainPath_throwsException() throws Exception { + public void newBuilder_emptyCertChainPath_throwsException() throws Exception { assertThrows( IllegalArgumentException.class, () -> - MtlsToS2AChannelCredentials.createBuilder( + MtlsToS2AChannelCredentials.newBuilder( /* s2aAddress= */ "s2a_address", /* privateKeyPath= */ "src/test/resources/client_key.pem", /* certChainPath= */ "", @@ -110,11 +110,11 @@ public void createBuilder_emptyCertChainPath_throwsException() throws Exception } @Test - public void createBuilder_emptyTrustBundlePath_throwsException() throws Exception { + public void newBuilder_emptyTrustBundlePath_throwsException() throws Exception { assertThrows( IllegalArgumentException.class, () -> - MtlsToS2AChannelCredentials.createBuilder( + MtlsToS2AChannelCredentials.newBuilder( /* s2aAddress= */ "s2a_address", /* privateKeyPath= */ "src/test/resources/client_key.pem", /* certChainPath= */ "src/test/resources/client_cert.pem", @@ -124,7 +124,7 @@ public void createBuilder_emptyTrustBundlePath_throwsException() throws Exceptio @Test public void build_s2AChannelCredentials_success() throws Exception { assertThat( - MtlsToS2AChannelCredentials.createBuilder( + MtlsToS2AChannelCredentials.newBuilder( /* s2aAddress= */ "s2a_address", /* privateKeyPath= */ "src/test/resources/client_key.pem", /* certChainPath= */ "src/test/resources/client_cert.pem", diff --git a/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java b/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java index a6133ed0af8..e766aa3f145 100644 --- a/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java +++ b/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java @@ -30,40 +30,40 @@ @RunWith(JUnit4.class) public final class S2AChannelCredentialsTest { @Test - public void createBuilder_nullArgument_throwsException() throws Exception { - assertThrows(IllegalArgumentException.class, () -> S2AChannelCredentials.createBuilder(null)); + public void newBuilder_nullArgument_throwsException() throws Exception { + assertThrows(IllegalArgumentException.class, () -> S2AChannelCredentials.newBuilder(null)); } @Test - public void createBuilder_emptyAddress_throwsException() throws Exception { - assertThrows(IllegalArgumentException.class, () -> S2AChannelCredentials.createBuilder("")); + public void newBuilder_emptyAddress_throwsException() throws Exception { + assertThrows(IllegalArgumentException.class, () -> S2AChannelCredentials.newBuilder("")); } @Test public void setLocalSpiffeId_nullArgument_throwsException() throws Exception { assertThrows( NullPointerException.class, - () -> S2AChannelCredentials.createBuilder("s2a_address").setLocalSpiffeId(null)); + () -> S2AChannelCredentials.newBuilder("s2a_address").setLocalSpiffeId(null)); } @Test public void setLocalHostname_nullArgument_throwsException() throws Exception { assertThrows( NullPointerException.class, - () -> S2AChannelCredentials.createBuilder("s2a_address").setLocalHostname(null)); + () -> S2AChannelCredentials.newBuilder("s2a_address").setLocalHostname(null)); } @Test public void setLocalUid_nullArgument_throwsException() throws Exception { assertThrows( NullPointerException.class, - () -> S2AChannelCredentials.createBuilder("s2a_address").setLocalUid(null)); + () -> S2AChannelCredentials.newBuilder("s2a_address").setLocalUid(null)); } @Test public void build_withLocalSpiffeId_succeeds() throws Exception { assertThat( - S2AChannelCredentials.createBuilder("s2a_address") + S2AChannelCredentials.newBuilder("s2a_address") .setLocalSpiffeId("spiffe://test") .build()) .isNotNull(); @@ -72,7 +72,7 @@ public void build_withLocalSpiffeId_succeeds() throws Exception { @Test public void build_withLocalHostname_succeeds() throws Exception { assertThat( - S2AChannelCredentials.createBuilder("s2a_address") + S2AChannelCredentials.newBuilder("s2a_address") .setLocalHostname("local_hostname") .build()) .isNotNull(); @@ -80,20 +80,20 @@ public void build_withLocalHostname_succeeds() throws Exception { @Test public void build_withLocalUid_succeeds() throws Exception { - assertThat(S2AChannelCredentials.createBuilder("s2a_address").setLocalUid("local_uid").build()) + assertThat(S2AChannelCredentials.newBuilder("s2a_address").setLocalUid("local_uid").build()) .isNotNull(); } @Test public void build_withNoLocalIdentity_succeeds() throws Exception { - assertThat(S2AChannelCredentials.createBuilder("s2a_address").build()) + assertThat(S2AChannelCredentials.newBuilder("s2a_address").build()) .isNotNull(); } @Test public void build_withTlsChannelCredentials_succeeds() throws Exception { assertThat( - S2AChannelCredentials.createBuilder("s2a_address") + S2AChannelCredentials.newBuilder("s2a_address") .setLocalSpiffeId("spiffe://test") .setS2AChannelCredentials(getTlsChannelCredentials()) .build()) diff --git a/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java b/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java index dc5909442bf..7845e7c3bcb 100644 --- a/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java +++ b/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java @@ -24,6 +24,7 @@ import io.grpc.Channel; import io.grpc.ChannelCredentials; import io.grpc.ClientCall; +import io.grpc.InsecureChannelCredentials; import io.grpc.ManagedChannel; import io.grpc.MethodDescriptor; import io.grpc.Server; @@ -42,7 +43,6 @@ import io.grpc.testing.protobuf.SimpleResponse; import io.grpc.testing.protobuf.SimpleServiceGrpc; import java.io.File; -import java.util.Optional; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.ClassRule; @@ -74,7 +74,7 @@ public void getChannelResource_success() { Resource resource = S2AHandshakerServiceChannel.getChannelResource( "localhost:" + plaintextServer.getPort(), - /* s2aChannelCredentials= */ Optional.empty()); + InsecureChannelCredentials.create()); assertThat(resource.toString()).isEqualTo("grpc-s2a-channel"); } @@ -96,11 +96,11 @@ public void getChannelResource_twoEqualChannels() { Resource resource = S2AHandshakerServiceChannel.getChannelResource( "localhost:" + plaintextServer.getPort(), - /* s2aChannelCredentials= */ Optional.empty()); + InsecureChannelCredentials.create()); Resource resourceTwo = S2AHandshakerServiceChannel.getChannelResource( "localhost:" + plaintextServer.getPort(), - /* s2aChannelCredentials= */ Optional.empty()); + InsecureChannelCredentials.create()); assertThat(resource).isEqualTo(resourceTwo); } @@ -125,10 +125,10 @@ public void getChannelResource_twoDistinctChannels() { Resource resource = S2AHandshakerServiceChannel.getChannelResource( "localhost:" + plaintextServer.getPort(), - /* s2aChannelCredentials= */ Optional.empty()); + InsecureChannelCredentials.create()); Resource resourceTwo = S2AHandshakerServiceChannel.getChannelResource( - "localhost:" + Utils.pickUnusedPort(), /* s2aChannelCredentials= */ Optional.empty()); + "localhost:" + Utils.pickUnusedPort(), InsecureChannelCredentials.create()); assertThat(resourceTwo).isNotEqualTo(resource); } @@ -153,7 +153,7 @@ public void close_success() { Resource resource = S2AHandshakerServiceChannel.getChannelResource( "localhost:" + plaintextServer.getPort(), - /* s2aChannelCredentials= */ Optional.empty()); + InsecureChannelCredentials.create()); Channel channel = resource.create(); resource.close(channel); StatusRuntimeException expected = @@ -191,7 +191,7 @@ public void newCall_performSimpleRpcSuccess() { Resource resource = S2AHandshakerServiceChannel.getChannelResource( "localhost:" + plaintextServer.getPort(), - /* s2aChannelCredentials= */ Optional.empty()); + InsecureChannelCredentials.create()); Channel channel = resource.create(); assertThat(channel).isInstanceOf(HandshakerServiceChannel.class); assertThat( @@ -256,7 +256,7 @@ public void create_succeedsAfterCloseIsCalledOnce() throws Exception { Resource resource = S2AHandshakerServiceChannel.getChannelResource( "localhost:" + plaintextServer.getPort(), - /* s2aChannelCredentials= */ Optional.empty()); + InsecureChannelCredentials.create()); Channel channelOne = resource.create(); resource.close(channelOne); @@ -308,15 +308,14 @@ private static Server createPlaintextServer() { ServerBuilder.forPort(Utils.pickUnusedPort()).addService(service).build()); } - private static Optional getTlsChannelCredentials() throws Exception { + private static ChannelCredentials getTlsChannelCredentials() throws Exception { File clientCert = new File("src/test/resources/client_cert.pem"); File clientKey = new File("src/test/resources/client_key.pem"); File rootCert = new File("src/test/resources/root_cert.pem"); - return Optional.of( - TlsChannelCredentials.newBuilder() + return TlsChannelCredentials.newBuilder() .keyManager(clientCert, clientKey) .trustManager(rootCert) - .build()); + .build(); } private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase { diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java index 19dda7a19e4..bae58f2f9ec 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java @@ -186,7 +186,7 @@ public void tearDown() throws Exception { @Test public void clientCommunicateUsingS2ACredentials_succeeds() throws Exception { ChannelCredentials credentials = - S2AChannelCredentials.createBuilder(s2aAddress).setLocalSpiffeId("test-spiffe-id").build(); + S2AChannelCredentials.newBuilder(s2aAddress).setLocalSpiffeId("test-spiffe-id").build(); ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, credentials).build(); assertThat(doUnaryRpc(channel)).isTrue(); @@ -194,7 +194,7 @@ public void clientCommunicateUsingS2ACredentials_succeeds() throws Exception { @Test public void clientCommunicateUsingS2ACredentialsNoLocalIdentity_succeeds() throws Exception { - ChannelCredentials credentials = S2AChannelCredentials.createBuilder(s2aAddress).build(); + ChannelCredentials credentials = S2AChannelCredentials.newBuilder(s2aAddress).build(); ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, credentials).build(); assertThat(doUnaryRpc(channel)).isTrue(); @@ -203,7 +203,7 @@ public void clientCommunicateUsingS2ACredentialsNoLocalIdentity_succeeds() throw @Test public void clientCommunicateUsingMtlsToS2ACredentials_succeeds() throws Exception { ChannelCredentials credentials = - MtlsToS2AChannelCredentials.createBuilder( + MtlsToS2AChannelCredentials.newBuilder( /* s2aAddress= */ mtlsS2AAddress, /* privateKeyPath= */ "src/test/resources/client_key.pem", /* certChainPath= */ "src/test/resources/client_cert.pem", @@ -218,7 +218,7 @@ public void clientCommunicateUsingMtlsToS2ACredentials_succeeds() throws Excepti @Test public void clientCommunicateUsingS2ACredentials_s2AdelayStart_succeeds() throws Exception { - ChannelCredentials credentials = S2AChannelCredentials.createBuilder(s2aDelayAddress).build(); + ChannelCredentials credentials = S2AChannelCredentials.newBuilder(s2aDelayAddress).build(); ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, credentials).build(); FutureTask rpc = new FutureTask<>(() -> doUnaryRpc(channel)); diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java index f130e52aac7..404910e8be0 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java @@ -115,7 +115,7 @@ public void createProtocolNegotiator_nullArgument() throws Exception { S2AGrpcChannelPool.create( SharedResourcePool.forResource( S2AHandshakerServiceChannel.getChannelResource( - "localhost:8080", /* s2aChannelCredentials= */ Optional.empty()))); + "localhost:8080", InsecureChannelCredentials.create()))); NullPointerTester tester = new NullPointerTester() diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java index bb90be12b6a..47fd154d949 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java @@ -21,13 +21,13 @@ import static org.junit.Assert.assertThrows; import com.google.common.truth.Expect; +import io.grpc.InsecureChannelCredentials; import io.grpc.internal.SharedResourcePool; import io.grpc.s2a.channel.S2AChannelPool; import io.grpc.s2a.channel.S2AGrpcChannelPool; import io.grpc.s2a.channel.S2AHandshakerServiceChannel; import io.grpc.stub.StreamObserver; import java.io.IOException; -import java.util.Optional; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -55,7 +55,7 @@ public void send_receiveOkStatus() throws Exception { S2AGrpcChannelPool.create( SharedResourcePool.forResource( S2AHandshakerServiceChannel.getChannelResource( - S2A_ADDRESS, /* s2aChannelCredentials= */ Optional.empty()))); + S2A_ADDRESS, InsecureChannelCredentials.create()))); S2AServiceGrpc.S2AServiceStub serviceStub = S2AServiceGrpc.newStub(channelPool.getChannel()); S2AStub newStub = S2AStub.newInstance(serviceStub); From 99be6e9852817fa5b85e0634984020d766e50d1f Mon Sep 17 00:00:00 2001 From: John Cormie Date: Mon, 23 Sep 2024 20:37:09 -0700 Subject: [PATCH 012/591] Address Android 11's package visibility rules. (#11551) --- .../android-binderchannel-status-codes.md | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/documentation/android-binderchannel-status-codes.md b/documentation/android-binderchannel-status-codes.md index dda0220bf8a..28bdd8907c1 100644 --- a/documentation/android-binderchannel-status-codes.md +++ b/documentation/android-binderchannel-status-codes.md @@ -23,15 +23,23 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ - 1 + 0 - Server app not installed + Server app not visible. + + bindService() returns false + +

UNIMPLEMENTED

“The operation is not implemented or is not supported / enabled in this service.” + + Give up - This is an error in the client manifest. - bindService() returns false + + + 1 -

UNIMPLEMENTED

“The operation is not implemented or is not supported / enabled in this service.” + Server app not installed - Direct the user to install/reinstall the server app. + Direct the user to install/reinstall the server app. @@ -90,6 +98,8 @@ Consider the table that follows as an BinderChannel-specific addendum to the “

PERMISSION_DENIED

“The caller does not have permission to execute the specified operation …” + Direct the user to update the server app in the hopes that a newer version fixes this error in its manifest. + 10 @@ -315,6 +325,7 @@ According to a review of the AOSP source code, there are in fact several cases: 1. The target package is not installed 2. The target package is installed but does not declare the target Service in its manifest. 3. The target package requests dangerous permissions but targets sdk <= M and therefore requires a permissions review, but the caller is not running in the foreground and so it would be inappropriate to launch the review UI. +4. The target package is not visible to the client due to [Android 11 package visibility rules](https://developer.android.com/training/package-visibility). Status code mapping: **UNIMPLEMENTED** @@ -322,6 +333,7 @@ Status code mapping: **UNIMPLEMENTED** Unfortunately `UNIMPLEMENTED` doesn’t capture (3) but none of the other canonical status codes do either and we expect this case to be extremely rare. +(4) is intentially indistinguishable from (1) by Android design so we can't handle it differently. However, as a client manifest error, it's not something reasonable apps would handle at runtime anyway. ### bindService() throws SecurityException @@ -382,4 +394,4 @@ Android’s Parcel class exposes a mechanism for marshalling certain types of `R The calling Activity or Service Context might be destroyed with a gRPC request in flight. Apps should cease operations when the Context hosting it goes away and this includes cancelling any outstanding RPCs. -Status code mapping: **CANCELLED** \ No newline at end of file +Status code mapping: **CANCELLED** From 2ff837ab6007baf41c75a04dc31bcdbb96fa1a3c Mon Sep 17 00:00:00 2001 From: "Mark S. Lewis" Date: Fri, 20 Sep 2024 16:14:59 +0100 Subject: [PATCH 013/591] Update protobuf-java to address CVE-2024-7254 Signed-off-by: Mark S. Lewis --- examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 2 +- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 2 +- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 2 +- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 2 +- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 2 +- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- gradle/libs.versions.toml | 2 +- 25 files changed, 28 insertions(+), 28 deletions(-) diff --git a/examples/build.gradle b/examples/build.gradle index c10b4eef46a..24f6d2b04f8 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.3' +def protobufVersion = '3.25.5' def protocVersion = protobufVersion dependencies { diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index 0d7d959de93..6b1f0ded169 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -25,7 +25,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.3' +def protocVersion = '3.25.5' dependencies { // grpc-alts transitively depends on grpc-netty-shaded, grpc-protobuf, and grpc-stub diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index 5565747cb19..97bca5f91b6 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -26,7 +26,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.3' +def protobufVersion = '3.25.5' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index 064d989c04c..dc399a78c9d 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -13,7 +13,7 @@ UTF-8 1.68.0-SNAPSHOT - 3.25.3 + 3.25.5 1.8 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index 554b5f758d9..0c45e6de7cf 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -26,7 +26,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.3' +def protobufVersion = '3.25.5' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index dfd650cdfa4..a8e32b347e5 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -13,7 +13,7 @@ UTF-8 1.68.0-SNAPSHOT - 3.25.3 + 3.25.5 1.8 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 47e812fde15..86edd557206 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -25,7 +25,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.3' +def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index d2cba1a7959..89478a2dfa6 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -13,7 +13,7 @@ UTF-8 1.68.0-SNAPSHOT - 3.25.3 + 3.25.5 1.8 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index a392018ba25..179ab3a74d9 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -26,7 +26,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.3' +def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index dcb8d420020..1a6f4719460 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -26,7 +26,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.3' +def protocVersion = '3.25.5' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index df8b0fde121..17817e2a374 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.3' +def protobufVersion = '3.25.5' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index c6d39887bac..991174af382 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -13,7 +13,7 @@ UTF-8 1.68.0-SNAPSHOT - 3.25.3 + 3.25.5 1.8 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index f996282bbb0..c31486095f3 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.3' +def protobufVersion = '3.25.5' def protocVersion = protobufVersion dependencies { diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index c84f9893980..10330d955ed 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -14,8 +14,8 @@ UTF-8 1.68.0-SNAPSHOT - 3.25.3 - 3.25.3 + 3.25.5 + 3.25.5 1.8 1.8 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 7f600c2bc53..fe21efcf0db 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.3' +def protobufVersion = '3.25.5' def protocVersion = protobufVersion dependencies { diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index fa2eaa41e36..072bd957dcf 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -14,8 +14,8 @@ UTF-8 1.68.0-SNAPSHOT - 3.25.3 - 3.25.3 + 3.25.5 + 3.25.5 1.8 1.8 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index 21264ffcc17..f08f9d492a5 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -25,7 +25,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.3' +def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index d087a532aff..4ca9343f887 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -19,7 +19,7 @@ java { } def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.3' +def protocVersion = '3.25.5' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index d7d5c50b7e6..e1efc8ee050 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -19,7 +19,7 @@ java { } def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.3' +def protocVersion = '3.25.5' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index 995e2d0979b..ebd14674578 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -17,7 +17,7 @@ java { } def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.3' +def protocVersion = '3.25.5' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}", diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index 8aad6b62bcb..8a16a902b72 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -25,7 +25,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.3' +def protocVersion = '3.25.5' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index e1d569a628c..972976ecdf1 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -13,7 +13,7 @@ UTF-8 1.68.0-SNAPSHOT - 3.25.3 + 3.25.5 1.8 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index 8339db77e0c..a3e23a19601 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.3' +def protocVersion = '3.25.5' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/pom.xml b/examples/pom.xml index 247df4a73ce..554c70a35ce 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -13,8 +13,8 @@ UTF-8 1.68.0-SNAPSHOT - 3.25.3 - 3.25.3 + 3.25.5 + 3.25.5 1.8 1.8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 488ead9ad86..8d7fb3766e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ nettytcnative = '2.0.65.Final' opencensus = "0.31.1" # Not upgrading to 4.x as it is not yet ABI compatible. # https://github.com/protocolbuffers/protobuf/issues/17247 -protobuf = "3.25.3" +protobuf = "3.25.5" [libraries] android-annotations = "com.google.android:annotations:4.1.1.4" From c92453fb14ef21674cd00194ec28bfa186f2eef2 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 24 Sep 2024 15:40:40 -0700 Subject: [PATCH 014/591] s2a: Disabling publishing until it is ready for users --- s2a/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/s2a/build.gradle b/s2a/build.gradle index 234f983fd5c..af2c879a753 100644 --- a/s2a/build.gradle +++ b/s2a/build.gradle @@ -1,6 +1,5 @@ plugins { id "java-library" - id "maven-publish" id "com.github.johnrengelman.shadow" id "com.google.osdetector" @@ -90,6 +89,7 @@ tasks.named("shadowJar").configure { relocate 'io.netty', 'io.grpc.netty.shaded.io.netty' } +plugins.withId('maven-publish') { publishing { publications { maven(MavenPublication) { @@ -111,3 +111,4 @@ publishing { } } } +} From 3e8ef8cf0ced8f910b4a0d2ce04f8771d1219bdd Mon Sep 17 00:00:00 2001 From: Vindhya Ningegowda Date: Tue, 24 Sep 2024 16:18:34 -0700 Subject: [PATCH 015/591] xds: Check for validity of xdsClient in ClusterImplLbHelper (#11553) * Added null check for xdsClient in onSubChannelState. This avoids NPE for xdsClient when LB is shutdown and onSubChannelState is called later as part of listener callback. As shutdown is racy and eventually consistent, this check would avoid calculating locality after LB is shutdown. --- xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index 0ea2c7dd75f..3b30b8fa036 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -236,7 +236,8 @@ public void start(SubchannelStateListener listener) { delegate().start(new SubchannelStateListener() { @Override public void onSubchannelState(ConnectivityStateInfo newState) { - if (newState.getState().equals(ConnectivityState.READY)) { + // Do nothing if LB has been shutdown + if (xdsClient != null && newState.getState().equals(ConnectivityState.READY)) { // Get locality based on the connected address attributes ClusterLocality updatedClusterLocality = createClusterLocalityFromAttributes( subchannel.getConnectedAddressAttributes()); From 5dbca0e80c03003fb72272f35bbc988f9175654b Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 25 Sep 2024 09:17:26 -0700 Subject: [PATCH 016/591] xds: Improve ClusterImpl's FakeSubchannel to verify state changes The main goal was to make sure subchannels went CONNECTING only after a connection was requested (since the test doesn't transition to CONNECTING from TF). That helps guarantee that the test is using the expected subchannel. The missing ClusterImplLB.requestConnection() doesn't actually matter much, as cluster manager doesn't propagate connection requests. --- .../io/grpc/xds/ClusterImplLoadBalancer.java | 7 ++++ .../grpc/xds/ClusterImplLoadBalancerTest.java | 33 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index 3b30b8fa036..2a9435aa72f 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -163,6 +163,13 @@ public void handleNameResolutionError(Status error) { } } + @Override + public void requestConnection() { + if (childSwitchLb != null) { + childSwitchLb.requestConnection(); + } + } + @Override public void shutdown() { if (dropStats != null) { diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index aaaed9554f4..c627d60a010 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -280,6 +281,7 @@ public void pick_addsLocalityLabel() { FakeLoadBalancer leafBalancer = Iterables.getOnlyElement(downstreamBalancers); leafBalancer.createSubChannel(); FakeSubchannel fakeSubchannel = helper.subchannels.poll(); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.CONNECTING)); fakeSubchannel.setConnectedEagIndex(0); fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); assertThat(currentState).isEqualTo(ConnectivityState.READY); @@ -309,6 +311,7 @@ public void recordLoadStats() { FakeLoadBalancer leafBalancer = Iterables.getOnlyElement(downstreamBalancers); Subchannel subchannel = leafBalancer.createSubChannel(); FakeSubchannel fakeSubchannel = helper.subchannels.poll(); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.CONNECTING)); fakeSubchannel.setConnectedEagIndex(0); fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); assertThat(currentState).isEqualTo(ConnectivityState.READY); @@ -407,6 +410,7 @@ public void pickFirstLoadReport_onUpdateAddress() { // Leaf balancer is created by Pick First. Get FakeSubchannel created to update attributes // A real subchannel would get these attributes from the connected address's EAG locality. FakeSubchannel fakeSubchannel = helper.subchannels.poll(); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.CONNECTING)); fakeSubchannel.setConnectedEagIndex(0); fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); assertThat(currentState).isEqualTo(ConnectivityState.READY); @@ -490,6 +494,7 @@ public void dropRpcsWithRespectToLbConfigDropCategories() { .isEqualTo(endpoint.getAddresses()); leafBalancer.createSubChannel(); FakeSubchannel fakeSubchannel = helper.subchannels.poll(); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.CONNECTING)); fakeSubchannel.setConnectedEagIndex(0); fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); @@ -571,6 +576,7 @@ private void subtest_maxConcurrentRequests_appliedByLbConfig(boolean enableCircu .isEqualTo(endpoint.getAddresses()); leafBalancer.createSubChannel(); FakeSubchannel fakeSubchannel = helper.subchannels.poll(); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.CONNECTING)); fakeSubchannel.setConnectedEagIndex(0); fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); assertThat(currentState).isEqualTo(ConnectivityState.READY); @@ -665,6 +671,7 @@ private void subtest_maxConcurrentRequests_appliedWithDefaultValue( .isEqualTo(endpoint.getAddresses()); leafBalancer.createSubChannel(); FakeSubchannel fakeSubchannel = helper.subchannels.poll(); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.CONNECTING)); fakeSubchannel.setConnectedEagIndex(0); fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); assertThat(currentState).isEqualTo(ConnectivityState.READY); @@ -943,6 +950,7 @@ Subchannel createSubChannel() { new FixedResultPicker(PickResult.withSubchannel(subchannel))); } }); + subchannel.requestConnection(); return subchannel; } } @@ -989,6 +997,8 @@ private static final class FakeSubchannel extends Subchannel { private final Attributes attrs; private SubchannelStateListener listener; private Attributes connectedAttributes; + private ConnectivityStateInfo state = ConnectivityStateInfo.forNonError(ConnectivityState.IDLE); + private boolean connectionRequested; private FakeSubchannel(List eags, Attributes attrs) { this.eags = eags; @@ -1006,6 +1016,9 @@ public void shutdown() { @Override public void requestConnection() { + if (state.getState() == ConnectivityState.IDLE) { + this.connectionRequested = true; + } } @Override @@ -1028,6 +1041,26 @@ public Attributes getConnectedAddressAttributes() { } public void updateState(ConnectivityStateInfo newState) { + switch (newState.getState()) { + case IDLE: + assertThat(state.getState()).isEqualTo(ConnectivityState.READY); + break; + case CONNECTING: + assertThat(state.getState()) + .isIn(Arrays.asList(ConnectivityState.IDLE, ConnectivityState.TRANSIENT_FAILURE)); + if (state.getState() == ConnectivityState.IDLE) { + assertWithMessage("Connection requested").that(this.connectionRequested).isTrue(); + this.connectionRequested = false; + } + break; + case READY: + case TRANSIENT_FAILURE: + assertThat(state.getState()).isEqualTo(ConnectivityState.CONNECTING); + break; + default: + break; + } + this.state = newState; listener.onSubchannelState(newState); } From 64e3801538624c81741798298cc2af759955d1d1 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Thu, 26 Sep 2024 21:04:57 +0530 Subject: [PATCH 017/591] Update RELEASING.md (#11559) 1. Removing $ when looking for the commit 'Start of development cycle...' because it produces empty result with the $. It seems how the squash was done may influence whether $ will work or not. 2. Added an explicit git push instruction at step 5 of tagging and what base branch to use, since it will cause conflict with the default base branch used of master. --- RELEASING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index bb1b77d0557..0add8991746 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -65,7 +65,7 @@ would be used to create all `v1.7` tags (e.g. `v1.7.0`, `v1.7.1`). ```bash git fetch upstream git checkout -b v$MAJOR.$MINOR.x \ - $(git log --pretty=format:%H --grep "^Start $MAJOR.$((MINOR+1)).0 development cycle$" upstream/master)^ + $(git log --pretty=format:%H --grep "^Start $MAJOR.$((MINOR+1)).0 development cycle" upstream/master)^ git push upstream v$MAJOR.$MINOR.x ``` 5. Continue with Google-internal steps at go/grpc-java/releasing, but stop @@ -132,7 +132,9 @@ Tagging the Release compiler/src/test{,Lite}/golden/Test{,Deprecated}Service.java.txt ./gradlew build git commit -a -m "Bump version to $MAJOR.$MINOR.$((PATCH+1))-SNAPSHOT" + git push -u origin release-v$MAJOR.$MINOR.$PATCH ``` + Raise a PR and set the base branch of the PR to v$MAJOR.$MINOR.x of the upstream grpc-java repo. 6. Go through PR review and push the release tag and updated release branch to GitHub (DO NOT click the merge button on the GitHub page): From 1c069375ceaa86f613769505b9c234c43ca6fffc Mon Sep 17 00:00:00 2001 From: erm-g <110920239+erm-g@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:01:11 -0400 Subject: [PATCH 018/591] core: SpiffeId parser (#11490) SpiffeId parser compliant with [official spec](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md) --- .../java/io/grpc/internal/SpiffeUtil.java | 122 +++++++++++ .../java/io/grpc/internal/SpiffeUtilTest.java | 196 ++++++++++++++++++ 2 files changed, 318 insertions(+) create mode 100644 core/src/main/java/io/grpc/internal/SpiffeUtil.java create mode 100644 core/src/test/java/io/grpc/internal/SpiffeUtilTest.java diff --git a/core/src/main/java/io/grpc/internal/SpiffeUtil.java b/core/src/main/java/io/grpc/internal/SpiffeUtil.java new file mode 100644 index 00000000000..bddce3d035e --- /dev/null +++ b/core/src/main/java/io/grpc/internal/SpiffeUtil.java @@ -0,0 +1,122 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Splitter; +import java.util.Locale; + +/** + * Helper utility to work with SPIFFE URIs. + * @see Standard + */ +public final class SpiffeUtil { + + private static final String PREFIX = "spiffe://"; + + private SpiffeUtil() {} + + /** + * Parses a URI string, applies validation rules described in SPIFFE standard, and, in case of + * success, returns parsed TrustDomain and Path. + * + * @param uri a String representing a SPIFFE ID + */ + public static SpiffeId parse(String uri) { + doInitialUriValidation(uri); + checkArgument(uri.toLowerCase(Locale.US).startsWith(PREFIX), "Spiffe Id must start with " + + PREFIX); + String domainAndPath = uri.substring(PREFIX.length()); + String trustDomain; + String path; + if (!domainAndPath.contains("/")) { + trustDomain = domainAndPath; + path = ""; + } else { + String[] parts = domainAndPath.split("/", 2); + trustDomain = parts[0]; + path = parts[1]; + checkArgument(!path.isEmpty(), "Path must not include a trailing '/'"); + } + validateTrustDomain(trustDomain); + validatePath(path); + if (!path.isEmpty()) { + path = "/" + path; + } + return new SpiffeId(trustDomain, path); + } + + private static void doInitialUriValidation(String uri) { + checkArgument(checkNotNull(uri, "uri").length() > 0, "Spiffe Id can't be empty"); + checkArgument(uri.length() <= 2048, "Spiffe Id maximum length is 2048 characters"); + checkArgument(!uri.contains("#"), "Spiffe Id must not contain query fragments"); + checkArgument(!uri.contains("?"), "Spiffe Id must not contain query parameters"); + } + + private static void validateTrustDomain(String trustDomain) { + checkArgument(!trustDomain.isEmpty(), "Trust Domain can't be empty"); + checkArgument(trustDomain.length() < 256, "Trust Domain maximum length is 255 characters"); + checkArgument(trustDomain.matches("[a-z0-9._-]+"), + "Trust Domain must contain only letters, numbers, dots, dashes, and underscores" + + " ([a-z0-9.-_])"); + } + + private static void validatePath(String path) { + if (path.isEmpty()) { + return; + } + checkArgument(!path.endsWith("/"), "Path must not include a trailing '/'"); + for (String segment : Splitter.on("/").split(path)) { + validatePathSegment(segment); + } + } + + private static void validatePathSegment(String pathSegment) { + checkArgument(!pathSegment.isEmpty(), "Individual path segments must not be empty"); + checkArgument(!(pathSegment.equals(".") || pathSegment.equals("..")), + "Individual path segments must not be relative path modifiers (i.e. ., ..)"); + checkArgument(pathSegment.matches("[a-zA-Z0-9._-]+"), + "Individual path segments must contain only letters, numbers, dots, dashes, and underscores" + + " ([a-zA-Z0-9.-_])"); + } + + /** + * Represents a SPIFFE ID as defined in the SPIFFE standard. + * @see Standard + */ + public static class SpiffeId { + + private final String trustDomain; + private final String path; + + private SpiffeId(String trustDomain, String path) { + this.trustDomain = trustDomain; + this.path = path; + } + + public String getTrustDomain() { + return trustDomain; + } + + public String getPath() { + return path; + } + } + +} diff --git a/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java b/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java new file mode 100644 index 00000000000..c3a98ce33e0 --- /dev/null +++ b/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.util.Arrays; +import java.util.Collection; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + + +@RunWith(Enclosed.class) +public class SpiffeUtilTest { + + @RunWith(Parameterized.class) + public static class ParseSuccessTest { + @Parameter + public String uri; + + @Parameter(1) + public String trustDomain; + + @Parameter(2) + public String path; + + @Test + public void parseSuccessTest() { + SpiffeUtil.SpiffeId spiffeId = SpiffeUtil.parse(uri); + assertEquals(trustDomain, spiffeId.getTrustDomain()); + assertEquals(path, spiffeId.getPath()); + } + + @Parameters(name = "spiffeId={0}") + public static Collection data() { + return Arrays.asList(new String[][] { + {"spiffe://example.com", "example.com", ""}, + {"spiffe://example.com/us", "example.com", "/us"}, + {"spIFfe://qa-staging.final_check.example.com/us", "qa-staging.final_check.example.com", + "/us"}, + {"spiffe://example.com/country/us/state/FL/city/Miami", "example.com", + "/country/us/state/FL/city/Miami"}, + {"SPIFFE://example.com/Czech.Republic/region0.1/city_of-Prague", "example.com", + "/Czech.Republic/region0.1/city_of-Prague"}, + {"spiffe://trust-domain-name/path", "trust-domain-name", "/path"}, + {"spiffe://staging.example.com/payments/mysql", "staging.example.com", "/payments/mysql"}, + {"spiffe://staging.example.com/payments/web-fe", "staging.example.com", + "/payments/web-fe"}, + {"spiffe://k8s-west.example.com/ns/staging/sa/default", "k8s-west.example.com", + "/ns/staging/sa/default"}, + {"spiffe://example.com/9eebccd2-12bf-40a6-b262-65fe0487d453", "example.com", + "/9eebccd2-12bf-40a6-b262-65fe0487d453"}, + {"spiffe://trustdomain/.a..", "trustdomain", "/.a.."}, + {"spiffe://trustdomain/...", "trustdomain", "/..."}, + {"spiffe://trustdomain/abcdefghijklmnopqrstuvwxyz", "trustdomain", + "/abcdefghijklmnopqrstuvwxyz"}, + {"spiffe://trustdomain/abc0123.-_", "trustdomain", "/abc0123.-_"}, + {"spiffe://trustdomain/0123456789", "trustdomain", "/0123456789"}, + {"spiffe://trustdomain0123456789/path", "trustdomain0123456789", "/path"}, + }); + } + } + + @RunWith(Parameterized.class) + public static class ParseFailureTest { + @Parameter + public String uri; + + @Test + public void parseFailureTest() { + assertThrows(IllegalArgumentException.class, () -> SpiffeUtil.parse(uri)); + } + + @Parameters(name = "spiffeId={0}") + public static Collection data() { + return Arrays.asList( + "spiffe:///", + "spiffe://example!com", + "spiffe://exampleя.com/workload-1", + "spiffe://example.com/us/florida/miamiя", + "spiffe:/trustdomain/path", + "spiffe:///path", + "spiffe://trust%20domain/path", + "spiffe://user@trustdomain/path", + "spiffe:// /", + "", + "http://trustdomain/path", + "//trustdomain/path", + "://trustdomain/path", + "piffe://trustdomain/path", + "://", + "://trustdomain", + "spiff", + "spiffe", + "spiffe:////", + "spiffe://trust.domain/../path" + ); + } + } + + public static class ExceptionMessageTest { + + @Test + public void spiffeUriFormatTest() { + NullPointerException npe = assertThrows(NullPointerException.class, () -> + SpiffeUtil.parse(null)); + assertEquals("uri", npe.getMessage()); + + IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> + SpiffeUtil.parse("https://example.com")); + assertEquals("Spiffe Id must start with spiffe://", iae.getMessage()); + + iae = assertThrows(IllegalArgumentException.class, () -> + SpiffeUtil.parse("spiffe://example.com/workload#1")); + assertEquals("Spiffe Id must not contain query fragments", iae.getMessage()); + + iae = assertThrows(IllegalArgumentException.class, () -> + SpiffeUtil.parse("spiffe://example.com/workload-1?t=1")); + assertEquals("Spiffe Id must not contain query parameters", iae.getMessage()); + } + + @Test + public void spiffeTrustDomainFormatTest() { + IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> + SpiffeUtil.parse("spiffe://")); + assertEquals("Trust Domain can't be empty", iae.getMessage()); + + iae = assertThrows(IllegalArgumentException.class, () -> + SpiffeUtil.parse("spiffe://eXample.com")); + assertEquals( + "Trust Domain must contain only letters, numbers, dots, dashes, and underscores " + + "([a-z0-9.-_])", + iae.getMessage()); + + StringBuilder longTrustDomain = new StringBuilder("spiffe://pi.eu."); + for (int i = 0; i < 50; i++) { + longTrustDomain.append("pi.eu"); + } + iae = assertThrows(IllegalArgumentException.class, () -> + SpiffeUtil.parse(longTrustDomain.toString())); + assertEquals("Trust Domain maximum length is 255 characters", iae.getMessage()); + + StringBuilder longSpiffe = new StringBuilder(String.format("spiffe://mydomain%scom/", "%21")); + for (int i = 0; i < 405; i++) { + longSpiffe.append("qwert"); + } + iae = assertThrows(IllegalArgumentException.class, () -> + SpiffeUtil.parse(longSpiffe.toString())); + assertEquals("Spiffe Id maximum length is 2048 characters", iae.getMessage()); + } + + @Test + public void spiffePathFormatTest() { + IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> + SpiffeUtil.parse("spiffe://example.com//")); + assertEquals("Path must not include a trailing '/'", iae.getMessage()); + + iae = assertThrows(IllegalArgumentException.class, () -> + SpiffeUtil.parse("spiffe://example.com/")); + assertEquals("Path must not include a trailing '/'", iae.getMessage()); + + iae = assertThrows(IllegalArgumentException.class, () -> + SpiffeUtil.parse("spiffe://example.com/us//miami")); + assertEquals("Individual path segments must not be empty", iae.getMessage()); + + iae = assertThrows(IllegalArgumentException.class, () -> + SpiffeUtil.parse("spiffe://example.com/us/.")); + assertEquals("Individual path segments must not be relative path modifiers (i.e. ., ..)", + iae.getMessage()); + + iae = assertThrows(IllegalArgumentException.class, () -> + SpiffeUtil.parse("spiffe://example.com/us!")); + assertEquals("Individual path segments must contain only letters, numbers, dots, dashes, and " + + "underscores ([a-zA-Z0-9.-_])", iae.getMessage()); + } + } +} \ No newline at end of file From 9faa0f4eb0bbac31228938d25a15b69401dcde33 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 25 Sep 2024 09:22:07 -0700 Subject: [PATCH 019/591] xds: Update ClusterImpl test to work with PFLeafLB --- .../grpc/xds/ClusterImplLoadBalancerTest.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index c627d60a010..7eba43ce278 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -53,6 +53,7 @@ import io.grpc.SynchronizationContext; import io.grpc.internal.FakeClock; import io.grpc.internal.ObjectPool; +import io.grpc.internal.PickFirstLoadBalancerProvider; import io.grpc.internal.PickSubchannelArgsImpl; import io.grpc.protobuf.ProtoUtils; import io.grpc.testing.TestMethodDescriptors; @@ -384,9 +385,7 @@ public void recordLoadStats() { assertThat(clusterStats.upstreamLocalityStatsList()).isEmpty(); // no longer reported } - // TODO(dnvindhya): This test has been added as a fix to verify - // https://github.com/grpc/grpc-java/issues/11434. - // Once we update PickFirstLeafLoadBalancer as default LoadBalancer, update the test. + // Verifies https://github.com/grpc/grpc-java/issues/11434. @Test public void pickFirstLoadReport_onUpdateAddress() { Locality locality1 = @@ -435,8 +434,17 @@ public void pickFirstLoadReport_onUpdateAddress() { fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.CONNECTING)); // Faksubchannel mimics update address and returns different locality - fakeSubchannel.setConnectedEagIndex(1); - fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); + if (PickFirstLoadBalancerProvider.isEnabledNewPickFirst()) { + fakeSubchannel.updateState(ConnectivityStateInfo.forTransientFailure( + Status.UNAVAILABLE.withDescription("Try second address instead"))); + fakeSubchannel = helper.subchannels.poll(); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.CONNECTING)); + fakeSubchannel.setConnectedEagIndex(0); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); + } else { + fakeSubchannel.setConnectedEagIndex(1); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); + } result = currentPicker.pickSubchannel(pickSubchannelArgs); assertThat(result.getStatus().isOk()).isTrue(); ClientStreamTracer streamTracer2 = result.getStreamTracerFactory().newClientStreamTracer( From fa18fec36e8b8dda00bb2ca005938cea26180e24 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Fri, 27 Sep 2024 08:47:56 -0700 Subject: [PATCH 020/591] s2a: Address minor comments on PR#11113 (#11540) * Use StandardCharsets in FakeS2AServerTest. * Use add instead of offer in S2AStub. * remove dead code in ProtoUtil.java. * Mark convertTlsProtocolVersion as VisibleForTesting. * S2AStub doesn't return responses at front of queue. * Remove global SHARED_RESOURCE_CHANNELS. * Don't suppress RethrowReflectiveOperationExceptionAsLinkageError. * Update javadoc. * Make clear which certs are used in tests + add how to regenerate. --- .../channel/S2AHandshakerServiceChannel.java | 14 +- .../io/grpc/s2a/handshaker/ProtoUtil.java | 28 +-- .../java/io/grpc/s2a/handshaker/S2AStub.java | 15 +- .../tokenmanager/AccessTokenManager.java | 3 +- .../S2AHandshakerServiceChannelTest.java | 12 +- .../io/grpc/s2a/handshaker/FakeS2AServer.java | 7 +- .../s2a/handshaker/FakeS2AServerTest.java | 18 +- .../io/grpc/s2a/handshaker/FakeWriter.java | 165 ++++++++---------- .../grpc/s2a/handshaker/IntegrationTest.java | 80 +-------- .../io/grpc/s2a/handshaker/ProtoUtilTest.java | 41 ----- .../handshaker/S2APrivateKeyMethodTest.java | 5 +- .../S2AProtocolNegotiatorFactoryTest.java | 7 +- .../io/grpc/s2a/handshaker/S2AStubTest.java | 38 ++-- s2a/src/test/resources/README.md | 37 ++++ s2a/src/test/resources/cert_chain_ec.pem | 36 ++++ s2a/src/test/resources/int_cert1_.cnf | 14 ++ s2a/src/test/resources/int_cert1_ec.pem | 12 ++ s2a/src/test/resources/int_cert2.cnf | 14 ++ s2a/src/test/resources/int_cert2_ec.pem | 12 ++ s2a/src/test/resources/int_key1_ec.pem | 5 + s2a/src/test/resources/int_key2_ec.pem | 5 + s2a/src/test/resources/leaf.cnf | 14 ++ s2a/src/test/resources/leaf_cert_ec.pem | 12 ++ s2a/src/test/resources/leaf_key_ec.pem | 5 + s2a/src/test/resources/root_cert_ec.pem | 12 ++ s2a/src/test/resources/root_ec.cnf | 14 ++ s2a/src/test/resources/root_key_ec.pem | 5 + 27 files changed, 345 insertions(+), 285 deletions(-) create mode 100644 s2a/src/test/resources/cert_chain_ec.pem create mode 100644 s2a/src/test/resources/int_cert1_.cnf create mode 100644 s2a/src/test/resources/int_cert1_ec.pem create mode 100644 s2a/src/test/resources/int_cert2.cnf create mode 100644 s2a/src/test/resources/int_cert2_ec.pem create mode 100644 s2a/src/test/resources/int_key1_ec.pem create mode 100644 s2a/src/test/resources/int_key2_ec.pem create mode 100644 s2a/src/test/resources/leaf.cnf create mode 100644 s2a/src/test/resources/leaf_cert_ec.pem create mode 100644 s2a/src/test/resources/leaf_key_ec.pem create mode 100644 s2a/src/test/resources/root_cert_ec.pem create mode 100644 s2a/src/test/resources/root_ec.cnf create mode 100644 s2a/src/test/resources/root_key_ec.pem diff --git a/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java b/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java index 443ea553e52..9d6950ce041 100644 --- a/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java +++ b/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java @@ -20,7 +20,6 @@ import static java.util.concurrent.TimeUnit.SECONDS; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Maps; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ChannelCredentials; @@ -30,16 +29,15 @@ import io.grpc.internal.SharedResourceHolder.Resource; import io.grpc.netty.NettyChannelBuilder; import java.time.Duration; -import java.util.concurrent.ConcurrentMap; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.concurrent.ThreadSafe; /** - * Provides APIs for managing gRPC channels to S2A servers. Each channel is local and plaintext. If - * credentials are provided, they are used to secure the channel. + * Provides APIs for managing gRPC channels to an S2A server. Each channel is local and plaintext. + * If credentials are provided, they are used to secure the channel. * - *

This is done as follows: for each S2A server, provides an implementation of gRPC's {@link + *

This is done as follows: for an S2A server, provides an implementation of gRPC's {@link * SharedResourceHolder.Resource} interface called a {@code Resource}. A {@code * Resource} is a factory for creating gRPC channels to the S2A server at a given address, * and a channel must be returned to the {@code Resource} when it is no longer needed. @@ -56,8 +54,6 @@ */ @ThreadSafe public final class S2AHandshakerServiceChannel { - private static final ConcurrentMap> SHARED_RESOURCE_CHANNELS = - Maps.newConcurrentMap(); private static final Duration CHANNEL_SHUTDOWN_TIMEOUT = Duration.ofSeconds(10); /** @@ -72,9 +68,7 @@ public final class S2AHandshakerServiceChannel { public static Resource getChannelResource( String s2aAddress, ChannelCredentials s2aChannelCredentials) { checkNotNull(s2aAddress); - checkNotNull(s2aChannelCredentials); - return SHARED_RESOURCE_CHANNELS.computeIfAbsent( - s2aAddress, channelResource -> new ChannelResource(s2aAddress, s2aChannelCredentials)); + return new ChannelResource(s2aAddress, s2aChannelCredentials); } /** diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java b/s2a/src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java index 59e3931d9e6..129cc2d60f1 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java @@ -16,36 +16,11 @@ package io.grpc.s2a.handshaker; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; /** Converts proto messages to Netty strings. */ final class ProtoUtil { - /** - * Converts {@link Ciphersuite} to its {@link String} representation. - * - * @param ciphersuite the {@link Ciphersuite} to be converted. - * @return a {@link String} representing the ciphersuite. - * @throws AssertionError if the {@link Ciphersuite} is not one of the supported ciphersuites. - */ - static String convertCiphersuite(Ciphersuite ciphersuite) { - switch (ciphersuite) { - case CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: - return "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"; - case CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: - return "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"; - case CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: - return "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"; - case CIPHERSUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256: - return "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; - case CIPHERSUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384: - return "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"; - case CIPHERSUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: - return "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"; - default: - throw new AssertionError( - String.format("Ciphersuite %d is not supported.", ciphersuite.getNumber())); - } - } /** * Converts a {@link TLSVersion} object to its {@link String} representation. @@ -54,6 +29,7 @@ static String convertCiphersuite(Ciphersuite ciphersuite) { * @return a {@link String} representation of the TLS version. * @throws AssertionError if the {@code tlsVersion} is not one of the supported TLS versions. */ + @VisibleForTesting static String convertTlsProtocolVersion(TLSVersion tlsVersion) { switch (tlsVersion) { case TLS_VERSION_1_3: diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java index 8249ca59d09..bf9b866ef93 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java @@ -84,6 +84,7 @@ BlockingQueue getResponses() { * @throws IOException if an unexpected response is received, or if the {@code reader} or {@code * writer} calls their {@code onError} method. */ + @SuppressWarnings("CheckReturnValue") public SessionResp send(SessionReq req) throws IOException, InterruptedException { if (doneWriting && doneReading) { logger.log(Level.INFO, "Stream to the S2A is closed."); @@ -92,9 +93,8 @@ public SessionResp send(SessionReq req) throws IOException, InterruptedException createWriterIfNull(); if (!responses.isEmpty()) { IOException exception = null; - SessionResp resp = null; try { - resp = responses.take().getResultOrThrow(); + responses.take().getResultOrThrow(); } catch (IOException e) { exception = e; } @@ -104,14 +104,15 @@ public SessionResp send(SessionReq req) throws IOException, InterruptedException "Received an unexpected response from a host at the S2A's address. The S2A might be" + " unavailable." + exception.getMessage()); + } else { + throw new IOException("Received an unexpected response from a host at the S2A's address."); } - return resp; } try { writer.onNext(req); } catch (RuntimeException e) { writer.onError(e); - responses.offer(Result.createWithThrowable(e)); + responses.add(Result.createWithThrowable(e)); } try { return responses.take().getResultOrThrow(); @@ -159,7 +160,7 @@ private class Reader implements StreamObserver { @Override public void onNext(SessionResp resp) { verify(!doneReading); - responses.offer(Result.createWithResponse(resp)); + responses.add(Result.createWithResponse(resp)); } /** @@ -169,7 +170,7 @@ public void onNext(SessionResp resp) { */ @Override public void onError(Throwable t) { - responses.offer(Result.createWithThrowable(t)); + responses.add(Result.createWithThrowable(t)); } /** @@ -180,7 +181,7 @@ public void onError(Throwable t) { public void onCompleted() { logger.log(Level.INFO, "Reading from the S2A is complete."); doneReading = true; - responses.offer( + responses.add( Result.createWithThrowable( new ConnectionClosedException("Reading from the S2A is complete."))); } diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java b/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java index 94549d11c87..da75cf0d4dd 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java @@ -27,7 +27,6 @@ public final class AccessTokenManager { private final TokenFetcher tokenFetcher; /** Creates an {@code AccessTokenManager} based on the environment where the application runs. */ - @SuppressWarnings("RethrowReflectiveOperationExceptionAsLinkageError") public static Optional create() { Optional tokenFetcher; try { @@ -38,7 +37,7 @@ public static Optional create() { } catch (ClassNotFoundException e) { tokenFetcher = Optional.empty(); } catch (ReflectiveOperationException e) { - throw new AssertionError(e); + throw new LinkageError(e.getMessage(), e); } return tokenFetcher.isPresent() ? Optional.of(new AccessTokenManager((TokenFetcher) tokenFetcher.get())) diff --git a/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java b/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java index 7845e7c3bcb..7281adb9794 100644 --- a/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java +++ b/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java @@ -89,10 +89,10 @@ public void getChannelResource_mtlsSuccess() throws Exception { /** * Creates two {@code Resoure}s for the same target address and verifies that they are - * equal. + * distinct. */ @Test - public void getChannelResource_twoEqualChannels() { + public void getChannelResource_twoUnEqualChannels() { Resource resource = S2AHandshakerServiceChannel.getChannelResource( "localhost:" + plaintextServer.getPort(), @@ -101,19 +101,19 @@ public void getChannelResource_twoEqualChannels() { S2AHandshakerServiceChannel.getChannelResource( "localhost:" + plaintextServer.getPort(), InsecureChannelCredentials.create()); - assertThat(resource).isEqualTo(resourceTwo); + assertThat(resource).isNotEqualTo(resourceTwo); } - /** Same as getChannelResource_twoEqualChannels, but use mTLS. */ + /** Same as getChannelResource_twoUnEqualChannels, but use mTLS. */ @Test - public void getChannelResource_mtlsTwoEqualChannels() throws Exception { + public void getChannelResource_mtlsTwoUnEqualChannels() throws Exception { Resource resource = S2AHandshakerServiceChannel.getChannelResource( "localhost:" + mtlsServer.getPort(), getTlsChannelCredentials()); Resource resourceTwo = S2AHandshakerServiceChannel.getChannelResource( "localhost:" + mtlsServer.getPort(), getTlsChannelCredentials()); - assertThat(resource).isEqualTo(resourceTwo); + assertThat(resource).isNotEqualTo(resourceTwo); } /** diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServer.java b/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServer.java index 66f636ada22..d630f57d90d 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServer.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServer.java @@ -17,6 +17,7 @@ package io.grpc.s2a.handshaker; import io.grpc.stub.StreamObserver; +import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.logging.Logger; @@ -38,7 +39,11 @@ public StreamObserver setUpSession(StreamObserver respo @Override public void onNext(SessionReq req) { logger.info("Received a request from client."); - responseObserver.onNext(writer.handleResponse(req)); + try { + responseObserver.onNext(writer.handleResponse(req)); + } catch (IOException e) { + responseObserver.onError(e); + } } @Override diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServerTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServerTest.java index e200d119867..a8868744f80 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServerTest.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServerTest.java @@ -29,6 +29,9 @@ import io.grpc.benchmarks.Utils; import io.grpc.s2a.handshaker.ValidatePeerCertificateChainReq.VerificationMode; import io.grpc.stub.StreamObserver; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -45,9 +48,7 @@ public final class FakeS2AServerTest { private static final Logger logger = Logger.getLogger(FakeS2AServerTest.class.getName()); private static final ImmutableList FAKE_CERT_DER_CHAIN = - ImmutableList.of( - ByteString.copyFrom( - new byte[] {'f', 'a', 'k', 'e', '-', 'd', 'e', 'r', '-', 'c', 'h', 'a', 'i', 'n'})); + ImmutableList.of(ByteString.copyFrom("fake-der-chain".getBytes(StandardCharsets.US_ASCII))); private int port; private String serverAddress; private SessionResp response = null; @@ -68,7 +69,7 @@ public void tearDown() { @Test public void callS2AServerOnce_getTlsConfiguration_returnsValidResult() - throws InterruptedException { + throws InterruptedException, IOException { ExecutorService executor = Executors.newSingleThreadExecutor(); logger.info("Client connecting to: " + serverAddress); ManagedChannel channel = @@ -122,9 +123,12 @@ public void onCompleted() {} GetTlsConfigurationResp.newBuilder() .setClientTlsConfiguration( GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() - .addCertificateChain(FakeWriter.LEAF_CERT) - .addCertificateChain(FakeWriter.INTERMEDIATE_CERT_2) - .addCertificateChain(FakeWriter.INTERMEDIATE_CERT_1) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.leafCertFile.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.cert1File.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.cert2File.toPath()), StandardCharsets.UTF_8)) .setMinTlsVersion(TLSVersion.TLS_VERSION_1_3) .setMaxTlsVersion(TLSVersion.TLS_VERSION_1_3) .addCiphersuites( diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeWriter.java b/s2a/src/test/java/io/grpc/s2a/handshaker/FakeWriter.java index 45961b81b7b..b0e84fdf962 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeWriter.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/FakeWriter.java @@ -23,7 +23,10 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.ByteString; import io.grpc.stub.StreamObserver; +import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; @@ -50,59 +53,18 @@ enum VerificationResult { FAILURE } - public static final String LEAF_CERT = - "-----BEGIN CERTIFICATE-----\n" - + "MIICkDCCAjagAwIBAgIUSAtcrPhNNs1zxv51lIfGOVtkw6QwCgYIKoZIzj0EAwIw\n" - + "QTEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xEDAOBgNVBAsMB2NvbnRleHQxFDAS\n" - + "BgorBgEEAdZ5AggBDAQyMDIyMCAXDTIzMDcxNDIyMzYwNFoYDzIwNTAxMTI5MjIz\n" - + "NjA0WjARMQ8wDQYDVQQDDAZ1bnVzZWQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\n" - + "AAQGFlJpLxJMh4HuUm0DKjnUF7larH3tJvroQ12xpk+pPKQepn4ILoq9lZ8Xd3jz\n" - + "U98eDRXG5f4VjnX98DDHE4Ido4IBODCCATQwDgYDVR0PAQH/BAQDAgeAMCAGA1Ud\n" - + "JQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMIGxBgNV\n" - + "HREBAf8EgaYwgaOGSnNwaWZmZTovL3NpZ25lci1yb2xlLmNvbnRleHQuc2VjdXJp\n" - + "dHktcmVhbG0ucHJvZC5nb29nbGUuY29tL3JvbGUvbGVhZi1yb2xlgjNzaWduZXIt\n" - + "cm9sZS5jb250ZXh0LnNlY3VyaXR5LXJlYWxtLnByb2Quc3BpZmZlLmdvb2eCIGZx\n" - + "ZG4tb2YtdGhlLW5vZGUucHJvZC5nb29nbGUuY29tMB0GA1UdDgQWBBSWSd5Fw6dI\n" - + "TGpt0m1Uxwf0iKqebzAfBgNVHSMEGDAWgBRm5agVVdpWfRZKM7u6OMuzHhqPcDAK\n" - + "BggqhkjOPQQDAgNIADBFAiB0sjRPSYy2eFq8Y0vQ8QN4AZ2NMajskvxnlifu7O4U\n" - + "RwIhANTh5Fkyx2nMYFfyl+W45dY8ODTw3HnlZ4b51hTAdkWl\n" - + "-----END CERTIFICATE-----"; - public static final String INTERMEDIATE_CERT_2 = - "-----BEGIN CERTIFICATE-----\n" - + "MIICQjCCAeigAwIBAgIUKxXRDlnWXefNV5lj5CwhDuXEq7MwCgYIKoZIzj0EAwIw\n" - + "OzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xEDAOBgNVBAsMB2NvbnRleHQxDjAM\n" - + "BgNVBAMMBTEyMzQ1MCAXDTIzMDcxNDIyMzYwNFoYDzIwNTAxMTI5MjIzNjA0WjBB\n" - + "MRcwFQYDVQQKDA5zZWN1cml0eS1yZWFsbTEQMA4GA1UECwwHY29udGV4dDEUMBIG\n" - + "CisGAQQB1nkCCAEMBDIwMjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT/Zu7x\n" - + "UYVyg+T/vg2H+y4I6t36Kc4qxD0eqqZjRLYBVKkUQHxBqc14t0DpoROMYQCNd4DF\n" - + "pcxv/9m6DaJbRk6Ao4HBMIG+MA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAG\n" - + "AQH/AgEBMFgGA1UdHgEB/wROMEygSjA1gjNzaWduZXItcm9sZS5jb250ZXh0LnNl\n" - + "Y3VyaXR5LXJlYWxtLnByb2Quc3BpZmZlLmdvb2cwEYIPcHJvZC5nb29nbGUuY29t\n" - + "MB0GA1UdDgQWBBRm5agVVdpWfRZKM7u6OMuzHhqPcDAfBgNVHSMEGDAWgBQcjNAh\n" - + "SCHTj+BW8KrzSSLo2ASEgjAKBggqhkjOPQQDAgNIADBFAiEA6KyGd9VxXDZceMZG\n" - + "IsbC40rtunFjLYI0mjZw9RcRWx8CIHCIiIHxafnDaCi+VB99NZfzAdu37g6pJptB\n" - + "gjIY71MO\n" - + "-----END CERTIFICATE-----"; - public static final String INTERMEDIATE_CERT_1 = - "-----BEGIN CERTIFICATE-----\n" - + "MIICODCCAd6gAwIBAgIUXtZECORWRSKnS9rRTJYkiALUXswwCgYIKoZIzj0EAwIw\n" - + "NzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xDTALBgNVBAsMBHJvb3QxDTALBgNV\n" - + "BAMMBDEyMzQwIBcNMjMwNzE0MjIzNjA0WhgPMjA1MDExMjkyMjM2MDRaMDsxFzAV\n" - + "BgNVBAoMDnNlY3VyaXR5LXJlYWxtMRAwDgYDVQQLDAdjb250ZXh0MQ4wDAYDVQQD\n" - + "DAUxMjM0NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAycVTZrjockbpD59f1a\n" - + "4l1SNL7nSyXz66Guz4eDveQqLmaMBg7vpACfO4CtiAGnolHEffuRtSkdM434m5En\n" - + "bXCjgcEwgb4wDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQIwWAYD\n" - + "VR0eAQH/BE4wTKBKMDWCM3NpZ25lci1yb2xlLmNvbnRleHQuc2VjdXJpdHktcmVh\n" - + "bG0ucHJvZC5zcGlmZmUuZ29vZzARgg9wcm9kLmdvb2dsZS5jb20wHQYDVR0OBBYE\n" - + "FByM0CFIIdOP4FbwqvNJIujYBISCMB8GA1UdIwQYMBaAFMX+vebuj/lYfYEC23IA\n" - + "8HoIW0HsMAoGCCqGSM49BAMCA0gAMEUCIQCfxeXEBd7UPmeImT16SseCRu/6cHxl\n" - + "kTDsq9sKZ+eXBAIgA+oViAVOUhUQO1/6Mjlczg8NmMy2vNtG4V/7g9dMMVU=\n" - + "-----END CERTIFICATE-----"; + public static final File leafCertFile = + new File("src/test/resources/leaf_cert_ec.pem"); + public static final File cert2File = + new File("src/test/resources/int_cert2_ec.pem"); + public static final File cert1File = + new File("src/test/resources/int_cert1_ec.pem"); + // src/test/resources/leaf_key_ec.pem private static final String PRIVATE_KEY = - "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgqA2U0ld1OOHLMXWf" - + "uyN4GSaqhhudEIaKkll3rdIq0M+hRANCAAQGFlJpLxJMh4HuUm0DKjnUF7larH3t" - + "JvroQ12xpk+pPKQepn4ILoq9lZ8Xd3jzU98eDRXG5f4VjnX98DDHE4Id"; + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgR2HBqtWTWu4NLiow" + + "ar8vh+9vAmCONE59C+jXNAb9r8ehRANCAATRM8ozcr8PTOVsZNWh+rTmJ6t+rODu" + + "g3LwWpUQq9h7AddjGlLrrTNrceOyO7nh9aEk5plKhs/h7PO8+vkEFsEx"; private static final ImmutableMap ALGORITHM_TO_SIGNATURE_INSTANCE_IDENTIFIER = ImmutableMap.of( @@ -167,24 +129,32 @@ void sendIoError() { } void sendGetTlsConfigResp() { - reader.onNext( - SessionResp.newBuilder() - .setGetTlsConfigurationResp( - GetTlsConfigurationResp.newBuilder() - .setClientTlsConfiguration( - GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() - .addCertificateChain(LEAF_CERT) - .addCertificateChain(INTERMEDIATE_CERT_2) - .addCertificateChain(INTERMEDIATE_CERT_1) - .setMinTlsVersion(TLS_VERSION_1_3) - .setMaxTlsVersion(TLS_VERSION_1_3) - .addCiphersuites( - Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) - .addCiphersuites( - Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) - .addCiphersuites( - Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256))) - .build()); + try { + reader.onNext( + SessionResp.newBuilder() + .setGetTlsConfigurationResp( + GetTlsConfigurationResp.newBuilder() + .setClientTlsConfiguration( + GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.leafCertFile.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.cert1File.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.cert2File.toPath()), StandardCharsets.UTF_8)) + .setMinTlsVersion(TLS_VERSION_1_3) + .setMaxTlsVersion(TLS_VERSION_1_3) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) + .addCiphersuites( + Ciphersuite + .CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256))) + .build()); + } catch (IOException e) { + reader.onError(e); + } } boolean isFakeWriterClosed() { @@ -195,7 +165,11 @@ boolean isFakeWriterClosed() { public void onNext(SessionReq sessionReq) { switch (behavior) { case OK_STATUS: - reader.onNext(handleResponse(sessionReq)); + try { + reader.onNext(handleResponse(sessionReq)); + } catch (IOException e) { + reader.onError(e); + } break; case EMPTY_RESPONSE: reader.onNext(SessionResp.getDefaultInstance()); @@ -216,25 +190,36 @@ public void onNext(SessionReq sessionReq) { reader.onCompleted(); break; case BAD_TLS_VERSION_RESPONSE: - reader.onNext( - SessionResp.newBuilder() - .setGetTlsConfigurationResp( - GetTlsConfigurationResp.newBuilder() - .setClientTlsConfiguration( - GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() - .addCertificateChain(LEAF_CERT) - .addCertificateChain(INTERMEDIATE_CERT_2) - .addCertificateChain(INTERMEDIATE_CERT_1) - .setMinTlsVersion(TLS_VERSION_1_3) - .setMaxTlsVersion(TLS_VERSION_1_2))) - .build()); + try { + reader.onNext( + SessionResp.newBuilder() + .setGetTlsConfigurationResp( + GetTlsConfigurationResp.newBuilder() + .setClientTlsConfiguration( + GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.leafCertFile.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.cert1File.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.cert2File.toPath()), StandardCharsets.UTF_8)) + .setMinTlsVersion(TLS_VERSION_1_3) + .setMaxTlsVersion(TLS_VERSION_1_2))) + .build()); + } catch (IOException e) { + reader.onError(e); + } break; default: - reader.onNext(handleResponse(sessionReq)); + try { + reader.onNext(handleResponse(sessionReq)); + } catch (IOException e) { + reader.onError(e); + } } } - SessionResp handleResponse(SessionReq sessionReq) { + SessionResp handleResponse(SessionReq sessionReq) throws IOException { if (sessionReq.hasGetTlsConfigurationReq()) { return handleGetTlsConfigurationReq(sessionReq.getGetTlsConfigurationReq()); } @@ -253,7 +238,8 @@ SessionResp handleResponse(SessionReq sessionReq) { .build(); } - private SessionResp handleGetTlsConfigurationReq(GetTlsConfigurationReq req) { + private SessionResp handleGetTlsConfigurationReq(GetTlsConfigurationReq req) + throws IOException { if (!req.getConnectionSide().equals(ConnectionSide.CONNECTION_SIDE_CLIENT)) { return SessionResp.newBuilder() .setStatus( @@ -267,9 +253,12 @@ private SessionResp handleGetTlsConfigurationReq(GetTlsConfigurationReq req) { GetTlsConfigurationResp.newBuilder() .setClientTlsConfiguration( GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() - .addCertificateChain(LEAF_CERT) - .addCertificateChain(INTERMEDIATE_CERT_2) - .addCertificateChain(INTERMEDIATE_CERT_1) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.leafCertFile.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.cert1File.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.cert2File.toPath()), StandardCharsets.UTF_8)) .setMinTlsVersion(TLS_VERSION_1_3) .setMaxTlsVersion(TLS_VERSION_1_3) .addCiphersuites( diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java index bae58f2f9ec..2a2b1f246ec 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java @@ -17,7 +17,6 @@ package io.grpc.s2a.handshaker; import static com.google.common.truth.Truth.assertThat; -import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.concurrent.TimeUnit.SECONDS; import io.grpc.ChannelCredentials; @@ -42,7 +41,6 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; -import java.io.ByteArrayInputStream; import java.io.File; import java.util.concurrent.FutureTask; import java.util.logging.Logger; @@ -58,72 +56,12 @@ public final class IntegrationTest { private static final Logger logger = Logger.getLogger(FakeS2AServer.class.getName()); - private static final String CERT_CHAIN = - "-----BEGIN CERTIFICATE-----\n" - + "MIICkDCCAjagAwIBAgIUSAtcrPhNNs1zxv51lIfGOVtkw6QwCgYIKoZIzj0EAwIw\n" - + "QTEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xEDAOBgNVBAsMB2NvbnRleHQxFDAS\n" - + "BgorBgEEAdZ5AggBDAQyMDIyMCAXDTIzMDcxNDIyMzYwNFoYDzIwNTAxMTI5MjIz\n" - + "NjA0WjARMQ8wDQYDVQQDDAZ1bnVzZWQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\n" - + "AAQGFlJpLxJMh4HuUm0DKjnUF7larH3tJvroQ12xpk+pPKQepn4ILoq9lZ8Xd3jz\n" - + "U98eDRXG5f4VjnX98DDHE4Ido4IBODCCATQwDgYDVR0PAQH/BAQDAgeAMCAGA1Ud\n" - + "JQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMIGxBgNV\n" - + "HREBAf8EgaYwgaOGSnNwaWZmZTovL3NpZ25lci1yb2xlLmNvbnRleHQuc2VjdXJp\n" - + "dHktcmVhbG0ucHJvZC5nb29nbGUuY29tL3JvbGUvbGVhZi1yb2xlgjNzaWduZXIt\n" - + "cm9sZS5jb250ZXh0LnNlY3VyaXR5LXJlYWxtLnByb2Quc3BpZmZlLmdvb2eCIGZx\n" - + "ZG4tb2YtdGhlLW5vZGUucHJvZC5nb29nbGUuY29tMB0GA1UdDgQWBBSWSd5Fw6dI\n" - + "TGpt0m1Uxwf0iKqebzAfBgNVHSMEGDAWgBRm5agVVdpWfRZKM7u6OMuzHhqPcDAK\n" - + "BggqhkjOPQQDAgNIADBFAiB0sjRPSYy2eFq8Y0vQ8QN4AZ2NMajskvxnlifu7O4U\n" - + "RwIhANTh5Fkyx2nMYFfyl+W45dY8ODTw3HnlZ4b51hTAdkWl\n" - + "-----END CERTIFICATE-----\n" - + "-----BEGIN CERTIFICATE-----\n" - + "MIICQjCCAeigAwIBAgIUKxXRDlnWXefNV5lj5CwhDuXEq7MwCgYIKoZIzj0EAwIw\n" - + "OzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xEDAOBgNVBAsMB2NvbnRleHQxDjAM\n" - + "BgNVBAMMBTEyMzQ1MCAXDTIzMDcxNDIyMzYwNFoYDzIwNTAxMTI5MjIzNjA0WjBB\n" - + "MRcwFQYDVQQKDA5zZWN1cml0eS1yZWFsbTEQMA4GA1UECwwHY29udGV4dDEUMBIG\n" - + "CisGAQQB1nkCCAEMBDIwMjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT/Zu7x\n" - + "UYVyg+T/vg2H+y4I6t36Kc4qxD0eqqZjRLYBVKkUQHxBqc14t0DpoROMYQCNd4DF\n" - + "pcxv/9m6DaJbRk6Ao4HBMIG+MA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAG\n" - + "AQH/AgEBMFgGA1UdHgEB/wROMEygSjA1gjNzaWduZXItcm9sZS5jb250ZXh0LnNl\n" - + "Y3VyaXR5LXJlYWxtLnByb2Quc3BpZmZlLmdvb2cwEYIPcHJvZC5nb29nbGUuY29t\n" - + "MB0GA1UdDgQWBBRm5agVVdpWfRZKM7u6OMuzHhqPcDAfBgNVHSMEGDAWgBQcjNAh\n" - + "SCHTj+BW8KrzSSLo2ASEgjAKBggqhkjOPQQDAgNIADBFAiEA6KyGd9VxXDZceMZG\n" - + "IsbC40rtunFjLYI0mjZw9RcRWx8CIHCIiIHxafnDaCi+VB99NZfzAdu37g6pJptB\n" - + "gjIY71MO\n" - + "-----END CERTIFICATE-----\n" - + "-----BEGIN CERTIFICATE-----\n" - + "MIICODCCAd6gAwIBAgIUXtZECORWRSKnS9rRTJYkiALUXswwCgYIKoZIzj0EAwIw\n" - + "NzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xDTALBgNVBAsMBHJvb3QxDTALBgNV\n" - + "BAMMBDEyMzQwIBcNMjMwNzE0MjIzNjA0WhgPMjA1MDExMjkyMjM2MDRaMDsxFzAV\n" - + "BgNVBAoMDnNlY3VyaXR5LXJlYWxtMRAwDgYDVQQLDAdjb250ZXh0MQ4wDAYDVQQD\n" - + "DAUxMjM0NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAycVTZrjockbpD59f1a\n" - + "4l1SNL7nSyXz66Guz4eDveQqLmaMBg7vpACfO4CtiAGnolHEffuRtSkdM434m5En\n" - + "bXCjgcEwgb4wDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQIwWAYD\n" - + "VR0eAQH/BE4wTKBKMDWCM3NpZ25lci1yb2xlLmNvbnRleHQuc2VjdXJpdHktcmVh\n" - + "bG0ucHJvZC5zcGlmZmUuZ29vZzARgg9wcm9kLmdvb2dsZS5jb20wHQYDVR0OBBYE\n" - + "FByM0CFIIdOP4FbwqvNJIujYBISCMB8GA1UdIwQYMBaAFMX+vebuj/lYfYEC23IA\n" - + "8HoIW0HsMAoGCCqGSM49BAMCA0gAMEUCIQCfxeXEBd7UPmeImT16SseCRu/6cHxl\n" - + "kTDsq9sKZ+eXBAIgA+oViAVOUhUQO1/6Mjlczg8NmMy2vNtG4V/7g9dMMVU=\n" - + "-----END CERTIFICATE-----"; - private static final String ROOT_PEM = - "-----BEGIN CERTIFICATE-----\n" - + "MIIBtTCCAVqgAwIBAgIUbAe+8OocndQXRBCElLBxBSdfdV8wCgYIKoZIzj0EAwIw\n" - + "NzEXMBUGA1UECgwOc2VjdXJpdHktcmVhbG0xDTALBgNVBAsMBHJvb3QxDTALBgNV\n" - + "BAMMBDEyMzQwIBcNMjMwNzE0MjIzNjA0WhgPMjA1MDExMjkyMjM2MDRaMDcxFzAV\n" - + "BgNVBAoMDnNlY3VyaXR5LXJlYWxtMQ0wCwYDVQQLDARyb290MQ0wCwYDVQQDDAQx\n" - + "MjM0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaMY2tBW5r1t0+vhayz0ZoGMF\n" - + "boX/ZmmCmIh0iTWg4madvwNOh74CMVVvDUlXZcuVqZ3vVIX/a7PTFVqUwQlKW6NC\n" - + "MEAwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMX+\n" - + "vebuj/lYfYEC23IA8HoIW0HsMAoGCCqGSM49BAMCA0kAMEYCIQDETd27nsUTXKWY\n" - + "CiOno78O09gK95NoTkPU5e2chJYMqAIhALYFAyh7PU5xgFQsN9hiqgsHUc5/pmBG\n" - + "BGjJ1iz8rWGJ\n" - + "-----END CERTIFICATE-----"; - private static final String PRIVATE_KEY = - "-----BEGIN PRIVATE KEY-----\n" - + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgqA2U0ld1OOHLMXWf\n" - + "uyN4GSaqhhudEIaKkll3rdIq0M+hRANCAAQGFlJpLxJMh4HuUm0DKjnUF7larH3t\n" - + "JvroQ12xpk+pPKQepn4ILoq9lZ8Xd3jzU98eDRXG5f4VjnX98DDHE4Id\n" - + "-----END PRIVATE KEY-----"; - + public static final File privateKeyFile = + new File("src/test/resources/leaf_key_ec.pem"); + public static final File rootCertFile = + new File("src/test/resources/root_cert_ec.pem"); + public static final File certChainFile = + new File("src/test/resources/cert_chain_ec.pem"); private String s2aAddress; private Server s2aServer; private String s2aDelayAddress; @@ -252,13 +190,11 @@ public static boolean doUnaryRpc(ManagedChannel channel) throws InterruptedExcep private static SslContext buildSslContext() throws SSLException { SslContextBuilder sslServerContextBuilder = - SslContextBuilder.forServer( - new ByteArrayInputStream(CERT_CHAIN.getBytes(UTF_8)), - new ByteArrayInputStream(PRIVATE_KEY.getBytes(UTF_8))); + SslContextBuilder.forServer(certChainFile, privateKeyFile); SslContext sslServerContext = GrpcSslContexts.configure(sslServerContextBuilder, SslProvider.OPENSSL) .protocols("TLSv1.3", "TLSv1.2") - .trustManager(new ByteArrayInputStream(ROOT_PEM.getBytes(UTF_8))) + .trustManager(rootCertFile) .clientAuth(ClientAuth.REQUIRE) .build(); diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/ProtoUtilTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/ProtoUtilTest.java index 6d134b43f7a..f54063b9e04 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/ProtoUtilTest.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/ProtoUtilTest.java @@ -30,47 +30,6 @@ public final class ProtoUtilTest { @Rule public final Expect expect = Expect.create(); - @Test - public void convertCiphersuite_success() { - expect - .that( - ProtoUtil.convertCiphersuite( - Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)) - .isEqualTo("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"); - expect - .that( - ProtoUtil.convertCiphersuite( - Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384)) - .isEqualTo("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"); - expect - .that( - ProtoUtil.convertCiphersuite( - Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256)) - .isEqualTo("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"); - expect - .that( - ProtoUtil.convertCiphersuite(Ciphersuite.CIPHERSUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256)) - .isEqualTo("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); - expect - .that( - ProtoUtil.convertCiphersuite(Ciphersuite.CIPHERSUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384)) - .isEqualTo("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"); - expect - .that( - ProtoUtil.convertCiphersuite( - Ciphersuite.CIPHERSUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256)) - .isEqualTo("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"); - } - - @Test - public void convertCiphersuite_withUnspecifiedCiphersuite_fails() { - AssertionError expected = - assertThrows( - AssertionError.class, - () -> ProtoUtil.convertCiphersuite(Ciphersuite.CIPHERSUITE_UNSPECIFIED)); - expect.that(expected).hasMessageThat().isEqualTo("Ciphersuite 0 is not supported."); - } - @Test public void convertTlsProtocolVersion_success() { expect diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2APrivateKeyMethodTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/S2APrivateKeyMethodTest.java index 8252aa245d7..fc8d42d09c0 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/S2APrivateKeyMethodTest.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/S2APrivateKeyMethodTest.java @@ -30,6 +30,8 @@ import io.netty.handler.ssl.OpenSslPrivateKeyMethod; import io.netty.handler.ssl.SslContextBuilder; import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.security.PublicKey; import java.security.Signature; import java.security.cert.CertificateFactory; @@ -61,7 +63,8 @@ private static PublicKey extractPublicKeyFromPem(String pem) throws Exception { private static boolean verifySignature( byte[] dataToSign, byte[] signature, String signatureAlgorithm) throws Exception { Signature sig = Signature.getInstance(signatureAlgorithm); - sig.initVerify(extractPublicKeyFromPem(FakeWriter.LEAF_CERT)); + sig.initVerify(extractPublicKeyFromPem(new String( + Files.readAllBytes(FakeWriter.leafCertFile.toPath()), StandardCharsets.UTF_8))); sig.update(dataToSign); return sig.verify(signature); } diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java index 404910e8be0..fa6c4fc858d 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java @@ -50,6 +50,7 @@ import io.netty.handler.codec.http2.Http2ConnectionEncoder; import io.netty.handler.codec.http2.Http2Settings; import io.netty.util.AsciiString; +import java.io.IOException; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -246,7 +247,11 @@ public StreamObserver setUpSession(StreamObserver respo return new StreamObserver() { @Override public void onNext(SessionReq req) { - responseObserver.onNext(writer.handleResponse(req)); + try { + responseObserver.onNext(writer.handleResponse(req)); + } catch (IOException e) { + responseObserver.onError(e); + } } @Override diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java index 47fd154d949..a1daf9948a1 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java @@ -28,6 +28,8 @@ import io.grpc.s2a.channel.S2AHandshakerServiceChannel; import io.grpc.stub.StreamObserver; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -82,9 +84,12 @@ public void send_clientTlsConfiguration_receiveOkStatus() throws Exception { GetTlsConfigurationResp.newBuilder() .setClientTlsConfiguration( GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() - .addCertificateChain(FakeWriter.LEAF_CERT) - .addCertificateChain(FakeWriter.INTERMEDIATE_CERT_2) - .addCertificateChain(FakeWriter.INTERMEDIATE_CERT_1) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.leafCertFile.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.cert1File.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(new String(Files.readAllBytes( + FakeWriter.cert2File.toPath()), StandardCharsets.UTF_8)) .setMinTlsVersion(TLSVersion.TLS_VERSION_1_3) .setMaxTlsVersion(TLSVersion.TLS_VERSION_1_3) .addCiphersuites( @@ -189,26 +194,13 @@ public void send_receiveManyUnexpectedResponse_expectResponsesEmpty() throws Exc @Test public void send_receiveDelayedResponse() throws Exception { writer.sendGetTlsConfigResp(); - SessionResp resp = stub.send(SessionReq.getDefaultInstance()); - SessionResp expected = - SessionResp.newBuilder() - .setGetTlsConfigurationResp( - GetTlsConfigurationResp.newBuilder() - .setClientTlsConfiguration( - GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() - .addCertificateChain(FakeWriter.LEAF_CERT) - .addCertificateChain(FakeWriter.INTERMEDIATE_CERT_2) - .addCertificateChain(FakeWriter.INTERMEDIATE_CERT_1) - .setMinTlsVersion(TLSVersion.TLS_VERSION_1_3) - .setMaxTlsVersion(TLSVersion.TLS_VERSION_1_3) - .addCiphersuites( - Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) - .addCiphersuites( - Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) - .addCiphersuites( - Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256))) - .build(); - assertThat(resp).ignoringRepeatedFieldOrder().isEqualTo(expected); + IOException expectedException = + assertThrows(IOException.class, () -> stub.send(SessionReq.getDefaultInstance())); + assertThat(expectedException) + .hasMessageThat() + .contains("Received an unexpected response from a host at the S2A's address."); + + assertThat(stub.getResponses()).isEmpty(); } @Test diff --git a/s2a/src/test/resources/README.md b/s2a/src/test/resources/README.md index 726b921a615..2250ffb1dec 100644 --- a/s2a/src/test/resources/README.md +++ b/s2a/src/test/resources/README.md @@ -29,4 +29,41 @@ Sign CSRs for server and client ``` openssl x509 -req -CA root_cert.pem -CAkey root_key.pem -in server.csr -out server_cert.pem -days 7305 -extfile config.cnf -extensions req_ext openssl x509 -req -CA root_cert.pem -CAkey root_key.pem -in client.csr -out client_cert.pem -days 7305 +``` + +Generate self-signed ECDSA root cert + +``` +openssl ecparam -name prime256v1 -genkey -noout -out temp.pem +openssl pkcs8 -topk8 -in temp.pem -out root_key_ec.pem -nocrypt +rm temp.pem +openssl req -x509 -days 7305 -new -key root_key_ec.pem -nodes -out root_cert_ec.pem -config root_ec.cnf -extensions 'v3_req' +``` + +Generate a chain of ECDSA certs + +``` +openssl ecparam -name prime256v1 -genkey -noout -out temp.pem +openssl pkcs8 -topk8 -in temp.pem -out int_key2_ec.pem -nocrypt +rm temp.pem +openssl req -key int_key2_ec.pem -new -out temp.csr -config int_cert2.cnf +openssl x509 -req -days 7305 -in temp.csr -CA root_cert_ec.pem -CAkey root_key_ec.pem -CAcreateserial -out int_cert2_ec.pem -extfile int_cert2.cnf -extensions 'v3_req' + + +openssl ecparam -name prime256v1 -genkey -noout -out temp.pem +openssl pkcs8 -topk8 -in temp.pem -out int_key1_ec.pem -nocrypt +rm temp.pem +openssl req -key int_key1_ec.pem -new -out temp.csr -config int_cert1.cnf +openssl x509 -req -days 7305 -in temp.csr -CA int_cert2_ec.pem -CAkey int_key2_ec.pem -CAcreateserial -out int_cert1_ec.pem -extfile int_cert1.cnf -extensions 'v3_req' + + +openssl ecparam -name prime256v1 -genkey -noout -out temp.pem +openssl pkcs8 -topk8 -in temp.pem -out leaf_key_ec.pem -nocrypt +rm temp.pem +openssl req -key leaf_key_ec.pem -new -out temp.csr -config leaf.cnf +openssl x509 -req -days 7305 -in temp.csr -CA int_cert1_ec.pem -CAkey int_key1_ec.pem -CAcreateserial -out leaf_cert_ec.pem -extfile leaf.cnf -extensions 'v3_req' +``` + +``` +cat leaf_cert_ec.pem int_cert1_ec.pem int_cert2_ec.pem > cert_chain_ec.pem ``` \ No newline at end of file diff --git a/s2a/src/test/resources/cert_chain_ec.pem b/s2a/src/test/resources/cert_chain_ec.pem new file mode 100644 index 00000000000..0e097d39bf2 --- /dev/null +++ b/s2a/src/test/resources/cert_chain_ec.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIB0jCCAXigAwIBAgIUBV1dftEhhEMTI83L6jpeJn2tuyQwCgYIKoZIzj0EAwIw +JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx +OTIzMDQwMFoXDTQ0MDkxOTIzMDQwMFowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC +b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0TPKM3K/ +D0zlbGTVofq05ierfqzg7oNy8FqVEKvYewHXYxpS660za3Hjsju54fWhJOaZSobP +4ezzvPr5BBbBMaOBgzCBgDAOBgNVHQ8BAf8EBAMCB4AwIAYDVR0lAQH/BBYwFAYI +KwYBBQUHAwIGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPvP7dnB +dg8ZoLB0w62tbvoIRRHPMB8GA1UdIwQYMBaAFHeH+MNh2fgyjNHYP9hLAv9Sl1yD +MAoGCCqGSM49BAMCA0gAMEUCIBcsImaxeFjxFXCXYNQJnde+rsEOgbeAHrAC0SZQ +NlB2AiEA4epDhw/o+6BfgDbqlZsNEHkScPrwupnBQGLQlmNJe2c= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB1zCCAX6gAwIBAgIUW4GYHncSLeb7Tmw7FMjX/DesReIwCgYIKoZIzj0EAwIw +JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx +OTIzMDA0MVoXDTQ0MDkxOTIzMDA0MVowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC +b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAqtg2E+h +Wfr5dnewqsCLwM0PohkB83Gh7V3i/TPFkNKF/V6pKdz5a3Z8sicG+g7uJX+eyOoD +43Z8woO7MgJl8aOBiTCBhjAOBgNVHQ8BAf8EBAMCAQYwIAYDVR0lAQH/BBYwFAYI +KwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE +FHeH+MNh2fgyjNHYP9hLAv9Sl1yDMB8GA1UdIwQYMBaAFP+PTOryxis9d7HVfqhP +MyMEgMZOMAoGCCqGSM49BAMCA0cAMEQCIHbzJvxHMIDPBRi1e047K0mqKKBSfViS +guiDSoQ5g5OuAiBT5ePqDLs4PyrK6XFkiEWoRX8Z5T9y419Go+fpLM+DaA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB1zCCAX6gAwIBAgIUBBKkTrqFxQUist2pK2uj8/DRnKMwCgYIKoZIzj0EAwIw +JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx +OTIyNTYwNloXDTQ0MDkxOTIyNTYwNlowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC +b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFW7/Te2z +jS8KlpF8RMMYaZtKf6EZlrZIIo5SO9j6baAKXVna9LmDCrzXnOLIeqOuZq0ODizU +i4DFALB2yd5BkaOBiTCBhjAOBgNVHQ8BAf8EBAMCAQYwIAYDVR0lAQH/BBYwFAYI +KwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQIwHQYDVR0OBBYE +FP+PTOryxis9d7HVfqhPMyMEgMZOMB8GA1UdIwQYMBaAFFITbB0BULPtynN9SMki +lEarWxcKMAoGCCqGSM49BAMCA0cAMEQCIHK4cTTx4Ti7Te9hA9VVtHoMCt5fL4Cl +XnQR6D5xW4pPAiAQ+CilQdZUhVK5bU6wbrwLgcwf+40ETK/KASId5970rQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/int_cert1_.cnf b/s2a/src/test/resources/int_cert1_.cnf new file mode 100644 index 00000000000..8eaf6570da1 --- /dev/null +++ b/s2a/src/test/resources/int_cert1_.cnf @@ -0,0 +1,14 @@ +[req] +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no + +[req_distinguished_name] +O = o +OU = ou +CN = cn + +[v3_req] +keyUsage = critical, keyCertSign, cRLSign +extendedKeyUsage = critical, clientAuth, serverAuth +basicConstraints = critical, CA:true, pathlen: 1 \ No newline at end of file diff --git a/s2a/src/test/resources/int_cert1_ec.pem b/s2a/src/test/resources/int_cert1_ec.pem new file mode 100644 index 00000000000..980de5aa900 --- /dev/null +++ b/s2a/src/test/resources/int_cert1_ec.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIB1zCCAX6gAwIBAgIUW4GYHncSLeb7Tmw7FMjX/DesReIwCgYIKoZIzj0EAwIw +JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx +OTIzMDA0MVoXDTQ0MDkxOTIzMDA0MVowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC +b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAqtg2E+h +Wfr5dnewqsCLwM0PohkB83Gh7V3i/TPFkNKF/V6pKdz5a3Z8sicG+g7uJX+eyOoD +43Z8woO7MgJl8aOBiTCBhjAOBgNVHQ8BAf8EBAMCAQYwIAYDVR0lAQH/BBYwFAYI +KwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE +FHeH+MNh2fgyjNHYP9hLAv9Sl1yDMB8GA1UdIwQYMBaAFP+PTOryxis9d7HVfqhP +MyMEgMZOMAoGCCqGSM49BAMCA0cAMEQCIHbzJvxHMIDPBRi1e047K0mqKKBSfViS +guiDSoQ5g5OuAiBT5ePqDLs4PyrK6XFkiEWoRX8Z5T9y419Go+fpLM+DaA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/int_cert2.cnf b/s2a/src/test/resources/int_cert2.cnf new file mode 100644 index 00000000000..c6a2559ce10 --- /dev/null +++ b/s2a/src/test/resources/int_cert2.cnf @@ -0,0 +1,14 @@ +[req] +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no + +[req_distinguished_name] +O = o +OU = ou +CN = cn + +[v3_req] +keyUsage = critical, keyCertSign, cRLSign +extendedKeyUsage = critical, clientAuth, serverAuth +basicConstraints = critical, CA:true, pathlen: 2 \ No newline at end of file diff --git a/s2a/src/test/resources/int_cert2_ec.pem b/s2a/src/test/resources/int_cert2_ec.pem new file mode 100644 index 00000000000..574fa0195de --- /dev/null +++ b/s2a/src/test/resources/int_cert2_ec.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIB1zCCAX6gAwIBAgIUBBKkTrqFxQUist2pK2uj8/DRnKMwCgYIKoZIzj0EAwIw +JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx +OTIyNTYwNloXDTQ0MDkxOTIyNTYwNlowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC +b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFW7/Te2z +jS8KlpF8RMMYaZtKf6EZlrZIIo5SO9j6baAKXVna9LmDCrzXnOLIeqOuZq0ODizU +i4DFALB2yd5BkaOBiTCBhjAOBgNVHQ8BAf8EBAMCAQYwIAYDVR0lAQH/BBYwFAYI +KwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQIwHQYDVR0OBBYE +FP+PTOryxis9d7HVfqhPMyMEgMZOMB8GA1UdIwQYMBaAFFITbB0BULPtynN9SMki +lEarWxcKMAoGCCqGSM49BAMCA0cAMEQCIHK4cTTx4Ti7Te9hA9VVtHoMCt5fL4Cl +XnQR6D5xW4pPAiAQ+CilQdZUhVK5bU6wbrwLgcwf+40ETK/KASId5970rQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/int_key1_ec.pem b/s2a/src/test/resources/int_key1_ec.pem new file mode 100644 index 00000000000..7ff3864746b --- /dev/null +++ b/s2a/src/test/resources/int_key1_ec.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgLIQUM1HkFM/LWND8 +jCZ4wHXjFZ1ZZmQolahkZB0O1VChRANCAAQCq2DYT6FZ+vl2d7CqwIvAzQ+iGQHz +caHtXeL9M8WQ0oX9Xqkp3PlrdnyyJwb6Du4lf57I6gPjdnzCg7syAmXx +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/s2a/src/test/resources/int_key2_ec.pem b/s2a/src/test/resources/int_key2_ec.pem new file mode 100644 index 00000000000..7f529ae855f --- /dev/null +++ b/s2a/src/test/resources/int_key2_ec.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgGfm6kyaAMMrmYGhS +jxprBwtcZdP6qXlU1cVIO5bOT8qhRANCAAQVbv9N7bONLwqWkXxEwxhpm0p/oRmW +tkgijlI72PptoApdWdr0uYMKvNec4sh6o65mrQ4OLNSLgMUAsHbJ3kGR +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/s2a/src/test/resources/leaf.cnf b/s2a/src/test/resources/leaf.cnf new file mode 100644 index 00000000000..d5b373cbc71 --- /dev/null +++ b/s2a/src/test/resources/leaf.cnf @@ -0,0 +1,14 @@ +[req] +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no + +[req_distinguished_name] +O = o +OU = ou +CN = cn + +[v3_req] +keyUsage = critical, digitalSignature +extendedKeyUsage = critical, clientAuth, serverAuth +basicConstraints = critical, CA:false \ No newline at end of file diff --git a/s2a/src/test/resources/leaf_cert_ec.pem b/s2a/src/test/resources/leaf_cert_ec.pem new file mode 100644 index 00000000000..39692b95fda --- /dev/null +++ b/s2a/src/test/resources/leaf_cert_ec.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIB0jCCAXigAwIBAgIUBV1dftEhhEMTI83L6jpeJn2tuyQwCgYIKoZIzj0EAwIw +JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx +OTIzMDQwMFoXDTQ0MDkxOTIzMDQwMFowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC +b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0TPKM3K/ +D0zlbGTVofq05ierfqzg7oNy8FqVEKvYewHXYxpS660za3Hjsju54fWhJOaZSobP +4ezzvPr5BBbBMaOBgzCBgDAOBgNVHQ8BAf8EBAMCB4AwIAYDVR0lAQH/BBYwFAYI +KwYBBQUHAwIGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPvP7dnB +dg8ZoLB0w62tbvoIRRHPMB8GA1UdIwQYMBaAFHeH+MNh2fgyjNHYP9hLAv9Sl1yD +MAoGCCqGSM49BAMCA0gAMEUCIBcsImaxeFjxFXCXYNQJnde+rsEOgbeAHrAC0SZQ +NlB2AiEA4epDhw/o+6BfgDbqlZsNEHkScPrwupnBQGLQlmNJe2c= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/leaf_key_ec.pem b/s2a/src/test/resources/leaf_key_ec.pem new file mode 100644 index 00000000000..d90ad8f4db8 --- /dev/null +++ b/s2a/src/test/resources/leaf_key_ec.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgR2HBqtWTWu4NLiow +ar8vh+9vAmCONE59C+jXNAb9r8ehRANCAATRM8ozcr8PTOVsZNWh+rTmJ6t+rODu +g3LwWpUQq9h7AddjGlLrrTNrceOyO7nh9aEk5plKhs/h7PO8+vkEFsEx +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/s2a/src/test/resources/root_cert_ec.pem b/s2a/src/test/resources/root_cert_ec.pem new file mode 100644 index 00000000000..0dd465e8e90 --- /dev/null +++ b/s2a/src/test/resources/root_cert_ec.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBrzCCAVWgAwIBAgIUGV+9j5V61CZaa6mbrchDag5miEQwCgYIKoZIzj0EAwIw +JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx +OTIyNDMwOFoXDTQ0MDkxOTIyNDMwOFowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC +b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDVPIq1ds +/MX52CX9YU1RdEeM89YP4o3BN8OiP2O4qcuc11k4Qu4Mo4RWeN9OJpNElTQJ0K8n +/rIvbmw8AIMquaNhMF8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdJQQWMBQGCCsGAQUF +BwMBBggrBgEFBQcDAjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRSE2wdAVCz +7cpzfUjJIpRGq1sXCjAKBggqhkjOPQQDAgNIADBFAiEA1TEfHWArDnepmtMDQ4wd +Q3uqPrV2Ye2KMO67/BHEGIQCIFu3JutXYYVU/CinwH89AJW+FJ7zokaPjCDkbiOH ++h+H +-----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/root_ec.cnf b/s2a/src/test/resources/root_ec.cnf new file mode 100644 index 00000000000..bee0b80a166 --- /dev/null +++ b/s2a/src/test/resources/root_ec.cnf @@ -0,0 +1,14 @@ +[req] +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no + +[req_distinguished_name] +O = o +OU = ou +CN = cn + +[v3_req] +keyUsage = critical, keyCertSign, cRLSign +extendedKeyUsage = serverAuth, clientAuth +basicConstraints = critical, CA:true \ No newline at end of file diff --git a/s2a/src/test/resources/root_key_ec.pem b/s2a/src/test/resources/root_key_ec.pem new file mode 100644 index 00000000000..ef5ee1445f9 --- /dev/null +++ b/s2a/src/test/resources/root_key_ec.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd5oZmQBOtMF0xfc3 +uRuw5EDhA1thJKKeHfrij9FMkfahRANCAAQNU8irV2z8xfnYJf1hTVF0R4zz1g/i +jcE3w6I/Y7ipy5zXWThC7gyjhFZ4304mk0SVNAnQryf+si9ubDwAgyq5 +-----END PRIVATE KEY----- \ No newline at end of file From d169a5de6fb94e974fd77ebabde52f01bc7aaca0 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Fri, 27 Sep 2024 17:22:38 -0700 Subject: [PATCH 021/591] interop-test: add opentelemetry tracing context propagation test (#11538) --- interop-testing/build.gradle | 1 + .../OpenTelemetryContextPropagationTest.java | 191 ++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 interop-testing/src/test/java/io/grpc/testing/integration/OpenTelemetryContextPropagationTest.java diff --git a/interop-testing/build.gradle b/interop-testing/build.gradle index a19efb00155..a85aec97adf 100644 --- a/interop-testing/build.gradle +++ b/interop-testing/build.gradle @@ -13,6 +13,7 @@ dependencies { implementation project(path: ':grpc-alts', configuration: 'shadow'), project(':grpc-auth'), project(':grpc-census'), + project(':grpc-opentelemetry'), project(':grpc-gcp-csm-observability'), project(':grpc-netty'), project(':grpc-okhttp'), diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/OpenTelemetryContextPropagationTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/OpenTelemetryContextPropagationTest.java new file mode 100644 index 00000000000..3884d977a6e --- /dev/null +++ b/interop-testing/src/test/java/io/grpc/testing/integration/OpenTelemetryContextPropagationTest.java @@ -0,0 +1,191 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.testing.integration; + +import static org.junit.Assert.assertEquals; + +import io.grpc.ForwardingServerCallListener; +import io.grpc.InsecureServerCredentials; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.ServerBuilder; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.netty.InternalNettyChannelBuilder; +import io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.NettyServerBuilder; +import io.grpc.opentelemetry.GrpcOpenTelemetry; +import io.grpc.opentelemetry.GrpcTraceBinContextPropagator; +import io.grpc.opentelemetry.InternalGrpcOpenTelemetry; +import io.grpc.testing.integration.Messages.SimpleRequest; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class OpenTelemetryContextPropagationTest extends AbstractInteropTest { + private final OpenTelemetrySdk openTelemetrySdk; + private final Tracer tracer; + private final GrpcOpenTelemetry grpcOpenTelemetry; + private final AtomicReference applicationSpan = new AtomicReference<>(); + private final boolean censusClient; + + @Parameterized.Parameters(name = "ContextPropagator={0}, CensusClient={1}") + public static Iterable data() { + return Arrays.asList(new Object[][] { + {W3CTraceContextPropagator.getInstance(), false}, + {GrpcTraceBinContextPropagator.defaultInstance(), false}, + {GrpcTraceBinContextPropagator.defaultInstance(), true} + }); + } + + public OpenTelemetryContextPropagationTest(TextMapPropagator textMapPropagator, + boolean isCensusClient) { + this.openTelemetrySdk = OpenTelemetrySdk.builder() + .setTracerProvider(SdkTracerProvider.builder().build()) + .setPropagators(ContextPropagators.create(TextMapPropagator.composite( + textMapPropagator + ))) + .build(); + this.tracer = openTelemetrySdk + .getTracer("grpc-java-interop-test"); + GrpcOpenTelemetry.Builder grpcOpentelemetryBuilder = GrpcOpenTelemetry.newBuilder() + .sdk(openTelemetrySdk); + InternalGrpcOpenTelemetry.enableTracing(grpcOpentelemetryBuilder, true); + grpcOpenTelemetry = grpcOpentelemetryBuilder.build(); + this.censusClient = isCensusClient; + } + + @Override + protected ServerBuilder getServerBuilder() { + NettyServerBuilder builder = NettyServerBuilder.forPort(0, InsecureServerCredentials.create()) + .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE); + builder.intercept(new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, ServerCallHandler next) { + ServerCall.Listener listener = next.startCall(call, headers); + return new ForwardingServerCallListener() { + @Override + protected ServerCall.Listener delegate() { + return listener; + } + + @Override + public void onMessage(ReqT request) { + applicationSpan.set(tracer.spanBuilder("InteropTest.Application.Span").startSpan()); + delegate().onMessage(request); + } + + @Override + public void onHalfClose() { + maybeCloseSpan(applicationSpan); + delegate().onHalfClose(); + } + + @Override + public void onCancel() { + maybeCloseSpan(applicationSpan); + delegate().onCancel(); + } + + @Override + public void onComplete() { + maybeCloseSpan(applicationSpan); + delegate().onComplete(); + } + }; + } + }); + // To ensure proper propagation of remote spans from gRPC to your application, this interceptor + // must be after any application interceptors that interact with spans. This allows the tracing + // information to be correctly passed along. However, it's fine for application-level onMessage + // handlers to access the span. + grpcOpenTelemetry.configureServerBuilder(builder); + return builder; + } + + private void maybeCloseSpan(AtomicReference applicationSpan) { + Span tmp = applicationSpan.get(); + if (tmp != null) { + tmp.end(); + } + } + + @Override + protected boolean metricsExpected() { + return false; + } + + @Override + protected ManagedChannelBuilder createChannelBuilder() { + NettyChannelBuilder builder = NettyChannelBuilder.forAddress(getListenAddress()) + .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE) + .usePlaintext(); + if (!censusClient) { + // Disabling census-tracing is necessary to avoid trace ID mismatches. + // This is because census-tracing overrides the grpc-trace-bin header with + // OpenTelemetry's GrpcTraceBinPropagator. + InternalNettyChannelBuilder.setTracingEnabled(builder, false); + grpcOpenTelemetry.configureChannelBuilder(builder); + } + return builder; + } + + @Test + public void otelSpanContextPropagation() { + Assume.assumeFalse(censusClient); + Span parentSpan = tracer.spanBuilder("Test.interopTest").startSpan(); + try (Scope scope = Context.current().with(parentSpan).makeCurrent()) { + blockingStub.unaryCall(SimpleRequest.getDefaultInstance()); + } + assertEquals(parentSpan.getSpanContext().getTraceId(), + applicationSpan.get().getSpanContext().getTraceId()); + } + + @Test + @SuppressWarnings("deprecation") + public void censusToOtelGrpcTraceBinPropagator() { + Assume.assumeTrue(censusClient); + io.opencensus.trace.Tracer censusTracer = io.opencensus.trace.Tracing.getTracer(); + io.opencensus.trace.Span parentSpan = censusTracer.spanBuilder("Test.interopTest") + .startSpan(); + io.grpc.Context context = io.opencensus.trace.unsafe.ContextUtils.withValue( + io.grpc.Context.current(), parentSpan); + io.grpc.Context previous = context.attach(); + try { + blockingStub.unaryCall(SimpleRequest.getDefaultInstance()); + assertEquals(parentSpan.getContext().getTraceId().toLowerBase16(), + applicationSpan.get().getSpanContext().getTraceId()); + } finally { + context.detach(previous); + } + } +} From a908b5e40db86c533a9f3245d14c15fdecf30378 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 27 Sep 2024 07:17:11 -0700 Subject: [PATCH 022/591] android: For UDS, use fake IP instead of localhost This avoids a DNS lookup, which can be slow and fail. Fixes #11442 --- .../src/main/java/io/grpc/android/UdsChannelBuilder.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/io/grpc/android/UdsChannelBuilder.java b/android/src/main/java/io/grpc/android/UdsChannelBuilder.java index e2dc7232378..7d41301704c 100644 --- a/android/src/main/java/io/grpc/android/UdsChannelBuilder.java +++ b/android/src/main/java/io/grpc/android/UdsChannelBuilder.java @@ -68,12 +68,15 @@ public static ManagedChannelBuilder forPath(String path, Namespace namespace) throw new UnsupportedOperationException("OkHttpChannelBuilder not found on the classpath"); } try { - // Target 'dns:///localhost' is unused, but necessary as an argument for OkHttpChannelBuilder. + // Target 'dns:///127.0.0.1' is unused, but necessary as an argument for OkHttpChannelBuilder. + // An IP address is used instead of localhost to avoid a DNS lookup (see #11442). This should + // work even if IPv4 is unavailable, as the DNS resolver doesn't need working IPv4 to parse an + // IPv4 address. Unavailable IPv4 fails when we connect(), not at resolution time. // TLS is unsupported because Conscrypt assumes the platform Socket implementation to improve // performance by using the file descriptor directly. Object o = OKHTTP_CHANNEL_BUILDER_CLASS .getMethod("forTarget", String.class, ChannelCredentials.class) - .invoke(null, "dns:///localhost", InsecureChannelCredentials.create()); + .invoke(null, "dns:///127.0.0.1", InsecureChannelCredentials.create()); ManagedChannelBuilder builder = OKHTTP_CHANNEL_BUILDER_CLASS.cast(o); OKHTTP_CHANNEL_BUILDER_CLASS .getMethod("socketFactory", SocketFactory.class) From 8c3496943c9cd5fdb74e5b6ba964aa9a37b15acf Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sat, 27 Jul 2024 10:53:19 -0700 Subject: [PATCH 023/591] xds: Have ClusterManagerLB use child map for preserving children Instead of doing a dance of supplementing config so the later createChildAddressesMap() won't delete children, just look at the existing children and don't delete any that shouldn't be deleted. --- .../io/grpc/util/MultiChildLoadBalancer.java | 9 ++- .../grpc/xds/ClusterManagerLoadBalancer.java | 76 +++++++------------ 2 files changed, 33 insertions(+), 52 deletions(-) diff --git a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java index 626c2e1104e..b05f4d98c85 100644 --- a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java @@ -79,7 +79,8 @@ protected MultiChildLoadBalancer(Helper helper) { /** * Override to utilize parsing of the policy configuration or alternative helper/lb generation. - * Override this if keys are not Endpoints or if child policies have configuration. + * Override this if keys are not Endpoints or if child policies have configuration. Null map + * values preserve the child without delivering the child an update. */ protected Map createChildAddressesMap( ResolvedAddresses resolvedAddresses) { @@ -181,8 +182,10 @@ private void updateChildrenWithResolvedAddresses( childLbState = createChildLbState(entry.getKey()); childLbStates.put(entry.getKey(), childLbState); } - childLbState.setResolvedAddresses(entry.getValue()); // update child - childLbState.lb.handleResolvedAddresses(entry.getValue()); // update child LB + if (entry.getValue() != null) { + childLbState.setResolvedAddresses(entry.getValue()); // update child + childLbState.lb.handleResolvedAddresses(entry.getValue()); // update child LB + } } } diff --git a/xds/src/main/java/io/grpc/xds/ClusterManagerLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterManagerLoadBalancer.java index c175b847c63..2573a7293d3 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterManagerLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterManagerLoadBalancer.java @@ -77,57 +77,43 @@ protected ChildLbState createChildLbState(Object key) { @Override protected Map createChildAddressesMap( ResolvedAddresses resolvedAddresses) { + lastResolvedAddresses = resolvedAddresses; + ClusterManagerConfig config = (ClusterManagerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); Map childAddresses = new HashMap<>(); - if (config != null) { - for (Map.Entry childPolicy : config.childPolicies.entrySet()) { - ResolvedAddresses addresses = resolvedAddresses.toBuilder() - .setLoadBalancingPolicyConfig(childPolicy.getValue()) - .build(); - childAddresses.put(childPolicy.getKey(), addresses); + + // Reactivate children with config; deactivate children without config + for (ChildLbState rawState : getChildLbStates()) { + ClusterManagerLbState state = (ClusterManagerLbState) rawState; + if (config.childPolicies.containsKey(state.getKey())) { + // Active child + if (state.deletionTimer != null) { + state.reactivateChild(); + } + } else { + // Inactive child + if (state.deletionTimer == null) { + state.deactivateChild(); + } + if (state.deletionTimer.isPending()) { + childAddresses.put(state.getKey(), null); // Preserve child, without config update + } } } + + for (Map.Entry childPolicy : config.childPolicies.entrySet()) { + ResolvedAddresses addresses = resolvedAddresses.toBuilder() + .setLoadBalancingPolicyConfig(childPolicy.getValue()) + .build(); + childAddresses.put(childPolicy.getKey(), addresses); + } logger.log( XdsLogLevel.INFO, - "Received cluster_manager lb config: child names={0}", childAddresses.keySet()); + "Received cluster_manager lb config: child names={0}", config.childPolicies.keySet()); return childAddresses; } - /** - * This is like the parent except that it doesn't shutdown the removed children since we want that - * to be done by the timer. - */ - @Override - public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - if (lastResolvedAddresses != null) { - // Handle deactivated children - ClusterManagerConfig config = (ClusterManagerConfig) - resolvedAddresses.getLoadBalancingPolicyConfig(); - ClusterManagerConfig lastConfig = (ClusterManagerConfig) - lastResolvedAddresses.getLoadBalancingPolicyConfig(); - Map adjChildPolicies = new HashMap<>(config.childPolicies); - for (Entry entry : lastConfig.childPolicies.entrySet()) { - ClusterManagerLbState state = (ClusterManagerLbState) getChildLbState(entry.getKey()); - if (adjChildPolicies.containsKey(entry.getKey())) { - if (state.deletionTimer != null) { - state.reactivateChild(); - } - } else if (state != null) { - adjChildPolicies.put(entry.getKey(), entry.getValue()); - if (state.deletionTimer == null) { - state.deactivateChild(); - } - } - } - config = new ClusterManagerConfig(adjChildPolicies); - resolvedAddresses = - resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(config).build(); - } - lastResolvedAddresses = resolvedAddresses; - return super.acceptResolvedAddresses(resolvedAddresses); - } - /** * Using the state of all children will calculate the current connectivity state, * update currentConnectivityState, generate a picker and then call @@ -232,14 +218,6 @@ class DeletionTask implements Runnable { @Override public void run() { - ClusterManagerConfig config = (ClusterManagerConfig) - lastResolvedAddresses.getLoadBalancingPolicyConfig(); - Map childPolicies = new HashMap<>(config.childPolicies); - Object removed = childPolicies.remove(getKey()); - assert removed != null; - config = new ClusterManagerConfig(childPolicies); - lastResolvedAddresses = - lastResolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(config).build(); acceptResolvedAddresses(lastResolvedAddresses); } } From 795e2cc3ff25666b5c7d8beed985dbd55fc40804 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sat, 31 Aug 2024 17:53:01 -0700 Subject: [PATCH 024/591] util: Simplify MultiChildLB.getChildLbState() Tests were converted to use getChildLbStateEag() if the argument was an EAG, so the instanceof was no longer necessary. --- util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java | 6 ------ .../test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java index b05f4d98c85..330ec9d5357 100644 --- a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java @@ -241,12 +241,6 @@ public final Collection getChildLbStates() { @VisibleForTesting public final ChildLbState getChildLbState(Object key) { - if (key == null) { - return null; - } - if (key instanceof EquivalentAddressGroup) { - key = new Endpoint((EquivalentAddressGroup) key); - } return childLbStates.get(key); } diff --git a/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java index 659bacd3626..64e18465597 100644 --- a/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java @@ -252,7 +252,7 @@ private List getSubchannels(LeastRequestLoadBalancer lb) { private LeastRequestLbState getChildLbState(PickResult pickResult) { EquivalentAddressGroup eag = pickResult.getSubchannel().getAddresses(); - return (LeastRequestLbState) loadBalancer.getChildLbState(eag); + return (LeastRequestLbState) loadBalancer.getChildLbStateEag(eag); } @Test From a140e1bb0cfa662bcdb7823d73320eb8d49046f1 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:49:09 -0700 Subject: [PATCH 025/591] s2a: Combine MtlsToS2ChannelCredentials and S2AChannelCredentials. (#11544) * Combine MtlsToS2ChannelCredentials and S2AChannelCredentials. * Check if file exists. * S2AChannelCredentials API requires credentials used for client-s2a channel. * remove MtlsToS2A library in BUILD. * Don't check state twice. * Don't check for file existence in tests. --- s2a/BUILD.bazel | 13 -- .../grpc/s2a/MtlsToS2AChannelCredentials.java | 89 ------------ .../io/grpc/s2a/S2AChannelCredentials.java | 25 ++-- .../s2a/MtlsToS2AChannelCredentialsTest.java | 135 ------------------ .../grpc/s2a/S2AChannelCredentialsTest.java | 68 ++++++--- .../grpc/s2a/handshaker/IntegrationTest.java | 35 +++-- 6 files changed, 79 insertions(+), 286 deletions(-) delete mode 100644 s2a/src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java delete mode 100644 s2a/src/test/java/io/grpc/s2a/MtlsToS2AChannelCredentialsTest.java diff --git a/s2a/BUILD.bazel b/s2a/BUILD.bazel index 5aeaedbe358..25ce2624f57 100644 --- a/s2a/BUILD.bazel +++ b/s2a/BUILD.bazel @@ -117,19 +117,6 @@ java_library( ], ) -java_library( - name = "mtls_to_s2av2_credentials", - srcs = ["src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java"], - visibility = ["//visibility:public"], - deps = [ - ":s2a_channel_pool", - ":s2av2_credentials", - "//api", - "//util", - artifact("com.google.guava:guava"), - ], -) - # bazel only accepts proto import with absolute path. genrule( name = "protobuf_imports", diff --git a/s2a/src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java b/s2a/src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java deleted file mode 100644 index e8eb01628ed..00000000000 --- a/s2a/src/main/java/io/grpc/s2a/MtlsToS2AChannelCredentials.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2024 The gRPC Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.grpc.s2a; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Strings.isNullOrEmpty; - -import io.grpc.ChannelCredentials; -import io.grpc.ExperimentalApi; -import io.grpc.TlsChannelCredentials; -import java.io.File; -import java.io.IOException; - -/** - * Configures an {@code S2AChannelCredentials.Builder} instance with credentials used to establish a - * connection with the S2A to support talking to the S2A over mTLS. - */ -@ExperimentalApi("https://github.com/grpc/grpc-java/issues/11533") -public final class MtlsToS2AChannelCredentials { - /** - * Creates a {@code S2AChannelCredentials.Builder} builder, that talks to the S2A over mTLS. - * - * @param s2aAddress the address of the S2A server used to secure the connection. - * @param privateKeyPath the path to the private key PEM to use for authenticating to the S2A. - * @param certChainPath the path to the cert chain PEM to use for authenticating to the S2A. - * @param trustBundlePath the path to the trust bundle PEM. - * @return a {@code MtlsToS2AChannelCredentials.Builder} instance. - */ - public static Builder newBuilder( - String s2aAddress, String privateKeyPath, String certChainPath, String trustBundlePath) { - checkArgument(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty."); - checkArgument(!isNullOrEmpty(privateKeyPath), "privateKeyPath must not be null or empty."); - checkArgument(!isNullOrEmpty(certChainPath), "certChainPath must not be null or empty."); - checkArgument(!isNullOrEmpty(trustBundlePath), "trustBundlePath must not be null or empty."); - return new Builder(s2aAddress, privateKeyPath, certChainPath, trustBundlePath); - } - - /** Builds an {@code MtlsToS2AChannelCredentials} instance. */ - public static final class Builder { - private final String s2aAddress; - private final String privateKeyPath; - private final String certChainPath; - private final String trustBundlePath; - - Builder( - String s2aAddress, String privateKeyPath, String certChainPath, String trustBundlePath) { - this.s2aAddress = s2aAddress; - this.privateKeyPath = privateKeyPath; - this.certChainPath = certChainPath; - this.trustBundlePath = trustBundlePath; - } - - public S2AChannelCredentials.Builder build() throws IOException { - checkState(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty."); - checkState(!isNullOrEmpty(privateKeyPath), "privateKeyPath must not be null or empty."); - checkState(!isNullOrEmpty(certChainPath), "certChainPath must not be null or empty."); - checkState(!isNullOrEmpty(trustBundlePath), "trustBundlePath must not be null or empty."); - File privateKeyFile = new File(privateKeyPath); - File certChainFile = new File(certChainPath); - File trustBundleFile = new File(trustBundlePath); - - ChannelCredentials channelToS2ACredentials = - TlsChannelCredentials.newBuilder() - .keyManager(certChainFile, privateKeyFile) - .trustManager(trustBundleFile) - .build(); - - return S2AChannelCredentials.newBuilder(s2aAddress) - .setS2AChannelCredentials(channelToS2ACredentials); - } - } - - private MtlsToS2AChannelCredentials() {} -} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java index ba0f6d72de1..12963a1395c 100644 --- a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java +++ b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java @@ -18,14 +18,12 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Strings.isNullOrEmpty; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.grpc.Channel; import io.grpc.ChannelCredentials; import io.grpc.ExperimentalApi; -import io.grpc.InsecureChannelCredentials; import io.grpc.internal.ObjectPool; import io.grpc.internal.SharedResourcePool; import io.grpc.netty.InternalNettyChannelCredentials; @@ -33,6 +31,7 @@ import io.grpc.s2a.channel.S2AHandshakerServiceChannel; import io.grpc.s2a.handshaker.S2AIdentity; import io.grpc.s2a.handshaker.S2AProtocolNegotiatorFactory; +import java.io.IOException; import javax.annotation.concurrent.NotThreadSafe; import org.checkerframework.checker.nullness.qual.Nullable; @@ -46,25 +45,27 @@ public final class S2AChannelCredentials { * Creates a channel credentials builder for establishing an S2A-secured connection. * * @param s2aAddress the address of the S2A server used to secure the connection. + * @param s2aChannelCredentials the credentials to be used when connecting to the S2A. * @return a {@code S2AChannelCredentials.Builder} instance. */ - public static Builder newBuilder(String s2aAddress) { + public static Builder newBuilder(String s2aAddress, ChannelCredentials s2aChannelCredentials) { checkArgument(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty."); - return new Builder(s2aAddress); + checkNotNull(s2aChannelCredentials, "S2A channel credentials must not be null"); + return new Builder(s2aAddress, s2aChannelCredentials); } /** Builds an {@code S2AChannelCredentials} instance. */ @NotThreadSafe public static final class Builder { private final String s2aAddress; + private final ChannelCredentials s2aChannelCredentials; private ObjectPool s2aChannelPool; - private ChannelCredentials s2aChannelCredentials; private @Nullable S2AIdentity localIdentity = null; - Builder(String s2aAddress) { + Builder(String s2aAddress, ChannelCredentials s2aChannelCredentials) { this.s2aAddress = s2aAddress; + this.s2aChannelCredentials = s2aChannelCredentials; this.s2aChannelPool = null; - this.s2aChannelCredentials = InsecureChannelCredentials.create(); } /** @@ -106,15 +107,7 @@ public Builder setLocalUid(String localUid) { return this; } - /** Sets the credentials to be used when connecting to the S2A. */ - @CanIgnoreReturnValue - public Builder setS2AChannelCredentials(ChannelCredentials s2aChannelCredentials) { - this.s2aChannelCredentials = s2aChannelCredentials; - return this; - } - - public ChannelCredentials build() { - checkState(!isNullOrEmpty(s2aAddress), "S2A address must not be null or empty."); + public ChannelCredentials build() throws IOException { ObjectPool s2aChannelPool = SharedResourcePool.forResource( S2AHandshakerServiceChannel.getChannelResource(s2aAddress, s2aChannelCredentials)); diff --git a/s2a/src/test/java/io/grpc/s2a/MtlsToS2AChannelCredentialsTest.java b/s2a/src/test/java/io/grpc/s2a/MtlsToS2AChannelCredentialsTest.java deleted file mode 100644 index 0fc4ecb3268..00000000000 --- a/s2a/src/test/java/io/grpc/s2a/MtlsToS2AChannelCredentialsTest.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2024 The gRPC Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.grpc.s2a; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public final class MtlsToS2AChannelCredentialsTest { - @Test - public void newBuilder_nullAddress_throwsException() throws Exception { - assertThrows( - IllegalArgumentException.class, - () -> - MtlsToS2AChannelCredentials.newBuilder( - /* s2aAddress= */ null, - /* privateKeyPath= */ "src/test/resources/client_key.pem", - /* certChainPath= */ "src/test/resources/client_cert.pem", - /* trustBundlePath= */ "src/test/resources/root_cert.pem")); - } - - @Test - public void newBuilder_nullPrivateKeyPath_throwsException() throws Exception { - assertThrows( - IllegalArgumentException.class, - () -> - MtlsToS2AChannelCredentials.newBuilder( - /* s2aAddress= */ "s2a_address", - /* privateKeyPath= */ null, - /* certChainPath= */ "src/test/resources/client_cert.pem", - /* trustBundlePath= */ "src/test/resources/root_cert.pem")); - } - - @Test - public void newBuilder_nullCertChainPath_throwsException() throws Exception { - assertThrows( - IllegalArgumentException.class, - () -> - MtlsToS2AChannelCredentials.newBuilder( - /* s2aAddress= */ "s2a_address", - /* privateKeyPath= */ "src/test/resources/client_key.pem", - /* certChainPath= */ null, - /* trustBundlePath= */ "src/test/resources/root_cert.pem")); - } - - @Test - public void newBuilder_nullTrustBundlePath_throwsException() throws Exception { - assertThrows( - IllegalArgumentException.class, - () -> - MtlsToS2AChannelCredentials.newBuilder( - /* s2aAddress= */ "s2a_address", - /* privateKeyPath= */ "src/test/resources/client_key.pem", - /* certChainPath= */ "src/test/resources/client_cert.pem", - /* trustBundlePath= */ null)); - } - - @Test - public void newBuilder_emptyAddress_throwsException() throws Exception { - assertThrows( - IllegalArgumentException.class, - () -> - MtlsToS2AChannelCredentials.newBuilder( - /* s2aAddress= */ "", - /* privateKeyPath= */ "src/test/resources/client_key.pem", - /* certChainPath= */ "src/test/resources/client_cert.pem", - /* trustBundlePath= */ "src/test/resources/root_cert.pem")); - } - - @Test - public void newBuilder_emptyPrivateKeyPath_throwsException() throws Exception { - assertThrows( - IllegalArgumentException.class, - () -> - MtlsToS2AChannelCredentials.newBuilder( - /* s2aAddress= */ "s2a_address", - /* privateKeyPath= */ "", - /* certChainPath= */ "src/test/resources/client_cert.pem", - /* trustBundlePath= */ "src/test/resources/root_cert.pem")); - } - - @Test - public void newBuilder_emptyCertChainPath_throwsException() throws Exception { - assertThrows( - IllegalArgumentException.class, - () -> - MtlsToS2AChannelCredentials.newBuilder( - /* s2aAddress= */ "s2a_address", - /* privateKeyPath= */ "src/test/resources/client_key.pem", - /* certChainPath= */ "", - /* trustBundlePath= */ "src/test/resources/root_cert.pem")); - } - - @Test - public void newBuilder_emptyTrustBundlePath_throwsException() throws Exception { - assertThrows( - IllegalArgumentException.class, - () -> - MtlsToS2AChannelCredentials.newBuilder( - /* s2aAddress= */ "s2a_address", - /* privateKeyPath= */ "src/test/resources/client_key.pem", - /* certChainPath= */ "src/test/resources/client_cert.pem", - /* trustBundlePath= */ "")); - } - - @Test - public void build_s2AChannelCredentials_success() throws Exception { - assertThat( - MtlsToS2AChannelCredentials.newBuilder( - /* s2aAddress= */ "s2a_address", - /* privateKeyPath= */ "src/test/resources/client_key.pem", - /* certChainPath= */ "src/test/resources/client_cert.pem", - /* trustBundlePath= */ "src/test/resources/root_cert.pem") - .build()) - .isInstanceOf(S2AChannelCredentials.Builder.class); - } -} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java b/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java index e766aa3f145..fd5bfd654f3 100644 --- a/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java +++ b/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertThrows; import io.grpc.ChannelCredentials; +import io.grpc.InsecureChannelCredentials; import io.grpc.TlsChannelCredentials; import java.io.File; import org.junit.Test; @@ -30,40 +31,51 @@ @RunWith(JUnit4.class) public final class S2AChannelCredentialsTest { @Test - public void newBuilder_nullArgument_throwsException() throws Exception { - assertThrows(IllegalArgumentException.class, () -> S2AChannelCredentials.newBuilder(null)); + public void newBuilder_nullAddress_throwsException() throws Exception { + assertThrows(IllegalArgumentException.class, () -> S2AChannelCredentials.newBuilder(null, + InsecureChannelCredentials.create())); } @Test public void newBuilder_emptyAddress_throwsException() throws Exception { - assertThrows(IllegalArgumentException.class, () -> S2AChannelCredentials.newBuilder("")); + assertThrows(IllegalArgumentException.class, () -> S2AChannelCredentials.newBuilder("", + InsecureChannelCredentials.create())); + } + + @Test + public void newBuilder_nullChannelCreds_throwsException() throws Exception { + assertThrows(NullPointerException.class, () -> S2AChannelCredentials + .newBuilder("s2a_address", null)); } @Test public void setLocalSpiffeId_nullArgument_throwsException() throws Exception { assertThrows( NullPointerException.class, - () -> S2AChannelCredentials.newBuilder("s2a_address").setLocalSpiffeId(null)); + () -> S2AChannelCredentials.newBuilder("s2a_address", + InsecureChannelCredentials.create()).setLocalSpiffeId(null)); } @Test public void setLocalHostname_nullArgument_throwsException() throws Exception { assertThrows( NullPointerException.class, - () -> S2AChannelCredentials.newBuilder("s2a_address").setLocalHostname(null)); + () -> S2AChannelCredentials.newBuilder("s2a_address", + InsecureChannelCredentials.create()).setLocalHostname(null)); } @Test public void setLocalUid_nullArgument_throwsException() throws Exception { assertThrows( NullPointerException.class, - () -> S2AChannelCredentials.newBuilder("s2a_address").setLocalUid(null)); + () -> S2AChannelCredentials.newBuilder("s2a_address", + InsecureChannelCredentials.create()).setLocalUid(null)); } @Test public void build_withLocalSpiffeId_succeeds() throws Exception { assertThat( - S2AChannelCredentials.newBuilder("s2a_address") + S2AChannelCredentials.newBuilder("s2a_address", InsecureChannelCredentials.create()) .setLocalSpiffeId("spiffe://test") .build()) .isNotNull(); @@ -72,7 +84,7 @@ public void build_withLocalSpiffeId_succeeds() throws Exception { @Test public void build_withLocalHostname_succeeds() throws Exception { assertThat( - S2AChannelCredentials.newBuilder("s2a_address") + S2AChannelCredentials.newBuilder("s2a_address", InsecureChannelCredentials.create()) .setLocalHostname("local_hostname") .build()) .isNotNull(); @@ -80,33 +92,47 @@ public void build_withLocalHostname_succeeds() throws Exception { @Test public void build_withLocalUid_succeeds() throws Exception { - assertThat(S2AChannelCredentials.newBuilder("s2a_address").setLocalUid("local_uid").build()) + assertThat(S2AChannelCredentials.newBuilder("s2a_address", + InsecureChannelCredentials.create()).setLocalUid("local_uid").build()) .isNotNull(); } @Test public void build_withNoLocalIdentity_succeeds() throws Exception { - assertThat(S2AChannelCredentials.newBuilder("s2a_address").build()) + assertThat(S2AChannelCredentials.newBuilder("s2a_address", + InsecureChannelCredentials.create()).build()) .isNotNull(); } - + @Test - public void build_withTlsChannelCredentials_succeeds() throws Exception { + public void build_withUseMtlsToS2ANoLocalIdentity_success() throws Exception { + ChannelCredentials s2aChannelCredentials = getTlsChannelCredentials(); assertThat( - S2AChannelCredentials.newBuilder("s2a_address") - .setLocalSpiffeId("spiffe://test") - .setS2AChannelCredentials(getTlsChannelCredentials()) + S2AChannelCredentials.newBuilder("s2a_address", s2aChannelCredentials) + .build()) + .isNotNull(); + } + + @Test + public void build_withUseMtlsToS2AWithLocalUid_success() throws Exception { + ChannelCredentials s2aChannelCredentials = getTlsChannelCredentials(); + assertThat( + S2AChannelCredentials.newBuilder("s2a_address", s2aChannelCredentials) + .setLocalUid("local_uid") .build()) .isNotNull(); } private static ChannelCredentials getTlsChannelCredentials() throws Exception { - File clientCert = new File("src/test/resources/client_cert.pem"); - File clientKey = new File("src/test/resources/client_key.pem"); - File rootCert = new File("src/test/resources/root_cert.pem"); + String privateKeyPath = "src/test/resources/client_key.pem"; + String certChainPath = "src/test/resources/client_cert.pem"; + String trustBundlePath = "src/test/resources/root_cert.pem"; + File privateKeyFile = new File(privateKeyPath); + File certChainFile = new File(certChainPath); + File trustBundleFile = new File(trustBundlePath); return TlsChannelCredentials.newBuilder() - .keyManager(clientCert, clientKey) - .trustManager(rootCert) - .build(); + .keyManager(certChainFile, privateKeyFile) + .trustManager(trustBundleFile) + .build(); } } \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java b/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java index 2a2b1f246ec..0fb9b12cf95 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java +++ b/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java @@ -21,15 +21,16 @@ import io.grpc.ChannelCredentials; import io.grpc.Grpc; +import io.grpc.InsecureChannelCredentials; import io.grpc.ManagedChannel; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.ServerCredentials; +import io.grpc.TlsChannelCredentials; import io.grpc.TlsServerCredentials; import io.grpc.benchmarks.Utils; import io.grpc.netty.GrpcSslContexts; import io.grpc.netty.NettyServerBuilder; -import io.grpc.s2a.MtlsToS2AChannelCredentials; import io.grpc.s2a.S2AChannelCredentials; import io.grpc.s2a.handshaker.FakeS2AServer; import io.grpc.stub.StreamObserver; @@ -124,7 +125,8 @@ public void tearDown() throws Exception { @Test public void clientCommunicateUsingS2ACredentials_succeeds() throws Exception { ChannelCredentials credentials = - S2AChannelCredentials.newBuilder(s2aAddress).setLocalSpiffeId("test-spiffe-id").build(); + S2AChannelCredentials.newBuilder(s2aAddress, InsecureChannelCredentials.create()) + .setLocalSpiffeId("test-spiffe-id").build(); ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, credentials).build(); assertThat(doUnaryRpc(channel)).isTrue(); @@ -132,7 +134,8 @@ public void clientCommunicateUsingS2ACredentials_succeeds() throws Exception { @Test public void clientCommunicateUsingS2ACredentialsNoLocalIdentity_succeeds() throws Exception { - ChannelCredentials credentials = S2AChannelCredentials.newBuilder(s2aAddress).build(); + ChannelCredentials credentials = S2AChannelCredentials.newBuilder(s2aAddress, + InsecureChannelCredentials.create()).build(); ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, credentials).build(); assertThat(doUnaryRpc(channel)).isTrue(); @@ -140,15 +143,22 @@ public void clientCommunicateUsingS2ACredentialsNoLocalIdentity_succeeds() throw @Test public void clientCommunicateUsingMtlsToS2ACredentials_succeeds() throws Exception { + String privateKeyPath = "src/test/resources/client_key.pem"; + String certChainPath = "src/test/resources/client_cert.pem"; + String trustBundlePath = "src/test/resources/root_cert.pem"; + File privateKeyFile = new File(privateKeyPath); + File certChainFile = new File(certChainPath); + File trustBundleFile = new File(trustBundlePath); + ChannelCredentials s2aChannelCredentials = + TlsChannelCredentials.newBuilder() + .keyManager(certChainFile, privateKeyFile) + .trustManager(trustBundleFile) + .build(); + ChannelCredentials credentials = - MtlsToS2AChannelCredentials.newBuilder( - /* s2aAddress= */ mtlsS2AAddress, - /* privateKeyPath= */ "src/test/resources/client_key.pem", - /* certChainPath= */ "src/test/resources/client_cert.pem", - /* trustBundlePath= */ "src/test/resources/root_cert.pem") - .build() - .setLocalSpiffeId("test-spiffe-id") - .build(); + S2AChannelCredentials.newBuilder(mtlsS2AAddress, s2aChannelCredentials) + .setLocalSpiffeId("test-spiffe-id") + .build(); ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, credentials).build(); assertThat(doUnaryRpc(channel)).isTrue(); @@ -156,7 +166,8 @@ public void clientCommunicateUsingMtlsToS2ACredentials_succeeds() throws Excepti @Test public void clientCommunicateUsingS2ACredentials_s2AdelayStart_succeeds() throws Exception { - ChannelCredentials credentials = S2AChannelCredentials.newBuilder(s2aDelayAddress).build(); + ChannelCredentials credentials = S2AChannelCredentials.newBuilder(s2aDelayAddress, + InsecureChannelCredentials.create()).build(); ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, credentials).build(); FutureTask rpc = new FutureTask<>(() -> doUnaryRpc(channel)); From 7b4b10930993df70916c6983c56c70a0e71a266f Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:55:42 -0700 Subject: [PATCH 026/591] s2a: remove channelPool from S2AChannelCredentials builder. (#11573) --- .../java/io/grpc/s2a/S2AChannelCredentials.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java index 12963a1395c..7beecdb3621 100644 --- a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java +++ b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java @@ -31,7 +31,6 @@ import io.grpc.s2a.channel.S2AHandshakerServiceChannel; import io.grpc.s2a.handshaker.S2AIdentity; import io.grpc.s2a.handshaker.S2AProtocolNegotiatorFactory; -import java.io.IOException; import javax.annotation.concurrent.NotThreadSafe; import org.checkerframework.checker.nullness.qual.Nullable; @@ -59,13 +58,11 @@ public static Builder newBuilder(String s2aAddress, ChannelCredentials s2aChanne public static final class Builder { private final String s2aAddress; private final ChannelCredentials s2aChannelCredentials; - private ObjectPool s2aChannelPool; private @Nullable S2AIdentity localIdentity = null; Builder(String s2aAddress, ChannelCredentials s2aChannelCredentials) { this.s2aAddress = s2aAddress; this.s2aChannelCredentials = s2aChannelCredentials; - this.s2aChannelPool = null; } /** @@ -107,16 +104,15 @@ public Builder setLocalUid(String localUid) { return this; } - public ChannelCredentials build() throws IOException { - ObjectPool s2aChannelPool = - SharedResourcePool.forResource( - S2AHandshakerServiceChannel.getChannelResource(s2aAddress, s2aChannelCredentials)); - checkNotNull(s2aChannelPool, "s2aChannelPool"); - this.s2aChannelPool = s2aChannelPool; + public ChannelCredentials build() { return InternalNettyChannelCredentials.create(buildProtocolNegotiatorFactory()); } InternalProtocolNegotiator.ClientFactory buildProtocolNegotiatorFactory() { + ObjectPool s2aChannelPool = + SharedResourcePool.forResource( + S2AHandshakerServiceChannel.getChannelResource(s2aAddress, s2aChannelCredentials)); + checkNotNull(s2aChannelPool, "s2aChannelPool"); return S2AProtocolNegotiatorFactory.createClientFactory(localIdentity, s2aChannelPool); } } From 50e442fea6bbb84dfb61c8606d05734c4b0866a7 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:05:14 -0700 Subject: [PATCH 027/591] s2a: Include full exception in IOException --- s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java index bf9b866ef93..ea4fb033a13 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java +++ b/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java @@ -102,8 +102,7 @@ public SessionResp send(SessionReq req) throws IOException, InterruptedException if (exception != null) { throw new IOException( "Received an unexpected response from a host at the S2A's address. The S2A might be" - + " unavailable." - + exception.getMessage()); + + " unavailable.", exception); } else { throw new IOException("Received an unexpected response from a host at the S2A's address."); } From fa26a8bc5e8f5f465e1d55de922be1b6b936f47f Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 1 Oct 2024 11:21:08 +0530 Subject: [PATCH 028/591] Make address resolution error use the default service config (#11577) Fixes #11040. --- .../io/grpc/internal/ManagedChannelImpl.java | 10 +++- .../grpc/internal/ManagedChannelImplTest.java | 53 ++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 07dcf9ee7bb..7e36086ac94 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -956,7 +956,15 @@ void updateConfigSelector(@Nullable InternalConfigSelector config) { // Must run in SynchronizationContext. void onConfigError() { if (configSelector.get() == INITIAL_PENDING_SELECTOR) { - updateConfigSelector(null); + // Apply Default Service Config if initial name resolution fails. + if (defaultServiceConfig != null) { + updateConfigSelector(defaultServiceConfig.getDefaultConfigSelector()); + lastServiceConfig = defaultServiceConfig; + channelLogger.log(ChannelLogLevel.ERROR, + "Initial Name Resolution error, using default service config"); + } else { + updateConfigSelector(null); + } } } diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 4d42056b689..bea14bcef47 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -84,6 +84,7 @@ import io.grpc.InternalChannelz; import io.grpc.InternalChannelz.ChannelStats; import io.grpc.InternalChannelz.ChannelTrace; +import io.grpc.InternalChannelz.ChannelTrace.Event.Severity; import io.grpc.InternalConfigSelector; import io.grpc.InternalInstrumented; import io.grpc.LoadBalancer; @@ -123,6 +124,7 @@ import io.grpc.internal.ManagedChannelImplBuilder.ClientTransportFactoryBuilder; import io.grpc.internal.ManagedChannelImplBuilder.FixedPortProvider; import io.grpc.internal.ManagedChannelImplBuilder.UnsupportedClientTransportFactoryBuilder; +import io.grpc.internal.ManagedChannelServiceConfig.MethodInfo; import io.grpc.internal.ServiceConfigUtil.PolicySelection; import io.grpc.internal.TestUtils.MockClientTransportInfo; import io.grpc.stub.ClientCalls; @@ -1127,6 +1129,55 @@ public void noMoreCallbackAfterLoadBalancerShutdown_configError() throws Interru verifyNoMoreInteractions(mockLoadBalancer); } + @Test + public void addressResolutionError_noPriorNameResolution_usesDefaultServiceConfig() + throws Exception { + Map rawServiceConfig = + parseConfig("{\"methodConfig\":[{" + + "\"name\":[{\"service\":\"service\"}]," + + "\"waitForReady\":true}]}"); + ManagedChannelServiceConfig managedChannelServiceConfig = + createManagedChannelServiceConfig(rawServiceConfig, null); + FakeNameResolverFactory nameResolverFactory = + new FakeNameResolverFactory.Builder(expectedUri) + .setServers(Collections.singletonList(new EquivalentAddressGroup(socketAddress))) + .setResolvedAtStart(false) + .build(); + nameResolverFactory.nextConfigOrError.set( + ConfigOrError.fromConfig(managedChannelServiceConfig)); + channelBuilder.nameResolverFactory(nameResolverFactory); + Map defaultServiceConfig = + parseConfig("{\"methodConfig\":[{" + + "\"name\":[{\"service\":\"service\"}]," + + "\"waitForReady\":true}]}"); + channelBuilder.defaultServiceConfig(defaultServiceConfig); + Status resolutionError = Status.UNAVAILABLE.withDescription("Resolution failed"); + channelBuilder.maxTraceEvents(10); + createChannel(); + FakeNameResolverFactory.FakeNameResolver resolver = nameResolverFactory.resolvers.get(0); + + resolver.listener.onError(resolutionError); + + InternalConfigSelector configSelector = channel.getConfigSelector(); + ManagedChannelServiceConfig config = + (ManagedChannelServiceConfig) configSelector.selectConfig(null).getConfig(); + MethodInfo methodConfig = config.getMethodConfig(method); + assertThat(methodConfig.waitForReady).isTrue(); + timer.forwardNanos(1234); + assertThat(getStats(channel).channelTrace.events).contains(new ChannelTrace.Event.Builder() + .setDescription("Initial Name Resolution error, using default service config") + .setSeverity(Severity.CT_ERROR) + .setTimestampNanos(0) + .build()); + + // Check that "lastServiceConfig" variable has been set above: a config resolution with the same + // config simply gets ignored and not gets reassigned. + resolver.resolved(); + timer.forwardNanos(1234); + assertThat(getStats(channel).channelTrace.events.stream().filter( + event -> event.description.equals("Service config changed")).count()).isEqualTo(0); + } + @Test public void interceptor() throws Exception { final AtomicLong atomic = new AtomicLong(); @@ -4595,7 +4646,7 @@ public void notUseDefaultImmediatelyIfEnableLookUp() throws Exception { int size = getStats(channel).channelTrace.events.size(); assertThat(getStats(channel).channelTrace.events.get(size - 1)) .isNotEqualTo(new ChannelTrace.Event.Builder() - .setDescription("Using default service config") + .setDescription("timer.forwardNanos(1234);") .setSeverity(ChannelTrace.Event.Severity.CT_INFO) .setTimestampNanos(timer.getTicker().read()) .build()); From f9ff5268857410a1e49ec4e603fcd529caf4e628 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 1 Oct 2024 20:39:24 +0530 Subject: [PATCH 029/591] Update README etc to reference 1.67.1 --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index cb38ad66394..f56aebfb3ac 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.66.0/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.66.0/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.67.1/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.67.1/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.66.0 + 1.67.1 runtime io.grpc grpc-protobuf - 1.66.0 + 1.67.1 io.grpc grpc-stub - 1.66.0 + 1.67.1 org.apache.tomcat @@ -79,18 +79,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.66.0' -implementation 'io.grpc:grpc-protobuf:1.66.0' -implementation 'io.grpc:grpc-stub:1.66.0' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.67.1' +implementation 'io.grpc:grpc-protobuf:1.67.1' +implementation 'io.grpc:grpc-stub:1.67.1' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.66.0' -implementation 'io.grpc:grpc-protobuf-lite:1.66.0' -implementation 'io.grpc:grpc-stub:1.66.0' +implementation 'io.grpc:grpc-okhttp:1.67.1' +implementation 'io.grpc:grpc-protobuf-lite:1.67.1' +implementation 'io.grpc:grpc-stub:1.67.1' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` @@ -99,7 +99,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.66.0 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.67.1 Development snapshots are available in [Sonatypes's snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/). @@ -131,7 +131,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use com.google.protobuf:protoc:3.25.3:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.66.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.67.1:exe:${os.detected.classifier} @@ -161,7 +161,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.66.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.67.1' } } generateProtoTasks { @@ -194,7 +194,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.66.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.67.1' } } generateProtoTasks { From 927d21541dbc34be245f09c8180358a94ae5c031 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:24:18 -0700 Subject: [PATCH 030/591] s2a: Move s2a implementation to internal package --- s2a/BUILD.bazel | 28 +++++++------- .../handshaker/S2AServiceGrpc.java | 38 +++++++++---------- .../io/grpc/s2a/S2AChannelCredentials.java | 6 +-- .../channel/S2AChannelPool.java | 2 +- .../channel/S2AGrpcChannelPool.java | 2 +- .../channel/S2AHandshakerServiceChannel.java | 2 +- .../handshaker/ConnectionClosedException.java | 2 +- .../GetAuthenticationMechanisms.java | 6 +-- .../{ => internal}/handshaker/ProtoUtil.java | 2 +- .../handshaker/S2AConnectionException.java | 2 +- .../handshaker/S2AIdentity.java | 2 +- .../handshaker/S2APrivateKeyMethod.java | 4 +- .../S2AProtocolNegotiatorFactory.java | 8 ++-- .../{ => internal}/handshaker/S2AStub.java | 2 +- .../handshaker/S2ATrustManager.java | 6 +-- .../handshaker/SslContextFactory.java | 4 +- .../tokenmanager/AccessTokenManager.java | 6 +-- .../tokenmanager/SingleTokenFetcher.java | 4 +- .../handshaker/tokenmanager/TokenFetcher.java | 4 +- s2a/src/main/proto/grpc/gcp/s2a/common.proto | 2 +- s2a/src/main/proto/grpc/gcp/s2a/s2a.proto | 2 +- .../main/proto/grpc/gcp/s2a/s2a_context.proto | 2 +- .../channel/S2AGrpcChannelPoolTest.java | 2 +- .../S2AHandshakerServiceChannelTest.java | 4 +- .../handshaker/FakeS2AServer.java | 2 +- .../handshaker/FakeS2AServerTest.java | 4 +- .../{ => internal}/handshaker/FakeWriter.java | 6 +-- .../GetAuthenticationMechanismsTest.java | 6 +-- .../handshaker/IntegrationTest.java | 4 +- .../handshaker/ProtoUtilTest.java | 2 +- .../handshaker/S2APrivateKeyMethodTest.java | 4 +- .../S2AProtocolNegotiatorFactoryTest.java | 12 +++--- .../handshaker/S2AStubTest.java | 8 ++-- .../handshaker/S2ATrustManagerTest.java | 4 +- .../handshaker/SslContextFactoryTest.java | 4 +- .../SingleTokenAccessTokenManagerTest.java | 4 +- 36 files changed, 101 insertions(+), 101 deletions(-) rename s2a/src/generated/main/grpc/io/grpc/s2a/{ => internal}/handshaker/S2AServiceGrpc.java (84%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/channel/S2AChannelPool.java (97%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/channel/S2AGrpcChannelPool.java (98%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/channel/S2AHandshakerServiceChannel.java (99%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/ConnectionClosedException.java (95%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/GetAuthenticationMechanisms.java (92%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/ProtoUtil.java (98%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/S2AConnectionException.java (95%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/S2AIdentity.java (98%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/S2APrivateKeyMethod.java (98%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/S2AProtocolNegotiatorFactory.java (97%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/S2AStub.java (99%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/S2ATrustManager.java (97%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/SslContextFactory.java (98%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/tokenmanager/AccessTokenManager.java (90%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/tokenmanager/SingleTokenFetcher.java (94%) rename s2a/src/main/java/io/grpc/s2a/{ => internal}/handshaker/tokenmanager/TokenFetcher.java (89%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/channel/S2AGrpcChannelPoolTest.java (99%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/channel/S2AHandshakerServiceChannelTest.java (99%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/handshaker/FakeS2AServer.java (97%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/handshaker/FakeS2AServerTest.java (98%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/handshaker/FakeWriter.java (98%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/handshaker/GetAuthenticationMechanismsTest.java (92%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/handshaker/IntegrationTest.java (98%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/handshaker/ProtoUtilTest.java (98%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/handshaker/S2APrivateKeyMethodTest.java (99%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/handshaker/S2AProtocolNegotiatorFactoryTest.java (96%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/handshaker/S2AStubTest.java (97%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/handshaker/S2ATrustManagerTest.java (99%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/handshaker/SslContextFactoryTest.java (98%) rename s2a/src/test/java/io/grpc/s2a/{ => internal}/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java (95%) diff --git a/s2a/BUILD.bazel b/s2a/BUILD.bazel index 25ce2624f57..676215e1e1e 100644 --- a/s2a/BUILD.bazel +++ b/s2a/BUILD.bazel @@ -5,7 +5,7 @@ load("@rules_jvm_external//:defs.bzl", "artifact") java_library( name = "s2a_channel_pool", srcs = glob([ - "src/main/java/io/grpc/s2a/channel/*.java", + "src/main/java/io/grpc/s2a/internal/channel/*.java", ]), deps = [ "//api", @@ -23,7 +23,7 @@ java_library( java_library( name = "s2a_identity", - srcs = ["src/main/java/io/grpc/s2a/handshaker/S2AIdentity.java"], + srcs = ["src/main/java/io/grpc/s2a/internal/handshaker/S2AIdentity.java"], deps = [ ":common_java_proto", artifact("com.google.errorprone:error_prone_annotations"), @@ -33,7 +33,7 @@ java_library( java_library( name = "token_fetcher", - srcs = ["src/main/java/io/grpc/s2a/handshaker/tokenmanager/TokenFetcher.java"], + srcs = ["src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/TokenFetcher.java"], deps = [ ":s2a_identity", ], @@ -42,7 +42,7 @@ java_library( java_library( name = "access_token_manager", srcs = [ - "src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java", + "src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/AccessTokenManager.java", ], deps = [ ":s2a_identity", @@ -54,7 +54,7 @@ java_library( java_library( name = "single_token_fetcher", srcs = [ - "src/main/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenFetcher.java", + "src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenFetcher.java", ], deps = [ ":s2a_identity", @@ -66,15 +66,15 @@ java_library( java_library( name = "s2a_handshaker", srcs = [ - "src/main/java/io/grpc/s2a/handshaker/ConnectionClosedException.java", - "src/main/java/io/grpc/s2a/handshaker/GetAuthenticationMechanisms.java", - "src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java", - "src/main/java/io/grpc/s2a/handshaker/S2AConnectionException.java", - "src/main/java/io/grpc/s2a/handshaker/S2APrivateKeyMethod.java", - "src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java", - "src/main/java/io/grpc/s2a/handshaker/S2AStub.java", - "src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java", - "src/main/java/io/grpc/s2a/handshaker/SslContextFactory.java", + "src/main/java/io/grpc/s2a/internal/handshaker/ConnectionClosedException.java", + "src/main/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanisms.java", + "src/main/java/io/grpc/s2a/internal/handshaker/ProtoUtil.java", + "src/main/java/io/grpc/s2a/internal/handshaker/S2AConnectionException.java", + "src/main/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethod.java", + "src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java", + "src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java", + "src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java", + "src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java", ], deps = [ ":access_token_manager", diff --git a/s2a/src/generated/main/grpc/io/grpc/s2a/handshaker/S2AServiceGrpc.java b/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java similarity index 84% rename from s2a/src/generated/main/grpc/io/grpc/s2a/handshaker/S2AServiceGrpc.java rename to s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java index b365954b189..d759128a4c5 100644 --- a/s2a/src/generated/main/grpc/io/grpc/s2a/handshaker/S2AServiceGrpc.java +++ b/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java @@ -1,4 +1,4 @@ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static io.grpc.MethodDescriptor.generateFullMethodName; @@ -15,29 +15,29 @@ private S2AServiceGrpc() {} public static final java.lang.String SERVICE_NAME = "grpc.gcp.s2a.S2AService"; // Static method descriptors that strictly reflect the proto. - private static volatile io.grpc.MethodDescriptor getSetUpSessionMethod; + private static volatile io.grpc.MethodDescriptor getSetUpSessionMethod; @io.grpc.stub.annotations.RpcMethod( fullMethodName = SERVICE_NAME + '/' + "SetUpSession", - requestType = io.grpc.s2a.handshaker.SessionReq.class, - responseType = io.grpc.s2a.handshaker.SessionResp.class, + requestType = io.grpc.s2a.internal.handshaker.SessionReq.class, + responseType = io.grpc.s2a.internal.handshaker.SessionResp.class, methodType = io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) - public static io.grpc.MethodDescriptor getSetUpSessionMethod() { - io.grpc.MethodDescriptor getSetUpSessionMethod; + public static io.grpc.MethodDescriptor getSetUpSessionMethod() { + io.grpc.MethodDescriptor getSetUpSessionMethod; if ((getSetUpSessionMethod = S2AServiceGrpc.getSetUpSessionMethod) == null) { synchronized (S2AServiceGrpc.class) { if ((getSetUpSessionMethod = S2AServiceGrpc.getSetUpSessionMethod) == null) { S2AServiceGrpc.getSetUpSessionMethod = getSetUpSessionMethod = - io.grpc.MethodDescriptor.newBuilder() + io.grpc.MethodDescriptor.newBuilder() .setType(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) .setFullMethodName(generateFullMethodName(SERVICE_NAME, "SetUpSession")) .setSampledToLocalTracing(true) .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( - io.grpc.s2a.handshaker.SessionReq.getDefaultInstance())) + io.grpc.s2a.internal.handshaker.SessionReq.getDefaultInstance())) .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( - io.grpc.s2a.handshaker.SessionResp.getDefaultInstance())) + io.grpc.s2a.internal.handshaker.SessionResp.getDefaultInstance())) .setSchemaDescriptor(new S2AServiceMethodDescriptorSupplier("SetUpSession")) .build(); } @@ -100,8 +100,8 @@ public interface AsyncService { * operations from the TLS handshake. * */ - default io.grpc.stub.StreamObserver setUpSession( - io.grpc.stub.StreamObserver responseObserver) { + default io.grpc.stub.StreamObserver setUpSession( + io.grpc.stub.StreamObserver responseObserver) { return io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall(getSetUpSessionMethod(), responseObserver); } } @@ -139,8 +139,8 @@ protected S2AServiceStub build( * operations from the TLS handshake. * */ - public io.grpc.stub.StreamObserver setUpSession( - io.grpc.stub.StreamObserver responseObserver) { + public io.grpc.stub.StreamObserver setUpSession( + io.grpc.stub.StreamObserver responseObserver) { return io.grpc.stub.ClientCalls.asyncBidiStreamingCall( getChannel().newCall(getSetUpSessionMethod(), getCallOptions()), responseObserver); } @@ -211,7 +211,7 @@ public io.grpc.stub.StreamObserver invoke( switch (methodId) { case METHODID_SET_UP_SESSION: return (io.grpc.stub.StreamObserver) serviceImpl.setUpSession( - (io.grpc.stub.StreamObserver) responseObserver); + (io.grpc.stub.StreamObserver) responseObserver); default: throw new AssertionError(); } @@ -224,8 +224,8 @@ public static final io.grpc.ServerServiceDefinition bindService(AsyncService ser getSetUpSessionMethod(), io.grpc.stub.ServerCalls.asyncBidiStreamingCall( new MethodHandlers< - io.grpc.s2a.handshaker.SessionReq, - io.grpc.s2a.handshaker.SessionResp>( + io.grpc.s2a.internal.handshaker.SessionReq, + io.grpc.s2a.internal.handshaker.SessionResp>( service, METHODID_SET_UP_SESSION))) .build(); } @@ -236,7 +236,7 @@ private static abstract class S2AServiceBaseDescriptorSupplier @java.lang.Override public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() { - return io.grpc.s2a.handshaker.S2AProto.getDescriptor(); + return io.grpc.s2a.internal.handshaker.S2AProto.getDescriptor(); } @java.lang.Override diff --git a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java index 7beecdb3621..2e040964dfa 100644 --- a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java +++ b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java @@ -28,9 +28,9 @@ import io.grpc.internal.SharedResourcePool; import io.grpc.netty.InternalNettyChannelCredentials; import io.grpc.netty.InternalProtocolNegotiator; -import io.grpc.s2a.channel.S2AHandshakerServiceChannel; -import io.grpc.s2a.handshaker.S2AIdentity; -import io.grpc.s2a.handshaker.S2AProtocolNegotiatorFactory; +import io.grpc.s2a.internal.channel.S2AHandshakerServiceChannel; +import io.grpc.s2a.internal.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.S2AProtocolNegotiatorFactory; import javax.annotation.concurrent.NotThreadSafe; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/s2a/src/main/java/io/grpc/s2a/channel/S2AChannelPool.java b/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AChannelPool.java similarity index 97% rename from s2a/src/main/java/io/grpc/s2a/channel/S2AChannelPool.java rename to s2a/src/main/java/io/grpc/s2a/internal/channel/S2AChannelPool.java index e5caf5e69bd..aaaa0fffd53 100644 --- a/s2a/src/main/java/io/grpc/s2a/channel/S2AChannelPool.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AChannelPool.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.channel; +package io.grpc.s2a.internal.channel; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.grpc.Channel; diff --git a/s2a/src/main/java/io/grpc/s2a/channel/S2AGrpcChannelPool.java b/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPool.java similarity index 98% rename from s2a/src/main/java/io/grpc/s2a/channel/S2AGrpcChannelPool.java rename to s2a/src/main/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPool.java index 4794cd9ee49..af911185e6c 100644 --- a/s2a/src/main/java/io/grpc/s2a/channel/S2AGrpcChannelPool.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPool.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.channel; +package io.grpc.s2a.internal.channel; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; diff --git a/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java b/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannel.java similarity index 99% rename from s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java rename to s2a/src/main/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannel.java index 9d6950ce041..7a2b3e70672 100644 --- a/s2a/src/main/java/io/grpc/s2a/channel/S2AHandshakerServiceChannel.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannel.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.channel; +package io.grpc.s2a.internal.channel; import static com.google.common.base.Preconditions.checkNotNull; import static java.util.concurrent.TimeUnit.SECONDS; diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/ConnectionClosedException.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/ConnectionClosedException.java similarity index 95% rename from s2a/src/main/java/io/grpc/s2a/handshaker/ConnectionClosedException.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/ConnectionClosedException.java index 1a7f86bda91..d6f1aa70f7c 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/ConnectionClosedException.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/ConnectionClosedException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import java.io.IOException; diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/GetAuthenticationMechanisms.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanisms.java similarity index 92% rename from s2a/src/main/java/io/grpc/s2a/handshaker/GetAuthenticationMechanisms.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanisms.java index 56d74a9b766..2d089183f91 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/GetAuthenticationMechanisms.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanisms.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import com.google.errorprone.annotations.Immutable; -import io.grpc.s2a.handshaker.S2AIdentity; -import io.grpc.s2a.handshaker.tokenmanager.AccessTokenManager; +import io.grpc.s2a.internal.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.tokenmanager.AccessTokenManager; import java.util.Optional; /** Retrieves the authentication mechanism for a given local identity. */ diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/ProtoUtil.java similarity index 98% rename from s2a/src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/ProtoUtil.java index 129cc2d60f1..1d88d5a2b55 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/ProtoUtil.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/ProtoUtil.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AConnectionException.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AConnectionException.java similarity index 95% rename from s2a/src/main/java/io/grpc/s2a/handshaker/S2AConnectionException.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AConnectionException.java index d976308ad22..9b6c244751b 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AConnectionException.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AConnectionException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; /** Exception that denotes a runtime error that was encountered when talking to the S2A server. */ @SuppressWarnings("serial") // This class is never serialized. diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AIdentity.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AIdentity.java similarity index 98% rename from s2a/src/main/java/io/grpc/s2a/handshaker/S2AIdentity.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AIdentity.java index c4fed7377ac..0b691248e91 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AIdentity.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AIdentity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.base.Preconditions.checkNotNull; diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2APrivateKeyMethod.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethod.java similarity index 98% rename from s2a/src/main/java/io/grpc/s2a/handshaker/S2APrivateKeyMethod.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethod.java index fb6d5761355..c7262f70ef7 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/S2APrivateKeyMethod.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethod.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @@ -22,7 +22,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.protobuf.ByteString; -import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.netty.handler.ssl.OpenSslPrivateKeyMethod; import java.io.IOException; import java.util.Optional; diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java similarity index 97% rename from s2a/src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java index 14bdc05238d..cb02d49ce9e 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @@ -37,9 +37,9 @@ import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; import io.grpc.netty.InternalProtocolNegotiators; import io.grpc.netty.InternalProtocolNegotiators.ProtocolNegotiationHandler; -import io.grpc.s2a.channel.S2AChannelPool; -import io.grpc.s2a.channel.S2AGrpcChannelPool; -import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.internal.channel.S2AChannelPool; +import io.grpc.s2a.internal.channel.S2AGrpcChannelPool; +import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java similarity index 99% rename from s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java index ea4fb033a13..0bfa3b4dac2 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/S2AStub.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java similarity index 97% rename from s2a/src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java index aafbb94c047..2f7e5750f88 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/S2ATrustManager.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; -import io.grpc.s2a.handshaker.S2AIdentity; -import io.grpc.s2a.handshaker.ValidatePeerCertificateChainReq.VerificationMode; +import io.grpc.s2a.internal.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.ValidatePeerCertificateChainReq.VerificationMode; import java.io.IOException; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/SslContextFactory.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java similarity index 98% rename from s2a/src/main/java/io/grpc/s2a/handshaker/SslContextFactory.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java index 1ac5887ebc4..72ace2c7885 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/SslContextFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.ImmutableSet; import io.grpc.netty.GrpcSslContexts; -import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.netty.handler.ssl.OpenSslContextOption; import io.netty.handler.ssl.OpenSslSessionContext; import io.netty.handler.ssl.OpenSslX509KeyManagerFactory; diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/AccessTokenManager.java similarity index 90% rename from s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/AccessTokenManager.java index da75cf0d4dd..71e55b29fcd 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/AccessTokenManager.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/AccessTokenManager.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker.tokenmanager; +package io.grpc.s2a.internal.handshaker.tokenmanager; -import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.S2AIdentity; import java.lang.reflect.Method; import java.util.Optional; import javax.annotation.concurrent.ThreadSafe; @@ -31,7 +31,7 @@ public static Optional create() { Optional tokenFetcher; try { Class singleTokenFetcherClass = - Class.forName("io.grpc.s2a.handshaker.tokenmanager.SingleTokenFetcher"); + Class.forName("io.grpc.s2a.internal.handshaker.tokenmanager.SingleTokenFetcher"); Method createTokenFetcher = singleTokenFetcherClass.getMethod("create"); tokenFetcher = (Optional) createTokenFetcher.invoke(null); } catch (ClassNotFoundException e) { diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenFetcher.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenFetcher.java similarity index 94% rename from s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenFetcher.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenFetcher.java index c3dffd2b715..a5402af9db2 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenFetcher.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenFetcher.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker.tokenmanager; +package io.grpc.s2a.internal.handshaker.tokenmanager; import com.google.common.annotations.VisibleForTesting; -import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.S2AIdentity; import java.util.Optional; /** Fetches a single access token via an environment variable. */ diff --git a/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/TokenFetcher.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/TokenFetcher.java similarity index 89% rename from s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/TokenFetcher.java rename to s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/TokenFetcher.java index 9eeddaad844..6827f095afe 100644 --- a/s2a/src/main/java/io/grpc/s2a/handshaker/tokenmanager/TokenFetcher.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/TokenFetcher.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker.tokenmanager; +package io.grpc.s2a.internal.handshaker.tokenmanager; -import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.S2AIdentity; /** Fetches tokens used to authenticate to S2A. */ interface TokenFetcher { diff --git a/s2a/src/main/proto/grpc/gcp/s2a/common.proto b/s2a/src/main/proto/grpc/gcp/s2a/common.proto index 749739553dd..1b999234669 100644 --- a/s2a/src/main/proto/grpc/gcp/s2a/common.proto +++ b/s2a/src/main/proto/grpc/gcp/s2a/common.proto @@ -21,7 +21,7 @@ package grpc.gcp.s2a; option java_multiple_files = true; option java_outer_classname = "CommonProto"; -option java_package = "io.grpc.s2a.handshaker"; +option java_package = "io.grpc.s2a.internal.handshaker"; // The TLS 1.0-1.2 ciphersuites that the application can negotiate when using // S2A. diff --git a/s2a/src/main/proto/grpc/gcp/s2a/s2a.proto b/s2a/src/main/proto/grpc/gcp/s2a/s2a.proto index 8a85e348c24..b3f153943db 100644 --- a/s2a/src/main/proto/grpc/gcp/s2a/s2a.proto +++ b/s2a/src/main/proto/grpc/gcp/s2a/s2a.proto @@ -24,7 +24,7 @@ import "grpc/gcp/s2a/s2a_context.proto"; option java_multiple_files = true; option java_outer_classname = "S2AProto"; -option java_package = "io.grpc.s2a.handshaker"; +option java_package = "io.grpc.s2a.internal.handshaker"; enum SignatureAlgorithm { S2A_SSL_SIGN_UNSPECIFIED = 0; diff --git a/s2a/src/main/proto/grpc/gcp/s2a/s2a_context.proto b/s2a/src/main/proto/grpc/gcp/s2a/s2a_context.proto index edaeaf22669..745b4d267d6 100644 --- a/s2a/src/main/proto/grpc/gcp/s2a/s2a_context.proto +++ b/s2a/src/main/proto/grpc/gcp/s2a/s2a_context.proto @@ -23,7 +23,7 @@ import "grpc/gcp/s2a/common.proto"; option java_multiple_files = true; option java_outer_classname = "S2AContextProto"; -option java_package = "io.grpc.s2a.handshaker"; +option java_package = "io.grpc.s2a.internal.handshaker"; message S2AContext { // The SPIFFE ID from the peer leaf certificate, if present. diff --git a/s2a/src/test/java/io/grpc/s2a/channel/S2AGrpcChannelPoolTest.java b/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPoolTest.java similarity index 99% rename from s2a/src/test/java/io/grpc/s2a/channel/S2AGrpcChannelPoolTest.java rename to s2a/src/test/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPoolTest.java index 260129f8f56..afae456abb1 100644 --- a/s2a/src/test/java/io/grpc/s2a/channel/S2AGrpcChannelPoolTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPoolTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.channel; +package io.grpc.s2a.internal.channel; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; diff --git a/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java b/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java similarity index 99% rename from s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java rename to s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java index 7281adb9794..9e09d10f7da 100644 --- a/s2a/src/test/java/io/grpc/s2a/channel/S2AHandshakerServiceChannelTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.channel; +package io.grpc.s2a.internal.channel; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; @@ -36,7 +36,7 @@ import io.grpc.benchmarks.Utils; import io.grpc.internal.SharedResourceHolder.Resource; import io.grpc.netty.NettyServerBuilder; -import io.grpc.s2a.channel.S2AHandshakerServiceChannel.HandshakerServiceChannel; +import io.grpc.s2a.internal.channel.S2AHandshakerServiceChannel.HandshakerServiceChannel; import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; import io.grpc.testing.protobuf.SimpleRequest; diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServer.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServer.java similarity index 97% rename from s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServer.java rename to s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServer.java index d630f57d90d..a1745d209d7 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServer.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import io.grpc.stub.StreamObserver; import java.io.IOException; diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServerTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java similarity index 98% rename from s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServerTest.java rename to s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java index a8868744f80..e61f8eea1a1 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeS2AServerTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; @@ -27,7 +27,7 @@ import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.benchmarks.Utils; -import io.grpc.s2a.handshaker.ValidatePeerCertificateChainReq.VerificationMode; +import io.grpc.s2a.internal.handshaker.ValidatePeerCertificateChainReq.VerificationMode; import io.grpc.stub.StreamObserver; import java.io.IOException; import java.nio.charset.StandardCharsets; diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeWriter.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java similarity index 98% rename from s2a/src/test/java/io/grpc/s2a/handshaker/FakeWriter.java rename to s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java index b0e84fdf962..6466a08eca9 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/FakeWriter.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; -import static io.grpc.s2a.handshaker.TLSVersion.TLS_VERSION_1_2; -import static io.grpc.s2a.handshaker.TLSVersion.TLS_VERSION_1_3; +import static io.grpc.s2a.internal.handshaker.TLSVersion.TLS_VERSION_1_2; +import static io.grpc.s2a.internal.handshaker.TLSVersion.TLS_VERSION_1_3; import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.CanIgnoreReturnValue; diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/GetAuthenticationMechanismsTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java similarity index 92% rename from s2a/src/test/java/io/grpc/s2a/handshaker/GetAuthenticationMechanismsTest.java rename to s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java index 884e1ec88eb..4c00b0746fc 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/GetAuthenticationMechanismsTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import com.google.common.truth.Expect; -import io.grpc.s2a.handshaker.S2AIdentity; -import io.grpc.s2a.handshaker.tokenmanager.SingleTokenFetcher; +import io.grpc.s2a.internal.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.tokenmanager.SingleTokenFetcher; import java.util.Optional; import org.junit.BeforeClass; import org.junit.Rule; diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java similarity index 98% rename from s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java rename to s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java index 0fb9b12cf95..e1ad3d278c3 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/IntegrationTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; @@ -32,7 +32,7 @@ import io.grpc.netty.GrpcSslContexts; import io.grpc.netty.NettyServerBuilder; import io.grpc.s2a.S2AChannelCredentials; -import io.grpc.s2a.handshaker.FakeS2AServer; +import io.grpc.s2a.internal.handshaker.FakeS2AServer; import io.grpc.stub.StreamObserver; import io.grpc.testing.protobuf.SimpleRequest; import io.grpc.testing.protobuf.SimpleResponse; diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/ProtoUtilTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/ProtoUtilTest.java similarity index 98% rename from s2a/src/test/java/io/grpc/s2a/handshaker/ProtoUtilTest.java rename to s2a/src/test/java/io/grpc/s2a/internal/handshaker/ProtoUtilTest.java index f54063b9e04..b685d0bc755 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/ProtoUtilTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/ProtoUtilTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static org.junit.Assert.assertThrows; diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2APrivateKeyMethodTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethodTest.java similarity index 99% rename from s2a/src/test/java/io/grpc/s2a/handshaker/S2APrivateKeyMethodTest.java rename to s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethodTest.java index fc8d42d09c0..c885783be99 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/S2APrivateKeyMethodTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethodTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; @@ -26,7 +26,7 @@ import com.google.common.truth.Expect; import com.google.protobuf.ByteString; import io.grpc.netty.GrpcSslContexts; -import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.netty.handler.ssl.OpenSslPrivateKeyMethod; import io.netty.handler.ssl.SslContextBuilder; import java.io.ByteArrayInputStream; diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java similarity index 96% rename from s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java rename to s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java index fa6c4fc858d..d469b07df0f 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AProtocolNegotiatorFactoryTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; @@ -35,11 +35,11 @@ import io.grpc.netty.GrpcHttp2ConnectionHandler; import io.grpc.netty.InternalProtocolNegotiator; import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; -import io.grpc.s2a.channel.S2AChannelPool; -import io.grpc.s2a.channel.S2AGrpcChannelPool; -import io.grpc.s2a.channel.S2AHandshakerServiceChannel; -import io.grpc.s2a.handshaker.S2AIdentity; -import io.grpc.s2a.handshaker.S2AProtocolNegotiatorFactory.S2AProtocolNegotiator; +import io.grpc.s2a.internal.channel.S2AChannelPool; +import io.grpc.s2a.internal.channel.S2AGrpcChannelPool; +import io.grpc.s2a.internal.channel.S2AHandshakerServiceChannel; +import io.grpc.s2a.internal.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.S2AProtocolNegotiatorFactory.S2AProtocolNegotiator; import io.grpc.stub.StreamObserver; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandler; diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java similarity index 97% rename from s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java rename to s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java index a1daf9948a1..2e3bfc02879 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/S2AStubTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; @@ -23,9 +23,9 @@ import com.google.common.truth.Expect; import io.grpc.InsecureChannelCredentials; import io.grpc.internal.SharedResourcePool; -import io.grpc.s2a.channel.S2AChannelPool; -import io.grpc.s2a.channel.S2AGrpcChannelPool; -import io.grpc.s2a.channel.S2AHandshakerServiceChannel; +import io.grpc.s2a.internal.channel.S2AChannelPool; +import io.grpc.s2a.internal.channel.S2AGrpcChannelPool; +import io.grpc.s2a.internal.channel.S2AHandshakerServiceChannel; import io.grpc.stub.StreamObserver; import java.io.IOException; import java.nio.charset.StandardCharsets; diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/S2ATrustManagerTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2ATrustManagerTest.java similarity index 99% rename from s2a/src/test/java/io/grpc/s2a/handshaker/S2ATrustManagerTest.java rename to s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2ATrustManagerTest.java index 384e1aba5cc..198001838aa 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/S2ATrustManagerTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2ATrustManagerTest.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; -import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.S2AIdentity; import java.io.ByteArrayInputStream; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/SslContextFactoryTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/SslContextFactoryTest.java similarity index 98% rename from s2a/src/test/java/io/grpc/s2a/handshaker/SslContextFactoryTest.java rename to s2a/src/test/java/io/grpc/s2a/internal/handshaker/SslContextFactoryTest.java index a2a66a7b563..fc3cfb5e441 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/SslContextFactoryTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/SslContextFactoryTest.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker; +package io.grpc.s2a.internal.handshaker; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.truth.Expect; -import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.netty.handler.ssl.OpenSslSessionContext; import io.netty.handler.ssl.SslContext; import java.security.GeneralSecurityException; diff --git a/s2a/src/test/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java similarity index 95% rename from s2a/src/test/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java rename to s2a/src/test/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java index 80adba07f20..5bf2ce05259 100644 --- a/s2a/src/test/java/io/grpc/s2a/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.grpc.s2a.handshaker.tokenmanager; +package io.grpc.s2a.internal.handshaker.tokenmanager; import static com.google.common.truth.Truth.assertThat; -import io.grpc.s2a.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.S2AIdentity; import java.util.Optional; import org.junit.Before; import org.junit.Test; From 9ab35a761bf4bc9d39ade5fd15b1e29fb31f061c Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 2 Oct 2024 11:03:44 -0700 Subject: [PATCH 031/591] util: Store only a list of children in MultiChildLB A map of children is still needed, but is created temporarily on update. The order of children is currently preserved, but we could use regular HashMaps if that is not useful. --- .../io/grpc/util/MultiChildLoadBalancer.java | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java index 330ec9d5357..3e56f41d038 100644 --- a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java @@ -24,7 +24,8 @@ import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; +import com.google.common.base.Objects; +import com.google.common.collect.Maps; import io.grpc.Attributes; import io.grpc.ConnectivityState; import io.grpc.EquivalentAddressGroup; @@ -37,12 +38,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -55,7 +53,8 @@ public abstract class MultiChildLoadBalancer extends LoadBalancer { private static final Logger logger = Logger.getLogger(MultiChildLoadBalancer.class.getName()); - private final Map childLbStates = new LinkedHashMap<>(); + // Modify by replacing the list to release memory when no longer used. + private List childLbStates = new ArrayList<>(0); private final Helper helper; // Set to true if currently in the process of handling resolved addresses. protected boolean resolvingAddresses; @@ -84,7 +83,8 @@ protected MultiChildLoadBalancer(Helper helper) { */ protected Map createChildAddressesMap( ResolvedAddresses resolvedAddresses) { - Map childAddresses = new HashMap<>(); + Map childAddresses = + Maps.newLinkedHashMapWithExpectedSize(resolvedAddresses.getAddresses().size()); for (EquivalentAddressGroup eag : resolvedAddresses.getAddresses()) { ResolvedAddresses addresses = resolvedAddresses.toBuilder() .setAddresses(Collections.singletonList(eag)) @@ -144,7 +144,7 @@ public void handleNameResolutionError(Status error) { @Override public void shutdown() { logger.log(Level.FINE, "Shutdown"); - for (ChildLbState state : childLbStates.values()) { + for (ChildLbState state : childLbStates) { state.shutdown(); } childLbStates.clear(); @@ -169,39 +169,37 @@ protected final AcceptResolvedAddrRetVal acceptResolvedAddressesInternal( return new AcceptResolvedAddrRetVal(unavailableStatus, null); } - updateChildrenWithResolvedAddresses(newChildAddresses); - - return new AcceptResolvedAddrRetVal(Status.OK, getRemovedChildren(newChildAddresses.keySet())); + List removed = updateChildrenWithResolvedAddresses(newChildAddresses); + return new AcceptResolvedAddrRetVal(Status.OK, removed); } - private void updateChildrenWithResolvedAddresses( + /** Returns removed children. */ + private List updateChildrenWithResolvedAddresses( Map newChildAddresses) { + // Create a map with the old values + Map oldStatesMap = + Maps.newLinkedHashMapWithExpectedSize(childLbStates.size()); + for (ChildLbState state : childLbStates) { + oldStatesMap.put(state.getKey(), state); + } + + // Move ChildLbStates from the map to a new list (preserving the new map's order) + List newChildLbStates = new ArrayList<>(newChildAddresses.size()); for (Map.Entry entry : newChildAddresses.entrySet()) { - ChildLbState childLbState = childLbStates.get(entry.getKey()); + ChildLbState childLbState = oldStatesMap.remove(entry.getKey()); if (childLbState == null) { childLbState = createChildLbState(entry.getKey()); - childLbStates.put(entry.getKey(), childLbState); } + newChildLbStates.add(childLbState); if (entry.getValue() != null) { childLbState.setResolvedAddresses(entry.getValue()); // update child childLbState.lb.handleResolvedAddresses(entry.getValue()); // update child LB } } - } - /** - * Identifies which children have been removed (are not part of the newChildKeys). - */ - private List getRemovedChildren(Set newChildKeys) { - List removedChildren = new ArrayList<>(); - // Do removals - for (Object key : ImmutableList.copyOf(childLbStates.keySet())) { - if (!newChildKeys.contains(key)) { - ChildLbState childLbState = childLbStates.remove(key); - removedChildren.add(childLbState); - } - } - return removedChildren; + childLbStates = newChildLbStates; + // Remaining entries in map are orphaned + return new ArrayList<>(oldStatesMap.values()); } protected final void shutdownRemoved(List removedChildren) { @@ -236,12 +234,17 @@ protected final Helper getHelper() { @VisibleForTesting public final Collection getChildLbStates() { - return childLbStates.values(); + return childLbStates; } @VisibleForTesting public final ChildLbState getChildLbState(Object key) { - return childLbStates.get(key); + for (ChildLbState state : childLbStates) { + if (Objects.equal(state.getKey(), key)) { + return state; + } + } + return null; } @VisibleForTesting From b8a0ba44af57475e43e4747607ce78a56ff08340 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Wed, 2 Oct 2024 12:55:21 -0700 Subject: [PATCH 032/591] s2a: clean up usage of certs (#11583) * use CertificateUtils. * Different names for each ec cert. * Generate rsa certs with ::1 IP + delete CSRs. * try. --- .../internal/handshaker/FakeS2AServer.java | 2 +- .../s2a/internal/handshaker/FakeWriter.java | 27 ++++---- s2a/src/test/resources/cert_chain_ec.pem | 63 ++++++++++--------- s2a/src/test/resources/client.csr | 16 ----- s2a/src/test/resources/client_cert.pem | 34 +++++----- s2a/src/test/resources/client_key.pem | 52 +++++++-------- s2a/src/test/resources/config.cnf | 2 +- s2a/src/test/resources/int_cert1_.cnf | 6 +- s2a/src/test/resources/int_cert1_ec.pem | 21 ++++--- s2a/src/test/resources/int_cert2.cnf | 6 +- s2a/src/test/resources/int_cert2_ec.pem | 21 ++++--- s2a/src/test/resources/int_key1_ec.pem | 6 +- s2a/src/test/resources/int_key2_ec.pem | 6 +- s2a/src/test/resources/leaf.cnf | 6 +- s2a/src/test/resources/leaf_cert_ec.pem | 21 ++++--- s2a/src/test/resources/leaf_key_ec.pem | 6 +- s2a/src/test/resources/root_cert.pem | 32 +++++----- s2a/src/test/resources/root_cert_ec.pem | 20 +++--- s2a/src/test/resources/root_ec.cnf | 6 +- s2a/src/test/resources/root_key.pem | 56 ++++++++--------- s2a/src/test/resources/root_key_ec.pem | 6 +- s2a/src/test/resources/server.csr | 16 ----- s2a/src/test/resources/server_cert.pem | 32 +++++----- s2a/src/test/resources/server_key.pem | 52 +++++++-------- 24 files changed, 246 insertions(+), 269 deletions(-) delete mode 100644 s2a/src/test/resources/client.csr delete mode 100644 s2a/src/test/resources/server.csr diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServer.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServer.java index a1745d209d7..2d19dd122ec 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServer.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServer.java @@ -28,7 +28,7 @@ public final class FakeS2AServer extends S2AServiceGrpc.S2AServiceImplBase { private final FakeWriter writer; - public FakeS2AServer() throws InvalidKeySpecException, NoSuchAlgorithmException { + public FakeS2AServer() throws InvalidKeySpecException, NoSuchAlgorithmException, IOException { this.writer = new FakeWriter(); this.writer.setVerificationResult(FakeWriter.VerificationResult.SUCCESS).initializePrivateKey(); } diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java index 6466a08eca9..4455e77b1e2 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java @@ -23,17 +23,18 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.ByteString; import io.grpc.stub.StreamObserver; +import io.grpc.util.CertificateUtils; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.Signature; import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Base64; /** A fake Writer Class to mock the behavior of S2A server. */ final class FakeWriter implements StreamObserver { @@ -59,12 +60,8 @@ enum VerificationResult { new File("src/test/resources/int_cert2_ec.pem"); public static final File cert1File = new File("src/test/resources/int_cert1_ec.pem"); - - // src/test/resources/leaf_key_ec.pem - private static final String PRIVATE_KEY = - "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgR2HBqtWTWu4NLiow" - + "ar8vh+9vAmCONE59C+jXNAb9r8ehRANCAATRM8ozcr8PTOVsZNWh+rTmJ6t+rODu" - + "g3LwWpUQq9h7AddjGlLrrTNrceOyO7nh9aEk5plKhs/h7PO8+vkEFsEx"; + public static final File keyFile = + new File("src/test/resources/leaf_key_ec.pem"); private static final ImmutableMap ALGORITHM_TO_SIGNATURE_INSTANCE_IDENTIFIER = ImmutableMap.of( @@ -107,10 +104,14 @@ FakeWriter setFailureReason(String failureReason) { } @CanIgnoreReturnValue - FakeWriter initializePrivateKey() throws InvalidKeySpecException, NoSuchAlgorithmException { - privateKey = - KeyFactory.getInstance("EC") - .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(PRIVATE_KEY))); + FakeWriter initializePrivateKey() throws InvalidKeySpecException, NoSuchAlgorithmException, + IOException, FileNotFoundException, UnsupportedEncodingException { + FileInputStream keyInputStream = new FileInputStream(keyFile); + try { + privateKey = CertificateUtils.getPrivateKey(keyInputStream); + } finally { + keyInputStream.close(); + } return this; } diff --git a/s2a/src/test/resources/cert_chain_ec.pem b/s2a/src/test/resources/cert_chain_ec.pem index 0e097d39bf2..a249904286c 100644 --- a/s2a/src/test/resources/cert_chain_ec.pem +++ b/s2a/src/test/resources/cert_chain_ec.pem @@ -1,36 +1,39 @@ -----BEGIN CERTIFICATE----- -MIIB0jCCAXigAwIBAgIUBV1dftEhhEMTI83L6jpeJn2tuyQwCgYIKoZIzj0EAwIw -JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx -OTIzMDQwMFoXDTQ0MDkxOTIzMDQwMFowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC -b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0TPKM3K/ -D0zlbGTVofq05ierfqzg7oNy8FqVEKvYewHXYxpS660za3Hjsju54fWhJOaZSobP -4ezzvPr5BBbBMaOBgzCBgDAOBgNVHQ8BAf8EBAMCB4AwIAYDVR0lAQH/BBYwFAYI -KwYBBQUHAwIGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPvP7dnB -dg8ZoLB0w62tbvoIRRHPMB8GA1UdIwQYMBaAFHeH+MNh2fgyjNHYP9hLAv9Sl1yD -MAoGCCqGSM49BAMCA0gAMEUCIBcsImaxeFjxFXCXYNQJnde+rsEOgbeAHrAC0SZQ -NlB2AiEA4epDhw/o+6BfgDbqlZsNEHkScPrwupnBQGLQlmNJe2c= +MIIB6jCCAZCgAwIBAgIUA98F2JkYZAyz9BdIkBK3P8Df7OUwCgYIKoZIzj0EAwIw +MjEOMAwGA1UECgwFaW50MU8xDzANBgNVBAsMBmludDFPVTEPMA0GA1UEAwwGaW50 +MUNOMB4XDTI0MTAwMTIxNDIwMFoXDTQ0MTAwMTIxNDIwMFowMjEOMAwGA1UECgwF +bGVhZk8xDzANBgNVBAsMBmxlYWZPVTEPMA0GA1UEAwwGbGVhZkNOMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEtpTTzt2VDTP6gO4uUIpg8sB63Ff4T4YPMoIGrrn3 +tU3f9j0Ysa5/xblM0LkwRImcrKKchYDiNm1wHkWo+qDImaOBgzCBgDAOBgNVHQ8B +Af8EBAMCB4AwIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMAwGA1Ud +EwEB/wQCMAAwHQYDVR0OBBYEFGzFBt/E6vDJRcH+Izy4MQ9AHycqMB8GA1UdIwQY +MBaAFBYs72Jv682/xzG3Tm8hItIFis//MAoGCCqGSM49BAMCA0gAMEUCIHUcqPTB +mQ4kXE0WoOUC8ZmzvthvfKjCNe0YogcjZgwWAiEAvapmWoQIO4qie25Ae9sYRCPq +5xAHztAquk5HLfwabow= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIB1zCCAX6gAwIBAgIUW4GYHncSLeb7Tmw7FMjX/DesReIwCgYIKoZIzj0EAwIw -JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx -OTIzMDA0MVoXDTQ0MDkxOTIzMDA0MVowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC -b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAqtg2E+h -Wfr5dnewqsCLwM0PohkB83Gh7V3i/TPFkNKF/V6pKdz5a3Z8sicG+g7uJX+eyOoD -43Z8woO7MgJl8aOBiTCBhjAOBgNVHQ8BAf8EBAMCAQYwIAYDVR0lAQH/BBYwFAYI -KwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE -FHeH+MNh2fgyjNHYP9hLAv9Sl1yDMB8GA1UdIwQYMBaAFP+PTOryxis9d7HVfqhP -MyMEgMZOMAoGCCqGSM49BAMCA0cAMEQCIHbzJvxHMIDPBRi1e047K0mqKKBSfViS -guiDSoQ5g5OuAiBT5ePqDLs4PyrK6XFkiEWoRX8Z5T9y419Go+fpLM+DaA== +MIIB8TCCAZagAwIBAgIUEXwpznJIlU+ELO7Qgb4UUGpfbj8wCgYIKoZIzj0EAwIw +MjEOMAwGA1UECgwFaW50Mk8xDzANBgNVBAsMBmludDJPVTEPMA0GA1UEAwwGaW50 +MkNOMB4XDTI0MTAwMTIxNDIwMFoXDTQ0MTAwMTIxNDIwMFowMjEOMAwGA1UECgwF +aW50MU8xDzANBgNVBAsMBmludDFPVTEPMA0GA1UEAwwGaW50MUNOMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEoenicrtL6ezEW2yLSXADscDJQ/fdbr+vJEU/aieV +wA2EnPbrdpvQZaz+pXtuZzBLZY50XI9y33E+/PvBFtZob6OBiTCBhjAOBgNVHQ8B +Af8EBAMCAQYwIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1Ud +EwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFBYs72Jv682/xzG3Tm8hItIFis//MB8G +A1UdIwQYMBaAFPhN6eGgVc36Kc50rREZhMdBIkgGMAoGCCqGSM49BAMCA0kAMEYC +IQDiPcbihg1iDi0m9CUn96IbWOTh1X75RfVJYcR3Q5T78AIhAK/fxZauDeWPzk2r +2/ohCQOZFHtAi9VRpr/TqNi3SaYt -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIB1zCCAX6gAwIBAgIUBBKkTrqFxQUist2pK2uj8/DRnKMwCgYIKoZIzj0EAwIw -JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx -OTIyNTYwNloXDTQ0MDkxOTIyNTYwNlowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC -b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFW7/Te2z -jS8KlpF8RMMYaZtKf6EZlrZIIo5SO9j6baAKXVna9LmDCrzXnOLIeqOuZq0ODizU -i4DFALB2yd5BkaOBiTCBhjAOBgNVHQ8BAf8EBAMCAQYwIAYDVR0lAQH/BBYwFAYI -KwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQIwHQYDVR0OBBYE -FP+PTOryxis9d7HVfqhPMyMEgMZOMB8GA1UdIwQYMBaAFFITbB0BULPtynN9SMki -lEarWxcKMAoGCCqGSM49BAMCA0cAMEQCIHK4cTTx4Ti7Te9hA9VVtHoMCt5fL4Cl -XnQR6D5xW4pPAiAQ+CilQdZUhVK5bU6wbrwLgcwf+40ETK/KASId5970rQ== +MIIB8DCCAZagAwIBAgIUNOH4wQEoKHvaQ9Xgd36vh5TnhfUwCgYIKoZIzj0EAwIw +MjEOMAwGA1UECgwFcm9vdE8xDzANBgNVBAsMBnJvb3RPVTEPMA0GA1UEAwwGcm9v +dENOMB4XDTI0MTAwMTIxNDIwMFoXDTQ0MTAwMTIxNDIwMFowMjEOMAwGA1UECgwF +aW50Mk8xDzANBgNVBAsMBmludDJPVTEPMA0GA1UEAwwGaW50MkNOMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAE44B/G4pzAvLpIUaPp8XNRtXuw8jeLgE40NjQMuqq +3jNs6ID/fv/jiRggLMXL3Tii1CisM4BRjg56/Owky1Fyv6OBiTCBhjAOBgNVHQ8B +Af8EBAMCAQYwIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1Ud +EwEB/wQIMAYBAf8CAQIwHQYDVR0OBBYEFPhN6eGgVc36Kc50rREZhMdBIkgGMB8G +A1UdIwQYMBaAFNHNBlllqi9koRtf7EBHjRMwVgWsMAoGCCqGSM49BAMCA0gAMEUC +IBd4bvqVeYSSUEGF1wB0KlYxn1L0Ub/LjgIUUQFAEwahAiEAgeArX63bnlI7u3dq +v/FGilvcLP3P3AvRozpHJiIZ860= -----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/client.csr b/s2a/src/test/resources/client.csr deleted file mode 100644 index 664f5a4cf86..00000000000 --- a/s2a/src/test/resources/client.csr +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIChzCCAW8CAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEAoSS3KtFgiXX4vAUNscFGIB/r2OOMgiZMKHz72dN0 -5kSxwdpQxpMIhwEoe0lhHNfOiuE7/r6VbGG9RGGIcQcoSonc3InPRfpnzfj9KohJ -i8pYkLL9EwElAEl9sWnvVKTza8jTApDP2Z/fntBEsWAMsLPpuRZT6tgN1sXe4vNG -4wufJSxuImyCVAx1fkZjRkYEKOtm1osnEDng4R0WXZ6S+q5lYzYPk1wxgbjdZu2U -fWxP6V63SphV0NFXTx0E401j2h258cIqTVj8lRX6dfl9gO0d43Rd+hSU7R4iXGEw -arixuH9g5H745AFf9H52twHPcNP9cEKBljBpSV5z3MvTkQIDAQABoC4wLAYJKoZI -hvcNAQkOMR8wHTAbBgNVHREEFDAShxAAAAAAAAAAAAAAAAAAAAAAMA0GCSqGSIb3 -DQEBCwUAA4IBAQCQHim3aIpGJs5u6JhEA07Rwm8YKyVALDEklhsHILlFhdNr2uV7 -S+3bHV79mDGjxNWvFcgK5h5ENkT60tXbhbie1gYmFT0RMCYHDsL09NGTh8G9Bbdl -UKeA9DMhRSYzE7Ks3Lo1dJvX7OAEI0qV77dGpQknufYpmHiBXuqtB9I0SpYi1c4O -9IUn/NY0yiYFPsIEsVRz/1dK97wazusLnijaMwNNhUc9bJwTyujhlr+b8ioPyADG -e+GDF97d0nQ8806DOJF4GTRKwaXD+R5zN5t4ULhZ7ERqLNeE9EnWRe4CvSGvBoNA -hIVeYaLd761Z9ZKvOnsgCr8qvMDilDFY6OfB ------END CERTIFICATE REQUEST----- \ No newline at end of file diff --git a/s2a/src/test/resources/client_cert.pem b/s2a/src/test/resources/client_cert.pem index b72f6991c91..837f8bb5019 100644 --- a/s2a/src/test/resources/client_cert.pem +++ b/s2a/src/test/resources/client_cert.pem @@ -1,18 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIC9DCCAdwCFB+cDXee2sIHjdlBhdNpTo+G2XAjMA0GCSqGSIb3DQEBCwUAMFkx -CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl -cm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzEw -MTcyMzA5MDNaFw00MzEwMTcyMzA5MDNaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKEktyrRYIl1+LwFDbHBRiAf -69jjjIImTCh8+9nTdOZEscHaUMaTCIcBKHtJYRzXzorhO/6+lWxhvURhiHEHKEqJ -3NyJz0X6Z834/SqISYvKWJCy/RMBJQBJfbFp71Sk82vI0wKQz9mf357QRLFgDLCz -6bkWU+rYDdbF3uLzRuMLnyUsbiJsglQMdX5GY0ZGBCjrZtaLJxA54OEdFl2ekvqu -ZWM2D5NcMYG43WbtlH1sT+let0qYVdDRV08dBONNY9odufHCKk1Y/JUV+nX5fYDt -HeN0XfoUlO0eIlxhMGq4sbh/YOR++OQBX/R+drcBz3DT/XBCgZYwaUlec9zL05EC -AwEAATANBgkqhkiG9w0BAQsFAAOCAQEARorc1t2OJnwm1lxhf2KpTpNvNOI9FJak -iSHz/MxhMdu4BG/dQHkKkWoVC6W2Kaimx4OImBwRlGEmGf4P0bXOLSTOumk2k1np -ZUbw7Z2cJzvBmT2BLoHRXcBvbFIBW5DJUSHR37eXEKP57BeD+Og4/3XhNzehSpTX -DRd2Ix/D39JjYA462nqPHQP8HDMf6+0BFmvf9ZRYmFucccYQRCUCKDqb8+wGf9W6 -tKNRE6qPG2jpAQ9qkgO7XuucbLvpywt5xj+yDRbOIq43l40mHaz4lRp697oaxjP8 -HSVcMydW3cluoW3AVInNIaqbM1dr6931MllK62DKipFtmCycq/56XA== +MIIDPTCCAiWgAwIBAgIUaarddwSWeE4jDC9kwxEr446ehqUwDQYJKoZIhvcNAQEL +BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X +DTI0MTAwMTIxNTk1NFoXDTQ0MTAwMTIxNTk1NFowFDESMBAGA1UEAwwJbG9jYWxo +b3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxlNsldt7yAU4KRuS +2D2/FjNIE1US5olBm4HteTr++41WaELZJqNLRPPp052jEQU3aKSYNGZvUUO6buu7 +eFpz2SBNUVMyvmzzocjVAyyf4NQvDazYHWOb+/YCeUppTRWriz4V5sn47qJTQ8cd +CGrTFeLHxUjx4nh/OiqVXP/KnF3EqPEuqph0ky7+GirnJgPRe+C5ERuGkJye8dmP +yWGA2lSS6MeDe7JZTAMi08bAn7BuNpeBkOzz1msGGI9PnUanUs7GOPWTDdcQAVY8 +KMvHCuGaNMGpb4rOR2mm8LlbAbpTPz8Pkw4QtMCLkgsrz2CzXpVwnLsU7nDXJAIO +B155lQIDAQABo0IwQDAdBgNVHQ4EFgQUSZEyIHLzkIw7AwkBaUjYfIrGVR4wHwYD +VR0jBBgwFoAUcq3dtxAVA410YWyM0B4e+4umbiwwDQYJKoZIhvcNAQELBQADggEB +AAz0bZ4ayrZLhA45xn0yvdpdqiCtiWikCRtxgE7VXHg/ziZJVMpBpAhbIGO5tIyd +lttnRXHwz5DUwKiba4/bCEFe229BshQEql5qaqcbGbFfSly11WeqqnwR1N7c8Gpv +pD9sVrx22seN0rTUk87MY/S7mzCxHqAx35zm/LTW3pWcgCTMKFHy4Gt4mpTnXkNA +WkhP2OhW5RLiu6Whi0BEdb2TGG1+ctamgijKXb+gJeef5ehlHXG8eU862KF5UlEA +NeQKBm/PpQxOMe0NdpatjN8QRoczku0Itiodng+OZ1o+2iSNG988uFRb3CUSnjtE +R/HL6ULAFzo59EpIYxruU/w= -----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/client_key.pem b/s2a/src/test/resources/client_key.pem index dd3e2ff78f1..38b93eb65c4 100644 --- a/s2a/src/test/resources/client_key.pem +++ b/s2a/src/test/resources/client_key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQChJLcq0WCJdfi8 -BQ2xwUYgH+vY44yCJkwofPvZ03TmRLHB2lDGkwiHASh7SWEc186K4Tv+vpVsYb1E -YYhxByhKidzcic9F+mfN+P0qiEmLyliQsv0TASUASX2xae9UpPNryNMCkM/Zn9+e -0ESxYAyws+m5FlPq2A3Wxd7i80bjC58lLG4ibIJUDHV+RmNGRgQo62bWiycQOeDh -HRZdnpL6rmVjNg+TXDGBuN1m7ZR9bE/pXrdKmFXQ0VdPHQTjTWPaHbnxwipNWPyV -Ffp1+X2A7R3jdF36FJTtHiJcYTBquLG4f2DkfvjkAV/0fna3Ac9w0/1wQoGWMGlJ -XnPcy9ORAgMBAAECggEALAUqoGDIHWUDyOEch5WDwZzWwc4PgTJTFbBm4G96fLkB -UjKAZG6gIrk3RM6b39Q4UQoMaJ/Jk+zzVi3Kpw3MfOhCVGC1JamtF8BP8IGAjdZ9 -8TFkHv/uCrEIzCFjRt00vhoDQq0qiom4/dppGYdikBbl3zDxRbM1vJkbNSY+FCGW -dA0uJ5XdMLR6lPeB5odqjUggnfUgPCOLdV/F+HkSM9NP1bzmHLiKznzwFsfat139 -7LdzJwNN5IX4Io6cxsxNlrX/NNvPkKdGv07Z6FYxWROyKCunjh48xFcQg0ltoRuq -R9P8/LwS8GYrcc1uC/uBc0e6VgM9D9fsvh+8SQtf3QKBgQDXX+z2GnsFoEs7xv9U -qN0HEX4jOkihZvFu43layUmeCeE8wlEctJ0TsM5Bd7FMoUG6e5/btwhsAIYW89Xn -l/R8OzxR6Kh952Dce4DAULuIeopiw7ASJwTZtO9lWhxw0hjM1hxXTG+xxOqQvsRX -c+d+vtvdIqyJ4ELfzg9kUtkdpwKBgQC/ig3cmej7dQdRAMn0YAwgwhuLkCqVFh4y -WIlqyPPejKf8RXubqgtaSYx/T7apP87SMMSfSLaUdrYAGjST6k+tG5cmwutPIbw/ -osL7U3hcIhjX3hfHgI69Ojcpplbd5yqTxZHpxIs6iAQCEqNuasLXIDMouqNhGF1D -YssD6qxcBwKBgQCdZqWvVrsB6ZwSG+UO4jpmqAofhMD/9FQOToCqMOF0dpP966WL -7RO/CEA06FzTPCblOuQhlyq4g8l7jMiPcSZkhIYY9oftO+Q2Pqxh4J6tp6DrfUh4 -e7u3v9wVnj2a1nD5gqFDy8D1kow7LLAhmbtdje7xNh4SxasaFWZ6U3IJkQKBgGS1 -F5i3q9IatCAZBBZjMb0/kfANevYsTPA3sPjec6q91c1EUzuDarisFx0RMn9Gt124 -mokNWEIzMHpZTO/AsOfZq92LeuF+YVYsI8y1FIGMw/csJOCWbXZ812gkt2OxGafc -p118I6BAx6q3VgrGQ2+M1JlDmIeCofa+SPPkPX+dAoGBAJrOgEJ+oyEaX/YR1g+f -33pWoPQbRCG7T4+Y0oetCCWIcMg1/IUvGUCGmRDxj5dMqB+a0vJtviQN9rjpSuNS -0EVw79AJkIjHhi6KDOfAuyBvzGhxpqxGufnQ2GU0QL65NxQfd290xkxikN0ZGtuB -SDgZoJxMOGYwf8EX5i9h27Db +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGU2yV23vIBTgp +G5LYPb8WM0gTVRLmiUGbge15Ov77jVZoQtkmo0tE8+nTnaMRBTdopJg0Zm9RQ7pu +67t4WnPZIE1RUzK+bPOhyNUDLJ/g1C8NrNgdY5v79gJ5SmlNFauLPhXmyfjuolND +xx0IatMV4sfFSPHieH86KpVc/8qcXcSo8S6qmHSTLv4aKucmA9F74LkRG4aQnJ7x +2Y/JYYDaVJLox4N7sllMAyLTxsCfsG42l4GQ7PPWawYYj0+dRqdSzsY49ZMN1xAB +Vjwoy8cK4Zo0walvis5HaabwuVsBulM/Pw+TDhC0wIuSCyvPYLNelXCcuxTucNck +Ag4HXnmVAgMBAAECggEAKuW9jXaBgiS63o1jyFkmvWcPNntG0M2sfrXuRzQfFgse +vwOCk8xrSflWQNsOe+58ayp6746ekl3LdBWSIbiy6SqG/sm3pp/LXNmjVYHv/QH4 +QYV643R5t1ihdVnGiBFhXwdpVleme/tpdjYZzgnJKak5W69o/nrgzhSK5ShAy2xM +j0XXbgdqG+4JxPb5BZmjHHfXAXUfgSORMdfArkbgFBRc9wL/6JVTXjeAMy5WX9qe +5UQsSOYkwc9P2snifC/jdIhjHQOkkx59O0FgukJEFZPoagVG1duWQbnNDr7QVHCJ +jV6dg9tIT4SXD3uPSPbgNGlRUseIakCzrhHARJuA2wKBgQD/h8zoh0KaqKyViCYw +XKOFpm1pAFnp2GiDOblxNubNFAXEWnC+FlkvO/z1s0zVuYELUqfxcYMSXJFEVelK +rfjZtoC5oxqWGqLo9iCj7pa8t+ipulYcLt2SWc7eZPD4T4lzeEf1Qz77aKcz34sa +dv9lzQkDvhR/Mv1VeEGFHiq2VwKBgQDGsLcTGH5Yxs//LRSY8TigBkQEDrH5NvXu +2jtAzZhy1Yhsoa5eiZkhnnzM6+n05ovfZLcy6s7dnwP1Y+C79vs+DKMBsodtDG5z +YpsB0VrXYa6P6pCqkcz0Bz9xdo5sOhAK3AKnX6jd29XBDdeYsw/lxHLG24wProTD +cCYFqtaj8wKBgQCaqKT68DL9zK14a8lBaDCIyexaqx3AjXzkP+Hfhi03XrEG4P5v +7rLYBeTbCUSt7vMN2V9QoTWFvYUm6SCkVJvTmcRblz6WL1T+z0l+LwAJBP7LC77m +m+77j2PH8yxt/iXhP6G97o+GNxdMLDbTM8bs5KZaH4fkXQY73uc5HMMZTQKBgEZS +7blYhf+t/ph2wD+RwVUCYrh86wkmJs2veCFro3WhlnO8lhbn5Mc9bTaqmVgQ8ZjT +8POYoDdYvPHxs+1TcYF4v4kuQziZmc5FLE/sZZauADb38tQsXrpQhmgGakpsEpmF +XXsYJJDB6lo2KATn+8x7R5SSyHQUdPEnlI2U9ft5AoGBAJw0NJiM1EzRS8xq0DmO +AvQaPjo01o2hH6wghws8gDQwrj0eHraHgVi7zo0VkaHJbO7ahKPudset3N7owJhA +CUAPPRtv5wn0amAyNz77f1dz4Gys3AkcchflqhbEaQpzKYx4kX0adclur4WJ/DVm +P7DI977SHCVB4FVMbXMEkBjN -----END PRIVATE KEY----- \ No newline at end of file diff --git a/s2a/src/test/resources/config.cnf b/s2a/src/test/resources/config.cnf index 38d9a9ccdb0..5f9a7710e92 100644 --- a/s2a/src/test/resources/config.cnf +++ b/s2a/src/test/resources/config.cnf @@ -14,4 +14,4 @@ emailAddress = Email Address subjectAltName = @alt_names [alt_names] -IP.1 = :: \ No newline at end of file +IP.1 = ::1 \ No newline at end of file diff --git a/s2a/src/test/resources/int_cert1_.cnf b/s2a/src/test/resources/int_cert1_.cnf index 8eaf6570da1..ba5a0f66a5e 100644 --- a/s2a/src/test/resources/int_cert1_.cnf +++ b/s2a/src/test/resources/int_cert1_.cnf @@ -4,9 +4,9 @@ req_extensions = v3_req prompt = no [req_distinguished_name] -O = o -OU = ou -CN = cn +O = int1O +OU = int1OU +CN = int1CN [v3_req] keyUsage = critical, keyCertSign, cRLSign diff --git a/s2a/src/test/resources/int_cert1_ec.pem b/s2a/src/test/resources/int_cert1_ec.pem index 980de5aa900..de83c2aba79 100644 --- a/s2a/src/test/resources/int_cert1_ec.pem +++ b/s2a/src/test/resources/int_cert1_ec.pem @@ -1,12 +1,13 @@ -----BEGIN CERTIFICATE----- -MIIB1zCCAX6gAwIBAgIUW4GYHncSLeb7Tmw7FMjX/DesReIwCgYIKoZIzj0EAwIw -JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx -OTIzMDA0MVoXDTQ0MDkxOTIzMDA0MVowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC -b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAqtg2E+h -Wfr5dnewqsCLwM0PohkB83Gh7V3i/TPFkNKF/V6pKdz5a3Z8sicG+g7uJX+eyOoD -43Z8woO7MgJl8aOBiTCBhjAOBgNVHQ8BAf8EBAMCAQYwIAYDVR0lAQH/BBYwFAYI -KwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE -FHeH+MNh2fgyjNHYP9hLAv9Sl1yDMB8GA1UdIwQYMBaAFP+PTOryxis9d7HVfqhP -MyMEgMZOMAoGCCqGSM49BAMCA0cAMEQCIHbzJvxHMIDPBRi1e047K0mqKKBSfViS -guiDSoQ5g5OuAiBT5ePqDLs4PyrK6XFkiEWoRX8Z5T9y419Go+fpLM+DaA== +MIIB8TCCAZagAwIBAgIUEXwpznJIlU+ELO7Qgb4UUGpfbj8wCgYIKoZIzj0EAwIw +MjEOMAwGA1UECgwFaW50Mk8xDzANBgNVBAsMBmludDJPVTEPMA0GA1UEAwwGaW50 +MkNOMB4XDTI0MTAwMTIxNDIwMFoXDTQ0MTAwMTIxNDIwMFowMjEOMAwGA1UECgwF +aW50MU8xDzANBgNVBAsMBmludDFPVTEPMA0GA1UEAwwGaW50MUNOMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEoenicrtL6ezEW2yLSXADscDJQ/fdbr+vJEU/aieV +wA2EnPbrdpvQZaz+pXtuZzBLZY50XI9y33E+/PvBFtZob6OBiTCBhjAOBgNVHQ8B +Af8EBAMCAQYwIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1Ud +EwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFBYs72Jv682/xzG3Tm8hItIFis//MB8G +A1UdIwQYMBaAFPhN6eGgVc36Kc50rREZhMdBIkgGMAoGCCqGSM49BAMCA0kAMEYC +IQDiPcbihg1iDi0m9CUn96IbWOTh1X75RfVJYcR3Q5T78AIhAK/fxZauDeWPzk2r +2/ohCQOZFHtAi9VRpr/TqNi3SaYt -----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/int_cert2.cnf b/s2a/src/test/resources/int_cert2.cnf index c6a2559ce10..f48524effb2 100644 --- a/s2a/src/test/resources/int_cert2.cnf +++ b/s2a/src/test/resources/int_cert2.cnf @@ -4,9 +4,9 @@ req_extensions = v3_req prompt = no [req_distinguished_name] -O = o -OU = ou -CN = cn +O = int2O +OU = int2OU +CN = int2CN [v3_req] keyUsage = critical, keyCertSign, cRLSign diff --git a/s2a/src/test/resources/int_cert2_ec.pem b/s2a/src/test/resources/int_cert2_ec.pem index 574fa0195de..4f502fda808 100644 --- a/s2a/src/test/resources/int_cert2_ec.pem +++ b/s2a/src/test/resources/int_cert2_ec.pem @@ -1,12 +1,13 @@ -----BEGIN CERTIFICATE----- -MIIB1zCCAX6gAwIBAgIUBBKkTrqFxQUist2pK2uj8/DRnKMwCgYIKoZIzj0EAwIw -JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx -OTIyNTYwNloXDTQ0MDkxOTIyNTYwNlowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC -b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFW7/Te2z -jS8KlpF8RMMYaZtKf6EZlrZIIo5SO9j6baAKXVna9LmDCrzXnOLIeqOuZq0ODizU -i4DFALB2yd5BkaOBiTCBhjAOBgNVHQ8BAf8EBAMCAQYwIAYDVR0lAQH/BBYwFAYI -KwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQIwHQYDVR0OBBYE -FP+PTOryxis9d7HVfqhPMyMEgMZOMB8GA1UdIwQYMBaAFFITbB0BULPtynN9SMki -lEarWxcKMAoGCCqGSM49BAMCA0cAMEQCIHK4cTTx4Ti7Te9hA9VVtHoMCt5fL4Cl -XnQR6D5xW4pPAiAQ+CilQdZUhVK5bU6wbrwLgcwf+40ETK/KASId5970rQ== +MIIB8DCCAZagAwIBAgIUNOH4wQEoKHvaQ9Xgd36vh5TnhfUwCgYIKoZIzj0EAwIw +MjEOMAwGA1UECgwFcm9vdE8xDzANBgNVBAsMBnJvb3RPVTEPMA0GA1UEAwwGcm9v +dENOMB4XDTI0MTAwMTIxNDIwMFoXDTQ0MTAwMTIxNDIwMFowMjEOMAwGA1UECgwF +aW50Mk8xDzANBgNVBAsMBmludDJPVTEPMA0GA1UEAwwGaW50MkNOMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAE44B/G4pzAvLpIUaPp8XNRtXuw8jeLgE40NjQMuqq +3jNs6ID/fv/jiRggLMXL3Tii1CisM4BRjg56/Owky1Fyv6OBiTCBhjAOBgNVHQ8B +Af8EBAMCAQYwIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1Ud +EwEB/wQIMAYBAf8CAQIwHQYDVR0OBBYEFPhN6eGgVc36Kc50rREZhMdBIkgGMB8G +A1UdIwQYMBaAFNHNBlllqi9koRtf7EBHjRMwVgWsMAoGCCqGSM49BAMCA0gAMEUC +IBd4bvqVeYSSUEGF1wB0KlYxn1L0Ub/LjgIUUQFAEwahAiEAgeArX63bnlI7u3dq +v/FGilvcLP3P3AvRozpHJiIZ860= -----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/int_key1_ec.pem b/s2a/src/test/resources/int_key1_ec.pem index 7ff3864746b..909c119b60c 100644 --- a/s2a/src/test/resources/int_key1_ec.pem +++ b/s2a/src/test/resources/int_key1_ec.pem @@ -1,5 +1,5 @@ -----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgLIQUM1HkFM/LWND8 -jCZ4wHXjFZ1ZZmQolahkZB0O1VChRANCAAQCq2DYT6FZ+vl2d7CqwIvAzQ+iGQHz -caHtXeL9M8WQ0oX9Xqkp3PlrdnyyJwb6Du4lf57I6gPjdnzCg7syAmXx +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgnYGMzs4siZ7Fy3mI +rmsqBdP6We4Zt+ndtOYEGaZDj06hRANCAASh6eJyu0vp7MRbbItJcAOxwMlD991u +v68kRT9qJ5XADYSc9ut2m9BlrP6le25nMEtljnRcj3LfcT78+8EW1mhv -----END PRIVATE KEY----- \ No newline at end of file diff --git a/s2a/src/test/resources/int_key2_ec.pem b/s2a/src/test/resources/int_key2_ec.pem index 7f529ae855f..520300d2560 100644 --- a/s2a/src/test/resources/int_key2_ec.pem +++ b/s2a/src/test/resources/int_key2_ec.pem @@ -1,5 +1,5 @@ -----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgGfm6kyaAMMrmYGhS -jxprBwtcZdP6qXlU1cVIO5bOT8qhRANCAAQVbv9N7bONLwqWkXxEwxhpm0p/oRmW -tkgijlI72PptoApdWdr0uYMKvNec4sh6o65mrQ4OLNSLgMUAsHbJ3kGR +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgzLSoAcENXIiQfBS7 +meBDCohT1rofhWSfD0m55qi8V3WhRANCAATjgH8binMC8ukhRo+nxc1G1e7DyN4u +ATjQ2NAy6qreM2zogP9+/+OJGCAsxcvdOKLUKKwzgFGODnr87CTLUXK/ -----END PRIVATE KEY----- \ No newline at end of file diff --git a/s2a/src/test/resources/leaf.cnf b/s2a/src/test/resources/leaf.cnf index d5b373cbc71..c21cee5568f 100644 --- a/s2a/src/test/resources/leaf.cnf +++ b/s2a/src/test/resources/leaf.cnf @@ -4,9 +4,9 @@ req_extensions = v3_req prompt = no [req_distinguished_name] -O = o -OU = ou -CN = cn +O = leafO +OU = leafOU +CN = leafCN [v3_req] keyUsage = critical, digitalSignature diff --git a/s2a/src/test/resources/leaf_cert_ec.pem b/s2a/src/test/resources/leaf_cert_ec.pem index 39692b95fda..ca48b821f60 100644 --- a/s2a/src/test/resources/leaf_cert_ec.pem +++ b/s2a/src/test/resources/leaf_cert_ec.pem @@ -1,12 +1,13 @@ -----BEGIN CERTIFICATE----- -MIIB0jCCAXigAwIBAgIUBV1dftEhhEMTI83L6jpeJn2tuyQwCgYIKoZIzj0EAwIw -JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx -OTIzMDQwMFoXDTQ0MDkxOTIzMDQwMFowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC -b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0TPKM3K/ -D0zlbGTVofq05ierfqzg7oNy8FqVEKvYewHXYxpS660za3Hjsju54fWhJOaZSobP -4ezzvPr5BBbBMaOBgzCBgDAOBgNVHQ8BAf8EBAMCB4AwIAYDVR0lAQH/BBYwFAYI -KwYBBQUHAwIGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPvP7dnB -dg8ZoLB0w62tbvoIRRHPMB8GA1UdIwQYMBaAFHeH+MNh2fgyjNHYP9hLAv9Sl1yD -MAoGCCqGSM49BAMCA0gAMEUCIBcsImaxeFjxFXCXYNQJnde+rsEOgbeAHrAC0SZQ -NlB2AiEA4epDhw/o+6BfgDbqlZsNEHkScPrwupnBQGLQlmNJe2c= +MIIB6jCCAZCgAwIBAgIUA98F2JkYZAyz9BdIkBK3P8Df7OUwCgYIKoZIzj0EAwIw +MjEOMAwGA1UECgwFaW50MU8xDzANBgNVBAsMBmludDFPVTEPMA0GA1UEAwwGaW50 +MUNOMB4XDTI0MTAwMTIxNDIwMFoXDTQ0MTAwMTIxNDIwMFowMjEOMAwGA1UECgwF +bGVhZk8xDzANBgNVBAsMBmxlYWZPVTEPMA0GA1UEAwwGbGVhZkNOMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEtpTTzt2VDTP6gO4uUIpg8sB63Ff4T4YPMoIGrrn3 +tU3f9j0Ysa5/xblM0LkwRImcrKKchYDiNm1wHkWo+qDImaOBgzCBgDAOBgNVHQ8B +Af8EBAMCB4AwIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMAwGA1Ud +EwEB/wQCMAAwHQYDVR0OBBYEFGzFBt/E6vDJRcH+Izy4MQ9AHycqMB8GA1UdIwQY +MBaAFBYs72Jv682/xzG3Tm8hItIFis//MAoGCCqGSM49BAMCA0gAMEUCIHUcqPTB +mQ4kXE0WoOUC8ZmzvthvfKjCNe0YogcjZgwWAiEAvapmWoQIO4qie25Ae9sYRCPq +5xAHztAquk5HLfwabow= -----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/leaf_key_ec.pem b/s2a/src/test/resources/leaf_key_ec.pem index d90ad8f4db8..b92b90ba1da 100644 --- a/s2a/src/test/resources/leaf_key_ec.pem +++ b/s2a/src/test/resources/leaf_key_ec.pem @@ -1,5 +1,5 @@ -----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgR2HBqtWTWu4NLiow -ar8vh+9vAmCONE59C+jXNAb9r8ehRANCAATRM8ozcr8PTOVsZNWh+rTmJ6t+rODu -g3LwWpUQq9h7AddjGlLrrTNrceOyO7nh9aEk5plKhs/h7PO8+vkEFsEx +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkvnGZBh3uIYfZiau +/0qN0YcQXlwwVVUh8EybjvKUlX2hRANCAAS2lNPO3ZUNM/qA7i5QimDywHrcV/hP +hg8yggauufe1Td/2PRixrn/FuUzQuTBEiZysopyFgOI2bXAeRaj6oMiZ -----END PRIVATE KEY----- \ No newline at end of file diff --git a/s2a/src/test/resources/root_cert.pem b/s2a/src/test/resources/root_cert.pem index 737e601691c..ccd0a46bc23 100644 --- a/s2a/src/test/resources/root_cert.pem +++ b/s2a/src/test/resources/root_cert.pem @@ -1,22 +1,22 @@ -----BEGIN CERTIFICATE----- -MIIDkzCCAnugAwIBAgIUb7RsINwsFgKf0Q0RuzfOgp48j6UwDQYJKoZIhvcNAQEL +MIIDkzCCAnugAwIBAgIUWemeXZdfqcqkP8/Eyj74oTJtoNQwDQYJKoZIhvcNAQEL BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X -DTIzMTAxNzIzMDczOFoXDTQzMTAxNzIzMDczOFowWTELMAkGA1UEBhMCQVUxEzAR +DTI0MTAwMTIxNTkxMVoXDTQ0MTAwMTIxNTkxMVowWTELMAkGA1UEBhMCQVUxEzAR BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAkIFnQLuhzYnm3rvmi/U7zMgEP2Tqgb3VC00frSXEV6olZcLgyC9g -0DAGdt9l9lP90DQTG5KCOtoW2BTqM/aaVpR0OaDFOCy90FIj6YyZLZ9w2PQxQcxS -GQHyEvWszTkNxeDyG1mPTj+Go8JLKqdvLg/9GUgPg6stxyAZwYhyUTGuEM4bv0sn -b3vmHRmIGJ/w6aLtd7nK8LkNHa3WVrbvRGHrzdMHfpzF/M/5fAk8GfRYugo39knf -VLKGyQCXNI8Y1iHGEmPqQZIFPTjBL6caIlbEV0VHlxoSOGB6JVxcllxAEvd6abqX -RJVJPQzzGfEnMNYp9SiZQ9bvDRUsUkWyYwIDAQABo1MwUTAdBgNVHQ4EFgQUAZMN -F9JAGHbA3jGOeu6bWFvSdWkwHwYDVR0jBBgwFoAUAZMNF9JAGHbA3jGOeu6bWFvS -dWkwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAicBli36ISJFu -lrJqOHVqTeNP6go0I35VGnP44nEEP5cBvRD3XntBFEk5D3mSNNOGt+2ncxom8VR9 -FsLuTfHAipXePJI6MSxFuBPea8V/YPBs3npk5f1FRvJ5vEgtzFvBjsKmp1dS9hH0 -KUWtWcsAkO2Anc/LVc0xxSidL8NjzYoEFqiki0TNNwCJjmd9XwnBLHW38sEb/pgy -KTyRpOyG3Zg2UDjBHiXPBrmIvVFLB6+LrPNvfr1k4HjIgVY539ZXUvVMDKytMrDY -h63EMDn4kkPpxXlufgWGybjN5D51OylyWBZLe+L1DQyWEg0Vd7GwPzb6p7bmI7MP -pooqbgbDpQ== +MIIBCgKCAQEAt3A04hy5lljv86Nu0LLQZ2hA+fcImHjt1p1Mxgcta/5oxfVLcerE +ZH+DAQLDtWzp9Up/vI57MM419GIL8Iszk7hnZRS/HWJ+2jewZJtz4i/g15dLr6+1 +uabMdPOWos60BwcLMxKEe6lJO1mV4z9d4NH4mAuMIHyM+ty0Klp9MfeDJtYEh0+z +AxJUHCixDTsnKJro7My7A3ZT7bvaMfXxS7XN6qlRgBfiCmXo/GKTFfmfBW/EZGkG +XOCxE2D79wYNhC41Q/ix0kwjEeOj2vgGFoiyblSdHdzvRXzsoQTEiZSM8lJDR2IT +ZbpgbBlknMU6efNWlS8P5damB9ZWXg3x4wIDAQABo1MwUTAdBgNVHQ4EFgQUcq3d +txAVA410YWyM0B4e+4umbiwwHwYDVR0jBBgwFoAUcq3dtxAVA410YWyM0B4e+4um +biwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEApZvaI9y7vjX/ +RRdvwf2Db9KlTE9nuVQ3AsrmG9Ml0p2X6U5aTetxdYBo2PuaaYHheF03JOH8zjpL +UfFzvbi52DPbfFAaDw/6NIAenXlg492leNvUFNjGGRyJO9R5/aDfv40/fT3Em5G5 +DnR8SeGQ9tI1t6xBBT+d+/MilSiEKVu8IIF/p0SwvEyR4pKo6wFVZR0ZiIj2v/FZ +P5Qk0Xhb+slpmaR3Wtx/mPl9Wb3kpPD4CAwhWDqFkKJql9/n9FvMjdwlCQKQGB26 +ZDXY3C0UTdktK5biNWRgAUVJEWBX6Q2amrxQHIn2d9RJ8uxCME/KBAntK+VxZE78 +w0JOvQ4Dpw== -----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/root_cert_ec.pem b/s2a/src/test/resources/root_cert_ec.pem index 0dd465e8e90..3d20dcfe83c 100644 --- a/s2a/src/test/resources/root_cert_ec.pem +++ b/s2a/src/test/resources/root_cert_ec.pem @@ -1,12 +1,12 @@ -----BEGIN CERTIFICATE----- -MIIBrzCCAVWgAwIBAgIUGV+9j5V61CZaa6mbrchDag5miEQwCgYIKoZIzj0EAwIw -JjEKMAgGA1UECgwBbzELMAkGA1UECwwCb3UxCzAJBgNVBAMMAmNuMB4XDTI0MDkx -OTIyNDMwOFoXDTQ0MDkxOTIyNDMwOFowJjEKMAgGA1UECgwBbzELMAkGA1UECwwC -b3UxCzAJBgNVBAMMAmNuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDVPIq1ds -/MX52CX9YU1RdEeM89YP4o3BN8OiP2O4qcuc11k4Qu4Mo4RWeN9OJpNElTQJ0K8n -/rIvbmw8AIMquaNhMF8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdJQQWMBQGCCsGAQUF -BwMBBggrBgEFBQcDAjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRSE2wdAVCz -7cpzfUjJIpRGq1sXCjAKBggqhkjOPQQDAgNIADBFAiEA1TEfHWArDnepmtMDQ4wd -Q3uqPrV2Ye2KMO67/BHEGIQCIFu3JutXYYVU/CinwH89AJW+FJ7zokaPjCDkbiOH -+h+H +MIIBxzCCAW2gAwIBAgIUN+H7Td9dhyvMrrzZhanevAfCN34wCgYIKoZIzj0EAwIw +MjEOMAwGA1UECgwFcm9vdE8xDzANBgNVBAsMBnJvb3RPVTEPMA0GA1UEAwwGcm9v +dENOMB4XDTI0MTAwMTIxNDIwMFoXDTQ0MTAwMTIxNDIwMFowMjEOMAwGA1UECgwF +cm9vdE8xDzANBgNVBAsMBnJvb3RPVTEPMA0GA1UEAwwGcm9vdENOMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEGnS2gVv6Bs0GtuUAOebR9E0fqaj3zi9mD97B/dgi +MLENhtVPJQzeePv6Ccap+73O0BINRNOl8tlHX0YaXDeEHKNhMF8wDgYDVR0PAQH/ +BAQDAgEGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTRzQZZZaovZKEbX+xAR40TMFYFrDAKBggqhkjOPQQD +AgNIADBFAiEAgnIyLs7FsZNsJjFgYzlaut4h23RxrpUYVCVZt/+x1Q0CIG3U6WGz +YaEyKoCtBHH9cAy76+pP/NU2f7/QuHU9Vymd -----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/root_ec.cnf b/s2a/src/test/resources/root_ec.cnf index bee0b80a166..d736865c831 100644 --- a/s2a/src/test/resources/root_ec.cnf +++ b/s2a/src/test/resources/root_ec.cnf @@ -4,9 +4,9 @@ req_extensions = v3_req prompt = no [req_distinguished_name] -O = o -OU = ou -CN = cn +O = rootO +OU = rootOU +CN = rootCN [v3_req] keyUsage = critical, keyCertSign, cRLSign diff --git a/s2a/src/test/resources/root_key.pem b/s2a/src/test/resources/root_key.pem index aae992426d7..34d0ffa61eb 100644 --- a/s2a/src/test/resources/root_key.pem +++ b/s2a/src/test/resources/root_key.pem @@ -1,30 +1,30 @@ -----BEGIN ENCRYPTED PRIVATE KEY----- -MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQInmQVkXP3TFcCAggA -MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECGeCAVH1pefxBIIEyD3Nj1Dy19oy -fogU+z8YBLXuSCx8s3zncYPF9nYlegGSSo0ace/WxfPu8AEPus1P2MxlxfcCQ1A+ -5+vMihtEpgpTg9R4RlLAWs45jz4AduGiwqW05+W5zgDn6g7p7HIL0+M5FxKRkAW0 -KEH4Jy8Vc1XQxkhOm1Q4NLI8PT94rcBDE9Od03sdrW/hQgaOFz5AWOlT5jF1uUOz -glF1RQQxfJygTB6qlPTC3BAaiAnWij3NOg5L5vvUhjLa7iOZOhRQBRkf4YtHsM+2 -rFy8Z7MeHOvrqFf8LXosNy3JreQW036rLGR0Xh5myATkNrEwA8df37AgLUmwqyfz -hjZefPW77LgMAXlaN8s345AGikOX8yQKEFzPV/Nag32p6t4oiRRcUUfdB4wzKi6T -mzZ6lKcGR3qqL4V6lJSV3I2fmgkYZnUwymolyu+1+CVYDLuE53TBi5dRXwgOghi7 -npw7PqqQCian8yxHF9c1rYukD0ov0/y8ratjOu9XoJG2/wWQJNvDkAyc3mSJf+3y -6Wtu1qhLszU8pZOGW0fK6bGyHSp+wkoah/vRzB0+yFjvuMIG6py2ZDQeqhqS3ZV2 -nZHHjj0tZ45Wbdf4k17ujEK34pFXluPH//zADnd6ym2W0t6x+jtqR5tYu3poORQg -jFgpudkn2RUSq8N/gIiHDwblYBxU2dmyzEVudv1zNgVSHyetGLxsFoNB7Prn89rJ -u24a/xtuCyC2pshWo3KiL74hkkCsC8rLbEAAbADheb35b+Ca3JnMwgyUHbHL6Hqf -EiVIgm14lB/1uz651X58Boo6tDFkgrxEtGDUIZm8yk2n0tGflp7BtYbMCw+7gqhb -XN4hlhFDcCJm8peXcyCtGajOnBuNO9JJDNYor6QjptaIpQBFb7/0rc7kyO12BIUv -F9mrCHF18Hd/9AtUO93+tyDAnL64Jqq9tUv8dOVtIfbcHXZSYHf24l0XAiKByb8y -9NQLUZkIuF4aUZVHV8ZBDdHNqjzqVglKQlGHdw1XBexSal5pC9HvknOmWBgl0aza -flzeTRPX7TPrMJDE5lgSy58czGpvZzhFYwOp6cwpfjNsiqdzD78Zs0xsRbNg519s -d+cLmbiU3plWCoYCuDb68eZRRzT+o41+QJG2PoMCpzPw5wMLl6HuW7HXMRFpZKJc -tPKpeTIzb8hjhA+TwVIVpTPHvvQehtTUQD2mRujdvNM6PF8tnuC3F3sB3PTjeeJg -uzfEfs3BynRTIj/gX6y87gzwsrwWIEN6U0cCbQ6J1EcgdQCiH8vbhIgfd4DkLgLN -Kkif+fI/HgBOqaiwSw3sHmWgB6PllVQOKH6qAiejTHR/UUvJTPvgKJFLunmBiF12 -N1bRge1sSXE1eLKVdi+dP1j0o6RxhaRrbX7ie3y/wYHwCJnb8h08DEprgCqoswFs -SuNKmvlibBHAsnOdhyCTOd9I5n8XzAUUp6mT+C5WDfl7qfYvh6IHFlSrhZ9aS9b6 -RY873cnphKbqU5d7Cr8Ufx4b4SgS+hEnuP8y5IToLQ3BONGQH2lu7nmd89wjW0uo -IMRXybwf/5FnKhEy8Aw+pD6AxiXC3DZVTKl3SHmjkYBDvNElsJVgygVTKgbOa1Z+ -ovIK/D7QV7Nv3uVortH8XA== +MIIFJDBWBgkqhkiG9w0BBQ0wSTAxBgkqhkiG9w0BBQwwJAQQJXNe391O3gaNbKLw +o60XrQICCAAwDAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQI4pf69+BBF8IEggTI +JuQ3p67U9k/NWMuYXaR9a6lv24YZ1qR6ieL5B6keCaCDVoQMb5V22O0vBqCVePgr +EG0yWIeeAsARMzAxE7Lnil6abSe7tij+LjEI9F7mV/1QSFt03PLVI+e7OcKNI+Nr +6vISEi8CaddekP8JDRhPMpgdWderZvogo3REpJ8GNIUddQzu1e3ZgDtOPquqcgqb +MH/HuPE3vjj4/l6ZpX+6DZKIvzjwtBQ4PMzSWLumzmYLItd3kz7UryN+9hKluSZp +D2KB24aUIQFbDxe2DMTi5c0QIiyzjwkv081ecNJOy2gYX3uiucr8/Ax3o21RNZtI +oKCmSPVEfYdrkdfkwuSOioVTbWBZBcSZo3L2bmCkSXTuheGurEw/TtQWXBgew0Bn +UQjEJgZy96PVsQeu3t+NRCacARQi4vfv7PVHlQW8fcfcC6CeNw7VIZ8aS7supqym +RJxzMY9ZnLwO9cgybXLYgosVZnvI7nOokJPfO1+KqBK01C1Sgc3tg8czKhRuztHu +qDO0GCZ7l+9/ku/WIy/5NiatNvRo5dMAOGxsSrjI9a7+EmenoIfd8/KREVX19D+R +gZRALVATHq83rF6BdsyTwya1QUr/J24EIlkOc4HbCBm5WxA2ZjNdDBZ+KhivYaS7 +l1qrbkFOhmBD9kYRbseBrxlzKUWJMGhOpw3xebut3HngLqyezLcjsXQuF3Iau5Hl +9QFcmSdLj2ZlNlQvmfNJX/r6a/K2LigruXCbvHWMqVsHd7XZdWJ/8wjm2AL97iON +mYFLP+ScfYom9qrF41jNkUKZiLk/ppvSHyWBAqbze+R9Zfpcf8ArCwuAL/JlEMzv +YkBv1DWKfzJpZHYX695MxrpS3C8m0IyXNxktBL3KTVvwZaIhSNBlNS3fdb9m8toR +Tz/LS8jseWpZ5D552/+KAa0Skhav3ZFpxmAS8BEyE/nI9Dwg9niYcZLWORWHAQPp +jraG0BkE7bn5No/k7E4rjFb+2N+36QxVacJI3neC8bQXVHP0BVUvrabOWFPnGivl +Ok91Eo8q5PUAsd15ZnKjTHzlD7zv7fF6ncBgj3P4L2Xrs6P34JOZEd4wixEUZYeC +Xe+SZrFyUr6CcNC45C6R3hDYqmrz0GK1ikkis3XcKT+C5flBYb9NRx8G9wyCuS6H +oHl0Rfbpc47wQTuajicMVO2El7syMPUAxjo3EfMzvjm7uCXLTHnXRnRt3Y5AkPGa +0kFE9Vm00PReRfQ7qbSUiOOHYa9NIsw1l2ZI+knP9XbY2HikELOpjgucrMxZF+ms +zit5YGD3NGZi5xcHZFZTs9L8kaJccXn5DtjA30eEiFzKqMtMKnwlrbSL55I1JXim +co1RLpRK2KQmtJHo1br3RH6jP7fePYzgDceDds5HKWz22pYFcVtlx4DeYH5vjdEp +i3yNQZ32jD2HYhgCK325QLP5S2UYmUOPWd4sEiwZMBPpPOlt0TqCdFKYgS2GHlSN +IYVBYelPUYsz9Kg0TFtLMZLNUmwsXJ+jqnLVtmFyoV6IIvbSCqQ9jxTbZQKxThK8 +A1G+nXBO41ZW8eQZUGx8CzbCj2JvtVThgErSRqAuYbvlUt7EI4Ac8veZC8rJIG0Q +ADkueb978o4OI6vpOdTYCmdTIoHWlpup -----END ENCRYPTED PRIVATE KEY----- \ No newline at end of file diff --git a/s2a/src/test/resources/root_key_ec.pem b/s2a/src/test/resources/root_key_ec.pem index ef5ee1445f9..5560a66d414 100644 --- a/s2a/src/test/resources/root_key_ec.pem +++ b/s2a/src/test/resources/root_key_ec.pem @@ -1,5 +1,5 @@ -----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd5oZmQBOtMF0xfc3 -uRuw5EDhA1thJKKeHfrij9FMkfahRANCAAQNU8irV2z8xfnYJf1hTVF0R4zz1g/i -jcE3w6I/Y7ipy5zXWThC7gyjhFZ4304mk0SVNAnQryf+si9ubDwAgyq5 +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgjfTyzPIlKV0zANQP +2s1C2FhbenE34QEsf83wjpuQrZWhRANCAAQadLaBW/oGzQa25QA55tH0TR+pqPfO +L2YP3sH92CIwsQ2G1U8lDN54+/oJxqn7vc7QEg1E06Xy2UdfRhpcN4Qc -----END PRIVATE KEY----- \ No newline at end of file diff --git a/s2a/src/test/resources/server.csr b/s2a/src/test/resources/server.csr deleted file mode 100644 index 1657b191133..00000000000 --- a/s2a/src/test/resources/server.csr +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIChzCCAW8CAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEAlPThqu8tfJ4hQKRiUw/vNPfo2L2LQU8NlrRL7rvV -71E345LGK1h/hM3MHp5VgEvaaIibb0hSNv/TYz3HVCQyNuPlcmkHZTJ9mB0icilU -rYWdM0LPIg46iThmIQVhMiNfpMKQLDLQ7o3Jktjm32OxnQdtYSV+7NFnw8/0pB4j -iaiBYfZIMeGzEJIOFG8GSNJG0pfCI71DyLRonIcb2XzfeDPHeWSF7lbIoMGAuKIE -2mXpwHmAjTMJzIShSgLqCvmbz7wR3ZeVMknXcgcqMmagGphy8SjizIWC5KRbrnRq -F22Ouxdat6scIevRXGp5nYawFYdpK9qo+82gEouVX3dtSQIDAQABoC4wLAYJKoZI -hvcNAQkOMR8wHTAbBgNVHREEFDAShxAAAAAAAAAAAAAAAAAAAAAAMA0GCSqGSIb3 -DQEBCwUAA4IBAQB2qU354OlNVunhZhiOFNwabovxLcgKoQz+GtJ2EzsMEza+NPvV -dttPxXzqL/U+gDghvGzSYGuh2yMfTTPO+XtZKpvMUmIWonN5jItbFwSTaWcoE8Qs -zFZokRuFJ9dy017u642mpdf6neUzjbfCjWs8+3jyFzWlkrMF3RlSTxPuksWjhXsX -dxxLNu8YWcsYRB3fODHqrlBNuDn+9kb9z8to+yq76MA0HtdDkjd/dfgghiTDJhqm -IcwhBXufwQUrOP4YiuiwM0mo7Xlhw65gnSmRcwR9ha98SV2zG5kiRYE+m+94bDbd -kGBRfhpQSzh1w09cVzmLgzkfxRShEB+bb9Ss ------END CERTIFICATE REQUEST----- \ No newline at end of file diff --git a/s2a/src/test/resources/server_cert.pem b/s2a/src/test/resources/server_cert.pem index 10a98cf5c21..909b83aa903 100644 --- a/s2a/src/test/resources/server_cert.pem +++ b/s2a/src/test/resources/server_cert.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDWjCCAkKgAwIBAgIUMZkgD5gtoa39H9jdI/ijVkyxC/swDQYJKoZIhvcNAQEL +MIIDWjCCAkKgAwIBAgIUAeWzyzIEetYf+ZWHj9NzH1JkLYkwDQYJKoZIhvcNAQEL BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X -DTIzMTAxNzIzMDg1M1oXDTQzMTAxNzIzMDg1M1owFDESMBAGA1UEAwwJbG9jYWxo -b3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlPThqu8tfJ4hQKRi -Uw/vNPfo2L2LQU8NlrRL7rvV71E345LGK1h/hM3MHp5VgEvaaIibb0hSNv/TYz3H -VCQyNuPlcmkHZTJ9mB0icilUrYWdM0LPIg46iThmIQVhMiNfpMKQLDLQ7o3Jktjm -32OxnQdtYSV+7NFnw8/0pB4jiaiBYfZIMeGzEJIOFG8GSNJG0pfCI71DyLRonIcb -2XzfeDPHeWSF7lbIoMGAuKIE2mXpwHmAjTMJzIShSgLqCvmbz7wR3ZeVMknXcgcq -MmagGphy8SjizIWC5KRbrnRqF22Ouxdat6scIevRXGp5nYawFYdpK9qo+82gEouV -X3dtSQIDAQABo18wXTAbBgNVHREEFDAShxAAAAAAAAAAAAAAAAAAAAAAMB0GA1Ud -DgQWBBTKJU+NK7Q6ZPccSigRCMBCBgjkaDAfBgNVHSMEGDAWgBQBkw0X0kAYdsDe -MY567ptYW9J1aTANBgkqhkiG9w0BAQsFAAOCAQEAXuCs6MGVoND8TaJ6qaDmqtpy -wKEW2hsGclI9yv5cMS0XCVTkmKYnIoijtqv6Pdh8PfhIx5oJqJC8Ml16w4Iou4+6 -kKF0DdzdQyiM0OlNCgLYPiR4rh0ZCAFFCvOsDum1g+b9JTFZGooK4TMd9thwms4D -SqpP5v1NWf/ZLH5TYnp2CkPzBxDlnMJZphuWtPHL+78TbgQuQaKu2nMLBGBJqtFi -HDOGxckgZuwBsy0c+aC/ZwaV7FdMP42kxUZduCEx8+BDSGwPoEpz6pwVIkjiyYAm -3O8FUeEPzYzwpkANIbbEIDWV6FVH9IahKRRkE+bL3BqoQkv8SMciEA5zWsPrbA== +DTI0MTAwMTIxNTk0NloXDTQ0MTAwMTIxNTk0NlowFDESMBAGA1UEAwwJbG9jYWxo +b3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1qnW7Pb06MgRNLzt +icv/ydl8W/lpPRjrJJb04/TtXbJ1hjnp7i796TfNGrJgHqEZnaR8q83lO0L38B2X +sJ04b3R+y+6HhH8+MbHejM7ybrTZRNQXip/Kxu4QLHBTQEsplycWLf42/R3cIk/X +vgxq5NsCsbk4xI4xwlcqC8FM1AHU0VrKxzHWVhZEM+/KovBAr/hRYln9CukeKjOf +UiVq58uuDAlJRC3yH2Rd/sqCDELvqRv17J6eYx2nJ3mSN5aBa0FwVjg6vr5Obddj +AWWIkgrlAr+a+OraxOrWElFfChBSvr/qHdJFWHeCdq/SAhow5uRhC69ScJf+7lrX +hsj1sQIDAQABo18wXTAbBgNVHREEFDAShxAAAAAAAAAAAAAAAAAAAAABMB0GA1Ud +DgQWBBRdDRg6GuDj8Sujmz4/rqfP0jZHbTAfBgNVHSMEGDAWgBRyrd23EBUDjXRh +bIzQHh77i6ZuLDANBgkqhkiG9w0BAQsFAAOCAQEAAEUS27+6p88CWYemMOY0iu0e +mp4YqG0XQSilbSnxrqnJb3N8pR3Yh6JJKnblQ6xdexfzrXlBA/v7nx+f8e9HS2QZ +KLtEIaEvNKL51JdOS6ebEzLVvhk98r2kpKM3wpT++/18HPlPK5W3rMQNsLOyAdvP +UX6TakhIfflRjz1DYXQ1ERvJOFw2HEmw6K6r2VwBhZKfwwzxmAHpVwniWXGbgyRF +79hG6rO1tv1K5LHAPIRs0h2Lh/VPxm2XiaNkdGyarUy5/NM+GoHErgxOBmYltn5Q +vAlZrgF2/mSXcUb7EHoXvoC9L4M7U/dRQD4Q1fQRJ/KjrhbDAC3gfZ4zorKoaQ== -----END CERTIFICATE----- \ No newline at end of file diff --git a/s2a/src/test/resources/server_key.pem b/s2a/src/test/resources/server_key.pem index 44f087dee94..edc37cb3855 100644 --- a/s2a/src/test/resources/server_key.pem +++ b/s2a/src/test/resources/server_key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCU9OGq7y18niFA -pGJTD+809+jYvYtBTw2WtEvuu9XvUTfjksYrWH+EzcwenlWAS9poiJtvSFI2/9Nj -PcdUJDI24+VyaQdlMn2YHSJyKVSthZ0zQs8iDjqJOGYhBWEyI1+kwpAsMtDujcmS -2ObfY7GdB21hJX7s0WfDz/SkHiOJqIFh9kgx4bMQkg4UbwZI0kbSl8IjvUPItGic -hxvZfN94M8d5ZIXuVsigwYC4ogTaZenAeYCNMwnMhKFKAuoK+ZvPvBHdl5UySddy -ByoyZqAamHLxKOLMhYLkpFuudGoXbY67F1q3qxwh69FcanmdhrAVh2kr2qj7zaAS -i5Vfd21JAgMBAAECggEACTBuN4hXywdKT92UP0GNZTwh/jT7QUUqNnDa+lhWI1Rk -WUK1vPjRrRSxEfZ8mdSUHbzHsf7JK6FungGyqUsuWdqHTh6SmTibLOYnONm54paK -kx38/0HXdJ2pF0Jos5ohDV3/XOqpnv3aQJfm7kMNMv3BTqvsf5mPiDHtCq7dTGGj -rGiLc0zirKZq79C6YSB1UMB01BsDl2ScflK8b3osT18uYx/BOdjLT4yZWQsU/nbB -OeF+ziWTTUAVjodGeTf+NYG7cFN/9N9PdSnAwuw8Nche3xZKbHTh2I578Zd4bsDX -H+hoMN862nzOXEvD6KyLB8xDdnEZ+p+njeDROJVmgQKBgQDQhzQEl/co1LYc5IDO -mynhCOtKJeRWBLhYEPIuaSY3qF+lrOWzqyOUNppWDx+HeKOq70X1Q+ETeSXtbaL1 -qHBkNcApQ2lStcpkR9whcVbr9NIWC8y8UQxyerEK3x3l0bZ99dfJ/z6lbxdS7prc -Hhxy6pUj8Q8AgpTZA8HfQUF1EQKBgQC23ek24kTVvWeWX2C/82H1Yfia6ITL7WHz -3aEJaZaO5JD3KmOSZgY88Ob3pkDTRYjFZND5zSB7PnM68gpo/OEDla6ZYtfwBWCX -q4QhFtv2obehobmDk+URVfvlOcBikoEP1i8oy7WdZ5CgC4gNKkkD15l68W+g5IIG -2ZOA97yUuQKBgDAzoI2TRxmUGciR9UhMy6Bt/F12ZtKPYsFQoXqi6aeh7wIP9kTS -wXWoLYLJGiOpekOv7X7lQujKbz7zweCBIAG5/wJKx9TLms4VYkgEt+/w9oMMFTZO -kc8Al14I9xNBp6p0In5Z1vRMupp79yX8e90AZpsZRLt8c8W6PZ1Kq0PRAoGBAKmD -7LzD46t/eJccs0M9CoG94Ac5pGCmHTdDLBTdnIO5vehhkwwTJ5U2e+T2aQFwY+kY -G+B1FrconQj3dk78nFoGV2Q5DJOjaHcwt7s0xZNLNj7O/HnMj3wSiP9lGcJGrP1R -P0ZCEIlph9fU2LnbiPPW2J/vT9uF+EMBTosvG9GBAoGAEVaDLLXOHj+oh1i6YY7s -0qokN2CdeKY4gG7iKjuDFb0r/l6R9uFvpUwJMhLEkF5SPQMyrzKFdnTpw3n/jnRa -AWG6GoV+D7LES+lHP5TXKKijbnHJdFjW8PtfDXHCJ6uGG91vH0TMMp1LqhcvGfTv -lcNGXkk6gUNSecxBC1uJfKE= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDWqdbs9vToyBE0 +vO2Jy//J2Xxb+Wk9GOsklvTj9O1dsnWGOenuLv3pN80asmAeoRmdpHyrzeU7Qvfw +HZewnThvdH7L7oeEfz4xsd6MzvJutNlE1BeKn8rG7hAscFNASymXJxYt/jb9Hdwi +T9e+DGrk2wKxuTjEjjHCVyoLwUzUAdTRWsrHMdZWFkQz78qi8ECv+FFiWf0K6R4q +M59SJWrny64MCUlELfIfZF3+yoIMQu+pG/Xsnp5jHacneZI3loFrQXBWODq+vk5t +12MBZYiSCuUCv5r46trE6tYSUV8KEFK+v+od0kVYd4J2r9ICGjDm5GELr1Jwl/7u +WteGyPWxAgMBAAECggEAFEAgcOlZME6TZPS/ueSfRET6mNieB2/+2sxM3OZhsBmi +QZ/cBCa1uFcVx8N1Et6iwn7ebfy199G4/xNjmHs0dDs6rPVbHnI8hUag1oq9TxlL +d9VERUUOxZZ2uyJ7kBCnI0XCL2OQf29eMXRzx093lBBfIDH3e39ojUtYwZQiMcuw +EPry0k4fVhymhKg9Wnmt5lMg4Mdc1TpPfmNFuTR0PZ1nAaVQglvH66qNKGVoWEhZ +paNLaKC4H2Jfa1AfAWl6Efy5JDMOfHF0ww0cDUrTzAeQ7jEh0UGyL1lX8W6kKRDa +0quUqxOJz9aQ8cyd27s2OQMlRtbXi/jhhVp7WLIrWQKBgQD9gKG5CgBO/L8nIj5o +EhHFhtfjEhdeXTAlenmxoBxUN7Pwkc2OvhNef7+T0+euwl50ieopWLoRxLZ2yY8l +E2b2+7EM6/8/wgt1bCVh5NCWrE63tLCx+wdht1oqciDXvuv5bJTf73sipgDTYYSV +gE+DHXq96mxVJXo1TLtQQpXMVQKBgQDYx0AbO0KP2TTNY5ChqVwthaETHjWs6z9p +U5WRgNYeXbUKg3l7JJk6zq72ZIBeqEr3d9mJqrk6HFKTh4c+LyjKyLjmY5wkmfHh +s6s1lCEgEoXKT3Fa+DxlsXltyxrJLzuf1h276jeL5bB6BmJNKLODcEoCx/ubrwOj +prdUSWqf7QKBgQCO/sg7AJE7/QY2pPJe8hJkQbP1unbEG/zUp0mOEKrqNqGhyh0R +r9ZtL9J5KMc/pRRy2Hjl6c7LxxLF3tyIJXGnUEKG73iEFokwK1jK569hzsB4j8w8 +GUYIsMyDtO0hxeiGQeGYkBX9bXZ5xkBrtH0lkLNz/ZAuV32gIzBmDalCIQKBgDGT +f+m6Z8KWHilKt+0A2n/eq7O/mO7u7hWcc/xOxqkzLRA2eTXcbN6yHfljiqgbPOnT +kwCU9r9/crMir59dEasutHqcFT2Zp2PCv0kFk33OPqLCAF6ZntZy/B5L8NhJ4Qzw +3uP28LUh1nZRt3GF+Wf56jMwoS49nEt0+UBhee0RAoGAS9YsJkbjBg2p3Gxvo5c0 +IjfZdcyS2ndTjXv+hFvkjMw0ULFT3dqpk+0asaCh5nrDUbVQyan+D8LgwSwNZy89 +e99bl//oliv/Om7lVFCKtBOhe+fIWHlrR0e2bemsQi/pgTURjYFuvjhR50dcKx96 +jLHvG4mTfStHaJ1gKGWvgWA= -----END PRIVATE KEY----- \ No newline at end of file From 959060a824945c835fde434fcfe3a9789e3f81e5 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:01:23 -0700 Subject: [PATCH 033/591] s2a: Address comments on S2A channel + stub (#11584) * delete HandshakerServiceChannel. * remove usage of S2AGrpcChannelPool + avoid creating Channel ref per conn. --- .../s2a/internal/channel/S2AChannelPool.java | 43 ------ .../internal/channel/S2AGrpcChannelPool.java | 109 --------------- .../channel/S2AHandshakerServiceChannel.java | 72 +++------- .../S2AProtocolNegotiatorFactory.java | 31 +++-- .../channel/S2AGrpcChannelPoolTest.java | 125 ------------------ .../S2AHandshakerServiceChannelTest.java | 125 +----------------- .../S2AProtocolNegotiatorFactoryTest.java | 41 +----- .../s2a/internal/handshaker/S2AStubTest.java | 15 +-- 8 files changed, 44 insertions(+), 517 deletions(-) delete mode 100644 s2a/src/main/java/io/grpc/s2a/internal/channel/S2AChannelPool.java delete mode 100644 s2a/src/main/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPool.java delete mode 100644 s2a/src/test/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPoolTest.java diff --git a/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AChannelPool.java b/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AChannelPool.java deleted file mode 100644 index aaaa0fffd53..00000000000 --- a/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AChannelPool.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2024 The gRPC Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.grpc.s2a.internal.channel; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import io.grpc.Channel; -import javax.annotation.concurrent.ThreadSafe; - -/** Manages a channel pool to be used for communication with the S2A. */ -@ThreadSafe -public interface S2AChannelPool extends AutoCloseable { - /** - * Retrieves an open channel to the S2A from the channel pool. - * - * @throws IllegalStateException if no channel is available. - */ - @CanIgnoreReturnValue - Channel getChannel(); - - /** Returns a channel to the channel pool. */ - void returnToPool(Channel channel); - - /** - * Returns all channels to the channel pool and closes the pool so that no new channels can be - * retrieved from the pool. - */ - @Override - void close(); -} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPool.java b/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPool.java deleted file mode 100644 index af911185e6c..00000000000 --- a/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPool.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2024 The gRPC Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.grpc.s2a.internal.channel; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import com.google.errorprone.annotations.concurrent.GuardedBy; -import io.grpc.Channel; -import io.grpc.internal.ObjectPool; -import javax.annotation.concurrent.ThreadSafe; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * Manages a gRPC channel pool and a cached gRPC channel to be used for communication with the S2A. - */ -@ThreadSafe -public final class S2AGrpcChannelPool implements S2AChannelPool { - private static final int MAX_NUMBER_USERS_OF_CACHED_CHANNEL = 100000; - private final ObjectPool channelPool; - - @GuardedBy("this") - private @Nullable Channel cachedChannel; - - @GuardedBy("this") - private int numberOfUsersOfCachedChannel = 0; - - private enum State { - OPEN, - CLOSED, - } - - ; - - @GuardedBy("this") - private State state = State.OPEN; - - public static S2AChannelPool create(ObjectPool channelPool) { - checkNotNull(channelPool, "Channel pool should not be null."); - return new S2AGrpcChannelPool(channelPool); - } - - private S2AGrpcChannelPool(ObjectPool channelPool) { - this.channelPool = channelPool; - } - - /** - * Retrieves a channel from {@code channelPool} if {@code channel} is null, and returns {@code - * channel} otherwise. - * - * @return a {@link Channel} obtained from the channel pool. - */ - @Override - public synchronized Channel getChannel() { - checkState(state.equals(State.OPEN), "Channel pool is not open."); - checkState( - numberOfUsersOfCachedChannel < MAX_NUMBER_USERS_OF_CACHED_CHANNEL, - "Max number of channels have been retrieved from the channel pool."); - if (cachedChannel == null) { - cachedChannel = channelPool.getObject(); - } - numberOfUsersOfCachedChannel += 1; - return cachedChannel; - } - - /** - * Returns {@code channel} to {@code channelPool}. - * - *

The caller must ensure that {@code channel} was retrieved from this channel pool. - */ - @Override - public synchronized void returnToPool(Channel channel) { - checkState(state.equals(State.OPEN), "Channel pool is not open."); - checkArgument( - cachedChannel != null && numberOfUsersOfCachedChannel > 0 && cachedChannel.equals(channel), - "Cannot return the channel to channel pool because the channel was not obtained from" - + " channel pool."); - numberOfUsersOfCachedChannel -= 1; - if (numberOfUsersOfCachedChannel == 0) { - channelPool.returnObject(channel); - cachedChannel = null; - } - } - - @Override - public synchronized void close() { - state = State.CLOSED; - numberOfUsersOfCachedChannel = 0; - if (cachedChannel != null) { - channelPool.returnObject(cachedChannel); - cachedChannel = null; - } - } -} \ No newline at end of file diff --git a/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannel.java b/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannel.java index 7a2b3e70672..b1ba88d1886 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannel.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannel.java @@ -19,13 +19,9 @@ import static com.google.common.base.Preconditions.checkNotNull; import static java.util.concurrent.TimeUnit.SECONDS; -import com.google.common.annotations.VisibleForTesting; -import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ChannelCredentials; -import io.grpc.ClientCall; import io.grpc.ManagedChannel; -import io.grpc.MethodDescriptor; import io.grpc.internal.SharedResourceHolder.Resource; import io.grpc.netty.NettyChannelBuilder; import java.time.Duration; @@ -55,6 +51,8 @@ @ThreadSafe public final class S2AHandshakerServiceChannel { private static final Duration CHANNEL_SHUTDOWN_TIMEOUT = Duration.ofSeconds(10); + private static final Logger logger = + Logger.getLogger(S2AHandshakerServiceChannel.class.getName()); /** * Returns a {@link SharedResourceHolder.Resource} instance for managing channels to an S2A server @@ -86,75 +84,35 @@ public ChannelResource(String targetAddress, ChannelCredentials channelCredentia } /** - * Creates a {@code HandshakerServiceChannel} instance to the service running at {@code + * Creates a {@code ManagedChannel} instance to the service running at {@code * targetAddress}. */ @Override public Channel create() { - ManagedChannel channel = - NettyChannelBuilder.forTarget(targetAddress, channelCredentials) + return NettyChannelBuilder.forTarget(targetAddress, channelCredentials) .directExecutor() + .idleTimeout(5, SECONDS) .build(); - return HandshakerServiceChannel.create(channel); } - /** Destroys a {@code HandshakerServiceChannel} instance. */ + /** Destroys a {@code ManagedChannel} instance. */ @Override public void close(Channel instanceChannel) { checkNotNull(instanceChannel); - HandshakerServiceChannel channel = (HandshakerServiceChannel) instanceChannel; - channel.close(); - } - - @Override - public String toString() { - return "grpc-s2a-channel"; - } - } - - /** - * Manages a channel using a {@link ManagedChannel} instance. - */ - @VisibleForTesting - static class HandshakerServiceChannel extends Channel { - private static final Logger logger = - Logger.getLogger(S2AHandshakerServiceChannel.class.getName()); - private final ManagedChannel delegate; - - static HandshakerServiceChannel create(ManagedChannel delegate) { - checkNotNull(delegate); - return new HandshakerServiceChannel(delegate); - } - - private HandshakerServiceChannel(ManagedChannel delegate) { - this.delegate = delegate; - } - - /** - * Returns the address of the service to which the {@code delegate} channel connects, which is - * typically of the form {@code host:port}. - */ - @Override - public String authority() { - return delegate.authority(); - } - - /** Creates a {@link ClientCall} that invokes the operations in {@link MethodDescriptor}. */ - @Override - public ClientCall newCall( - MethodDescriptor methodDescriptor, CallOptions options) { - return delegate.newCall(methodDescriptor, options); - } - - @SuppressWarnings("FutureReturnValueIgnored") - public void close() { - delegate.shutdownNow(); + ManagedChannel channel = (ManagedChannel) instanceChannel; + channel.shutdownNow(); try { - delegate.awaitTermination(CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS); + channel.awaitTermination(CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.log(Level.WARNING, "Channel to S2A was not shutdown."); } + + } + + @Override + public String toString() { + return "grpc-s2a-channel"; } } diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java index cb02d49ce9e..188faf63435 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java @@ -37,8 +37,6 @@ import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; import io.grpc.netty.InternalProtocolNegotiators; import io.grpc.netty.InternalProtocolNegotiators.ProtocolNegotiationHandler; -import io.grpc.s2a.internal.channel.S2AChannelPool; -import io.grpc.s2a.internal.channel.S2AGrpcChannelPool; import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerAdapter; @@ -70,17 +68,16 @@ public final class S2AProtocolNegotiatorFactory { public static InternalProtocolNegotiator.ClientFactory createClientFactory( @Nullable S2AIdentity localIdentity, ObjectPool s2aChannelPool) { checkNotNull(s2aChannelPool, "S2A channel pool should not be null."); - S2AChannelPool channelPool = S2AGrpcChannelPool.create(s2aChannelPool); - return new S2AClientProtocolNegotiatorFactory(localIdentity, channelPool); + return new S2AClientProtocolNegotiatorFactory(localIdentity, s2aChannelPool); } static final class S2AClientProtocolNegotiatorFactory implements InternalProtocolNegotiator.ClientFactory { private final @Nullable S2AIdentity localIdentity; - private final S2AChannelPool channelPool; + private final ObjectPool channelPool; S2AClientProtocolNegotiatorFactory( - @Nullable S2AIdentity localIdentity, S2AChannelPool channelPool) { + @Nullable S2AIdentity localIdentity, ObjectPool channelPool) { this.localIdentity = localIdentity; this.channelPool = channelPool; } @@ -100,13 +97,14 @@ public int getDefaultPort() { @VisibleForTesting static final class S2AProtocolNegotiator implements ProtocolNegotiator { - private final S2AChannelPool channelPool; + private final ObjectPool channelPool; + private final Channel channel; private final Optional localIdentity; private final ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)); static S2AProtocolNegotiator createForClient( - S2AChannelPool channelPool, @Nullable S2AIdentity localIdentity) { + ObjectPool channelPool, @Nullable S2AIdentity localIdentity) { checkNotNull(channelPool, "Channel pool should not be null."); if (localIdentity == null) { return new S2AProtocolNegotiator(channelPool, Optional.empty()); @@ -123,9 +121,11 @@ static S2AProtocolNegotiator createForClient( return HostAndPort.fromString(authority).getHost(); } - private S2AProtocolNegotiator(S2AChannelPool channelPool, Optional localIdentity) { + private S2AProtocolNegotiator(ObjectPool channelPool, + Optional localIdentity) { this.channelPool = channelPool; this.localIdentity = localIdentity; + this.channel = channelPool.getObject(); } @Override @@ -139,13 +139,13 @@ public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { String hostname = getHostNameFromAuthority(grpcHandler.getAuthority()); checkArgument(!isNullOrEmpty(hostname), "hostname should not be null or empty."); return new S2AProtocolNegotiationHandler( - grpcHandler, channelPool, localIdentity, hostname, service); + grpcHandler, channel, localIdentity, hostname, service); } @Override public void close() { service.shutdown(); - channelPool.close(); + channelPool.returnObject(channel); } } @@ -180,7 +180,7 @@ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { } private static final class S2AProtocolNegotiationHandler extends ProtocolNegotiationHandler { - private final S2AChannelPool channelPool; + private final Channel channel; private final Optional localIdentity; private final String hostname; private final GrpcHttp2ConnectionHandler grpcHandler; @@ -188,7 +188,7 @@ private static final class S2AProtocolNegotiationHandler extends ProtocolNegotia private S2AProtocolNegotiationHandler( GrpcHttp2ConnectionHandler grpcHandler, - S2AChannelPool channelPool, + Channel channel, Optional localIdentity, String hostname, ListeningExecutorService service) { @@ -204,7 +204,7 @@ public void handlerAdded(ChannelHandlerContext ctx) { }, grpcHandler.getNegotiationLogger()); this.grpcHandler = grpcHandler; - this.channelPool = channelPool; + this.channel = channel; this.localIdentity = localIdentity; this.hostname = hostname; checkNotNull(service, "service should not be null."); @@ -217,8 +217,7 @@ protected void handlerAdded0(ChannelHandlerContext ctx) { BufferReadsHandler bufferReads = new BufferReadsHandler(); ctx.pipeline().addBefore(ctx.name(), /* name= */ null, bufferReads); - Channel ch = channelPool.getChannel(); - S2AServiceGrpc.S2AServiceStub stub = S2AServiceGrpc.newStub(ch); + S2AServiceGrpc.S2AServiceStub stub = S2AServiceGrpc.newStub(channel); S2AStub s2aStub = S2AStub.newInstance(stub); ListenableFuture sslContextFuture = diff --git a/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPoolTest.java b/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPoolTest.java deleted file mode 100644 index afae456abb1..00000000000 --- a/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AGrpcChannelPoolTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2024 The gRPC Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.grpc.s2a.internal.channel; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; -import static org.mockito.Mockito.mock; - -import io.grpc.Channel; -import io.grpc.internal.ObjectPool; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Unit tests for {@link S2AGrpcChannelPool}. */ -@RunWith(JUnit4.class) -public final class S2AGrpcChannelPoolTest { - @Test - public void getChannel_success() throws Exception { - FakeChannelPool fakeChannelPool = new FakeChannelPool(); - S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(fakeChannelPool); - - Channel channel = s2aChannelPool.getChannel(); - - assertThat(channel).isNotNull(); - assertThat(fakeChannelPool.isChannelCached()).isTrue(); - assertThat(s2aChannelPool.getChannel()).isEqualTo(channel); - } - - @Test - public void returnToPool_success() throws Exception { - FakeChannelPool fakeChannelPool = new FakeChannelPool(); - S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(fakeChannelPool); - - s2aChannelPool.returnToPool(s2aChannelPool.getChannel()); - - assertThat(fakeChannelPool.isChannelCached()).isFalse(); - } - - @Test - public void returnToPool_channelStillCachedBecauseMultipleChannelsRetrieved() throws Exception { - FakeChannelPool fakeChannelPool = new FakeChannelPool(); - S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(fakeChannelPool); - - s2aChannelPool.getChannel(); - s2aChannelPool.returnToPool(s2aChannelPool.getChannel()); - - assertThat(fakeChannelPool.isChannelCached()).isTrue(); - } - - @Test - public void returnToPool_failureBecauseChannelWasNotFromPool() throws Exception { - S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(new FakeChannelPool()); - - IllegalArgumentException expected = - assertThrows( - IllegalArgumentException.class, - () -> s2aChannelPool.returnToPool(mock(Channel.class))); - assertThat(expected) - .hasMessageThat() - .isEqualTo( - "Cannot return the channel to channel pool because the channel was not obtained from" - + " channel pool."); - } - - @Test - public void close_success() throws Exception { - FakeChannelPool fakeChannelPool = new FakeChannelPool(); - try (S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(fakeChannelPool)) { - s2aChannelPool.getChannel(); - } - - assertThat(fakeChannelPool.isChannelCached()).isFalse(); - } - - @Test - public void close_poolIsUnusable() throws Exception { - S2AChannelPool s2aChannelPool = S2AGrpcChannelPool.create(new FakeChannelPool()); - s2aChannelPool.close(); - - IllegalStateException expected = - assertThrows(IllegalStateException.class, s2aChannelPool::getChannel); - - assertThat(expected).hasMessageThat().isEqualTo("Channel pool is not open."); - } - - private static class FakeChannelPool implements ObjectPool { - private final Channel mockChannel = mock(Channel.class); - private @Nullable Channel cachedChannel = null; - - @Override - public Channel getObject() { - if (cachedChannel == null) { - cachedChannel = mockChannel; - } - return cachedChannel; - } - - @Override - public Channel returnObject(Object object) { - assertThat(object).isSameInstanceAs(mockChannel); - cachedChannel = null; - return null; - } - - public boolean isChannelCached() { - return (cachedChannel != null); - } - } -} \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java b/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java index 9e09d10f7da..16409721ff5 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java @@ -20,13 +20,10 @@ import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static org.junit.Assert.assertThrows; -import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ChannelCredentials; -import io.grpc.ClientCall; import io.grpc.InsecureChannelCredentials; import io.grpc.ManagedChannel; -import io.grpc.MethodDescriptor; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.ServerCredentials; @@ -36,14 +33,12 @@ import io.grpc.benchmarks.Utils; import io.grpc.internal.SharedResourceHolder.Resource; import io.grpc.netty.NettyServerBuilder; -import io.grpc.s2a.internal.channel.S2AHandshakerServiceChannel.HandshakerServiceChannel; import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; import io.grpc.testing.protobuf.SimpleRequest; import io.grpc.testing.protobuf.SimpleResponse; import io.grpc.testing.protobuf.SimpleServiceGrpc; import java.io.File; -import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -183,72 +178,7 @@ public void close_mtlsSuccess() throws Exception { } /** - * Verifies that an {@code HandshakerServiceChannel}'s {@code newCall} method can be used to - * perform a simple RPC. - */ - @Test - public void newCall_performSimpleRpcSuccess() { - Resource resource = - S2AHandshakerServiceChannel.getChannelResource( - "localhost:" + plaintextServer.getPort(), - InsecureChannelCredentials.create()); - Channel channel = resource.create(); - assertThat(channel).isInstanceOf(HandshakerServiceChannel.class); - assertThat( - SimpleServiceGrpc.newBlockingStub(channel).unaryRpc(SimpleRequest.getDefaultInstance())) - .isEqualToDefaultInstance(); - } - - /** Same as newCall_performSimpleRpcSuccess, but use mTLS. */ - @Test - public void newCall_mtlsPerformSimpleRpcSuccess() throws Exception { - Resource resource = - S2AHandshakerServiceChannel.getChannelResource( - "localhost:" + mtlsServer.getPort(), getTlsChannelCredentials()); - Channel channel = resource.create(); - assertThat(channel).isInstanceOf(HandshakerServiceChannel.class); - assertThat( - SimpleServiceGrpc.newBlockingStub(channel).unaryRpc(SimpleRequest.getDefaultInstance())) - .isEqualToDefaultInstance(); - } - - /** Creates a {@code HandshakerServiceChannel} instance and verifies its authority. */ - @Test - public void authority_success() throws Exception { - ManagedChannel channel = new FakeManagedChannel(true); - HandshakerServiceChannel eventLoopHoldingChannel = - HandshakerServiceChannel.create(channel); - assertThat(eventLoopHoldingChannel.authority()).isEqualTo("FakeManagedChannel"); - } - - /** - * Creates and closes a {@code HandshakerServiceChannel} when its {@code ManagedChannel} - * terminates successfully. - */ - @Test - public void close_withDelegateTerminatedSuccess() throws Exception { - ManagedChannel channel = new FakeManagedChannel(true); - HandshakerServiceChannel eventLoopHoldingChannel = - HandshakerServiceChannel.create(channel); - eventLoopHoldingChannel.close(); - assertThat(channel.isShutdown()).isTrue(); - } - - /** - * Creates and closes a {@code HandshakerServiceChannel} when its {@code ManagedChannel} does not - * terminate successfully. - */ - @Test - public void close_withDelegateTerminatedFailure() throws Exception { - ManagedChannel channel = new FakeManagedChannel(false); - HandshakerServiceChannel eventLoopHoldingChannel = - HandshakerServiceChannel.create(channel); - eventLoopHoldingChannel.close(); - assertThat(channel.isShutdown()).isTrue(); - } - - /** - * Creates and closes a {@code HandshakerServiceChannel}, creates a new channel from the same + * Creates and closes a {@code ManagedChannel}, creates a new channel from the same * resource, and verifies that this second channel is useable. */ @Test @@ -261,7 +191,7 @@ public void create_succeedsAfterCloseIsCalledOnce() throws Exception { resource.close(channelOne); Channel channelTwo = resource.create(); - assertThat(channelTwo).isInstanceOf(HandshakerServiceChannel.class); + assertThat(channelTwo).isInstanceOf(ManagedChannel.class); assertThat( SimpleServiceGrpc.newBlockingStub(channelTwo) .unaryRpc(SimpleRequest.getDefaultInstance())) @@ -279,7 +209,7 @@ public void create_mtlsSucceedsAfterCloseIsCalledOnce() throws Exception { resource.close(channelOne); Channel channelTwo = resource.create(); - assertThat(channelTwo).isInstanceOf(HandshakerServiceChannel.class); + assertThat(channelTwo).isInstanceOf(ManagedChannel.class); assertThat( SimpleServiceGrpc.newBlockingStub(channelTwo) .unaryRpc(SimpleRequest.getDefaultInstance())) @@ -325,53 +255,4 @@ public void unaryRpc(SimpleRequest request, StreamObserver strea streamObserver.onCompleted(); } } - - private static class FakeManagedChannel extends ManagedChannel { - private final boolean isDelegateTerminatedSuccess; - private boolean isShutdown = false; - - FakeManagedChannel(boolean isDelegateTerminatedSuccess) { - this.isDelegateTerminatedSuccess = isDelegateTerminatedSuccess; - } - - @Override - public String authority() { - return "FakeManagedChannel"; - } - - @Override - public ClientCall newCall( - MethodDescriptor methodDescriptor, CallOptions options) { - throw new UnsupportedOperationException("This method should not be called."); - } - - @Override - public ManagedChannel shutdown() { - throw new UnsupportedOperationException("This method should not be called."); - } - - @Override - public boolean isShutdown() { - return isShutdown; - } - - @Override - public boolean isTerminated() { - throw new UnsupportedOperationException("This method should not be called."); - } - - @Override - public ManagedChannel shutdownNow() { - isShutdown = true; - return null; - } - - @Override - public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { - if (isDelegateTerminatedSuccess) { - return true; - } - throw new InterruptedException("Await termination was interrupted."); - } - } } diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java index d469b07df0f..48c512c4e5c 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java @@ -23,9 +23,7 @@ import com.google.common.testing.NullPointerTester; import com.google.common.testing.NullPointerTester.Visibility; import io.grpc.Channel; -import io.grpc.Grpc; import io.grpc.InsecureChannelCredentials; -import io.grpc.ManagedChannel; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.benchmarks.Utils; @@ -35,8 +33,6 @@ import io.grpc.netty.GrpcHttp2ConnectionHandler; import io.grpc.netty.InternalProtocolNegotiator; import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; -import io.grpc.s2a.internal.channel.S2AChannelPool; -import io.grpc.s2a.internal.channel.S2AGrpcChannelPool; import io.grpc.s2a.internal.channel.S2AHandshakerServiceChannel; import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.grpc.s2a.internal.handshaker.S2AProtocolNegotiatorFactory.S2AProtocolNegotiator; @@ -52,8 +48,6 @@ import io.netty.util.AsciiString; import java.io.IOException; import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.After; import org.junit.Before; @@ -112,15 +106,14 @@ public void createProtocolNegotiatorFactory_nullArgument() throws Exception { @Test public void createProtocolNegotiator_nullArgument() throws Exception { - S2AChannelPool pool = - S2AGrpcChannelPool.create( + ObjectPool pool = SharedResourcePool.forResource( S2AHandshakerServiceChannel.getChannelResource( - "localhost:8080", InsecureChannelCredentials.create()))); + "localhost:8080", InsecureChannelCredentials.create())); NullPointerTester tester = new NullPointerTester() - .setDefault(S2AChannelPool.class, pool) + .setDefault(ObjectPool.class, pool) .setDefault(Optional.class, Optional.empty()); tester.testStaticMethods(S2AProtocolNegotiator.class, Visibility.PACKAGE); @@ -175,15 +168,9 @@ public void closeProtocolNegotiator_verifyProtocolNegotiatorIsClosedOnClientSide @Test public void createChannelHandler_addHandlerToMockContext() throws Exception { - ExecutorService executor = Executors.newSingleThreadExecutor(); - ManagedChannel channel = - Grpc.newChannelBuilder(authority, InsecureChannelCredentials.create()) - .executor(executor) - .build(); - FakeS2AChannelPool fakeChannelPool = new FakeS2AChannelPool(channel); ProtocolNegotiator clientNegotiator = S2AProtocolNegotiatorFactory.S2AProtocolNegotiator.createForClient( - fakeChannelPool, LOCAL_IDENTITY); + channelPool, LOCAL_IDENTITY); ChannelHandler channelHandler = clientNegotiator.newHandler(fakeConnectionHandler); @@ -191,26 +178,6 @@ public void createChannelHandler_addHandlerToMockContext() throws Exception { verify(mockChannelHandlerContext).fireUserEventTriggered("event"); } - /** A {@link S2AChannelPool} that returns the given channel. */ - private static class FakeS2AChannelPool implements S2AChannelPool { - private final Channel channel; - - FakeS2AChannelPool(Channel channel) { - this.channel = channel; - } - - @Override - public Channel getChannel() { - return channel; - } - - @Override - public void returnToPool(Channel channel) {} - - @Override - public void close() {} - } - /** A {@code GrpcHttp2ConnectionHandler} that does nothing. */ private static class FakeConnectionHandler extends GrpcHttp2ConnectionHandler { private static final Http2ConnectionDecoder DECODER = mock(Http2ConnectionDecoder.class); diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java index 2e3bfc02879..bf99ef3f944 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java @@ -21,10 +21,10 @@ import static org.junit.Assert.assertThrows; import com.google.common.truth.Expect; +import io.grpc.Channel; import io.grpc.InsecureChannelCredentials; +import io.grpc.internal.ObjectPool; import io.grpc.internal.SharedResourcePool; -import io.grpc.s2a.internal.channel.S2AChannelPool; -import io.grpc.s2a.internal.channel.S2AGrpcChannelPool; import io.grpc.s2a.internal.channel.S2AHandshakerServiceChannel; import io.grpc.stub.StreamObserver; import java.io.IOException; @@ -53,12 +53,11 @@ public void setUp() { @Test public void send_receiveOkStatus() throws Exception { - S2AChannelPool channelPool = - S2AGrpcChannelPool.create( - SharedResourcePool.forResource( - S2AHandshakerServiceChannel.getChannelResource( - S2A_ADDRESS, InsecureChannelCredentials.create()))); - S2AServiceGrpc.S2AServiceStub serviceStub = S2AServiceGrpc.newStub(channelPool.getChannel()); + ObjectPool channelPool = + SharedResourcePool.forResource( + S2AHandshakerServiceChannel.getChannelResource( + S2A_ADDRESS, InsecureChannelCredentials.create())); + S2AServiceGrpc.S2AServiceStub serviceStub = S2AServiceGrpc.newStub(channelPool.getObject()); S2AStub newStub = S2AStub.newInstance(serviceStub); IOException expected = From 6f3542297c2fb46a38cab65d510fc2ce76200045 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 27 Sep 2024 07:06:17 -0700 Subject: [PATCH 034/591] okhttp: Don't warn about missing Conscrypt When running on the JDK, it is quite normal for Conscrypt not to be present. We'll end up using the JDK 9 ALPN API and everything will be fine. On Android, it would be extremely rare for someone to completely remove the default Android security providers, so the warning was almost never going to trigger on that platform anyway. --- .../okhttp/main/java/io/grpc/okhttp/internal/Platform.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Platform.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Platform.java index 6ed3bc50b81..29ea8055b26 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Platform.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Platform.java @@ -283,7 +283,7 @@ private static boolean isAtLeastAndroid41() { /** * Select the first recognized security provider according to the preference order returned by - * {@link Security#getProviders}. If a recognized provider is not found then warn but continue. + * {@link Security#getProviders}. */ private static Provider getAndroidSecurityProvider() { Provider[] providers = Security.getProviders(); @@ -295,7 +295,6 @@ private static Provider getAndroidSecurityProvider() { } } } - logger.log(Level.WARNING, "Unable to find Conscrypt"); return null; } From 9bb06af9633ea7e0cd4e90eadb037abaa423f2fd Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Wed, 2 Oct 2024 17:03:47 -0700 Subject: [PATCH 035/591] Change PickFirstLeafLoadBalancer to only have 1 subchannel at a time (#11520) * Change PickFirstLeafLoadBalancer to only have 1 subchannel at a time if environment variable GRPC_SERIALIZE_RETRIES == true. Cache serializingRetries value so that it doesn't have to look up the flag every time. Clear the correct task when READY in processSubchannelState and move the logic to cancelScheduledTasks Cleanup based on PR review remove unneeded checks for shutdown. * Fix previously broken tests * Shutdown previous subchannel when run off end of index. * Provide option to disable subchannel retries to let PFLeafLB take control of retries. * InternalSubchannel internally goes to IDLE when sees TF when reconnect is disabled. Remove an extra index.increment in LeafLB --- api/src/main/java/io/grpc/LoadBalancer.java | 6 + .../io/grpc/internal/InternalSubchannel.java | 30 ++-- .../io/grpc/internal/ManagedChannelImpl.java | 4 +- .../internal/PickFirstLeafLoadBalancer.java | 96 +++++++++++-- .../grpc/internal/InternalSubchannelTest.java | 65 ++++++++- .../PickFirstLeafLoadBalancerTest.java | 134 ++++++++++++++---- .../io/grpc/xds/RingHashLoadBalancerTest.java | 10 +- 7 files changed, 292 insertions(+), 53 deletions(-) diff --git a/api/src/main/java/io/grpc/LoadBalancer.java b/api/src/main/java/io/grpc/LoadBalancer.java index 0fbce5fa5be..6d74006b396 100644 --- a/api/src/main/java/io/grpc/LoadBalancer.java +++ b/api/src/main/java/io/grpc/LoadBalancer.java @@ -121,6 +121,12 @@ public abstract class LoadBalancer { HEALTH_CONSUMER_LISTENER_ARG_KEY = LoadBalancer.CreateSubchannelArgs.Key.create("internal:health-check-consumer-listener"); + @Internal + public static final LoadBalancer.CreateSubchannelArgs.Key + DISABLE_SUBCHANNEL_RECONNECT_KEY = + LoadBalancer.CreateSubchannelArgs.Key.createWithDefault( + "internal:disable-subchannel-reconnect", Boolean.FALSE); + @Internal public static final Attributes.Key HAS_HEALTH_PRODUCER_LISTENER_KEY = diff --git a/core/src/main/java/io/grpc/internal/InternalSubchannel.java b/core/src/main/java/io/grpc/internal/InternalSubchannel.java index 70e42e2f5f1..27a80f7c191 100644 --- a/core/src/main/java/io/grpc/internal/InternalSubchannel.java +++ b/core/src/main/java/io/grpc/internal/InternalSubchannel.java @@ -45,6 +45,7 @@ import io.grpc.InternalInstrumented; import io.grpc.InternalLogId; import io.grpc.InternalWithLogId; +import io.grpc.LoadBalancer; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.Status; @@ -77,6 +78,7 @@ final class InternalSubchannel implements InternalInstrumented, Tr private final CallTracer callsTracer; private final ChannelTracer channelTracer; private final ChannelLogger channelLogger; + private final boolean reconnectDisabled; private final List transportFilters; @@ -159,13 +161,15 @@ protected void handleNotInUse() { private volatile Attributes connectedAddressAttributes; - InternalSubchannel(List addressGroups, String authority, String userAgent, - BackoffPolicy.Provider backoffPolicyProvider, - ClientTransportFactory transportFactory, ScheduledExecutorService scheduledExecutor, - Supplier stopwatchSupplier, SynchronizationContext syncContext, Callback callback, - InternalChannelz channelz, CallTracer callsTracer, ChannelTracer channelTracer, - InternalLogId logId, ChannelLogger channelLogger, - List transportFilters) { + InternalSubchannel(LoadBalancer.CreateSubchannelArgs args, String authority, String userAgent, + BackoffPolicy.Provider backoffPolicyProvider, + ClientTransportFactory transportFactory, + ScheduledExecutorService scheduledExecutor, + Supplier stopwatchSupplier, SynchronizationContext syncContext, + Callback callback, InternalChannelz channelz, CallTracer callsTracer, + ChannelTracer channelTracer, InternalLogId logId, + ChannelLogger channelLogger, List transportFilters) { + List addressGroups = args.getAddresses(); Preconditions.checkNotNull(addressGroups, "addressGroups"); Preconditions.checkArgument(!addressGroups.isEmpty(), "addressGroups is empty"); checkListHasNoNulls(addressGroups, "addressGroups contains null entry"); @@ -187,6 +191,7 @@ protected void handleNotInUse() { this.logId = Preconditions.checkNotNull(logId, "logId"); this.channelLogger = Preconditions.checkNotNull(channelLogger, "channelLogger"); this.transportFilters = transportFilters; + this.reconnectDisabled = args.getOption(LoadBalancer.DISABLE_SUBCHANNEL_RECONNECT_KEY); } ChannelLogger getChannelLogger() { @@ -289,6 +294,11 @@ public void run() { } gotoState(ConnectivityStateInfo.forTransientFailure(status)); + + if (reconnectDisabled) { + return; + } + if (reconnectPolicy == null) { reconnectPolicy = backoffPolicyProvider.get(); } @@ -337,7 +347,11 @@ private void gotoState(final ConnectivityStateInfo newState) { if (state.getState() != newState.getState()) { Preconditions.checkState(state.getState() != SHUTDOWN, "Cannot transition out of SHUTDOWN to " + newState); - state = newState; + if (reconnectDisabled && newState.getState() == TRANSIENT_FAILURE) { + state = ConnectivityStateInfo.forNonError(IDLE); + } else { + state = newState; + } callback.onStateChange(InternalSubchannel.this, newState); } } diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 7e36086ac94..36d79f4011b 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -1483,7 +1483,7 @@ void onStateChange(InternalSubchannel is, ConnectivityStateInfo newState) { } final InternalSubchannel internalSubchannel = new InternalSubchannel( - addressGroup, + CreateSubchannelArgs.newBuilder().setAddresses(addressGroup).build(), authority, userAgent, backoffPolicyProvider, oobTransportFactory, oobTransportFactory.getScheduledExecutorService(), stopwatchSupplier, syncContext, // All callback methods are run from syncContext @@ -1915,7 +1915,7 @@ void onNotInUse(InternalSubchannel is) { } final InternalSubchannel internalSubchannel = new InternalSubchannel( - args.getAddresses(), + args, authority(), userAgent, backoffPolicyProvider, diff --git a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java index bfa462e16e1..6f4794fdd46 100644 --- a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java +++ b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java @@ -64,17 +64,26 @@ final class PickFirstLeafLoadBalancer extends LoadBalancer { private int numTf = 0; private boolean firstPass = true; @Nullable - private ScheduledHandle scheduleConnectionTask; + private ScheduledHandle scheduleConnectionTask = null; private ConnectivityState rawConnectivityState = IDLE; private ConnectivityState concludedState = IDLE; - private final boolean enableHappyEyeballs = - PickFirstLoadBalancerProvider.isEnabledHappyEyeballs(); + private final boolean enableHappyEyeballs = !isSerializingRetries() + && PickFirstLoadBalancerProvider.isEnabledHappyEyeballs(); private boolean notAPetiolePolicy = true; // means not under a petiole policy + private final BackoffPolicy.Provider bkoffPolProvider = new ExponentialBackoffPolicy.Provider(); + private BackoffPolicy reconnectPolicy; + @Nullable + private ScheduledHandle reconnectTask = null; + private final boolean serializingRetries = isSerializingRetries(); PickFirstLeafLoadBalancer(Helper helper) { this.helper = checkNotNull(helper, "helper"); } + static boolean isSerializingRetries() { + return GrpcUtil.getFlag("GRPC_SERIALIZE_RETRIES", false); + } + @Override public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { if (rawConnectivityState == SHUTDOWN) { @@ -225,9 +234,10 @@ void processSubchannelState(SubchannelData subchannelData, ConnectivityStateInfo return; } - if (newState == IDLE) { + if (newState == IDLE && subchannelData.state == READY) { helper.refreshNameResolution(); } + // If we are transitioning from a TRANSIENT_FAILURE to CONNECTING or IDLE we ignore this state // transition and still keep the LB in TRANSIENT_FAILURE state. This is referred to as "sticky // transient failure". Only a subchannel state change to READY will get the LB out of @@ -277,6 +287,8 @@ void processSubchannelState(SubchannelData subchannelData, ConnectivityStateInfo if (addressIndex.increment()) { cancelScheduleTask(); requestConnection(); // is recursive so might hit the end of the addresses + } else { + scheduleBackoff(); } } @@ -304,6 +316,39 @@ void processSubchannelState(SubchannelData subchannelData, ConnectivityStateInfo } } + /** + * Only called after all addresses attempted and failed (TRANSIENT_FAILURE). + */ + private void scheduleBackoff() { + if (!serializingRetries) { + return; + } + + class EndOfCurrentBackoff implements Runnable { + @Override + public void run() { + reconnectTask = null; + addressIndex.reset(); + requestConnection(); + } + } + + // Just allow the previous one to trigger when ready if we're already in backoff + if (reconnectTask != null) { + return; + } + + if (reconnectPolicy == null) { + reconnectPolicy = bkoffPolProvider.get(); + } + long delayNanos = reconnectPolicy.nextBackoffNanos(); + reconnectTask = helper.getSynchronizationContext().schedule( + new EndOfCurrentBackoff(), + delayNanos, + TimeUnit.NANOSECONDS, + helper.getScheduledExecutorService()); + } + private void updateHealthCheckedState(SubchannelData subchannelData) { if (subchannelData.state != READY) { return; @@ -337,6 +382,11 @@ public void shutdown() { rawConnectivityState = SHUTDOWN; concludedState = SHUTDOWN; cancelScheduleTask(); + if (reconnectTask != null) { + reconnectTask.cancel(); + reconnectTask = null; + } + reconnectPolicy = null; for (SubchannelData subchannelData : subchannels.values()) { subchannelData.getSubchannel().shutdown(); @@ -350,6 +400,12 @@ public void shutdown() { * that all other subchannels must be shutdown. */ private void shutdownRemaining(SubchannelData activeSubchannelData) { + if (reconnectTask != null) { + reconnectTask.cancel(); + reconnectTask = null; + } + reconnectPolicy = null; + cancelScheduleTask(); for (SubchannelData subchannelData : subchannels.values()) { if (!subchannelData.getSubchannel().equals(activeSubchannelData.subchannel)) { @@ -391,8 +447,17 @@ public void requestConnection() { scheduleNextConnection(); break; case TRANSIENT_FAILURE: - addressIndex.increment(); - requestConnection(); + if (!serializingRetries) { + addressIndex.increment(); + requestConnection(); + } else { + if (!addressIndex.isValid()) { + scheduleBackoff(); + } else { + subchannelData.subchannel.requestConnection(); + subchannelData.updateState(CONNECTING); + } + } break; default: // Wait for current subchannel to change state @@ -438,9 +503,10 @@ private SubchannelData createNewSubchannel(SocketAddress addr, Attributes attrs) HealthListener hcListener = new HealthListener(); final Subchannel subchannel = helper.createSubchannel( CreateSubchannelArgs.newBuilder() - .setAddresses(Lists.newArrayList( - new EquivalentAddressGroup(addr, attrs))) - .addOption(HEALTH_CONSUMER_LISTENER_ARG_KEY, hcListener) + .setAddresses(Lists.newArrayList( + new EquivalentAddressGroup(addr, attrs))) + .addOption(HEALTH_CONSUMER_LISTENER_ARG_KEY, hcListener) + .addOption(LoadBalancer.DISABLE_SUBCHANNEL_RECONNECT_KEY, serializingRetries) .build()); if (subchannel == null) { log.warning("Was not able to create subchannel for " + addr); @@ -458,7 +524,7 @@ private SubchannelData createNewSubchannel(SocketAddress addr, Attributes attrs) } private boolean isPassComplete() { - if (addressIndex.isValid() || subchannels.size() < addressIndex.size()) { + if (subchannels.size() < addressIndex.size()) { return false; } for (SubchannelData sc : subchannels.values()) { @@ -646,6 +712,16 @@ public int size() { } } + @VisibleForTesting + int getGroupIndex() { + return addressIndex.groupIndex; + } + + @VisibleForTesting + boolean isIndexValid() { + return addressIndex.isValid(); + } + private static final class SubchannelData { private final Subchannel subchannel; private ConnectivityState state; diff --git a/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java b/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java index e4d9f27ed46..b75fd43a743 100644 --- a/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java +++ b/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java @@ -46,6 +46,7 @@ import io.grpc.InternalChannelz; import io.grpc.InternalLogId; import io.grpc.InternalWithLogId; +import io.grpc.LoadBalancer; import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.internal.InternalSubchannel.CallTracingTransport; @@ -309,10 +310,57 @@ public void constructor_eagListWithNull_throws() { verify(mockBackoffPolicy2, times(backoff2Consulted)).nextBackoffNanos(); } + @Test public void twoAddressesReconnectDisabled() { + SocketAddress addr1 = mock(SocketAddress.class); + SocketAddress addr2 = mock(SocketAddress.class); + createInternalSubchannel(true, + new EquivalentAddressGroup(Arrays.asList(addr1, addr2))); + assertEquals(IDLE, internalSubchannel.getState()); + + assertNull(internalSubchannel.obtainActiveTransport()); + assertExactCallbackInvokes("onStateChange:CONNECTING"); + assertEquals(CONNECTING, internalSubchannel.getState()); + verify(mockTransportFactory).newClientTransport(eq(addr1), any(), any()); + // Let this one fail without success + transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + // Still in CONNECTING + assertNull(internalSubchannel.obtainActiveTransport()); + assertNoCallbackInvoke(); + assertEquals(CONNECTING, internalSubchannel.getState()); + + // Second attempt will start immediately. Still no back-off policy. + verify(mockBackoffPolicyProvider, times(0)).get(); + verify(mockTransportFactory, times(1)) + .newClientTransport( + eq(addr2), + eq(createClientTransportOptions()), + isA(TransportLogger.class)); + assertNull(internalSubchannel.obtainActiveTransport()); + // Fail this one too + assertNoCallbackInvoke(); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + // All addresses have failed, but we aren't controlling retries. + assertEquals(IDLE, internalSubchannel.getState()); + assertExactCallbackInvokes("onStateChange:" + UNAVAILABLE_STATE); + // Backoff reset and first back-off interval begins + verify(mockBackoffPolicy1, never()).nextBackoffNanos(); + verify(mockBackoffPolicyProvider, never()).get(); + assertTrue("Nothing should have been scheduled", fakeClock.getPendingTasks().isEmpty()); + + // Should follow orders and create an active transport. + internalSubchannel.obtainActiveTransport(); + assertExactCallbackInvokes("onStateChange:CONNECTING"); + assertEquals(CONNECTING, internalSubchannel.getState()); + + // Shouldn't have anything scheduled, so shouldn't do anything + assertTrue("Nothing should have been scheduled 2", fakeClock.getPendingTasks().isEmpty()); + } + @Test public void twoAddressesReconnect() { SocketAddress addr1 = mock(SocketAddress.class); SocketAddress addr2 = mock(SocketAddress.class); - createInternalSubchannel(addr1, addr2); + createInternalSubchannel(false, + new EquivalentAddressGroup(Arrays.asList(addr1, addr2))); assertEquals(IDLE, internalSubchannel.getState()); // Invocation counters int transportsAddr1 = 0; @@ -1377,11 +1425,24 @@ private void createInternalSubchannel(SocketAddress ... addrs) { } private void createInternalSubchannel(EquivalentAddressGroup ... addrs) { + createInternalSubchannel(false, addrs); + } + + private void createInternalSubchannel(boolean reconnectDisabled, + EquivalentAddressGroup ... addrs) { List addressGroups = Arrays.asList(addrs); InternalLogId logId = InternalLogId.allocate("Subchannel", /*details=*/ AUTHORITY); ChannelTracer subchannelTracer = new ChannelTracer(logId, 10, fakeClock.getTimeProvider().currentTimeNanos(), "Subchannel"); - internalSubchannel = new InternalSubchannel(addressGroups, AUTHORITY, USER_AGENT, + LoadBalancer.CreateSubchannelArgs.Builder argBuilder = + LoadBalancer.CreateSubchannelArgs.newBuilder().setAddresses(addressGroups); + if (reconnectDisabled) { + argBuilder.addOption(LoadBalancer.DISABLE_SUBCHANNEL_RECONNECT_KEY, reconnectDisabled); + } + LoadBalancer.CreateSubchannelArgs createSubchannelArgs = argBuilder.build(); + internalSubchannel = new InternalSubchannel( + createSubchannelArgs, + AUTHORITY, USER_AGENT, mockBackoffPolicyProvider, mockTransportFactory, fakeClock.getScheduledExecutorService(), fakeClock.getStopwatchSupplier(), syncContext, mockInternalSubchannelCallback, channelz, CallTracer.getDefaultFactory().create(), diff --git a/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java b/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java index 63915bddc99..f0031a6ae62 100644 --- a/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java @@ -27,10 +27,12 @@ import static io.grpc.LoadBalancer.HEALTH_CONSUMER_LISTENER_ARG_KEY; import static io.grpc.LoadBalancer.IS_PETIOLE_POLICY; import static io.grpc.internal.PickFirstLeafLoadBalancer.CONNECTION_DELAY_INTERVAL_MS; +import static io.grpc.internal.PickFirstLeafLoadBalancer.isSerializingRetries; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assume.assumeTrue; import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -73,7 +75,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.junit.After; -import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -92,14 +93,22 @@ public class PickFirstLeafLoadBalancerTest { public static final Status CONNECTION_ERROR = Status.UNAVAILABLE.withDescription("Simulated connection error"); - - @Parameterized.Parameters(name = "{0}") - public static List enableHappyEyeballs() { - return Arrays.asList(true, false); + public static final String GRPC_SERIALIZE_RETRIES = "GRPC_SERIALIZE_RETRIES"; + + @Parameterized.Parameters(name = "{0}-{1}") + public static List data() { + return Arrays.asList(new Object[][] { + {false, false}, + {false, true}, + {true, false}}); } - @Parameterized.Parameter + @Parameterized.Parameter(value = 0) + public boolean serializeRetries; + + @Parameterized.Parameter(value = 1) public boolean enableHappyEyeballs; + private PickFirstLeafLoadBalancer loadBalancer; private final List servers = Lists.newArrayList(); private static final Attributes.Key FOO = Attributes.Key.create("foo"); @@ -137,13 +146,22 @@ public void uncaughtException(Thread t, Throwable e) { private PickSubchannelArgs mockArgs; private String originalHappyEyeballsEnabledValue; + private String originalSerializeRetriesValue; + + private long backoffMillis; @Before public void setUp() { + assumeTrue(!serializeRetries || !enableHappyEyeballs); // they are not compatible + + backoffMillis = TimeUnit.SECONDS.toMillis(1); + originalSerializeRetriesValue = System.getProperty(GRPC_SERIALIZE_RETRIES); + System.setProperty(GRPC_SERIALIZE_RETRIES, Boolean.toString(serializeRetries)); + originalHappyEyeballsEnabledValue = System.getProperty(PickFirstLoadBalancerProvider.GRPC_PF_USE_HAPPY_EYEBALLS); System.setProperty(PickFirstLoadBalancerProvider.GRPC_PF_USE_HAPPY_EYEBALLS, - enableHappyEyeballs ? "true" : "false"); + Boolean.toString(enableHappyEyeballs)); for (int i = 1; i <= 5; i++) { SocketAddress addr = new FakeSocketAddress("server" + i); @@ -176,6 +194,11 @@ public void setUp() { @After public void tearDown() { + if (originalSerializeRetriesValue == null) { + System.clearProperty(GRPC_SERIALIZE_RETRIES); + } else { + System.setProperty(GRPC_SERIALIZE_RETRIES, originalSerializeRetriesValue); + } if (originalHappyEyeballsEnabledValue == null) { System.clearProperty(PickFirstLoadBalancerProvider.GRPC_PF_USE_HAPPY_EYEBALLS); } else { @@ -498,6 +521,9 @@ public void healthCheckFlow() { inOrder.verify(mockHelper).updateBalancingState(eq(READY), pickerCaptor.capture()); assertThat(pickerCaptor.getValue().pickSubchannel(mockArgs) .getSubchannel()).isSameInstanceAs(mockSubchannel1); + verify(mockHelper, atLeast(0)).getSynchronizationContext(); + verify(mockHelper, atLeast(0)).getScheduledExecutorService(); + verifyNoMoreInteractions(mockHelper); healthListener2.onSubchannelState(ConnectivityStateInfo.forNonError(READY)); verifyNoMoreInteractions(mockHelper); @@ -520,20 +546,7 @@ public void pickAfterStateChangeAfterResolution() { inOrder.verify(mockSubchannel1).start(stateListenerCaptor.capture()); stateListeners[0] = stateListenerCaptor.getValue(); - if (enableHappyEyeballs) { - forwardTimeByConnectionDelay(); - inOrder.verify(mockSubchannel2).start(stateListenerCaptor.capture()); - stateListeners[1] = stateListenerCaptor.getValue(); - forwardTimeByConnectionDelay(); - inOrder.verify(mockSubchannel3).start(stateListenerCaptor.capture()); - stateListeners[2] = stateListenerCaptor.getValue(); - forwardTimeByConnectionDelay(); - inOrder.verify(mockSubchannel4).start(stateListenerCaptor.capture()); - stateListeners[3] = stateListenerCaptor.getValue(); - } - - reset(mockHelper); - + stateListeners[0].onSubchannelState(ConnectivityStateInfo.forNonError(READY)); stateListeners[0].onSubchannelState(ConnectivityStateInfo.forNonError(IDLE)); inOrder.verify(mockHelper).refreshNameResolution(); inOrder.verify(mockHelper).updateBalancingState(eq(IDLE), pickerCaptor.capture()); @@ -543,11 +556,23 @@ public void pickAfterStateChangeAfterResolution() { stateListeners[0].onSubchannelState(ConnectivityStateInfo.forNonError(CONNECTING)); Status error = Status.UNAVAILABLE.withDescription("boom!"); + reset(mockHelper); if (enableHappyEyeballs) { - for (SubchannelStateListener listener : stateListeners) { - listener.onSubchannelState(ConnectivityStateInfo.forTransientFailure(error)); - } + stateListeners[0].onSubchannelState(ConnectivityStateInfo.forTransientFailure(error)); + forwardTimeByConnectionDelay(); + inOrder.verify(mockSubchannel2).start(stateListenerCaptor.capture()); + stateListeners[1] = stateListenerCaptor.getValue(); + stateListeners[1].onSubchannelState(ConnectivityStateInfo.forTransientFailure(error)); + forwardTimeByConnectionDelay(); + inOrder.verify(mockSubchannel3).start(stateListenerCaptor.capture()); + stateListeners[2] = stateListenerCaptor.getValue(); + stateListeners[2].onSubchannelState(ConnectivityStateInfo.forTransientFailure(error)); + forwardTimeByConnectionDelay(); + inOrder.verify(mockSubchannel4).start(stateListenerCaptor.capture()); + stateListeners[3] = stateListenerCaptor.getValue(); + stateListeners[3].onSubchannelState(ConnectivityStateInfo.forTransientFailure(error)); + forwardTimeByConnectionDelay(); } else { stateListeners[0].onSubchannelState(ConnectivityStateInfo.forTransientFailure(error)); for (int i = 1; i < stateListeners.length; i++) { @@ -589,6 +614,8 @@ public void pickAfterResolutionAfterTransientValue() { // Transition from TRANSIENT_ERROR to CONNECTING should also be ignored. stateListener.onSubchannelState(ConnectivityStateInfo.forNonError(CONNECTING)); + verify(mockHelper, atLeast(0)).getSynchronizationContext(); + verify(mockHelper, atLeast(0)).getScheduledExecutorService(); verifyNoMoreInteractions(mockHelper); assertEquals(error, pickerCaptor.getValue().pickSubchannel(mockArgs).getStatus()); } @@ -619,6 +646,8 @@ public void pickWithDupAddressesUpDownUp() { // Transition from TRANSIENT_ERROR to CONNECTING should also be ignored. stateListener.onSubchannelState(ConnectivityStateInfo.forNonError(CONNECTING)); + verify(mockHelper, atLeast(0)).getSynchronizationContext(); + verify(mockHelper, atLeast(0)).getScheduledExecutorService(); verifyNoMoreInteractions(mockHelper); assertEquals(error, pickerCaptor.getValue().pickSubchannel(mockArgs).getStatus()); @@ -651,6 +680,8 @@ public void pickWithDupEagsUpDownUp() { // Transition from TRANSIENT_ERROR to CONNECTING should also be ignored. stateListener.onSubchannelState(ConnectivityStateInfo.forNonError(CONNECTING)); + verify(mockHelper, atLeast(0)).getSynchronizationContext(); + verify(mockHelper, atLeast(0)).getScheduledExecutorService(); verifyNoMoreInteractions(mockHelper); assertEquals(error, pickerCaptor.getValue().pickSubchannel(mockArgs).getStatus()); @@ -1518,6 +1549,8 @@ public void updateAddresses_intersecting_ready() { @Test public void updateAddresses_intersecting_transient_failure() { + assumeTrue(!isSerializingRetries()); + // Starting first connection attempt InOrder inOrder = inOrder(mockHelper, mockSubchannel1, mockSubchannel2, mockSubchannel3, mockSubchannel4); // captor: captures @@ -1782,6 +1815,8 @@ public void updateAddresses_identical_ready() { @Test public void updateAddresses_identical_transient_failure() { + assumeTrue(!isSerializingRetries()); + InOrder inOrder = inOrder(mockHelper, mockSubchannel1, mockSubchannel2, mockSubchannel3, mockSubchannel4); // Creating first set of endpoints/addresses @@ -2295,7 +2330,7 @@ public void ready_then_transient_failure_again() { @Test public void happy_eyeballs_trigger_connection_delay() { - Assume.assumeTrue(enableHappyEyeballs); // This test is only for happy eyeballs + assumeTrue(enableHappyEyeballs); // This test is only for happy eyeballs // Starting first connection attempt InOrder inOrder = inOrder(mockHelper, mockSubchannel1, mockSubchannel2, mockSubchannel3, mockSubchannel4); @@ -2340,7 +2375,7 @@ public void happy_eyeballs_trigger_connection_delay() { @Test public void happy_eyeballs_connection_results_happen_after_get_to_end() { - Assume.assumeTrue(enableHappyEyeballs); // This test is only for happy eyeballs + assumeTrue(enableHappyEyeballs); // This test is only for happy eyeballs InOrder inOrder = inOrder(mockHelper, mockSubchannel1, mockSubchannel2, mockSubchannel3); Status error = Status.UNAUTHENTICATED.withDescription("simulated failure"); @@ -2393,7 +2428,7 @@ public void happy_eyeballs_connection_results_happen_after_get_to_end() { @Test public void happy_eyeballs_pick_pushes_index_over_end() { - Assume.assumeTrue(enableHappyEyeballs); // This test is only for happy eyeballs + assumeTrue(enableHappyEyeballs); // This test is only for happy eyeballs InOrder inOrder = inOrder(mockHelper, mockSubchannel1, mockSubchannel2, mockSubchannel3, mockSubchannel2n2, mockSubchannel3n2); @@ -2471,7 +2506,7 @@ public void happy_eyeballs_pick_pushes_index_over_end() { @Test public void happy_eyeballs_fail_then_trigger_connection_delay() { - Assume.assumeTrue(enableHappyEyeballs); // This test is only for happy eyeballs + assumeTrue(enableHappyEyeballs); // This test is only for happy eyeballs // Starting first connection attempt InOrder inOrder = inOrder(mockHelper, mockSubchannel1, mockSubchannel2, mockSubchannel3); assertEquals(IDLE, loadBalancer.getConcludedConnectivityState()); @@ -2550,6 +2585,44 @@ public void advance_index_then_request_connection() { loadBalancer.requestConnection(); // should be handled without throwing exception } + @Test + public void serialized_retries_two_passes() { + assumeTrue(serializeRetries); // This test is only for serialized retries + + InOrder inOrder = inOrder(mockHelper, mockSubchannel1, mockSubchannel2, mockSubchannel3); + Status error = Status.UNAUTHENTICATED.withDescription("simulated failure"); + + List addrs = + Lists.newArrayList(servers.get(0), servers.get(1), servers.get(2)); + Subchannel[] subchannels = new Subchannel[]{mockSubchannel1, mockSubchannel2, mockSubchannel3}; + SubchannelStateListener[] listeners = new SubchannelStateListener[subchannels.length]; + loadBalancer.acceptResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(addrs).build()); + forwardTimeByConnectionDelay(2); + for (int i = 0; i < subchannels.length; i++) { + inOrder.verify(subchannels[i]).start(stateListenerCaptor.capture()); + inOrder.verify(subchannels[i]).requestConnection(); + listeners[i] = stateListenerCaptor.getValue(); + listeners[i].onSubchannelState(ConnectivityStateInfo.forTransientFailure(error)); + } + assertEquals(TRANSIENT_FAILURE, loadBalancer.getConcludedConnectivityState()); + assertFalse("Index should be at end", loadBalancer.isIndexValid()); + + forwardTimeByBackoffDelay(); // should trigger retry + for (int i = 0; i < subchannels.length; i++) { + inOrder.verify(subchannels[i]).requestConnection(); + listeners[i].onSubchannelState(ConnectivityStateInfo.forTransientFailure(error)); // cascade + } + inOrder.verify(subchannels[0], never()).requestConnection(); // should wait for backoff delay + + forwardTimeByBackoffDelay(); // should trigger retry again + for (int i = 0; i < subchannels.length; i++) { + inOrder.verify(subchannels[i]).requestConnection(); + assertEquals(i, loadBalancer.getGroupIndex()); + listeners[i].onSubchannelState(ConnectivityStateInfo.forTransientFailure(error)); // cascade + } + } + @Test public void index_looping() { Attributes.Key key = Attributes.Key.create("some-key"); @@ -2689,6 +2762,11 @@ private void forwardTimeByConnectionDelay(int times) { } } + private void forwardTimeByBackoffDelay() { + backoffMillis = (long) (backoffMillis * 1.8); // backoff factor default is 1.6 with Jitter .2 + fakeClock.forwardTime(backoffMillis, TimeUnit.MILLISECONDS); + } + private void acceptXSubchannels(int num) { List newServers = new ArrayList<>(); for (int i = 0; i < num; i++) { diff --git a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java index 047ba71bbe0..dd7de3691a7 100644 --- a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java @@ -150,7 +150,8 @@ public void subchannelLazyConnectUntilPicked() { assertThat(result.getStatus().isOk()).isTrue(); assertThat(result.getSubchannel()).isNull(); Subchannel subchannel = Iterables.getOnlyElement(subchannels.values()); - int expectedTimes = PickFirstLoadBalancerProvider.isEnabledHappyEyeballs() ? 1 : 2; + int expectedTimes = PickFirstLoadBalancerProvider.isEnabledNewPickFirst() + && !PickFirstLoadBalancerProvider.isEnabledHappyEyeballs() ? 1 : 2; verify(subchannel, times(expectedTimes)).requestConnection(); verify(helper).updateBalancingState(eq(CONNECTING), any(SubchannelPicker.class)); verify(helper).createSubchannel(any(CreateSubchannelArgs.class)); @@ -184,7 +185,8 @@ public void subchannelNotAutoReconnectAfterReenteringIdle() { pickerCaptor.getValue().pickSubchannel(args); Subchannel subchannel = subchannels.get(Collections.singletonList(childLbState.getEag())); InOrder inOrder = Mockito.inOrder(helper, subchannel); - int expectedTimes = PickFirstLoadBalancerProvider.isEnabledHappyEyeballs() ? 1 : 2; + int expectedTimes = PickFirstLoadBalancerProvider.isEnabledHappyEyeballs() + || !PickFirstLoadBalancerProvider.isEnabledNewPickFirst() ? 2 : 1; inOrder.verify(subchannel, times(expectedTimes)).requestConnection(); deliverSubchannelState(subchannel, CSI_READY); inOrder.verify(helper).updateBalancingState(eq(READY), any(SubchannelPicker.class)); @@ -443,8 +445,10 @@ public void skipFailingHosts_pickNextNonFailingHost() { PickResult result = pickerCaptor.getValue().pickSubchannel(args); assertThat(result.getStatus().isOk()).isTrue(); assertThat(result.getSubchannel()).isNull(); // buffer request - int expectedTimes = PickFirstLoadBalancerProvider.isEnabledHappyEyeballs() ? 1 : 2; // verify kicked off connection to server2 + int expectedTimes = PickFirstLoadBalancerProvider.isEnabledHappyEyeballs() + || !PickFirstLoadBalancerProvider.isEnabledNewPickFirst() ? 2 : 1; + verify(getSubChannel(servers.get(1)), times(expectedTimes)).requestConnection(); assertThat(subchannels.size()).isEqualTo(2); // no excessive connection From 35f0d56894d50c0105ab60eb121fe8363c386333 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:45:01 -0700 Subject: [PATCH 036/591] s2a: don't use reflection to load token manager (#11590) --- s2a/BUILD.bazel | 31 +++---------------- .../tokenmanager/AccessTokenManager.java | 15 ++------- 2 files changed, 7 insertions(+), 39 deletions(-) diff --git a/s2a/BUILD.bazel b/s2a/BUILD.bazel index 676215e1e1e..283828a9f7e 100644 --- a/s2a/BUILD.bazel +++ b/s2a/BUILD.bazel @@ -32,33 +32,13 @@ java_library( ) java_library( - name = "token_fetcher", - srcs = ["src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/TokenFetcher.java"], - deps = [ - ":s2a_identity", - ], -) - -java_library( - name = "access_token_manager", - srcs = [ - "src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/AccessTokenManager.java", - ], + name = "token_manager", + srcs = glob([ + "src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/*.java", + ]), deps = [ ":s2a_identity", - ":token_fetcher", artifact("com.google.code.findbugs:jsr305"), - ], -) - -java_library( - name = "single_token_fetcher", - srcs = [ - "src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenFetcher.java", - ], - deps = [ - ":s2a_identity", - ":token_fetcher", artifact("com.google.guava:guava"), ], ) @@ -77,13 +57,12 @@ java_library( "src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java", ], deps = [ - ":access_token_manager", + ":token_manager", ":common_java_proto", ":s2a_channel_pool", ":s2a_identity", ":s2a_java_proto", ":s2a_java_grpc_proto", - ":single_token_fetcher", "//api", "//core:internal", "//netty", diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/AccessTokenManager.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/AccessTokenManager.java index 71e55b29fcd..65fca46bbb2 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/AccessTokenManager.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/AccessTokenManager.java @@ -17,7 +17,6 @@ package io.grpc.s2a.internal.handshaker.tokenmanager; import io.grpc.s2a.internal.handshaker.S2AIdentity; -import java.lang.reflect.Method; import java.util.Optional; import javax.annotation.concurrent.ThreadSafe; @@ -28,19 +27,9 @@ public final class AccessTokenManager { /** Creates an {@code AccessTokenManager} based on the environment where the application runs. */ public static Optional create() { - Optional tokenFetcher; - try { - Class singleTokenFetcherClass = - Class.forName("io.grpc.s2a.internal.handshaker.tokenmanager.SingleTokenFetcher"); - Method createTokenFetcher = singleTokenFetcherClass.getMethod("create"); - tokenFetcher = (Optional) createTokenFetcher.invoke(null); - } catch (ClassNotFoundException e) { - tokenFetcher = Optional.empty(); - } catch (ReflectiveOperationException e) { - throw new LinkageError(e.getMessage(), e); - } + Optional tokenFetcher = SingleTokenFetcher.create(); return tokenFetcher.isPresent() - ? Optional.of(new AccessTokenManager((TokenFetcher) tokenFetcher.get())) + ? Optional.of(new AccessTokenManager(tokenFetcher.get())) : Optional.empty(); } From 94a0a0d1c7af25aaf46f6f36353afc16e45f6b2b Mon Sep 17 00:00:00 2001 From: vinodhabib <47808007+vinodhabib@users.noreply.github.com> Date: Fri, 4 Oct 2024 11:42:25 +0530 Subject: [PATCH 037/591] example-gauth: Use application default creds instead of file argument (#11595) Also removed unnecessary refreshAccessToken() and fixed the reference to README.md. Fixes #5677 --- examples/example-gauth/README.md | 21 +++++++-------- .../examples/googleAuth/GoogleAuthClient.java | 26 +++++++++---------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/examples/example-gauth/README.md b/examples/example-gauth/README.md index 622c14cb57b..b49d346a9be 100644 --- a/examples/example-gauth/README.md +++ b/examples/example-gauth/README.md @@ -43,13 +43,13 @@ gcloud pubsub topics create Topic1 5. You will now need to set up [authentication](https://cloud.google.com/docs/authentication/) and a [service account](https://cloud.google.com/docs/authentication/#service_accounts) in order to access Pub/Sub via gRPC APIs as described [here](https://cloud.google.com/iam/docs/creating-managing-service-accounts). -Assign the [role](https://cloud.google.com/iam/docs/granting-roles-to-service-accounts) `Project -> Owner` +(**Note:** This step is unnecessary on Google platforms (Google App Engine / Google Cloud Shell / Google Compute Engine) as it will +automatically use the in-built Google credentials). Assign the [role](https://cloud.google.com/iam/docs/granting-roles-to-service-accounts) `Project -> Owner` and for Key type select JSON. Once you click `Create`, a JSON file containing your key is downloaded to your computer. Note down the path of this file or copy this file to the computer and file system where you will be running the example application as described later. Assume this JSON file is available at -`/path/to/JSON/file`. You can also use the `gcloud` shell commands to -[create the service account](https://cloud.google.com/iam/docs/creating-managing-service-accounts#iam-service-accounts-create-gcloud) -and [the JSON file](https://cloud.google.com/iam/docs/creating-managing-service-account-keys#iam-service-account-keys-create-gcloud). +`/path/to/JSON/file` Set the value of the environment variable GOOGLE_APPLICATION_CREDENTIALS to this file path. You can also use the `gcloud` shell commands to +[create the service account](https://cloud.google.com/iam/docs/creating-managing-service-accounts#iam-service-accounts-create-gcloud). #### To build the examples @@ -62,19 +62,18 @@ $ ../gradlew installDist #### How to run the example: -`google-auth-client` requires two command line arguments for the location of the JSON file and the project ID: +`google-auth-client` requires one command line argument for the project ID: ```text -USAGE: GoogleAuthClient +USAGE: GoogleAuthClient ``` -The first argument is the location of the JSON file you created in step 5 above. -The second argument is the project ID in the form "projects/xyz123" where "xyz123" is +The first argument is the project ID in the form "projects/xyz123" where "xyz123" is the project ID of the project you created (or used) in step 2 above. ```bash # Run the client -./build/install/example-gauth/bin/google-auth-client /path/to/JSON/file projects/xyz123 +./build/install/example-gauth/bin/google-auth-client projects/xyz123 ``` That's it! The client will show the list of Pub/Sub topics for the project as follows: @@ -93,7 +92,7 @@ the project ID of the project you created (or used) in step 2 above. ``` $ mvn verify $ # Run the client - $ mvn exec:java -Dexec.mainClass=io.grpc.examples.googleAuth.GoogleAuthClient -Dexec.args="/path/to/JSON/file projects/xyz123" + $ mvn exec:java -Dexec.mainClass=io.grpc.examples.googleAuth.GoogleAuthClient -Dexec.args="projects/xyz123" ``` ## Bazel @@ -101,5 +100,5 @@ the project ID of the project you created (or used) in step 2 above. ``` $ bazel build :google-auth-client $ # Run the client - $ ../bazel-bin/google-auth-client /path/to/JSON/file projects/xyz123 + $ ../bazel-bin/google-auth-client projects/xyz123 ``` \ No newline at end of file diff --git a/examples/example-gauth/src/main/java/io/grpc/examples/googleAuth/GoogleAuthClient.java b/examples/example-gauth/src/main/java/io/grpc/examples/googleAuth/GoogleAuthClient.java index 4d3dd044376..eb0d9feedfc 100644 --- a/examples/example-gauth/src/main/java/io/grpc/examples/googleAuth/GoogleAuthClient.java +++ b/examples/example-gauth/src/main/java/io/grpc/examples/googleAuth/GoogleAuthClient.java @@ -33,7 +33,7 @@ /** * Example to illustrate use of Google credentials as described in - * @see Google Auth Example README + * @see Google Auth Example README * * Also @see Google Cloud Pubsub via gRPC */ @@ -52,7 +52,7 @@ public class GoogleAuthClient { * * @param host host to connect to - typically "pubsub.googleapis.com" * @param port port to connect to - typically 443 - the TLS port - * @param callCredentials the Google call credentials created from a JSON file + * @param callCredentials the Google call credentials */ public GoogleAuthClient(String host, int port, CallCredentials callCredentials) { // Google API invocation requires a secure channel. Channels are secure by default (SSL/TLS) @@ -63,7 +63,7 @@ public GoogleAuthClient(String host, int port, CallCredentials callCredentials) * Construct our gRPC client that connects to the pubsub server using an existing channel. * * @param channel channel that has been built already - * @param callCredentials the Google call credentials created from a JSON file + * @param callCredentials the Google call credentials */ GoogleAuthClient(ManagedChannel channel, CallCredentials callCredentials) { this.channel = channel; @@ -101,32 +101,30 @@ public void getTopics(String projectID) { /** * The app requires 2 arguments as described in - * @see Google Auth Example README + * @see Google Auth Example README * - * arg0 = location of the JSON file for the service account you created in the GCP console - * arg1 = project name in the form "projects/balmy-cirrus-225307" where "balmy-cirrus-225307" is + * arg0 = project name in the form "projects/balmy-cirrus-225307" where "balmy-cirrus-225307" is * the project ID for the project you created. * + * On non-Google platforms, the GOOGLE_APPLICATION_CREDENTIALS env variable should be set to the + * location of the JSON file for the service account you created in the GCP console. */ public static void main(String[] args) throws Exception { - if (args.length < 2) { - logger.severe("Usage: please pass 2 arguments:\n" + - "arg0 = location of the JSON file for the service account you created in the GCP console\n" + - "arg1 = project name in the form \"projects/xyz\" where \"xyz\" is the project ID of the project you created.\n"); + if (args.length < 1) { + logger.severe("Usage: please pass 1 argument:\n" + + "arg0 = project name in the form \"projects/xyz\" where \"xyz\" is the project ID of the project you created.\n"); System.exit(1); } - GoogleCredentials credentials = GoogleCredentials.fromStream(new FileInputStream(args[0])); + GoogleCredentials credentials = GoogleCredentials.getApplicationDefault(); // We need to create appropriate scope as per https://cloud.google.com/storage/docs/authentication#oauth-scopes credentials = credentials.createScoped(Arrays.asList("https://www.googleapis.com/auth/cloud-platform")); - // credentials must be refreshed before the access token is available - credentials.refreshAccessToken(); GoogleAuthClient client = new GoogleAuthClient("pubsub.googleapis.com", 443, MoreCallCredentials.from(credentials)); try { - client.getTopics(args[1]); + client.getTopics(args[0]); } finally { client.shutdown(); } From 1ded8aff814d4064902337f264acec8b5cde419c Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 7 Oct 2024 17:55:56 +0530 Subject: [PATCH 038/591] On result2 resolution result have addresses or error (#11330) Combined success / error status passed via ResolutionResult to the NameResolver.Listener2 interface's onResult2 method - Addresses in the success case or address resolution error in the failure case now get set in ResolutionResult::addressesOrError by the internal name resolvers. --- api/src/main/java/io/grpc/NameResolver.java | 71 +++++++---- api/src/main/java/io/grpc/StatusOr.java | 102 ++++++++++++++++ .../test/java/io/grpc/NameResolverTest.java | 110 ++++++++++++++++++ api/src/test/java/io/grpc/StatusOrTest.java | 81 +++++++++++++ build.gradle | 1 + .../io/grpc/internal/DnsNameResolver.java | 22 +++- .../io/grpc/internal/ManagedChannelImpl.java | 28 +++-- .../internal/ManagedChannelImplBuilder.java | 7 +- .../io/grpc/internal/DnsNameResolverTest.java | 65 ++++++++--- .../ManagedChannelImplIdlenessTest.java | 3 +- .../grpc/internal/ManagedChannelImplTest.java | 36 ++++-- .../grpc/grpclb/GrpclbNameResolverTest.java | 19 ++- .../java/io/grpc/netty/UdsNameResolver.java | 8 +- .../grpc/netty/UdsNameResolverProvider.java | 2 +- .../netty/UdsNameResolverProviderTest.java | 41 +++++-- .../io/grpc/netty/UdsNameResolverTest.java | 29 ++++- .../testing/FakeNameResolverProvider.java | 6 +- .../xds/ClusterResolverLoadBalancerTest.java | 4 +- .../java/io/grpc/xds/XdsNameResolverTest.java | 10 +- .../grpc/xds/XdsSecurityClientServerTest.java | 4 +- 20 files changed, 541 insertions(+), 108 deletions(-) create mode 100644 api/src/main/java/io/grpc/StatusOr.java create mode 100644 api/src/test/java/io/grpc/StatusOrTest.java diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java index bfb9c2a43a1..b9590ab5d5a 100644 --- a/api/src/main/java/io/grpc/NameResolver.java +++ b/api/src/main/java/io/grpc/NameResolver.java @@ -20,13 +20,13 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.MoreObjects; +import com.google.common.base.MoreObjects.ToStringHelper; import com.google.common.base.Objects; import com.google.errorprone.annotations.InlineMe; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.URI; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -95,7 +95,8 @@ public void onError(Status error) { @Override public void onResult(ResolutionResult resolutionResult) { - listener.onAddresses(resolutionResult.getAddresses(), resolutionResult.getAttributes()); + listener.onAddresses(resolutionResult.getAddressesOrError().getValue(), + resolutionResult.getAttributes()); } }); } @@ -218,19 +219,21 @@ public abstract static class Listener2 implements Listener { @Override @Deprecated @InlineMe( - replacement = "this.onResult(ResolutionResult.newBuilder().setAddresses(servers)" - + ".setAttributes(attributes).build())", - imports = "io.grpc.NameResolver.ResolutionResult") + replacement = "this.onResult2(ResolutionResult.newBuilder().setAddressesOrError(" + + "StatusOr.fromValue(servers)).setAttributes(attributes).build())", + imports = {"io.grpc.NameResolver.ResolutionResult", "io.grpc.StatusOr"}) public final void onAddresses( List servers, @ResolutionResultAttr Attributes attributes) { // TODO(jihuncho) need to promote Listener2 if we want to use ConfigOrError - onResult( - ResolutionResult.newBuilder().setAddresses(servers).setAttributes(attributes).build()); + onResult2( + ResolutionResult.newBuilder().setAddressesOrError( + StatusOr.fromValue(servers)).setAttributes(attributes).build()); } /** * Handles updates on resolved addresses and attributes. If - * {@link ResolutionResult#getAddresses()} is empty, {@link #onError(Status)} will be called. + * {@link ResolutionResult#getAddressesOrError()} is empty, {@link #onError(Status)} will be + * called. * * @param resolutionResult the resolved server addresses, attributes, and Service Config. * @since 1.21.0 @@ -584,17 +587,17 @@ public abstract static class ServiceConfigParser { */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770") public static final class ResolutionResult { - private final List addresses; + private final StatusOr> addressesOrError; @ResolutionResultAttr private final Attributes attributes; @Nullable private final ConfigOrError serviceConfig; ResolutionResult( - List addresses, + StatusOr> addressesOrError, @ResolutionResultAttr Attributes attributes, ConfigOrError serviceConfig) { - this.addresses = Collections.unmodifiableList(new ArrayList<>(addresses)); + this.addressesOrError = addressesOrError; this.attributes = checkNotNull(attributes, "attributes"); this.serviceConfig = serviceConfig; } @@ -615,7 +618,7 @@ public static Builder newBuilder() { */ public Builder toBuilder() { return newBuilder() - .setAddresses(addresses) + .setAddressesOrError(addressesOrError) .setAttributes(attributes) .setServiceConfig(serviceConfig); } @@ -624,9 +627,20 @@ public Builder toBuilder() { * Gets the addresses resolved by name resolution. * * @since 1.21.0 + * @deprecated Will be superseded by getAddressesOrError */ + @Deprecated public List getAddresses() { - return addresses; + return addressesOrError.getValue(); + } + + /** + * Gets the addresses resolved by name resolution or the error in doing so. + * + * @since 1.65.0 + */ + public StatusOr> getAddressesOrError() { + return addressesOrError; } /** @@ -652,11 +666,11 @@ public ConfigOrError getServiceConfig() { @Override public String toString() { - return MoreObjects.toStringHelper(this) - .add("addresses", addresses) - .add("attributes", attributes) - .add("serviceConfig", serviceConfig) - .toString(); + ToStringHelper stringHelper = MoreObjects.toStringHelper(this); + stringHelper.add("addressesOrError", addressesOrError.toString()); + stringHelper.add("attributes", attributes); + stringHelper.add("serviceConfigOrError", serviceConfig); + return stringHelper.toString(); } /** @@ -668,7 +682,7 @@ public boolean equals(Object obj) { return false; } ResolutionResult that = (ResolutionResult) obj; - return Objects.equal(this.addresses, that.addresses) + return Objects.equal(this.addressesOrError, that.addressesOrError) && Objects.equal(this.attributes, that.attributes) && Objects.equal(this.serviceConfig, that.serviceConfig); } @@ -678,7 +692,7 @@ public boolean equals(Object obj) { */ @Override public int hashCode() { - return Objects.hashCode(addresses, attributes, serviceConfig); + return Objects.hashCode(addressesOrError, attributes, serviceConfig); } /** @@ -688,7 +702,8 @@ public int hashCode() { */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770") public static final class Builder { - private List addresses = Collections.emptyList(); + private StatusOr> addresses = + StatusOr.fromValue(Collections.emptyList()); private Attributes attributes = Attributes.EMPTY; @Nullable private ConfigOrError serviceConfig; @@ -700,9 +715,21 @@ public static final class Builder { * Sets the addresses resolved by name resolution. This field is required. * * @since 1.21.0 + * @deprecated Will be superseded by setAddressesOrError */ + @Deprecated public Builder setAddresses(List addresses) { - this.addresses = addresses; + setAddressesOrError(StatusOr.fromValue(addresses)); + return this; + } + + /** + * Sets the addresses resolved by name resolution or the error in doing so. This field is + * required. + * @param addresses Resolved addresses or an error in resolving addresses + */ + public Builder setAddressesOrError(StatusOr> addresses) { + this.addresses = checkNotNull(addresses, "StatusOr addresses cannot be null."); return this; } diff --git a/api/src/main/java/io/grpc/StatusOr.java b/api/src/main/java/io/grpc/StatusOr.java new file mode 100644 index 00000000000..8a88d9e62d0 --- /dev/null +++ b/api/src/main/java/io/grpc/StatusOr.java @@ -0,0 +1,102 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.MoreObjects; +import com.google.common.base.MoreObjects.ToStringHelper; +import com.google.common.base.Objects; +import javax.annotation.Nullable; + +/** Either a Status or a value. */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/11563") +public class StatusOr { + private StatusOr(Status status, T value) { + this.status = status; + this.value = value; + } + + /** Construct from a value. */ + public static StatusOr fromValue(@Nullable T value) { + StatusOr result = new StatusOr(null, value); + return result; + } + + /** Construct from a non-Ok status. */ + public static StatusOr fromStatus(Status status) { + StatusOr result = new StatusOr(checkNotNull(status, "status"), null); + checkArgument(!status.isOk(), "cannot use OK status: %s", status); + return result; + } + + /** Returns whether there is a value. */ + public boolean hasValue() { + return status == null; + } + + /** + * Returns the value if set or throws exception if there is no value set. This method is meant + * to be called after checking the return value of hasValue() first. + */ + public @Nullable T getValue() { + if (status != null) { + throw new IllegalStateException("No value present."); + } + return value; + } + + /** Returns the status. If there is a value (which can be null), returns OK. */ + public Status getStatus() { + return status == null ? Status.OK : status; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof StatusOr)) { + return false; + } + StatusOr otherStatus = (StatusOr) other; + if (hasValue() != otherStatus.hasValue()) { + return false; + } + if (hasValue()) { + return Objects.equal(value, otherStatus.value); + } + return Objects.equal(status, otherStatus.status); + } + + @Override + public int hashCode() { + return Objects.hashCode(status, value); + } + + @Override + public String toString() { + ToStringHelper stringHelper = MoreObjects.toStringHelper(this); + if (status == null) { + stringHelper.add("value", value); + } else { + stringHelper.add("error", status); + } + return stringHelper.toString(); + } + + private final Status status; + private final T value; +} diff --git a/api/src/test/java/io/grpc/NameResolverTest.java b/api/src/test/java/io/grpc/NameResolverTest.java index f825de354af..1bc32ee7b1d 100644 --- a/api/src/test/java/io/grpc/NameResolverTest.java +++ b/api/src/test/java/io/grpc/NameResolverTest.java @@ -17,20 +17,43 @@ package io.grpc; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import com.google.common.base.Objects; +import io.grpc.NameResolver.ConfigOrError; +import io.grpc.NameResolver.Listener2; +import io.grpc.NameResolver.ResolutionResult; import io.grpc.NameResolver.ServiceConfigParser; import java.lang.Thread.UncaughtExceptionHandler; +import java.net.SocketAddress; +import java.util.Collections; +import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** Unit tests for the inner classes in {@link NameResolver}. */ @RunWith(JUnit4.class) public class NameResolverTest { + private static final List ADDRESSES = + Collections.singletonList( + new EquivalentAddressGroup(new FakeSocketAddress("fake-address-1"), Attributes.EMPTY)); + private static final Attributes.Key YOLO_KEY = Attributes.Key.create("yolo"); + private static Attributes ATTRIBUTES = Attributes.newBuilder() + .set(YOLO_KEY, "To be, or not to be?").build(); + private static ConfigOrError CONFIG = ConfigOrError.fromConfig("foo"); + + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); private final int defaultPort = 293; private final ProxyDetector proxyDetector = mock(ProxyDetector.class); private final SynchronizationContext syncContext = @@ -41,6 +64,7 @@ public class NameResolverTest { private final ChannelLogger channelLogger = mock(ChannelLogger.class); private final Executor executor = Executors.newSingleThreadExecutor(); private final String overrideAuthority = "grpc.io"; + @Mock NameResolver.Listener mockListener; @Test public void args() { @@ -80,4 +104,90 @@ private NameResolver.Args createArgs() { .setOverrideAuthority(overrideAuthority) .build(); } + + @Test + @SuppressWarnings("deprecation") + public void startOnOldListener_wrapperListener2UsedToStart() { + final Listener2[] listener2 = new Listener2[1]; + NameResolver nameResolver = new NameResolver() { + @Override + public String getServiceAuthority() { + return null; + } + + @Override + public void shutdown() {} + + @Override + public void start(Listener2 listener2Arg) { + listener2[0] = listener2Arg; + } + }; + nameResolver.start(mockListener); + + listener2[0].onResult(ResolutionResult.newBuilder().setAddresses(ADDRESSES) + .setAttributes(ATTRIBUTES).build()); + verify(mockListener).onAddresses(eq(ADDRESSES), eq(ATTRIBUTES)); + listener2[0].onError(Status.CANCELLED); + verify(mockListener).onError(Status.CANCELLED); + } + + @Test + @SuppressWarnings({"deprecation", "InlineMeInliner"}) + public void listener2AddressesToListener2ResolutionResultConversion() { + final ResolutionResult[] resolutionResult = new ResolutionResult[1]; + NameResolver.Listener2 listener2 = new Listener2() { + @Override + public void onResult(ResolutionResult resolutionResultArg) { + resolutionResult[0] = resolutionResultArg; + } + + @Override + public void onError(Status error) {} + }; + + listener2.onAddresses(ADDRESSES, ATTRIBUTES); + + assertThat(resolutionResult[0].getAddressesOrError().getValue()).isEqualTo(ADDRESSES); + assertThat(resolutionResult[0].getAttributes()).isEqualTo(ATTRIBUTES); + } + + @Test + public void resolutionResult_toString_addressesAttributesAndConfig() { + ResolutionResult resolutionResult = ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromValue(ADDRESSES)) + .setAttributes(ATTRIBUTES) + .setServiceConfig(CONFIG) + .build(); + + assertThat(resolutionResult.toString()).isEqualTo( + "ResolutionResult{addressesOrError=StatusOr{value=" + + "[[[FakeSocketAddress-fake-address-1]/{}]]}, attributes={yolo=To be, or not to be?}, " + + "serviceConfigOrError=ConfigOrError{config=foo}}"); + } + + @Test + public void resolutionResult_hashCode() { + ResolutionResult resolutionResult = ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromValue(ADDRESSES)) + .setAttributes(ATTRIBUTES) + .setServiceConfig(CONFIG) + .build(); + + assertThat(resolutionResult.hashCode()).isEqualTo( + Objects.hashCode(StatusOr.fromValue(ADDRESSES), ATTRIBUTES, CONFIG)); + } + + private static class FakeSocketAddress extends SocketAddress { + final String name; + + FakeSocketAddress(String name) { + this.name = name; + } + + @Override + public String toString() { + return "FakeSocketAddress-" + name; + } + } } diff --git a/api/src/test/java/io/grpc/StatusOrTest.java b/api/src/test/java/io/grpc/StatusOrTest.java new file mode 100644 index 00000000000..f63a314a2bb --- /dev/null +++ b/api/src/test/java/io/grpc/StatusOrTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import static com.google.common.truth.Truth.assertThat; +import static junit.framework.TestCase.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link StatusOr}. **/ +@RunWith(JUnit4.class) +public class StatusOrTest { + + @Test + public void getValue_throwsIfNoValuePresent() { + try { + StatusOr.fromStatus(Status.ABORTED).getValue(); + + fail("Expected exception."); + } catch (IllegalStateException expected) { } + } + + @Test + @SuppressWarnings("TruthIncompatibleType") + public void equals_differentValueTypes() { + assertThat(StatusOr.fromValue(1)).isNotEqualTo(StatusOr.fromValue("1")); + } + + @Test + public void equals_differentValues() { + assertThat(StatusOr.fromValue(1)).isNotEqualTo(StatusOr.fromValue(2)); + } + + @Test + public void equals_sameValues() { + assertThat(StatusOr.fromValue(1)).isEqualTo(StatusOr.fromValue(1)); + } + + @Test + public void equals_differentStatuses() { + assertThat(StatusOr.fromStatus(Status.ABORTED)).isNotEqualTo( + StatusOr.fromStatus(Status.CANCELLED)); + } + + @Test + public void equals_sameStatuses() { + assertThat(StatusOr.fromStatus(Status.ABORTED)).isEqualTo(StatusOr.fromStatus(Status.ABORTED)); + } + + @Test + public void toString_value() { + assertThat(StatusOr.fromValue(1).toString()).isEqualTo("StatusOr{value=1}"); + } + + @Test + public void toString_nullValue() { + assertThat(StatusOr.fromValue(null).toString()).isEqualTo("StatusOr{value=null}"); + } + + @Test + public void toString_errorStatus() { + assertThat(StatusOr.fromStatus(Status.ABORTED).toString()).isEqualTo( + "StatusOr{error=Status{code=ABORTED, description=null, cause=null}}"); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 740f534e136..6afe010de4d 100644 --- a/build.gradle +++ b/build.gradle @@ -500,3 +500,4 @@ configurations { } tasks.register('checkForUpdates', CheckForUpdatesTask, project.configurations.checkForUpdates, "libs") + diff --git a/core/src/main/java/io/grpc/internal/DnsNameResolver.java b/core/src/main/java/io/grpc/internal/DnsNameResolver.java index df51d6f2c5c..b59de833d7c 100644 --- a/core/src/main/java/io/grpc/internal/DnsNameResolver.java +++ b/core/src/main/java/io/grpc/internal/DnsNameResolver.java @@ -32,6 +32,7 @@ import io.grpc.ProxiedSocketAddress; import io.grpc.ProxyDetector; import io.grpc.Status; +import io.grpc.StatusOr; import io.grpc.SynchronizationContext; import io.grpc.internal.SharedResourceHolder.Resource; import java.io.IOException; @@ -59,7 +60,7 @@ * A DNS-based {@link NameResolver}. * *

Each {@code A} or {@code AAAA} record emits an {@link EquivalentAddressGroup} in the list - * passed to {@link NameResolver.Listener2#onResult(ResolutionResult)}. + * passed to {@link NameResolver.Listener2#onResult2(ResolutionResult)}. * * @see DnsNameResolverProvider */ @@ -313,15 +314,20 @@ public void run() { if (logger.isLoggable(Level.FINER)) { logger.finer("Using proxy address " + proxiedAddr); } - resolutionResultBuilder.setAddresses(Collections.singletonList(proxiedAddr)); + resolutionResultBuilder.setAddressesOrError( + StatusOr.fromValue(Collections.singletonList(proxiedAddr))); } else { result = doResolve(false); if (result.error != null) { - savedListener.onError(result.error); + InternalResolutionResult finalResult = result; + syncContext.execute(() -> + savedListener.onResult2(ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromStatus(finalResult.error)) + .build())); return; } if (result.addresses != null) { - resolutionResultBuilder.setAddresses(result.addresses); + resolutionResultBuilder.setAddressesOrError(StatusOr.fromValue(result.addresses)); } if (result.config != null) { resolutionResultBuilder.setServiceConfig(result.config); @@ -334,8 +340,12 @@ public void run() { savedListener.onResult2(resolutionResultBuilder.build()); }); } catch (IOException e) { - savedListener.onError( - Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e)); + syncContext.execute(() -> + savedListener.onResult2(ResolutionResult.newBuilder() + .setAddressesOrError( + StatusOr.fromStatus( + Status.UNAVAILABLE.withDescription( + "Unable to resolve host " + host).withCause(e))).build())); } finally { final boolean succeed = result != null && result.error == null; syncContext.execute(new Runnable() { diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 36d79f4011b..cda4299acec 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -81,6 +81,7 @@ import io.grpc.NameResolverRegistry; import io.grpc.ProxyDetector; import io.grpc.Status; +import io.grpc.StatusOr; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; import io.grpc.internal.AutoConfiguredLoadBalancerFactory.AutoConfiguredLoadBalancer; @@ -1701,7 +1702,13 @@ public Status onResult2(final ResolutionResult resolutionResult) { return Status.OK; } - List servers = resolutionResult.getAddresses(); + StatusOr> serversOrError = + resolutionResult.getAddressesOrError(); + if (!serversOrError.hasValue()) { + handleErrorInSyncContext(serversOrError.getStatus()); + return serversOrError.getStatus(); + } + List servers = serversOrError.getValue(); channelLogger.log( ChannelLogLevel.DEBUG, "Resolved address: {0}, config={1}", @@ -1709,10 +1716,10 @@ public Status onResult2(final ResolutionResult resolutionResult) { resolutionResult.getAttributes()); if (lastResolutionState != ResolutionState.SUCCESS) { - channelLogger.log(ChannelLogLevel.INFO, "Address resolved: {0}", servers); + channelLogger.log(ChannelLogLevel.INFO, "Address resolved: {0}", + servers); lastResolutionState = ResolutionState.SUCCESS; } - ConfigOrError configOrError = resolutionResult.getServiceConfig(); InternalConfigSelector resolvedConfigSelector = resolutionResult.getAttributes().get(InternalConfigSelector.KEY); @@ -1788,7 +1795,7 @@ public Status onResult2(final ResolutionResult resolutionResult) { } try { - // TODO(creamsoup): when `servers` is empty and lastResolutionStateCopy == SUCCESS + // TODO(creamsoup): when `serversOrError` is empty and lastResolutionStateCopy == SUCCESS // and lbNeedAddress, it shouldn't call the handleServiceConfigUpdate. But, // lbNeedAddress is not deterministic serviceConfigUpdated = true; @@ -1814,12 +1821,13 @@ public Status onResult2(final ResolutionResult resolutionResult) { } Attributes attributes = attrBuilder.build(); - return helper.lb.tryAcceptResolvedAddresses( - ResolvedAddresses.newBuilder() - .setAddresses(servers) - .setAttributes(attributes) - .setLoadBalancingPolicyConfig(effectiveServiceConfig.getLoadBalancingConfig()) - .build()); + ResolvedAddresses.Builder resolvedAddresses = ResolvedAddresses.newBuilder() + .setAddresses(serversOrError.getValue()) + .setAttributes(attributes) + .setLoadBalancingPolicyConfig(effectiveServiceConfig.getLoadBalancingConfig()); + Status addressAcceptanceStatus = helper.lb.tryAcceptResolvedAddresses( + resolvedAddresses.build()); + return addressAcceptanceStatus; } return Status.OK; } diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java index 7da9125087e..48a255472e1 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java @@ -45,6 +45,7 @@ import io.grpc.NameResolverProvider; import io.grpc.NameResolverRegistry; import io.grpc.ProxyDetector; +import io.grpc.StatusOr; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.SocketAddress; @@ -877,9 +878,11 @@ public String getServiceAuthority() { @Override public void start(Listener2 listener) { - listener.onResult( + listener.onResult2( ResolutionResult.newBuilder() - .setAddresses(Collections.singletonList(new EquivalentAddressGroup(address))) + .setAddressesOrError( + StatusOr.fromValue( + Collections.singletonList(new EquivalentAddressGroup(address)))) .setAttributes(Attributes.EMPTY) .build()); } diff --git a/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java b/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java index 0512171f4e7..be304ad326b 100644 --- a/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java +++ b/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; @@ -152,12 +153,11 @@ public void close(Executor instance) {} private NameResolver.Listener2 mockListener; @Captor private ArgumentCaptor resultCaptor; - @Captor - private ArgumentCaptor errorCaptor; @Nullable private String networkaddressCacheTtlPropertyValue; @Mock private RecordFetcher recordFetcher; + @Mock private ProxyDetector mockProxyDetector; private RetryingNameResolver newResolver(String name, int defaultPort) { return newResolver( @@ -570,7 +570,7 @@ public List resolveAddress(String host) throws Exception { ArgumentCaptor ac = ArgumentCaptor.forClass(ResolutionResult.class); verify(mockListener).onResult2(ac.capture()); verifyNoMoreInteractions(mockListener); - assertThat(ac.getValue().getAddresses()).isEmpty(); + assertThat(ac.getValue().getAddressesOrError().getValue()).isEmpty(); assertThat(ac.getValue().getServiceConfig()).isNull(); verify(mockResourceResolver, never()).resolveSrv(anyString()); @@ -578,6 +578,39 @@ public List resolveAddress(String host) throws Exception { assertEquals(0, fakeExecutor.numPendingTasks()); } + @Test + public void resolve_addressResolutionError() throws Exception { + DnsNameResolver.enableTxt = true; + when(mockProxyDetector.proxyFor(any(SocketAddress.class))).thenThrow(new IOException()); + RetryingNameResolver resolver = newResolver( + "addr.fake:1234", 443, mockProxyDetector, Stopwatch.createUnstarted()); + DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver(); + dnsResolver.setAddressResolver(new AddressResolver() { + @Override + public List resolveAddress(String host) throws Exception { + return Collections.emptyList(); + } + }); + ResourceResolver mockResourceResolver = mock(ResourceResolver.class); + when(mockResourceResolver.resolveTxt(anyString())) + .thenReturn(Collections.emptyList()); + + dnsResolver.setResourceResolver(mockResourceResolver); + + resolver.start(mockListener); + assertThat(fakeExecutor.runDueTasks()).isEqualTo(1); + + ArgumentCaptor ac = ArgumentCaptor.forClass(ResolutionResult.class); + verify(mockListener).onResult2(ac.capture()); + verifyNoMoreInteractions(mockListener); + assertThat(ac.getValue().getAddressesOrError().getStatus().getCode()).isEqualTo( + Status.UNAVAILABLE.getCode()); + assertThat(ac.getValue().getAddressesOrError().getStatus().getDescription()).isEqualTo( + "Unable to resolve host addr.fake"); + assertThat(ac.getValue().getAddressesOrError().getStatus().getCause()) + .isInstanceOf(IOException.class); + } + // Load balancer rejects the empty addresses. @Test public void resolve_emptyResult_notAccepted() throws Exception { @@ -604,7 +637,7 @@ public List resolveAddress(String host) throws Exception { ArgumentCaptor ac = ArgumentCaptor.forClass(ResolutionResult.class); verify(mockListener).onResult2(ac.capture()); verifyNoMoreInteractions(mockListener); - assertThat(ac.getValue().getAddresses()).isEmpty(); + assertThat(ac.getValue().getAddressesOrError().getValue()).isEmpty(); assertThat(ac.getValue().getServiceConfig()).isNull(); verify(mockResourceResolver, never()).resolveSrv(anyString()); @@ -632,7 +665,7 @@ public void resolve_nullResourceResolver() throws Exception { ResolutionResult result = resultCaptor.getValue(); InetSocketAddress resolvedBackendAddr = (InetSocketAddress) Iterables.getOnlyElement( - Iterables.getOnlyElement(result.getAddresses()).getAddresses()); + Iterables.getOnlyElement(result.getAddressesOrError().getValue()).getAddresses()); assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr); verify(mockAddressResolver).resolveAddress(name); assertThat(result.getServiceConfig()).isNull(); @@ -647,6 +680,7 @@ public void resolve_nullResourceResolver_addressFailure() throws Exception { AddressResolver mockAddressResolver = mock(AddressResolver.class); when(mockAddressResolver.resolveAddress(anyString())) .thenThrow(new IOException("no addr")); + when(mockListener.onResult2(isA(ResolutionResult.class))).thenReturn(Status.UNAVAILABLE); String name = "foo.googleapis.com"; RetryingNameResolver resolver = newResolver(name, 81); @@ -655,8 +689,8 @@ public void resolve_nullResourceResolver_addressFailure() throws Exception { dnsResolver.setResourceResolver(null); resolver.start(mockListener); assertEquals(1, fakeExecutor.runDueTasks()); - verify(mockListener).onError(errorCaptor.capture()); - Status errorStatus = errorCaptor.getValue(); + verify(mockListener).onResult2(resultCaptor.capture()); + Status errorStatus = resultCaptor.getValue().getAddressesOrError().getStatus(); assertThat(errorStatus.getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(errorStatus.getCause()).hasMessageThat().contains("no addr"); @@ -704,7 +738,7 @@ public ConfigOrError parseServiceConfig(Map rawServiceConfig) { ResolutionResult result = resultCaptor.getValue(); InetSocketAddress resolvedBackendAddr = (InetSocketAddress) Iterables.getOnlyElement( - Iterables.getOnlyElement(result.getAddresses()).getAddresses()); + Iterables.getOnlyElement(result.getAddressesOrError().getValue()).getAddresses()); assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr); assertThat(result.getServiceConfig().getConfig()).isNotNull(); verify(mockAddressResolver).resolveAddress(name); @@ -720,6 +754,7 @@ public void resolve_addressFailure_neverLookUpServiceConfig() throws Exception { AddressResolver mockAddressResolver = mock(AddressResolver.class); when(mockAddressResolver.resolveAddress(anyString())) .thenThrow(new IOException("no addr")); + when(mockListener.onResult2(isA(ResolutionResult.class))).thenReturn(Status.UNAVAILABLE); String name = "foo.googleapis.com"; ResourceResolver mockResourceResolver = mock(ResourceResolver.class); @@ -729,8 +764,8 @@ public void resolve_addressFailure_neverLookUpServiceConfig() throws Exception { dnsResolver.setResourceResolver(mockResourceResolver); resolver.start(mockListener); assertEquals(1, fakeExecutor.runDueTasks()); - verify(mockListener).onError(errorCaptor.capture()); - Status errorStatus = errorCaptor.getValue(); + verify(mockListener).onResult2(resultCaptor.capture()); + Status errorStatus = resultCaptor.getValue().getAddressesOrError().getStatus(); assertThat(errorStatus.getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(errorStatus.getCause()).hasMessageThat().contains("no addr"); verify(mockResourceResolver, never()).resolveTxt(anyString()); @@ -762,7 +797,7 @@ public void resolve_serviceConfigLookupFails_nullServiceConfig() throws Exceptio ResolutionResult result = resultCaptor.getValue(); InetSocketAddress resolvedBackendAddr = (InetSocketAddress) Iterables.getOnlyElement( - Iterables.getOnlyElement(result.getAddresses()).getAddresses()); + Iterables.getOnlyElement(result.getAddressesOrError().getValue()).getAddresses()); assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr); verify(mockAddressResolver).resolveAddress(name); assertThat(result.getServiceConfig()).isNull(); @@ -794,7 +829,7 @@ public void resolve_serviceConfigMalformed_serviceConfigError() throws Exception ResolutionResult result = resultCaptor.getValue(); InetSocketAddress resolvedBackendAddr = (InetSocketAddress) Iterables.getOnlyElement( - Iterables.getOnlyElement(result.getAddresses()).getAddresses()); + Iterables.getOnlyElement(result.getAddressesOrError().getValue()).getAddresses()); assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr); verify(mockAddressResolver).resolveAddress(name); assertThat(result.getServiceConfig()).isNotNull(); @@ -859,7 +894,7 @@ public HttpConnectProxiedSocketAddress proxyFor(SocketAddress targetAddress) { assertEquals(1, fakeExecutor.runDueTasks()); verify(mockListener).onResult2(resultCaptor.capture()); - List result = resultCaptor.getValue().getAddresses(); + List result = resultCaptor.getValue().getAddressesOrError().getValue(); assertThat(result).hasSize(1); EquivalentAddressGroup eag = result.get(0); assertThat(eag.getAddresses()).hasSize(1); @@ -1299,9 +1334,9 @@ private List createAddressList(int n) throws UnknownHostException { private static void assertAnswerMatches( List addrs, int port, ResolutionResult resolutionResult) { - assertThat(resolutionResult.getAddresses()).hasSize(addrs.size()); + assertThat(resolutionResult.getAddressesOrError().getValue()).hasSize(addrs.size()); for (int i = 0; i < addrs.size(); i++) { - EquivalentAddressGroup addrGroup = resolutionResult.getAddresses().get(i); + EquivalentAddressGroup addrGroup = resolutionResult.getAddressesOrError().getValue().get(i); InetSocketAddress socketAddr = (InetSocketAddress) Iterables.getOnlyElement(addrGroup.getAddresses()); assertEquals("Addr " + i, port, socketAddr.getPort()); diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java index 90008c1be30..293d0e70961 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java @@ -63,6 +63,7 @@ import io.grpc.NameResolver.ResolutionResult; import io.grpc.NameResolverProvider; import io.grpc.Status; +import io.grpc.StatusOr; import io.grpc.StringMarshaller; import io.grpc.internal.FakeClock.ScheduledTask; import io.grpc.internal.ManagedChannelImplBuilder.UnsupportedClientTransportFactoryBuilder; @@ -615,7 +616,7 @@ private void deliverResolutionResult() { // the NameResolver. ResolutionResult resolutionResult = ResolutionResult.newBuilder() - .setAddresses(servers) + .setAddressesOrError(StatusOr.fromValue(servers)) .setAttributes(Attributes.EMPTY) .build(); nameResolverListenerCaptor.getValue().onResult(resolutionResult); diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index bea14bcef47..16700096827 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -116,6 +116,7 @@ import io.grpc.ServerMethodDefinition; import io.grpc.Status; import io.grpc.Status.Code; +import io.grpc.StatusOr; import io.grpc.StringMarshaller; import io.grpc.SynchronizationContext; import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; @@ -1056,7 +1057,7 @@ public void noMoreCallbackAfterLoadBalancerShutdown() { verifyNoMoreInteractions(mockLoadBalancer); } - @Test + @Test public void noMoreCallbackAfterLoadBalancerShutdown_configError() throws InterruptedException { FakeNameResolverFactory nameResolverFactory = new FakeNameResolverFactory.Builder(expectedUri) @@ -1095,7 +1096,10 @@ public void noMoreCallbackAfterLoadBalancerShutdown_configError() throws Interru verify(stateListener2).onSubchannelState(stateInfoCaptor.capture()); assertSame(CONNECTING, stateInfoCaptor.getValue().getState()); - resolver.listener.onError(resolutionError); + channel.syncContext.execute(() -> + resolver.listener.onResult2( + ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromStatus(resolutionError)).build())); verify(mockLoadBalancer).handleNameResolutionError(resolutionError); verifyNoMoreInteractions(mockLoadBalancer); @@ -1117,13 +1121,10 @@ public void noMoreCallbackAfterLoadBalancerShutdown_configError() throws Interru verifyNoMoreInteractions(stateListener1, stateListener2); // No more callback should be delivered to LoadBalancer after it's shut down - resolver.listener.onResult( - ResolutionResult.newBuilder() - .setAddresses(new ArrayList<>()) - .setServiceConfig( - ConfigOrError.fromError(Status.UNAVAILABLE.withDescription("Resolution failed"))) - .build()); - Thread.sleep(1100); + channel.syncContext.execute(() -> + resolver.listener.onResult2( + ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromStatus(resolutionError)).build())); assertThat(timer.getPendingTasks()).isEmpty(); resolver.resolved(); verifyNoMoreInteractions(mockLoadBalancer); @@ -3286,11 +3287,19 @@ public void channelTracing_nameResolvedEvent_zeorAndNonzeroBackends_usesListener assertThat(getStats(channel).channelTrace.events).hasSize(prevSize); prevSize = getStats(channel).channelTrace.events.size(); - nameResolverFactory.resolvers.get(0).listener.onError(Status.INTERNAL); + channel.syncContext.execute(() -> + nameResolverFactory.resolvers.get(0).listener.onResult2( + ResolutionResult.newBuilder() + .setAddressesOrError( + StatusOr.fromStatus(Status.INTERNAL)).build())); assertThat(getStats(channel).channelTrace.events).hasSize(prevSize + 1); prevSize = getStats(channel).channelTrace.events.size(); - nameResolverFactory.resolvers.get(0).listener.onError(Status.INTERNAL); + channel.syncContext.execute(() -> + nameResolverFactory.resolvers.get(0).listener.onResult2( + ResolutionResult.newBuilder() + .setAddressesOrError( + StatusOr.fromStatus(Status.INTERNAL)).build())); assertThat(getStats(channel).channelTrace.events).hasSize(prevSize); prevSize = getStats(channel).channelTrace.events.size(); @@ -4919,7 +4928,10 @@ final class FakeNameResolver extends NameResolver { void resolved() { if (error != null) { - listener.onError(error); + syncContext.execute(() -> + listener.onResult2( + ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromStatus(error)).build())); return; } ResolutionResult.Builder builder = diff --git a/grpclb/src/test/java/io/grpc/grpclb/GrpclbNameResolverTest.java b/grpclb/src/test/java/io/grpc/grpclb/GrpclbNameResolverTest.java index c195a78e6f4..1aa11ecf9af 100644 --- a/grpclb/src/test/java/io/grpc/grpclb/GrpclbNameResolverTest.java +++ b/grpclb/src/test/java/io/grpc/grpclb/GrpclbNameResolverTest.java @@ -96,7 +96,6 @@ public void close(Executor instance) {} } @Captor private ArgumentCaptor resultCaptor; - @Captor private ArgumentCaptor errorCaptor; @Mock private ServiceConfigParser serviceConfigParser; @Mock private NameResolver.Listener2 mockListener; @@ -154,7 +153,7 @@ public List resolveSrv(String host) throws Exception { verify(mockListener).onResult2(resultCaptor.capture()); ResolutionResult result = resultCaptor.getValue(); - assertThat(result.getAddresses()).isEmpty(); + assertThat(result.getAddressesOrError().getValue()).isEmpty(); assertThat(result.getAttributes()).isEqualTo(Attributes.EMPTY); assertThat(result.getServiceConfig()).isNull(); } @@ -196,7 +195,7 @@ public ConfigOrError answer(InvocationOnMock invocation) { ResolutionResult result = resultCaptor.getValue(); InetSocketAddress resolvedBackendAddr = (InetSocketAddress) Iterables.getOnlyElement( - Iterables.getOnlyElement(result.getAddresses()).getAddresses()); + Iterables.getOnlyElement(result.getAddressesOrError().getValue()).getAddresses()); assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr); EquivalentAddressGroup resolvedBalancerAddr = Iterables.getOnlyElement(result.getAttributes().get(GrpclbConstants.ATTR_LB_ADDRS)); @@ -227,7 +226,7 @@ public void resolve_nullResourceResolver() throws Exception { assertThat(fakeClock.runDueTasks()).isEqualTo(1); verify(mockListener).onResult2(resultCaptor.capture()); ResolutionResult result = resultCaptor.getValue(); - assertThat(result.getAddresses()) + assertThat(result.getAddressesOrError().getValue()) .containsExactly( new EquivalentAddressGroup(new InetSocketAddress(backendAddr, DEFAULT_PORT))); assertThat(result.getAttributes()).isEqualTo(Attributes.EMPTY); @@ -245,8 +244,8 @@ public void resolve_nullResourceResolver_addressFailure() throws Exception { resolver.start(mockListener); assertThat(fakeClock.runDueTasks()).isEqualTo(1); - verify(mockListener).onError(errorCaptor.capture()); - Status errorStatus = errorCaptor.getValue(); + verify(mockListener).onResult2(resultCaptor.capture()); + Status errorStatus = resultCaptor.getValue().getAddressesOrError().getStatus(); assertThat(errorStatus.getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(errorStatus.getCause()).hasMessageThat().contains("no addr"); } @@ -274,7 +273,7 @@ public void resolve_addressFailure_stillLookUpBalancersAndServiceConfig() throws assertThat(fakeClock.runDueTasks()).isEqualTo(1); verify(mockListener).onResult2(resultCaptor.capture()); ResolutionResult result = resultCaptor.getValue(); - assertThat(result.getAddresses()).isEmpty(); + assertThat(result.getAddressesOrError().getValue()).isEmpty(); EquivalentAddressGroup resolvedBalancerAddr = Iterables.getOnlyElement(result.getAttributes().get(GrpclbConstants.ATTR_LB_ADDRS)); assertThat(resolvedBalancerAddr.getAttributes().get(GrpclbConstants.ATTR_LB_ADDR_AUTHORITY)) @@ -311,7 +310,7 @@ public void resolveAll_balancerLookupFails_stillLookUpServiceConfig() throws Exc InetSocketAddress resolvedBackendAddr = (InetSocketAddress) Iterables.getOnlyElement( - Iterables.getOnlyElement(result.getAddresses()).getAddresses()); + Iterables.getOnlyElement(result.getAddressesOrError().getValue()).getAddresses()); assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr); assertThat(result.getAttributes().get(GrpclbConstants.ATTR_LB_ADDRS)).isNull(); verify(mockAddressResolver).resolveAddress(hostName); @@ -335,8 +334,8 @@ public void resolve_addressAndBalancersLookupFail_neverLookupServiceConfig() thr resolver.start(mockListener); assertThat(fakeClock.runDueTasks()).isEqualTo(1); - verify(mockListener).onError(errorCaptor.capture()); - Status errorStatus = errorCaptor.getValue(); + verify(mockListener).onResult2(resultCaptor.capture()); + Status errorStatus = resultCaptor.getValue().getAddressesOrError().getStatus(); assertThat(errorStatus.getCode()).isEqualTo(Code.UNAVAILABLE); verify(mockAddressResolver).resolveAddress(hostName); verify(mockResourceResolver, never()).resolveTxt("_grpc_config." + hostName); diff --git a/netty/src/main/java/io/grpc/netty/UdsNameResolver.java b/netty/src/main/java/io/grpc/netty/UdsNameResolver.java index 8fa8ea06250..de14dc8b460 100644 --- a/netty/src/main/java/io/grpc/netty/UdsNameResolver.java +++ b/netty/src/main/java/io/grpc/netty/UdsNameResolver.java @@ -22,16 +22,16 @@ import com.google.common.base.Preconditions; import io.grpc.EquivalentAddressGroup; import io.grpc.NameResolver; +import io.grpc.StatusOr; import io.netty.channel.unix.DomainSocketAddress; import java.util.ArrayList; -import java.util.Collections; import java.util.List; final class UdsNameResolver extends NameResolver { private NameResolver.Listener2 listener; private final String authority; - UdsNameResolver(String authority, String targetPath) { + UdsNameResolver(String authority, String targetPath, Args args) { checkArgument(authority == null, "non-null authority not supported"); this.authority = targetPath; } @@ -57,8 +57,8 @@ private void resolve() { ResolutionResult.Builder resolutionResultBuilder = ResolutionResult.newBuilder(); List servers = new ArrayList<>(1); servers.add(new EquivalentAddressGroup(new DomainSocketAddress(authority))); - resolutionResultBuilder.setAddresses(Collections.unmodifiableList(servers)); - listener.onResult(resolutionResultBuilder.build()); + resolutionResultBuilder.setAddressesOrError(StatusOr.fromValue(servers)); + listener.onResult2(resolutionResultBuilder.build()); } @Override diff --git a/netty/src/main/java/io/grpc/netty/UdsNameResolverProvider.java b/netty/src/main/java/io/grpc/netty/UdsNameResolverProvider.java index 9f594193b4c..fe6300057fd 100644 --- a/netty/src/main/java/io/grpc/netty/UdsNameResolverProvider.java +++ b/netty/src/main/java/io/grpc/netty/UdsNameResolverProvider.java @@ -34,7 +34,7 @@ public final class UdsNameResolverProvider extends NameResolverProvider { @Override public UdsNameResolver newNameResolver(URI targetUri, NameResolver.Args args) { if (SCHEME.equals(targetUri.getScheme())) { - return new UdsNameResolver(targetUri.getAuthority(), getTargetPathFromUri(targetUri)); + return new UdsNameResolver(targetUri.getAuthority(), getTargetPathFromUri(targetUri), args); } else { return null; } diff --git a/netty/src/test/java/io/grpc/netty/UdsNameResolverProviderTest.java b/netty/src/test/java/io/grpc/netty/UdsNameResolverProviderTest.java index 6a329c8fc68..9dacf00cfad 100644 --- a/netty/src/test/java/io/grpc/netty/UdsNameResolverProviderTest.java +++ b/netty/src/test/java/io/grpc/netty/UdsNameResolverProviderTest.java @@ -18,10 +18,16 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import io.grpc.ChannelLogger; import io.grpc.EquivalentAddressGroup; import io.grpc.NameResolver; +import io.grpc.NameResolver.ServiceConfigParser; +import io.grpc.SynchronizationContext; +import io.grpc.internal.FakeClock; +import io.grpc.internal.GrpcUtil; import io.netty.channel.unix.DomainSocketAddress; import java.net.SocketAddress; import java.net.URI; @@ -39,7 +45,7 @@ /** Unit tests for {@link UdsNameResolverProvider}. */ @RunWith(JUnit4.class) public class UdsNameResolverProviderTest { - + private static final int DEFAULT_PORT = 887; @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @@ -51,16 +57,29 @@ public class UdsNameResolverProviderTest { UdsNameResolverProvider udsNameResolverProvider = new UdsNameResolverProvider(); + private final SynchronizationContext syncContext = new SynchronizationContext( + (t, e) -> { + throw new AssertionError(e); + }); + private final FakeClock fakeExecutor = new FakeClock(); + private final NameResolver.Args args = NameResolver.Args.newBuilder() + .setDefaultPort(DEFAULT_PORT) + .setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR) + .setSynchronizationContext(syncContext) + .setServiceConfigParser(mock(ServiceConfigParser.class)) + .setChannelLogger(mock(ChannelLogger.class)) + .setScheduledExecutorService(fakeExecutor.getScheduledExecutorService()) + .build(); @Test public void testUnixRelativePath() { UdsNameResolver udsNameResolver = - udsNameResolverProvider.newNameResolver(URI.create("unix:sock.sock"), null); + udsNameResolverProvider.newNameResolver(URI.create("unix:sock.sock"), args); assertThat(udsNameResolver).isNotNull(); udsNameResolver.start(mockListener); - verify(mockListener).onResult(resultCaptor.capture()); + verify(mockListener).onResult2(resultCaptor.capture()); NameResolver.ResolutionResult result = resultCaptor.getValue(); - List list = result.getAddresses(); + List list = result.getAddressesOrError().getValue(); assertThat(list).isNotNull(); assertThat(list).hasSize(1); EquivalentAddressGroup eag = list.get(0); @@ -75,12 +94,12 @@ public void testUnixRelativePath() { @Test public void testUnixAbsolutePath() { UdsNameResolver udsNameResolver = - udsNameResolverProvider.newNameResolver(URI.create("unix:/sock.sock"), null); + udsNameResolverProvider.newNameResolver(URI.create("unix:/sock.sock"), args); assertThat(udsNameResolver).isNotNull(); udsNameResolver.start(mockListener); - verify(mockListener).onResult(resultCaptor.capture()); + verify(mockListener).onResult2(resultCaptor.capture()); NameResolver.ResolutionResult result = resultCaptor.getValue(); - List list = result.getAddresses(); + List list = result.getAddressesOrError().getValue(); assertThat(list).isNotNull(); assertThat(list).hasSize(1); EquivalentAddressGroup eag = list.get(0); @@ -95,12 +114,12 @@ public void testUnixAbsolutePath() { @Test public void testUnixAbsoluteAlternatePath() { UdsNameResolver udsNameResolver = - udsNameResolverProvider.newNameResolver(URI.create("unix:///sock.sock"), null); + udsNameResolverProvider.newNameResolver(URI.create("unix:///sock.sock"), args); assertThat(udsNameResolver).isNotNull(); udsNameResolver.start(mockListener); - verify(mockListener).onResult(resultCaptor.capture()); + verify(mockListener).onResult2(resultCaptor.capture()); NameResolver.ResolutionResult result = resultCaptor.getValue(); - List list = result.getAddresses(); + List list = result.getAddressesOrError().getValue(); assertThat(list).isNotNull(); assertThat(list).hasSize(1); EquivalentAddressGroup eag = list.get(0); @@ -115,7 +134,7 @@ public void testUnixAbsoluteAlternatePath() { @Test public void testUnixPathWithAuthority() { try { - udsNameResolverProvider.newNameResolver(URI.create("unix://localhost/sock.sock"), null); + udsNameResolverProvider.newNameResolver(URI.create("unix://localhost/sock.sock"), args); fail("exception expected"); } catch (IllegalArgumentException e) { assertThat(e).hasMessageThat().isEqualTo("non-null authority not supported"); diff --git a/netty/src/test/java/io/grpc/netty/UdsNameResolverTest.java b/netty/src/test/java/io/grpc/netty/UdsNameResolverTest.java index 8eb010e23e5..22790a41c77 100644 --- a/netty/src/test/java/io/grpc/netty/UdsNameResolverTest.java +++ b/netty/src/test/java/io/grpc/netty/UdsNameResolverTest.java @@ -18,10 +18,16 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import io.grpc.ChannelLogger; import io.grpc.EquivalentAddressGroup; import io.grpc.NameResolver; +import io.grpc.NameResolver.ServiceConfigParser; +import io.grpc.SynchronizationContext; +import io.grpc.internal.FakeClock; +import io.grpc.internal.GrpcUtil; import io.netty.channel.unix.DomainSocketAddress; import java.net.SocketAddress; import java.util.List; @@ -41,7 +47,20 @@ public class UdsNameResolverTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); - + private static final int DEFAULT_PORT = 887; + private final FakeClock fakeExecutor = new FakeClock(); + private final SynchronizationContext syncContext = new SynchronizationContext( + (t, e) -> { + throw new AssertionError(e); + }); + private final NameResolver.Args args = NameResolver.Args.newBuilder() + .setDefaultPort(DEFAULT_PORT) + .setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR) + .setSynchronizationContext(syncContext) + .setServiceConfigParser(mock(ServiceConfigParser.class)) + .setChannelLogger(mock(ChannelLogger.class)) + .setScheduledExecutorService(fakeExecutor.getScheduledExecutorService()) + .build(); @Mock private NameResolver.Listener2 mockListener; @@ -52,11 +71,11 @@ public class UdsNameResolverTest { @Test public void testValidTargetPath() { - udsNameResolver = new UdsNameResolver(null, "sock.sock"); + udsNameResolver = new UdsNameResolver(null, "sock.sock", args); udsNameResolver.start(mockListener); - verify(mockListener).onResult(resultCaptor.capture()); + verify(mockListener).onResult2(resultCaptor.capture()); NameResolver.ResolutionResult result = resultCaptor.getValue(); - List list = result.getAddresses(); + List list = result.getAddressesOrError().getValue(); assertThat(list).isNotNull(); assertThat(list).hasSize(1); EquivalentAddressGroup eag = list.get(0); @@ -72,7 +91,7 @@ public void testValidTargetPath() { @Test public void testNonNullAuthority() { try { - udsNameResolver = new UdsNameResolver("authority", "sock.sock"); + udsNameResolver = new UdsNameResolver("authority", "sock.sock", args); fail("exception expected"); } catch (IllegalArgumentException e) { assertThat(e).hasMessageThat().isEqualTo("non-null authority not supported"); diff --git a/testing/src/main/java/io/grpc/internal/testing/FakeNameResolverProvider.java b/testing/src/main/java/io/grpc/internal/testing/FakeNameResolverProvider.java index 4664dbcc436..c77f7f8945a 100644 --- a/testing/src/main/java/io/grpc/internal/testing/FakeNameResolverProvider.java +++ b/testing/src/main/java/io/grpc/internal/testing/FakeNameResolverProvider.java @@ -21,6 +21,7 @@ import io.grpc.NameResolver; import io.grpc.NameResolverProvider; import io.grpc.Status; +import io.grpc.StatusOr; import java.net.SocketAddress; import java.net.URI; import java.util.Collection; @@ -81,9 +82,10 @@ public void start(Listener2 listener) { if (shutdown) { listener.onError(Status.FAILED_PRECONDITION.withDescription("Resolver is shutdown")); } else { - listener.onResult( + listener.onResult2( ResolutionResult.newBuilder() - .setAddresses(ImmutableList.of(new EquivalentAddressGroup(address))) + .setAddressesOrError( + StatusOr.fromValue(ImmutableList.of(new EquivalentAddressGroup(address)))) .build()); } } diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index d3c6bc8f9a0..0ecd77b12cb 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -52,6 +52,7 @@ import io.grpc.NameResolverRegistry; import io.grpc.Status; import io.grpc.Status.Code; +import io.grpc.StatusOr; import io.grpc.SynchronizationContext; import io.grpc.internal.BackoffPolicy; import io.grpc.internal.FakeClock; @@ -1306,7 +1307,8 @@ public void shutdown() { } private void deliverEndpointAddresses(List addresses) { - listener.onResult(ResolutionResult.newBuilder().setAddresses(addresses).build()); + listener.onResult(ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromValue(addresses)).build()); } private void deliverError(Status error) { diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 24c2a43b83a..76b92cd8c03 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -737,7 +737,7 @@ public void resolved_simpleCallFailedToRoute_routeWithNonForwardingAction() { ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); - assertThat(result.getAddresses()).isEmpty(); + assertThat(result.getAddressesOrError().getValue()).isEmpty(); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster2), (Map) result.getServiceConfig().getConfig()); @@ -1071,7 +1071,7 @@ public void resolved_simpleCallSucceeds_routeToWeightedCluster() { ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); - assertThat(result.getAddresses()).isEmpty(); + assertThat(result.getAddressesOrError().getValue()).isEmpty(); assertServiceConfigForLoadBalancingConfig( Arrays.asList(cluster1, cluster2), (Map) result.getServiceConfig().getConfig()); assertThat(result.getAttributes().get(InternalXdsAttributes.XDS_CLIENT_POOL)).isNotNull(); @@ -1100,7 +1100,7 @@ public void resolved_simpleCallSucceeds_routeToRls() { ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); - assertThat(result.getAddresses()).isEmpty(); + assertThat(result.getAddressesOrError().getValue()).isEmpty(); @SuppressWarnings("unchecked") Map resultServiceConfig = (Map) result.getServiceConfig().getConfig(); List> rawLbConfigs = @@ -1181,7 +1181,7 @@ public void resolved_simpleCallSucceeds_routeToRls() { private void assertEmptyResolutionResult(String resource) { verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); - assertThat(result.getAddresses()).isEmpty(); + assertThat(result.getAddressesOrError().getValue()).isEmpty(); assertThat((Map) result.getServiceConfig().getConfig()).isEmpty(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); Result configResult = configSelector.selectConfig( @@ -1260,7 +1260,7 @@ private InternalConfigSelector resolveToClusters() { ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); - assertThat(result.getAddresses()).isEmpty(); + assertThat(result.getAddressesOrError().getValue()).isEmpty(); assertServiceConfigForLoadBalancingConfig( Arrays.asList(cluster1, cluster2), (Map) result.getServiceConfig().getConfig()); assertThat(result.getAttributes().get(InternalXdsAttributes.XDS_CLIENT_POOL)).isNotNull(); diff --git a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java index 2c349eec4af..c6b8e7515b2 100644 --- a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java @@ -43,6 +43,7 @@ import io.grpc.Server; import io.grpc.ServerCredentials; import io.grpc.Status; +import io.grpc.StatusOr; import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; @@ -520,7 +521,8 @@ public void refresh() { } void resolved() { - ResolutionResult.Builder builder = ResolutionResult.newBuilder().setAddresses(servers); + ResolutionResult.Builder builder = ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromValue(servers)); listener.onResult(builder.build()); } From 2aae68e11726e35576d78f8e568f9e389ed330bc Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Mon, 7 Oct 2024 10:44:27 -0700 Subject: [PATCH 039/591] report uncompressed message size when it does not need compression (#11598) --- .../io/grpc/internal/MessageDeframer.java | 3 +- .../io/grpc/internal/MessageDeframerTest.java | 33 +++++++++++-------- .../OpenTelemetryTracingModule.java | 1 - 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/MessageDeframer.java b/core/src/main/java/io/grpc/internal/MessageDeframer.java index c8b250c2143..13a01efec0a 100644 --- a/core/src/main/java/io/grpc/internal/MessageDeframer.java +++ b/core/src/main/java/io/grpc/internal/MessageDeframer.java @@ -406,7 +406,8 @@ private void processBody() { // There is no reliable way to get the uncompressed size per message when it's compressed, // because the uncompressed bytes are provided through an InputStream whose total size is // unknown until all bytes are read, and we don't know when it happens. - statsTraceCtx.inboundMessageRead(currentMessageSeqNo, inboundBodyWireSize, -1); + statsTraceCtx.inboundMessageRead(currentMessageSeqNo, inboundBodyWireSize, + (compressedFlag || fullStreamDecompressor != null) ? -1 : inboundBodyWireSize); inboundBodyWireSize = 0; InputStream stream = compressedFlag ? getCompressedBody() : getUncompressedBody(); nextFrame.touch(); diff --git a/core/src/test/java/io/grpc/internal/MessageDeframerTest.java b/core/src/test/java/io/grpc/internal/MessageDeframerTest.java index 1ec1ccb2082..8f1b908e999 100644 --- a/core/src/test/java/io/grpc/internal/MessageDeframerTest.java +++ b/core/src/test/java/io/grpc/internal/MessageDeframerTest.java @@ -133,7 +133,7 @@ public void simplePayload() { assertEquals(Bytes.asList(new byte[]{3, 14}), bytes(producer.getValue().next())); verify(listener, atLeastOnce()).bytesRead(anyInt()); verifyNoMoreInteractions(listener); - checkStats(tracer, transportTracer.getStats(), fakeClock, 2, 2); + checkStats(tracer, transportTracer.getStats(), fakeClock, useGzipInflatingBuffer, 2, 2); } @Test @@ -148,7 +148,7 @@ public void smallCombinedPayloads() { verify(listener, atLeastOnce()).bytesRead(anyInt()); assertEquals(Bytes.asList(new byte[]{14, 15}), bytes(streams.get(1).next())); verifyNoMoreInteractions(listener); - checkStats(tracer, transportTracer.getStats(), fakeClock, 1, 1, 2, 2); + checkStats(tracer, transportTracer.getStats(), fakeClock, useGzipInflatingBuffer, 1, 1, 2, 2); } @Test @@ -162,7 +162,7 @@ public void endOfStreamWithPayloadShouldNotifyEndOfStream() { verify(listener).deframerClosed(false); verify(listener, atLeastOnce()).bytesRead(anyInt()); verifyNoMoreInteractions(listener); - checkStats(tracer, transportTracer.getStats(), fakeClock, 1, 1); + checkStats(tracer, transportTracer.getStats(), fakeClock, useGzipInflatingBuffer, 1, 1); } @Test @@ -177,7 +177,7 @@ public void endOfStreamShouldNotifyEndOfStream() { } verify(listener).deframerClosed(false); verifyNoMoreInteractions(listener); - checkStats(tracer, transportTracer.getStats(), fakeClock); + checkStats(tracer, transportTracer.getStats(), fakeClock, false); } @Test @@ -189,7 +189,7 @@ public void endOfStreamWithPartialMessageShouldNotifyDeframerClosedWithPartialMe verify(listener, atLeastOnce()).bytesRead(anyInt()); verify(listener).deframerClosed(true); verifyNoMoreInteractions(listener); - checkStats(tracer, transportTracer.getStats(), fakeClock); + checkStats(tracer, transportTracer.getStats(), fakeClock, false); } @Test @@ -206,7 +206,7 @@ public void endOfStreamWithInvalidGzipBlockShouldNotifyDeframerClosedWithPartial deframer.closeWhenComplete(); verify(listener).deframerClosed(true); verifyNoMoreInteractions(listener); - checkStats(tracer, transportTracer.getStats(), fakeClock); + checkStats(tracer, transportTracer.getStats(), fakeClock, false); } @Test @@ -228,10 +228,11 @@ public void payloadSplitBetweenBuffers() { tracer, transportTracer.getStats(), fakeClock, + true, 7 /* msg size */ + 2 /* second buffer adds two bytes of overhead in deflate block */, 7); } else { - checkStats(tracer, transportTracer.getStats(), fakeClock, 7, 7); + checkStats(tracer, transportTracer.getStats(), fakeClock, false, 7, 7); } } @@ -248,7 +249,7 @@ public void frameHeaderSplitBetweenBuffers() { assertEquals(Bytes.asList(new byte[]{3}), bytes(producer.getValue().next())); verify(listener, atLeastOnce()).bytesRead(anyInt()); verifyNoMoreInteractions(listener); - checkStats(tracer, transportTracer.getStats(), fakeClock, 1, 1); + checkStats(tracer, transportTracer.getStats(), fakeClock, useGzipInflatingBuffer, 1, 1); } @Test @@ -259,7 +260,7 @@ public void emptyPayload() { assertEquals(Bytes.asList(), bytes(producer.getValue().next())); verify(listener, atLeastOnce()).bytesRead(anyInt()); verifyNoMoreInteractions(listener); - checkStats(tracer, transportTracer.getStats(), fakeClock, 0, 0); + checkStats(tracer, transportTracer.getStats(), fakeClock, useGzipInflatingBuffer, 0, 0); } @Test @@ -273,9 +274,10 @@ public void largerFrameSize() { verify(listener, atLeastOnce()).bytesRead(anyInt()); verifyNoMoreInteractions(listener); if (useGzipInflatingBuffer) { - checkStats(tracer, transportTracer.getStats(), fakeClock, 8 /* compressed size */, 1000); + checkStats(tracer, transportTracer.getStats(), fakeClock,true, + 8 /* compressed size */, 1000); } else { - checkStats(tracer, transportTracer.getStats(), fakeClock, 1000, 1000); + checkStats(tracer, transportTracer.getStats(), fakeClock, false, 1000, 1000); } } @@ -292,7 +294,7 @@ public void endOfStreamCallbackShouldWaitForMessageDelivery() { verify(listener).deframerClosed(false); verify(listener, atLeastOnce()).bytesRead(anyInt()); verifyNoMoreInteractions(listener); - checkStats(tracer, transportTracer.getStats(), fakeClock, 1, 1); + checkStats(tracer, transportTracer.getStats(), fakeClock, useGzipInflatingBuffer, 1, 1); } @Test @@ -308,6 +310,7 @@ public void compressed() { verify(listener).messagesAvailable(producer.capture()); assertEquals(Bytes.asList(new byte[1000]), bytes(producer.getValue().next())); verify(listener, atLeastOnce()).bytesRead(anyInt()); + checkStats(tracer, transportTracer.getStats(), fakeClock, true, 29, 1000); verifyNoMoreInteractions(listener); } @@ -502,7 +505,8 @@ public void sizeEnforcingInputStream_markReset() throws IOException { * @param sizes in the format {wire0, uncompressed0, wire1, uncompressed1, ...} */ private static void checkStats( - TestBaseStreamTracer tracer, TransportStats transportStats, FakeClock clock, long... sizes) { + TestBaseStreamTracer tracer, TransportStats transportStats, FakeClock clock, + boolean compressed, long... sizes) { assertEquals(0, sizes.length % 2); int count = sizes.length / 2; long expectedWireSize = 0; @@ -510,7 +514,8 @@ private static void checkStats( for (int i = 0; i < count; i++) { assertEquals("inboundMessage(" + i + ")", tracer.nextInboundEvent()); assertEquals( - String.format(Locale.US, "inboundMessageRead(%d, %d, -1)", i, sizes[i * 2]), + String.format(Locale.US, "inboundMessageRead(%d, %d, %d)", i, sizes[i * 2], + compressed ? -1 : sizes[i * 2 + 1]), tracer.nextInboundEvent()); expectedWireSize += sizes[i * 2]; expectedUncompressedSize += sizes[i * 2 + 1]; diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java index 6f2d3268ae0..838ee0797a7 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java @@ -217,7 +217,6 @@ public void outboundMessageSent( @Override public void inboundMessageRead( int seqNo, long optionalWireSize, long optionalUncompressedSize) { - //TODO(yifeizhuang): needs support from message deframer. if (optionalWireSize != optionalUncompressedSize) { recordInboundCompressedMessage(span, seqNo, optionalWireSize); } From 0a3c03446c35cab44d86e5aa4c5e53892a308947 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:56:43 -0700 Subject: [PATCH 040/591] s2a: Correct type of exception thrown (#11588) * throw IllegalArgumentException in ProtoUtil. * throw exception in TrustManager in more standard way. * handle IllegalArgumentException in SslContextFactory. * Don't throw error on unknown TLS version. --- .../grpc/s2a/internal/handshaker/ProtoUtil.java | 11 ++++++++--- .../s2a/internal/handshaker/S2ATrustManager.java | 8 ++++---- .../internal/handshaker/SslContextFactory.java | 6 ++++-- .../s2a/internal/handshaker/ProtoUtilTest.java | 16 +++++++--------- .../handshaker/SslContextFactoryTest.java | 2 +- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/ProtoUtil.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/ProtoUtil.java index 1d88d5a2b55..1f24727a083 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/ProtoUtil.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/ProtoUtil.java @@ -27,7 +27,8 @@ final class ProtoUtil { * * @param tlsVersion the {@link TLSVersion} object to be converted. * @return a {@link String} representation of the TLS version. - * @throws AssertionError if the {@code tlsVersion} is not one of the supported TLS versions. + * @throws IllegalArgumentException if the {@code tlsVersion} is not one of + * the supported TLS versions. */ @VisibleForTesting static String convertTlsProtocolVersion(TLSVersion tlsVersion) { @@ -41,7 +42,7 @@ static String convertTlsProtocolVersion(TLSVersion tlsVersion) { case TLS_VERSION_1_0: return "TLSv1"; default: - throw new AssertionError( + throw new IllegalArgumentException( String.format("TLS version %d is not supported.", tlsVersion.getNumber())); } } @@ -62,7 +63,11 @@ static ImmutableSet buildTlsProtocolVersionSet( } if (versionNumber >= minTlsVersion.getNumber() && versionNumber <= maxTlsVersion.getNumber()) { - tlsVersions.add(convertTlsProtocolVersion(tlsVersion)); + try { + tlsVersions.add(convertTlsProtocolVersion(tlsVersion)); + } catch (IllegalArgumentException e) { + continue; + } } } return tlsVersions.build(); diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java index 2f7e5750f88..406545b30bf 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java @@ -120,10 +120,10 @@ private void checkPeerTrusted(X509Certificate[] chain, boolean isCheckingClientC SessionResp resp; try { resp = stub.send(reqBuilder.build()); - } catch (IOException | InterruptedException e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } + } catch (IOException e) { + throw new CertificateException("Failed to send request to S2A.", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new CertificateException("Failed to send request to S2A.", e); } if (resp.hasStatus() && resp.getStatus().getCode() != 0) { diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java index 72ace2c7885..3e5481daa9e 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java @@ -138,11 +138,13 @@ private static void configureSslContextWithClientTlsConfiguration( NoSuchAlgorithmException, UnrecoverableKeyException { sslContextBuilder.keyManager(createKeylessManager(clientTlsConfiguration)); - ImmutableSet tlsVersions = + ImmutableSet tlsVersions; + tlsVersions = ProtoUtil.buildTlsProtocolVersionSet( clientTlsConfiguration.getMinTlsVersion(), clientTlsConfiguration.getMaxTlsVersion()); if (tlsVersions.isEmpty()) { - throw new S2AConnectionException("Set of TLS versions received from S2A server is empty."); + throw new S2AConnectionException("Set of TLS versions received from S2A server is" + + " empty or not supported."); } sslContextBuilder.protocols(tlsVersions); } diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/ProtoUtilTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/ProtoUtilTest.java index b685d0bc755..f60aa1a189b 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/ProtoUtilTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/ProtoUtilTest.java @@ -46,9 +46,9 @@ public void convertTlsProtocolVersion_success() { @Test public void convertTlsProtocolVersion_withUnknownTlsVersion_fails() { - AssertionError expected = + IllegalArgumentException expected = assertThrows( - AssertionError.class, + IllegalArgumentException.class, () -> ProtoUtil.convertTlsProtocolVersion(TLSVersion.TLS_VERSION_UNSPECIFIED)); expect.that(expected).hasMessageThat().isEqualTo("TLS version 0 is not supported."); } @@ -79,12 +79,10 @@ public void buildTlsProtocolVersionSet_success() { @Test public void buildTlsProtocolVersionSet_failure() { - AssertionError expected = - assertThrows( - AssertionError.class, - () -> - ProtoUtil.buildTlsProtocolVersionSet( - TLSVersion.TLS_VERSION_UNSPECIFIED, TLSVersion.TLS_VERSION_1_3)); - expect.that(expected).hasMessageThat().isEqualTo("TLS version 0 is not supported."); + expect + .that( + ProtoUtil.buildTlsProtocolVersionSet( + TLSVersion.TLS_VERSION_UNSPECIFIED, TLSVersion.TLS_VERSION_1_3)) + .isEqualTo(ImmutableSet.of("TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3")); } } \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/SslContextFactoryTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/SslContextFactoryTest.java index fc3cfb5e441..17b834abf2a 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/SslContextFactoryTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/SslContextFactoryTest.java @@ -142,7 +142,7 @@ public void createForClient_getsBadTlsVersionsFromServer_throwsError() throws Ex assertThat(expected) .hasMessageThat() - .contains("Set of TLS versions received from S2A server is empty."); + .contains("Set of TLS versions received from S2A server is empty or not supported."); } @Test From 9d252c2466127081ab8a28ea1127240ccca16741 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:57:32 -0700 Subject: [PATCH 041/591] Don't use Utils.pickUnusedPort. (#11601) --- .../channel/S2AHandshakerServiceChannelTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java b/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java index 16409721ff5..46f705d2f77 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java @@ -30,7 +30,6 @@ import io.grpc.StatusRuntimeException; import io.grpc.TlsChannelCredentials; import io.grpc.TlsServerCredentials; -import io.grpc.benchmarks.Utils; import io.grpc.internal.SharedResourceHolder.Resource; import io.grpc.netty.NettyServerBuilder; import io.grpc.stub.StreamObserver; @@ -123,7 +122,7 @@ public void getChannelResource_twoDistinctChannels() { InsecureChannelCredentials.create()); Resource resourceTwo = S2AHandshakerServiceChannel.getChannelResource( - "localhost:" + Utils.pickUnusedPort(), InsecureChannelCredentials.create()); + "localhost:" + plaintextServer.getPort() + 1, InsecureChannelCredentials.create()); assertThat(resourceTwo).isNotEqualTo(resource); } @@ -135,7 +134,7 @@ public void getChannelResource_mtlsTwoDistinctChannels() throws Exception { "localhost:" + mtlsServer.getPort(), getTlsChannelCredentials()); Resource resourceTwo = S2AHandshakerServiceChannel.getChannelResource( - "localhost:" + Utils.pickUnusedPort(), getTlsChannelCredentials()); + "localhost:" + mtlsServer.getPort() + 1, getTlsChannelCredentials()); assertThat(resourceTwo).isNotEqualTo(resource); } @@ -229,13 +228,13 @@ private static Server createMtlsServer() throws Exception { .clientAuth(TlsServerCredentials.ClientAuth.REQUIRE) .build(); return grpcCleanup.register( - NettyServerBuilder.forPort(Utils.pickUnusedPort(), creds).addService(service).build()); + NettyServerBuilder.forPort(0, creds).addService(service).build()); } private static Server createPlaintextServer() { SimpleServiceImpl service = new SimpleServiceImpl(); return grpcCleanup.register( - ServerBuilder.forPort(Utils.pickUnusedPort()).addService(service).build()); + ServerBuilder.forPort(0).addService(service).build()); } private static ChannelCredentials getTlsChannelCredentials() throws Exception { From e59ae5fad0efa8702260790eb74c76643a8e4018 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Tue, 8 Oct 2024 17:00:33 -0700 Subject: [PATCH 042/591] rename grpc-context-override-opentelemetry and publish artifact (#11599) --- contextstorage/build.gradle | 3 +-- settings.gradle | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/contextstorage/build.gradle b/contextstorage/build.gradle index 10c7f3a3a86..39ea003d462 100644 --- a/contextstorage/build.gradle +++ b/contextstorage/build.gradle @@ -1,7 +1,6 @@ plugins { id "java-library" - // until we are confident we like the name - //id "maven-publish" + id "maven-publish" id "ru.vyarus.animalsniffer" } diff --git a/settings.gradle b/settings.gradle index b661e0f52db..b451242f7c1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -77,7 +77,7 @@ include ":grpc-istio-interop-testing" include ":grpc-inprocess" include ":grpc-util" include ":grpc-opentelemetry" -include ":grpc-opentelemetry-context-storage-override" +include ":grpc-context-override-opentelemetry" project(':grpc-api').projectDir = "$rootDir/api" as File project(':grpc-core').projectDir = "$rootDir/core" as File @@ -114,7 +114,7 @@ project(':grpc-istio-interop-testing').projectDir = "$rootDir/istio-interop-test project(':grpc-inprocess').projectDir = "$rootDir/inprocess" as File project(':grpc-util').projectDir = "$rootDir/util" as File project(':grpc-opentelemetry').projectDir = "$rootDir/opentelemetry" as File -project(':grpc-opentelemetry-context-storage-override').projectDir = "$rootDir/contextstorage" as File +project(':grpc-context-override-opentelemetry').projectDir = "$rootDir/contextstorage" as File if (settings.hasProperty('skipCodegen') && skipCodegen.toBoolean()) { println '*** Skipping the build of codegen and compilation of proto files because skipCodegen=true' From 2e9c3e19fbd50000ed964acc70f6fe814841db40 Mon Sep 17 00:00:00 2001 From: Vindhya Ningegowda Date: Tue, 8 Oct 2024 17:28:14 -0700 Subject: [PATCH 043/591] xds: Update error handling for ADS stream close and failure scenarios (#11596) When an ADS stream in closed with a non-OK status after receiving a response, new status will be updated to OK status. This makes the fail behavior consistent with gRFC A57. --- .../grpc/xds/client/ControlPlaneClient.java | 45 +++++++++------ .../io/grpc/xds/client/XdsClientImpl.java | 12 ++-- .../grpc/xds/GrpcXdsClientImplTestBase.java | 55 +++++++++++++++---- 3 files changed, 81 insertions(+), 31 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java index 3074d1120ad..5ac979277c4 100644 --- a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java +++ b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java @@ -16,7 +16,6 @@ package io.grpc.xds.client; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; @@ -60,7 +59,6 @@ */ final class ControlPlaneClient { - public static final String CLOSED_BY_SERVER = "Closed by server"; private final SynchronizationContext syncContext; private final InternalLogId logId; private final XdsLogger logger; @@ -358,11 +356,7 @@ public void run() { @Override public void onStatusReceived(final Status status) { syncContext.execute(() -> { - if (status.isOk()) { - handleRpcStreamClosed(Status.UNAVAILABLE.withDescription(CLOSED_BY_SERVER)); - } else { - handleRpcStreamClosed(status); - } + handleRpcStreamClosed(status); }); } @@ -381,7 +375,7 @@ final void handleRpcResponse(XdsResourceType type, String versionInfo, List> subscriberMap : - resourceSubscribers.values()) { - for (ResourceSubscriber subscriber : subscriberMap.values()) { - if (!subscriber.hasResult()) { - subscriber.onError(error, null); + if (!error.isOk()) { + for (Map> subscriberMap : + resourceSubscribers.values()) { + for (ResourceSubscriber subscriber : subscriberMap.values()) { + if (!subscriber.hasResult()) { + subscriber.onError(error, null); + } } } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index d41630cdb4a..a9fda599183 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -3331,6 +3331,43 @@ public void useIndependentRpcContext() { } } + @Test + public void streamClosedWithNoResponse() { + xdsClient.watchXdsResource(XdsListenerResource.getInstance(),LDS_RESOURCE, ldsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),RDS_RESOURCE, + rdsResourceWatcher); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + // Management server closes the RPC stream before sending any response. + call.sendCompleted(); + verify(ldsResourceWatcher, Mockito.timeout(1000).times(1)) + .onError(errorCaptor.capture()); + verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, + "ADS stream closed with OK before receiving a response"); + verify(rdsResourceWatcher).onError(errorCaptor.capture()); + verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, + "ADS stream closed with OK before receiving a response"); + } + + @Test + public void streamClosedAfterSendingResponses() { + xdsClient.watchXdsResource(XdsListenerResource.getInstance(),LDS_RESOURCE, ldsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),RDS_RESOURCE, + rdsResourceWatcher); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + ScheduledTask ldsResourceTimeout = + Iterables.getOnlyElement(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); + ScheduledTask rdsResourceTimeout = + Iterables.getOnlyElement(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); + call.sendResponse(LDS, testListenerRds, VERSION_1, "0000"); + assertThat(ldsResourceTimeout.isCancelled()).isTrue(); + call.sendResponse(RDS, testRouteConfig, VERSION_1, "0000"); + assertThat(rdsResourceTimeout.isCancelled()).isTrue(); + // Management server closes the RPC stream after sending responses. + call.sendCompleted(); + verify(ldsResourceWatcher, never()).onError(errorCaptor.capture()); + verify(rdsResourceWatcher, never()).onError(errorCaptor.capture()); + } + @Test public void streamClosedAndRetryWithBackoff() { InOrder inOrder = Mockito.inOrder(backoffPolicyProvider, backoffPolicy1, backoffPolicy2); @@ -3408,10 +3445,10 @@ public void streamClosedAndRetryWithBackoff() { call.sendError(Status.DEADLINE_EXCEEDED.asException()); verify(ldsResourceWatcher, times(2)).onError(errorCaptor.capture()); verify(rdsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verify(cdsResourceWatcher, times(3)).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.DEADLINE_EXCEEDED, ""); - verify(edsResourceWatcher, times(3)).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.DEADLINE_EXCEEDED, ""); + verify(cdsResourceWatcher, times(2)).onError(errorCaptor.capture()); + verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + verify(edsResourceWatcher, times(2)).onError(errorCaptor.capture()); + verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); // Reset backoff sequence and retry after backoff. inOrder.verify(backoffPolicyProvider).get(); @@ -3430,9 +3467,9 @@ public void streamClosedAndRetryWithBackoff() { call.sendError(Status.UNAVAILABLE.asException()); verify(ldsResourceWatcher, times(2)).onError(errorCaptor.capture()); verify(rdsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verify(cdsResourceWatcher, times(4)).onError(errorCaptor.capture()); + verify(cdsResourceWatcher, times(3)).onError(errorCaptor.capture()); verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); - verify(edsResourceWatcher, times(4)).onError(errorCaptor.capture()); + verify(edsResourceWatcher, times(3)).onError(errorCaptor.capture()); verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); // Retry after backoff. @@ -3516,10 +3553,8 @@ public void streamClosedAndRetryRestartsResourceInitialFetchTimerForUnresolvedRe assertThat(edsResourceTimeout.isCancelled()).isTrue(); verify(ldsResourceWatcher, never()).onError(errorCaptor.capture()); verify(rdsResourceWatcher, never()).onError(errorCaptor.capture()); - verify(cdsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); - verify(edsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); + verify(cdsResourceWatcher, never()).onError(errorCaptor.capture()); + verify(edsResourceWatcher, never()).onError(errorCaptor.capture()); fakeClock.forwardNanos(10L); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(0); From 2129078deeef481767f85bc134cfc3d2b28ac4e9 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Tue, 8 Oct 2024 17:44:40 -0700 Subject: [PATCH 044/591] core: fix test flakiness in retriableStream hedging deadlock test (#11606) --- core/src/test/java/io/grpc/internal/RetriableStreamTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/src/test/java/io/grpc/internal/RetriableStreamTest.java b/core/src/test/java/io/grpc/internal/RetriableStreamTest.java index 21ec46668fc..658ed70a135 100644 --- a/core/src/test/java/io/grpc/internal/RetriableStreamTest.java +++ b/core/src/test/java/io/grpc/internal/RetriableStreamTest.java @@ -2592,9 +2592,7 @@ public void run() { .closed(Status.fromCode(NON_FATAL_STATUS_CODE_1), REFUSED, new Metadata()); } finally { transport2Lock.unlock(); - if (transport1Lock.tryLock()) { - transport1Lock.unlock(); - } + transport1Lock.unlock(); } } }, "Thread-transport2"); From d628396ec7b84286d49e172cf1ced07101c0f177 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:31:18 -0700 Subject: [PATCH 045/591] s2a: Add S2AStub cleanup handler. (#11600) * Add S2AStub cleanup handler. * Give TLS and Cleanup handlers name + update comment. * Don't add TLS handler twice. * Don't remove explicitly, since done by fireProtocolNegotiationEvent. * plumb S2AStub close to handshake end + add integration test. * close stub when TLS negotiation fails. --- .../netty/InternalProtocolNegotiators.java | 11 ++-- .../io/grpc/netty/NettyChannelBuilder.java | 3 +- .../io/grpc/netty/ProtocolNegotiators.java | 27 ++++++-- .../grpc/netty/NettyClientTransportTest.java | 4 +- .../grpc/netty/ProtocolNegotiatorsTest.java | 12 ++-- .../io/grpc/s2a/S2AChannelCredentials.java | 14 +++- .../S2AProtocolNegotiatorFactory.java | 66 +++++++++++++------ .../grpc/s2a/internal/handshaker/S2AStub.java | 8 ++- .../internal/handshaker/IntegrationTest.java | 23 +++++++ .../S2AProtocolNegotiatorFactoryTest.java | 8 +-- 10 files changed, 134 insertions(+), 42 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java index b9c6a77982a..8aeb44d0fc2 100644 --- a/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java @@ -24,6 +24,7 @@ import io.netty.channel.ChannelHandler; import io.netty.handler.ssl.SslContext; import io.netty.util.AsciiString; +import java.util.Optional; import java.util.concurrent.Executor; /** @@ -40,9 +41,10 @@ private InternalProtocolNegotiators() {} * @param executorPool a dedicated {@link Executor} pool for time-consuming TLS tasks */ public static InternalProtocolNegotiator.ProtocolNegotiator tls(SslContext sslContext, - ObjectPool executorPool) { + ObjectPool executorPool, + Optional handshakeCompleteRunnable) { final io.grpc.netty.ProtocolNegotiator negotiator = ProtocolNegotiators.tls(sslContext, - executorPool); + executorPool, handshakeCompleteRunnable); final class TlsNegotiator implements InternalProtocolNegotiator.ProtocolNegotiator { @Override @@ -70,7 +72,7 @@ public void close() { * may happen immediately, even before the TLS Handshake is complete. */ public static InternalProtocolNegotiator.ProtocolNegotiator tls(SslContext sslContext) { - return tls(sslContext, null); + return tls(sslContext, null, Optional.empty()); } /** @@ -167,7 +169,8 @@ public static ChannelHandler grpcNegotiationHandler(GrpcHttp2ConnectionHandler n public static ChannelHandler clientTlsHandler( ChannelHandler next, SslContext sslContext, String authority, ChannelLogger negotiationLogger) { - return new ClientTlsHandler(next, sslContext, authority, null, negotiationLogger); + return new ClientTlsHandler(next, sslContext, authority, null, negotiationLogger, + Optional.empty()); } public static class ProtocolNegotiationHandler diff --git a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java index 305ad128454..d1d9810b485 100644 --- a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java +++ b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java @@ -63,6 +63,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -604,7 +605,7 @@ static ProtocolNegotiator createProtocolNegotiatorByType( case PLAINTEXT_UPGRADE: return ProtocolNegotiators.plaintextUpgrade(); case TLS: - return ProtocolNegotiators.tls(sslContext, executorPool); + return ProtocolNegotiators.tls(sslContext, executorPool, Optional.empty()); default: throw new IllegalArgumentException("Unsupported negotiationType: " + negotiationType); } diff --git a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java index 80df5d0e3c7..84370ac8153 100644 --- a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java @@ -72,6 +72,7 @@ import java.nio.channels.ClosedChannelException; import java.util.Arrays; import java.util.EnumSet; +import java.util.Optional; import java.util.Set; import java.util.concurrent.Executor; import java.util.logging.Level; @@ -543,16 +544,18 @@ protected void userEventTriggered0(ChannelHandlerContext ctx, Object evt) throws static final class ClientTlsProtocolNegotiator implements ProtocolNegotiator { public ClientTlsProtocolNegotiator(SslContext sslContext, - ObjectPool executorPool) { + ObjectPool executorPool, Optional handshakeCompleteRunnable) { this.sslContext = checkNotNull(sslContext, "sslContext"); this.executorPool = executorPool; if (this.executorPool != null) { this.executor = this.executorPool.getObject(); } + this.handshakeCompleteRunnable = handshakeCompleteRunnable; } private final SslContext sslContext; private final ObjectPool executorPool; + private final Optional handshakeCompleteRunnable; private Executor executor; @Override @@ -565,7 +568,7 @@ public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { ChannelHandler gnh = new GrpcNegotiationHandler(grpcHandler); ChannelLogger negotiationLogger = grpcHandler.getNegotiationLogger(); ChannelHandler cth = new ClientTlsHandler(gnh, sslContext, grpcHandler.getAuthority(), - this.executor, negotiationLogger); + this.executor, negotiationLogger, handshakeCompleteRunnable); return new WaitUntilActiveHandler(cth, negotiationLogger); } @@ -583,15 +586,18 @@ static final class ClientTlsHandler extends ProtocolNegotiationHandler { private final String host; private final int port; private Executor executor; + private final Optional handshakeCompleteRunnable; ClientTlsHandler(ChannelHandler next, SslContext sslContext, String authority, - Executor executor, ChannelLogger negotiationLogger) { + Executor executor, ChannelLogger negotiationLogger, + Optional handshakeCompleteRunnable) { super(next, negotiationLogger); this.sslContext = checkNotNull(sslContext, "sslContext"); HostPort hostPort = parseAuthority(authority); this.host = hostPort.host; this.port = hostPort.port; this.executor = executor; + this.handshakeCompleteRunnable = handshakeCompleteRunnable; } @Override @@ -620,6 +626,9 @@ protected void userEventTriggered0(ChannelHandlerContext ctx, Object evt) throws Exception ex = unavailableException("Failed ALPN negotiation: Unable to find compatible protocol"); logSslEngineDetails(Level.FINE, ctx, "TLS negotiation failed.", ex); + if (handshakeCompleteRunnable.isPresent()) { + handshakeCompleteRunnable.get().run(); + } ctx.fireExceptionCaught(ex); } } else { @@ -634,6 +643,9 @@ protected void userEventTriggered0(ChannelHandlerContext ctx, Object evt) throws .withCause(t) .asRuntimeException(); } + if (handshakeCompleteRunnable.isPresent()) { + handshakeCompleteRunnable.get().run(); + } ctx.fireExceptionCaught(t); } } else { @@ -649,6 +661,9 @@ private void propagateTlsComplete(ChannelHandlerContext ctx, SSLSession session) .set(Grpc.TRANSPORT_ATTR_SSL_SESSION, session) .build(); replaceProtocolNegotiationEvent(existingPne.withAttributes(attrs).withSecurity(security)); + if (handshakeCompleteRunnable.isPresent()) { + handshakeCompleteRunnable.get().run(); + } fireProtocolNegotiationEvent(ctx); } } @@ -683,8 +698,8 @@ static HostPort parseAuthority(String authority) { * @param executorPool a dedicated {@link Executor} pool for time-consuming TLS tasks */ public static ProtocolNegotiator tls(SslContext sslContext, - ObjectPool executorPool) { - return new ClientTlsProtocolNegotiator(sslContext, executorPool); + ObjectPool executorPool, Optional handshakeCompleteRunnable) { + return new ClientTlsProtocolNegotiator(sslContext, executorPool, handshakeCompleteRunnable); } /** @@ -693,7 +708,7 @@ public static ProtocolNegotiator tls(SslContext sslContext, * may happen immediately, even before the TLS Handshake is complete. */ public static ProtocolNegotiator tls(SslContext sslContext) { - return tls(sslContext, null); + return tls(sslContext, null, Optional.empty()); } public static ProtocolNegotiator.ClientFactory tlsClientFactory(SslContext sslContext) { diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index 9777bb0926c..7cb269b7d62 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -105,6 +105,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -766,7 +767,8 @@ public void tlsNegotiationServerExecutorShouldSucceed() throws Exception { .trustManager(caCert) .keyManager(clientCert, clientKey) .build(); - ProtocolNegotiator negotiator = ProtocolNegotiators.tls(clientContext, clientExecutorPool); + ProtocolNegotiator negotiator = ProtocolNegotiators.tls(clientContext, clientExecutorPool, + Optional.empty()); // after starting the client, the Executor in the client pool should be used assertEquals(true, clientExecutorPool.isInUse()); final NettyClientTransport transport = newTransport(negotiator); diff --git a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java index 6939d835892..2ccdb2de543 100644 --- a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java +++ b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java @@ -120,6 +120,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.Queue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -876,7 +877,7 @@ public String applicationProtocol() { DefaultEventLoopGroup elg = new DefaultEventLoopGroup(1); ClientTlsHandler handler = new ClientTlsHandler(grpcHandler, sslContext, - "authority", elg, noopLogger); + "authority", elg, noopLogger, Optional.empty()); pipeline.addLast(handler); pipeline.replace(SslHandler.class, null, goodSslHandler); pipeline.fireUserEventTriggered(ProtocolNegotiationEvent.DEFAULT); @@ -914,7 +915,7 @@ public String applicationProtocol() { .applicationProtocolConfig(apn).build(); ClientTlsHandler handler = new ClientTlsHandler(grpcHandler, sslContext, - "authority", elg, noopLogger); + "authority", elg, noopLogger, Optional.empty()); pipeline.addLast(handler); pipeline.replace(SslHandler.class, null, goodSslHandler); pipeline.fireUserEventTriggered(ProtocolNegotiationEvent.DEFAULT); @@ -938,7 +939,7 @@ public String applicationProtocol() { DefaultEventLoopGroup elg = new DefaultEventLoopGroup(1); ClientTlsHandler handler = new ClientTlsHandler(grpcHandler, sslContext, - "authority", elg, noopLogger); + "authority", elg, noopLogger, Optional.empty()); pipeline.addLast(handler); final AtomicReference error = new AtomicReference<>(); @@ -966,7 +967,7 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { @Test public void clientTlsHandler_closeDuringNegotiation() throws Exception { ClientTlsHandler handler = new ClientTlsHandler(grpcHandler, sslContext, - "authority", null, noopLogger); + "authority", null, noopLogger, Optional.empty()); pipeline.addLast(new WriteBufferingAndExceptionHandler(handler)); ChannelFuture pendingWrite = channel.writeAndFlush(NettyClientHandler.NOOP_MESSAGE); @@ -1228,7 +1229,8 @@ public void clientTlsHandler_firesNegotiation() throws Exception { serverSslContext = GrpcSslContexts.forServer(server1Chain, server1Key).build(); } FakeGrpcHttp2ConnectionHandler gh = FakeGrpcHttp2ConnectionHandler.newHandler(); - ClientTlsProtocolNegotiator pn = new ClientTlsProtocolNegotiator(clientSslContext, null); + ClientTlsProtocolNegotiator pn = new ClientTlsProtocolNegotiator(clientSslContext, + null, Optional.empty()); WriteBufferingAndExceptionHandler clientWbaeh = new WriteBufferingAndExceptionHandler(pn.newHandler(gh)); diff --git a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java index 2e040964dfa..2cbdf7e4c5f 100644 --- a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java +++ b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java @@ -31,6 +31,7 @@ import io.grpc.s2a.internal.channel.S2AHandshakerServiceChannel; import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.grpc.s2a.internal.handshaker.S2AProtocolNegotiatorFactory; +import io.grpc.s2a.internal.handshaker.S2AStub; import javax.annotation.concurrent.NotThreadSafe; import org.checkerframework.checker.nullness.qual.Nullable; @@ -59,6 +60,7 @@ public static final class Builder { private final String s2aAddress; private final ChannelCredentials s2aChannelCredentials; private @Nullable S2AIdentity localIdentity = null; + private @Nullable S2AStub stub = null; Builder(String s2aAddress, ChannelCredentials s2aChannelCredentials) { this.s2aAddress = s2aAddress; @@ -104,6 +106,16 @@ public Builder setLocalUid(String localUid) { return this; } + /** + * Sets the stub to use to communicate with S2A. This is only used for testing that the + * stream to S2A gets closed. + */ + public Builder setStub(S2AStub stub) { + checkNotNull(stub); + this.stub = stub; + return this; + } + public ChannelCredentials build() { return InternalNettyChannelCredentials.create(buildProtocolNegotiatorFactory()); } @@ -113,7 +125,7 @@ InternalProtocolNegotiator.ClientFactory buildProtocolNegotiatorFactory() { SharedResourcePool.forResource( S2AHandshakerServiceChannel.getChannelResource(s2aAddress, s2aChannelCredentials)); checkNotNull(s2aChannelPool, "s2aChannelPool"); - return S2AProtocolNegotiatorFactory.createClientFactory(localIdentity, s2aChannelPool); + return S2AProtocolNegotiatorFactory.createClientFactory(localIdentity, s2aChannelPool, stub); } } diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java index 188faf63435..7ad9de991cf 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java @@ -63,28 +63,35 @@ public final class S2AProtocolNegotiatorFactory { * @param localIdentity the identity of the client; if none is provided, the S2A will use the * client's default identity. * @param s2aChannelPool a pool of shared channels that can be used to connect to the S2A. + * @param stub the stub to use to communicate with S2A. If none is provided the channelPool + * will be used to create the stub. This is exposed for verifying the stream to S2A gets + * closed in tests. * @return a factory for creating a client-side protocol negotiator. */ public static InternalProtocolNegotiator.ClientFactory createClientFactory( - @Nullable S2AIdentity localIdentity, ObjectPool s2aChannelPool) { + @Nullable S2AIdentity localIdentity, ObjectPool s2aChannelPool, + @Nullable S2AStub stub) { checkNotNull(s2aChannelPool, "S2A channel pool should not be null."); - return new S2AClientProtocolNegotiatorFactory(localIdentity, s2aChannelPool); + return new S2AClientProtocolNegotiatorFactory(localIdentity, s2aChannelPool, stub); } static final class S2AClientProtocolNegotiatorFactory implements InternalProtocolNegotiator.ClientFactory { private final @Nullable S2AIdentity localIdentity; private final ObjectPool channelPool; + private final @Nullable S2AStub stub; S2AClientProtocolNegotiatorFactory( - @Nullable S2AIdentity localIdentity, ObjectPool channelPool) { + @Nullable S2AIdentity localIdentity, ObjectPool channelPool, + @Nullable S2AStub stub) { this.localIdentity = localIdentity; this.channelPool = channelPool; + this.stub = stub; } @Override public ProtocolNegotiator newNegotiator() { - return S2AProtocolNegotiator.createForClient(channelPool, localIdentity); + return S2AProtocolNegotiator.createForClient(channelPool, localIdentity, stub); } @Override @@ -98,18 +105,20 @@ public int getDefaultPort() { static final class S2AProtocolNegotiator implements ProtocolNegotiator { private final ObjectPool channelPool; - private final Channel channel; + private @Nullable Channel channel = null; private final Optional localIdentity; + private final @Nullable S2AStub stub; private final ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)); static S2AProtocolNegotiator createForClient( - ObjectPool channelPool, @Nullable S2AIdentity localIdentity) { + ObjectPool channelPool, @Nullable S2AIdentity localIdentity, + @Nullable S2AStub stub) { checkNotNull(channelPool, "Channel pool should not be null."); if (localIdentity == null) { - return new S2AProtocolNegotiator(channelPool, Optional.empty()); + return new S2AProtocolNegotiator(channelPool, Optional.empty(), stub); } else { - return new S2AProtocolNegotiator(channelPool, Optional.of(localIdentity)); + return new S2AProtocolNegotiator(channelPool, Optional.of(localIdentity), stub); } } @@ -122,10 +131,13 @@ static S2AProtocolNegotiator createForClient( } private S2AProtocolNegotiator(ObjectPool channelPool, - Optional localIdentity) { + Optional localIdentity, @Nullable S2AStub stub) { this.channelPool = channelPool; this.localIdentity = localIdentity; - this.channel = channelPool.getObject(); + this.stub = stub; + if (this.stub == null) { + this.channel = channelPool.getObject(); + } } @Override @@ -139,13 +151,15 @@ public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { String hostname = getHostNameFromAuthority(grpcHandler.getAuthority()); checkArgument(!isNullOrEmpty(hostname), "hostname should not be null or empty."); return new S2AProtocolNegotiationHandler( - grpcHandler, channel, localIdentity, hostname, service); + grpcHandler, channel, localIdentity, hostname, service, stub); } @Override public void close() { service.shutdown(); - channelPool.returnObject(channel); + if (channel != null) { + channelPool.returnObject(channel); + } } } @@ -180,18 +194,20 @@ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { } private static final class S2AProtocolNegotiationHandler extends ProtocolNegotiationHandler { - private final Channel channel; + private final @Nullable Channel channel; private final Optional localIdentity; private final String hostname; private final GrpcHttp2ConnectionHandler grpcHandler; private final ListeningExecutorService service; + private final @Nullable S2AStub stub; private S2AProtocolNegotiationHandler( GrpcHttp2ConnectionHandler grpcHandler, Channel channel, Optional localIdentity, String hostname, - ListeningExecutorService service) { + ListeningExecutorService service, + @Nullable S2AStub stub) { super( // superclass (InternalProtocolNegotiators.ProtocolNegotiationHandler) expects 'next' // handler but we don't have a next handler _yet_. So we "disable" superclass's behavior @@ -209,6 +225,7 @@ public void handlerAdded(ChannelHandlerContext ctx) { this.hostname = hostname; checkNotNull(service, "service should not be null."); this.service = service; + this.stub = stub; } @Override @@ -217,8 +234,13 @@ protected void handlerAdded0(ChannelHandlerContext ctx) { BufferReadsHandler bufferReads = new BufferReadsHandler(); ctx.pipeline().addBefore(ctx.name(), /* name= */ null, bufferReads); - S2AServiceGrpc.S2AServiceStub stub = S2AServiceGrpc.newStub(channel); - S2AStub s2aStub = S2AStub.newInstance(stub); + S2AStub s2aStub; + if (this.stub == null) { + checkNotNull(channel, "Channel to S2A should not be null"); + s2aStub = S2AStub.newInstance(S2AServiceGrpc.newStub(channel)); + } else { + s2aStub = this.stub; + } ListenableFuture sslContextFuture = service.submit(() -> SslContextFactory.createForClient(s2aStub, hostname, localIdentity)); @@ -230,11 +252,17 @@ public void onSuccess(SslContext sslContext) { ChannelHandler handler = InternalProtocolNegotiators.tls( sslContext, - SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR)) + SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR), + Optional.of(new Runnable() { + @Override + public void run() { + s2aStub.close(); + } + })) .newHandler(grpcHandler); - // Remove the bufferReads handler and delegate the rest of the handshake to the TLS - // handler. + // Delegate the rest of the handshake to the TLS handler. and remove the + // bufferReads handler. ctx.pipeline().addAfter(ctx.name(), /* name= */ null, handler); fireProtocolNegotiationEvent(ctx); ctx.pipeline().remove(bufferReads); diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java index 0bfa3b4dac2..c5ac8f96d96 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java @@ -33,7 +33,7 @@ /** Reads and writes messages to and from the S2A. */ @NotThreadSafe -class S2AStub implements AutoCloseable { +public class S2AStub implements AutoCloseable { private static final Logger logger = Logger.getLogger(S2AStub.class.getName()); private static final long HANDSHAKE_RPC_DEADLINE_SECS = 20; private final StreamObserver reader = new Reader(); @@ -42,6 +42,7 @@ class S2AStub implements AutoCloseable { private StreamObserver writer; private boolean doneReading = false; private boolean doneWriting = false; + private boolean isClosed = false; static S2AStub newInstance(S2AServiceGrpc.S2AServiceStub serviceStub) { checkNotNull(serviceStub); @@ -136,6 +137,11 @@ public void close() { if (writer != null) { writer.onCompleted(); } + isClosed = true; + } + + public boolean isClosed() { + return isClosed; } /** Create a new writer if the writer is null. */ diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java index e1ad3d278c3..d842fde8dea 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; +import io.grpc.Channel; import io.grpc.ChannelCredentials; import io.grpc.Grpc; import io.grpc.InsecureChannelCredentials; @@ -29,9 +30,12 @@ import io.grpc.TlsChannelCredentials; import io.grpc.TlsServerCredentials; import io.grpc.benchmarks.Utils; +import io.grpc.internal.ObjectPool; +import io.grpc.internal.SharedResourcePool; import io.grpc.netty.GrpcSslContexts; import io.grpc.netty.NettyServerBuilder; import io.grpc.s2a.S2AChannelCredentials; +import io.grpc.s2a.internal.channel.S2AHandshakerServiceChannel; import io.grpc.s2a.internal.handshaker.FakeS2AServer; import io.grpc.stub.StreamObserver; import io.grpc.testing.protobuf.SimpleRequest; @@ -141,6 +145,25 @@ public void clientCommunicateUsingS2ACredentialsNoLocalIdentity_succeeds() throw assertThat(doUnaryRpc(channel)).isTrue(); } + @Test + public void clientCommunicateUsingS2ACredentialsSucceeds_verifyStreamToS2AClosed() + throws Exception { + ObjectPool s2aChannelPool = + SharedResourcePool.forResource( + S2AHandshakerServiceChannel.getChannelResource(s2aAddress, + InsecureChannelCredentials.create())); + Channel ch = s2aChannelPool.getObject(); + S2AStub stub = S2AStub.newInstance(S2AServiceGrpc.newStub(ch)); + ChannelCredentials credentials = + S2AChannelCredentials.newBuilder(s2aAddress, InsecureChannelCredentials.create()) + .setLocalSpiffeId("test-spiffe-id").setStub(stub).build(); + ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, credentials).build(); + + s2aChannelPool.returnObject(ch); + assertThat(doUnaryRpc(channel)).isTrue(); + assertThat(stub.isClosed()).isTrue(); + } + @Test public void clientCommunicateUsingMtlsToS2ACredentials_succeeds() throws Exception { String privateKeyPath = "src/test/resources/client_key.pem"; diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java index 48c512c4e5c..e537687c287 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java @@ -122,7 +122,7 @@ public void createProtocolNegotiator_nullArgument() throws Exception { @Test public void createProtocolNegotiatorFactory_getsDefaultPort_succeeds() throws Exception { InternalProtocolNegotiator.ClientFactory clientFactory = - S2AProtocolNegotiatorFactory.createClientFactory(LOCAL_IDENTITY, channelPool); + S2AProtocolNegotiatorFactory.createClientFactory(LOCAL_IDENTITY, channelPool, null); assertThat(clientFactory.getDefaultPort()).isEqualTo(S2AProtocolNegotiatorFactory.DEFAULT_PORT); } @@ -146,7 +146,7 @@ public void s2aProtocolNegotiator_getHostNameOnValidAuthority_returnsValidHostna public void createProtocolNegotiatorFactory_buildsAnS2AProtocolNegotiatorOnClientSide_succeeds() throws Exception { InternalProtocolNegotiator.ClientFactory clientFactory = - S2AProtocolNegotiatorFactory.createClientFactory(LOCAL_IDENTITY, channelPool); + S2AProtocolNegotiatorFactory.createClientFactory(LOCAL_IDENTITY, channelPool, null); ProtocolNegotiator clientNegotiator = clientFactory.newNegotiator(); @@ -158,7 +158,7 @@ public void createProtocolNegotiatorFactory_buildsAnS2AProtocolNegotiatorOnClien public void closeProtocolNegotiator_verifyProtocolNegotiatorIsClosedOnClientSide() throws Exception { InternalProtocolNegotiator.ClientFactory clientFactory = - S2AProtocolNegotiatorFactory.createClientFactory(LOCAL_IDENTITY, channelPool); + S2AProtocolNegotiatorFactory.createClientFactory(LOCAL_IDENTITY, channelPool, null); ProtocolNegotiator clientNegotiator = clientFactory.newNegotiator(); clientNegotiator.close(); @@ -170,7 +170,7 @@ public void closeProtocolNegotiator_verifyProtocolNegotiatorIsClosedOnClientSide public void createChannelHandler_addHandlerToMockContext() throws Exception { ProtocolNegotiator clientNegotiator = S2AProtocolNegotiatorFactory.S2AProtocolNegotiator.createForClient( - channelPool, LOCAL_IDENTITY); + channelPool, LOCAL_IDENTITY, null); ChannelHandler channelHandler = clientNegotiator.newHandler(fakeConnectionHandler); From a01a9e2340a6c86ffc48db44c88b02d8ac57c184 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:32:10 -0700 Subject: [PATCH 046/591] Enable publishing. (#11581) --- s2a/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/s2a/build.gradle b/s2a/build.gradle index af2c879a753..900283d9a07 100644 --- a/s2a/build.gradle +++ b/s2a/build.gradle @@ -1,5 +1,6 @@ plugins { id "java-library" + id "maven-publish" id "com.github.johnrengelman.shadow" id "com.google.osdetector" From ca43d78f588354f37a4226c881a081095657aaf4 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Fri, 11 Oct 2024 10:28:51 +0530 Subject: [PATCH 047/591] inprocess: Support tracing message sizes (#11406) --- .../io/grpc/census/CensusModulesTest.java | 9 +- .../grpc/internal/AbstractTransportTest.java | 92 ++++++------------- .../inprocess/InProcessChannelBuilder.java | 30 ++++-- .../io/grpc/inprocess/InProcessTransport.java | 60 ++++++++++-- .../io/grpc/inprocess/InternalInProcess.java | 2 +- .../AnonymousInProcessTransportTest.java | 2 +- .../inprocess/InProcessTransportTest.java | 88 +++++++++++++++--- .../StandaloneInProcessTransportTest.java | 7 -- .../OpenTelemetryMetricsModuleTest.java | 9 +- .../OpenTelemetryTracingModuleTest.java | 11 ++- 10 files changed, 207 insertions(+), 103 deletions(-) diff --git a/census/src/test/java/io/grpc/census/CensusModulesTest.java b/census/src/test/java/io/grpc/census/CensusModulesTest.java index 6ccaf78314f..9e0b4d935d3 100644 --- a/census/src/test/java/io/grpc/census/CensusModulesTest.java +++ b/census/src/test/java/io/grpc/census/CensusModulesTest.java @@ -56,6 +56,7 @@ import io.grpc.ClientInterceptors; import io.grpc.ClientStreamTracer; import io.grpc.Context; +import io.grpc.KnownLength; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.ServerCall; @@ -99,6 +100,7 @@ import io.opencensus.trace.Tracer; import io.opencensus.trace.propagation.BinaryFormat; import io.opencensus.trace.propagation.SpanContextParseException; +import java.io.IOException; import java.io.InputStream; import java.util.HashSet; import java.util.List; @@ -136,7 +138,7 @@ public class CensusModulesTest { ClientStreamTracer.StreamInfo.newBuilder() .setCallOptions(CallOptions.DEFAULT.withOption(NAME_RESOLUTION_DELAYED, 10L)).build(); - private static class StringInputStream extends InputStream { + private static class StringInputStream extends InputStream implements KnownLength { final String string; StringInputStream(String string) { @@ -149,6 +151,11 @@ public int read() { // passed to the InProcess server and consumed by MARSHALLER.parse(). throw new UnsupportedOperationException("Should not be called"); } + + @Override + public int available() throws IOException { + return string == null ? 0 : string.length(); + } } private static final MethodDescriptor.Marshaller MARSHALLER = diff --git a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java index 62cbdc4f67b..75ea2678709 100644 --- a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java +++ b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java @@ -136,13 +136,6 @@ protected abstract InternalServer newServer( */ protected abstract String testAuthority(InternalServer server); - /** - * Returns true (which is default) if the transport reports message sizes to StreamTracers. - */ - protected boolean sizesReported() { - return true; - } - protected final Attributes eagAttrs() { return EAG_ATTRS; } @@ -163,9 +156,9 @@ public void log(ChannelLogLevel level, String messageFormat, Object... args) {} * tests in an indeterminate state. */ protected InternalServer server; - private ServerTransport serverTransport; - private ManagedClientTransport client; - private MethodDescriptor methodDescriptor = + protected ServerTransport serverTransport; + protected ManagedClientTransport client; + protected MethodDescriptor methodDescriptor = MethodDescriptor.newBuilder() .setType(MethodDescriptor.MethodType.UNKNOWN) .setFullMethodName("service/method") @@ -182,22 +175,22 @@ public void log(ChannelLogLevel level, String messageFormat, Object... args) {} "tracer-key", Metadata.ASCII_STRING_MARSHALLER); private final String tracerKeyValue = "tracer-key-value"; - private ManagedClientTransport.Listener mockClientTransportListener + protected ManagedClientTransport.Listener mockClientTransportListener = mock(ManagedClientTransport.Listener.class); - private MockServerListener serverListener = new MockServerListener(); + protected MockServerListener serverListener = new MockServerListener(); private ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); - private final TestClientStreamTracer clientStreamTracer1 = new TestHeaderClientStreamTracer(); + protected final TestClientStreamTracer clientStreamTracer1 = new TestHeaderClientStreamTracer(); private final TestClientStreamTracer clientStreamTracer2 = new TestHeaderClientStreamTracer(); - private final ClientStreamTracer[] tracers = new ClientStreamTracer[] { + protected final ClientStreamTracer[] tracers = new ClientStreamTracer[] { clientStreamTracer1, clientStreamTracer2 }; private final ClientStreamTracer[] noopTracers = new ClientStreamTracer[] { new ClientStreamTracer() {} }; - private final TestServerStreamTracer serverStreamTracer1 = new TestServerStreamTracer(); + protected final TestServerStreamTracer serverStreamTracer1 = new TestServerStreamTracer(); private final TestServerStreamTracer serverStreamTracer2 = new TestServerStreamTracer(); - private final ServerStreamTracer.Factory serverStreamTracerFactory = mock( + protected final ServerStreamTracer.Factory serverStreamTracerFactory = mock( ServerStreamTracer.Factory.class, delegatesTo(new ServerStreamTracer.Factory() { final ArrayDeque tracers = @@ -857,26 +850,16 @@ public void basicStream() throws Exception { message.close(); assertThat(clientStreamTracer1.nextOutboundEvent()) .matches("outboundMessageSent\\(0, -?[0-9]+, -?[0-9]+\\)"); - if (sizesReported()) { - assertThat(clientStreamTracer1.getOutboundWireSize()).isGreaterThan(0L); - assertThat(clientStreamTracer1.getOutboundUncompressedSize()).isGreaterThan(0L); - } else { - assertThat(clientStreamTracer1.getOutboundWireSize()).isEqualTo(0L); - assertThat(clientStreamTracer1.getOutboundUncompressedSize()).isEqualTo(0L); - } + assertThat(clientStreamTracer1.getOutboundWireSize()).isGreaterThan(0L); + assertThat(clientStreamTracer1.getOutboundUncompressedSize()).isGreaterThan(0L); assertThat(serverStreamTracer1.nextInboundEvent()).isEqualTo("inboundMessage(0)"); assertNull("no additional message expected", serverStreamListener.messageQueue.poll()); clientStream.halfClose(); assertTrue(serverStreamListener.awaitHalfClosed(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - if (sizesReported()) { - assertThat(serverStreamTracer1.getInboundWireSize()).isGreaterThan(0L); - assertThat(serverStreamTracer1.getInboundUncompressedSize()).isGreaterThan(0L); - } else { - assertThat(serverStreamTracer1.getInboundWireSize()).isEqualTo(0L); - assertThat(serverStreamTracer1.getInboundUncompressedSize()).isEqualTo(0L); - } + assertThat(serverStreamTracer1.getInboundWireSize()).isGreaterThan(0L); + assertThat(serverStreamTracer1.getInboundUncompressedSize()).isGreaterThan(0L); assertThat(serverStreamTracer1.nextInboundEvent()) .matches("inboundMessageRead\\(0, -?[0-9]+, -?[0-9]+\\)"); @@ -907,25 +890,15 @@ public void basicStream() throws Exception { assertNotNull("message expected", message); assertThat(serverStreamTracer1.nextOutboundEvent()) .matches("outboundMessageSent\\(0, -?[0-9]+, -?[0-9]+\\)"); - if (sizesReported()) { - assertThat(serverStreamTracer1.getOutboundWireSize()).isGreaterThan(0L); - assertThat(serverStreamTracer1.getOutboundUncompressedSize()).isGreaterThan(0L); - } else { - assertThat(serverStreamTracer1.getOutboundWireSize()).isEqualTo(0L); - assertThat(serverStreamTracer1.getOutboundUncompressedSize()).isEqualTo(0L); - } + assertThat(serverStreamTracer1.getOutboundWireSize()).isGreaterThan(0L); + assertThat(serverStreamTracer1.getOutboundUncompressedSize()).isGreaterThan(0L); assertTrue(clientStreamTracer1.getInboundHeaders()); assertThat(clientStreamTracer1.nextInboundEvent()).isEqualTo("inboundMessage(0)"); assertEquals("Hi. Who are you?", methodDescriptor.parseResponse(message)); assertThat(clientStreamTracer1.nextInboundEvent()) .matches("inboundMessageRead\\(0, -?[0-9]+, -?[0-9]+\\)"); - if (sizesReported()) { - assertThat(clientStreamTracer1.getInboundWireSize()).isGreaterThan(0L); - assertThat(clientStreamTracer1.getInboundUncompressedSize()).isGreaterThan(0L); - } else { - assertThat(clientStreamTracer1.getInboundWireSize()).isEqualTo(0L); - assertThat(clientStreamTracer1.getInboundUncompressedSize()).isEqualTo(0L); - } + assertThat(clientStreamTracer1.getInboundWireSize()).isGreaterThan(0L); + assertThat(clientStreamTracer1.getInboundUncompressedSize()).isGreaterThan(0L); message.close(); assertNull("no additional message expected", clientStreamListener.messageQueue.poll()); @@ -1285,17 +1258,10 @@ public void onReady() { serverStream.close(Status.OK, new Metadata()); assertTrue(clientStreamTracer1.getOutboundHeaders()); assertTrue(clientStreamTracer1.getInboundHeaders()); - if (sizesReported()) { - assertThat(clientStreamTracer1.getInboundWireSize()).isGreaterThan(0L); - assertThat(clientStreamTracer1.getInboundUncompressedSize()).isGreaterThan(0L); - assertThat(serverStreamTracer1.getOutboundWireSize()).isGreaterThan(0L); - assertThat(serverStreamTracer1.getOutboundUncompressedSize()).isGreaterThan(0L); - } else { - assertThat(clientStreamTracer1.getInboundWireSize()).isEqualTo(0L); - assertThat(clientStreamTracer1.getInboundUncompressedSize()).isEqualTo(0L); - assertThat(serverStreamTracer1.getOutboundWireSize()).isEqualTo(0L); - assertThat(serverStreamTracer1.getOutboundUncompressedSize()).isEqualTo(0L); - } + assertThat(clientStreamTracer1.getInboundWireSize()).isGreaterThan(0L); + assertThat(clientStreamTracer1.getInboundUncompressedSize()).isGreaterThan(0L); + assertThat(serverStreamTracer1.getOutboundWireSize()).isGreaterThan(0L); + assertThat(serverStreamTracer1.getOutboundUncompressedSize()).isGreaterThan(0L); assertNull(clientStreamTracer1.getInboundTrailers()); assertSame(status, clientStreamTracer1.getStatus()); // There is a race between client cancelling and server closing. The final status seen by the @@ -2184,7 +2150,7 @@ private static void runIfNotNull(Runnable runnable) { } } - private static void startTransport( + protected static void startTransport( ManagedClientTransport clientTransport, ManagedClientTransport.Listener listener) { runIfNotNull(clientTransport.start(listener)); @@ -2202,7 +2168,7 @@ public void streamCreated(Attributes transportAttrs, Metadata metadata) { } } - private static class MockServerListener implements ServerListener { + public static class MockServerListener implements ServerListener { public final BlockingQueue listeners = new LinkedBlockingQueue<>(); private final SettableFuture shutdown = SettableFuture.create(); @@ -2233,7 +2199,7 @@ public MockServerTransportListener takeListenerOrFail(long timeout, TimeUnit uni } } - private static class MockServerTransportListener implements ServerTransportListener { + public static class MockServerTransportListener implements ServerTransportListener { public final ServerTransport transport; public final BlockingQueue streams = new LinkedBlockingQueue<>(); private final SettableFuture terminated = SettableFuture.create(); @@ -2281,8 +2247,8 @@ public StreamCreation takeStreamOrFail(long timeout, TimeUnit unit) } } - private static class ServerStreamListenerBase implements ServerStreamListener { - private final BlockingQueue messageQueue = new LinkedBlockingQueue<>(); + public static class ServerStreamListenerBase implements ServerStreamListener { + public final BlockingQueue messageQueue = new LinkedBlockingQueue<>(); // Would have used Void instead of Object, but null elements are not allowed private final BlockingQueue readyQueue = new LinkedBlockingQueue<>(); private final CountDownLatch halfClosedLatch = new CountDownLatch(1); @@ -2341,8 +2307,8 @@ public void closed(Status status) { } } - private static class ClientStreamListenerBase implements ClientStreamListener { - private final BlockingQueue messageQueue = new LinkedBlockingQueue<>(); + public static class ClientStreamListenerBase implements ClientStreamListener { + public final BlockingQueue messageQueue = new LinkedBlockingQueue<>(); // Would have used Void instead of Object, but null elements are not allowed private final BlockingQueue readyQueue = new LinkedBlockingQueue<>(); private final SettableFuture headers = SettableFuture.create(); @@ -2399,7 +2365,7 @@ public void closed(Status status, RpcProgress rpcProgress, Metadata trailers) { } } - private static class StreamCreation { + public static class StreamCreation { public final ServerStream stream; public final String method; public final Metadata headers; diff --git a/inprocess/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java index c000b66b2a2..4e711a94004 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java @@ -94,6 +94,7 @@ public static InProcessChannelBuilder forAddress(String name, int port) { private ScheduledExecutorService scheduledExecutorService; private int maxInboundMetadataSize = Integer.MAX_VALUE; private boolean transportIncludeStatusCause = false; + private long assumedMessageSize = -1; private InProcessChannelBuilder(@Nullable SocketAddress directAddress, @Nullable String target) { @@ -117,10 +118,6 @@ public ClientTransportFactory buildClientTransportFactory() { managedChannelImplBuilder.setStatsRecordStartedRpcs(false); managedChannelImplBuilder.setStatsRecordFinishedRpcs(false); managedChannelImplBuilder.setStatsRecordRetryMetrics(false); - - // By default, In-process transport should not be retriable as that leaks memory. Since - // there is no wire, bytes aren't calculated so buffer limit isn't respected - managedChannelImplBuilder.disableRetry(); } @Internal @@ -225,9 +222,24 @@ public InProcessChannelBuilder propagateCauseWithStatus(boolean enable) { return this; } + /** + * Assumes RPC messages are the specified size. This avoids serializing + * messages for metrics and retry memory tracking. This can dramatically + * improve performance when accurate message sizes are not needed and if + * nothing else needs the serialized message. + * @param assumedMessageSize length of InProcess transport's messageSize. + * @return this + * @throws IllegalArgumentException if assumedMessageSize is negative. + */ + public InProcessChannelBuilder assumedMessageSize(long assumedMessageSize) { + checkArgument(assumedMessageSize >= 0, "assumedMessageSize must be >= 0"); + this.assumedMessageSize = assumedMessageSize; + return this; + } + ClientTransportFactory buildTransportFactory() { - return new InProcessClientTransportFactory( - scheduledExecutorService, maxInboundMetadataSize, transportIncludeStatusCause); + return new InProcessClientTransportFactory(scheduledExecutorService, + maxInboundMetadataSize, transportIncludeStatusCause, assumedMessageSize); } void setStatsEnabled(boolean value) { @@ -243,15 +255,17 @@ static final class InProcessClientTransportFactory implements ClientTransportFac private final int maxInboundMetadataSize; private boolean closed; private final boolean includeCauseWithStatus; + private long assumedMessageSize; private InProcessClientTransportFactory( @Nullable ScheduledExecutorService scheduledExecutorService, - int maxInboundMetadataSize, boolean includeCauseWithStatus) { + int maxInboundMetadataSize, boolean includeCauseWithStatus, long assumedMessageSize) { useSharedTimer = scheduledExecutorService == null; timerService = useSharedTimer ? SharedResourceHolder.get(GrpcUtil.TIMER_SERVICE) : scheduledExecutorService; this.maxInboundMetadataSize = maxInboundMetadataSize; this.includeCauseWithStatus = includeCauseWithStatus; + this.assumedMessageSize = assumedMessageSize; } @Override @@ -263,7 +277,7 @@ public ConnectionClientTransport newClientTransport( // TODO(carl-mastrangelo): Pass channelLogger in. return new InProcessTransport( addr, maxInboundMetadataSize, options.getAuthority(), options.getUserAgent(), - options.getEagAttributes(), includeCauseWithStatus); + options.getEagAttributes(), includeCauseWithStatus, assumedMessageSize); } @Override diff --git a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java index ae8ad143d2c..ae597e34af0 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java @@ -22,6 +22,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Optional; +import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import io.grpc.Attributes; @@ -35,6 +36,7 @@ import io.grpc.InternalChannelz.SocketStats; import io.grpc.InternalLogId; import io.grpc.InternalMetadata; +import io.grpc.KnownLength; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.SecurityLevel; @@ -59,6 +61,7 @@ import io.grpc.internal.ServerTransportListener; import io.grpc.internal.StatsTraceContext; import io.grpc.internal.StreamListener; +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.net.SocketAddress; import java.util.ArrayDeque; @@ -95,6 +98,8 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans private ServerTransportListener serverTransportListener; private Attributes serverStreamAttributes; private ManagedClientTransport.Listener clientTransportListener; + // The size is assumed from the sender's side. + private final long assumedMessageSize; @GuardedBy("this") private boolean shutdown; @GuardedBy("this") @@ -135,8 +140,8 @@ protected void handleNotInUse() { }; private InProcessTransport(SocketAddress address, int maxInboundMetadataSize, String authority, - String userAgent, Attributes eagAttrs, - Optional optionalServerListener, boolean includeCauseWithStatus) { + String userAgent, Attributes eagAttrs, Optional optionalServerListener, + boolean includeCauseWithStatus, long assumedMessageSize) { this.address = address; this.clientMaxInboundMetadataSize = maxInboundMetadataSize; this.authority = authority; @@ -151,22 +156,23 @@ private InProcessTransport(SocketAddress address, int maxInboundMetadataSize, St this.optionalServerListener = optionalServerListener; logId = InternalLogId.allocate(getClass(), address.toString()); this.includeCauseWithStatus = includeCauseWithStatus; + this.assumedMessageSize = assumedMessageSize; } public InProcessTransport( SocketAddress address, int maxInboundMetadataSize, String authority, String userAgent, - Attributes eagAttrs, boolean includeCauseWithStatus) { + Attributes eagAttrs, boolean includeCauseWithStatus, long assumedMessageSize) { this(address, maxInboundMetadataSize, authority, userAgent, eagAttrs, - Optional.absent(), includeCauseWithStatus); + Optional.absent(), includeCauseWithStatus, assumedMessageSize); } InProcessTransport( String name, int maxInboundMetadataSize, String authority, String userAgent, Attributes eagAttrs, ObjectPool serverSchedulerPool, List serverStreamTracerFactories, - ServerListener serverListener, boolean includeCauseWithStatus) { + ServerListener serverListener, boolean includeCauseWithStatus, long assumedMessageSize) { this(new InProcessSocketAddress(name), maxInboundMetadataSize, authority, userAgent, eagAttrs, - Optional.of(serverListener), includeCauseWithStatus); + Optional.of(serverListener), includeCauseWithStatus, assumedMessageSize); this.serverMaxInboundMetadataSize = maxInboundMetadataSize; this.serverSchedulerPool = serverSchedulerPool; this.serverStreamTracerFactories = serverStreamTracerFactories; @@ -507,6 +513,22 @@ private void clientCancelled(Status status) { @Override public void writeMessage(InputStream message) { + long messageLength; + try { + if (assumedMessageSize != -1) { + messageLength = assumedMessageSize; + } else if (message instanceof KnownLength || message instanceof ByteArrayInputStream) { + messageLength = message.available(); + } else { + InputStream oldMessage = message; + byte[] payload = ByteStreams.toByteArray(message); + messageLength = payload.length; + message = new ByteArrayInputStream(payload); + oldMessage.close(); + } + } catch (Exception e) { + throw new RuntimeException("Error processing the message length", e); + } synchronized (this) { if (closed) { return; @@ -515,6 +537,11 @@ public void writeMessage(InputStream message) { statsTraceCtx.outboundMessageSent(outboundSeqNo, -1, -1); clientStream.statsTraceCtx.inboundMessage(outboundSeqNo); clientStream.statsTraceCtx.inboundMessageRead(outboundSeqNo, -1, -1); + statsTraceCtx.outboundUncompressedSize(messageLength); + statsTraceCtx.outboundWireSize(messageLength); + // messageLength should be same at receiver's end as no actual wire is involved. + clientStream.statsTraceCtx.inboundUncompressedSize(messageLength); + clientStream.statsTraceCtx.inboundWireSize(messageLength); outboundSeqNo++; StreamListener.MessageProducer producer = new SingleMessageProducer(message); if (clientRequested > 0) { @@ -778,6 +805,22 @@ private void serverClosed(Status serverListenerStatus, Status serverTracerStatus @Override public void writeMessage(InputStream message) { + long messageLength; + try { + if (assumedMessageSize != -1) { + messageLength = assumedMessageSize; + } else if (message instanceof KnownLength || message instanceof ByteArrayInputStream) { + messageLength = message.available(); + } else { + InputStream oldMessage = message; + byte[] payload = ByteStreams.toByteArray(message); + messageLength = payload.length; + message = new ByteArrayInputStream(payload); + oldMessage.close(); + } + } catch (Exception e) { + throw new RuntimeException("Error processing the message length", e); + } synchronized (this) { if (closed) { return; @@ -786,6 +829,11 @@ public void writeMessage(InputStream message) { statsTraceCtx.outboundMessageSent(outboundSeqNo, -1, -1); serverStream.statsTraceCtx.inboundMessage(outboundSeqNo); serverStream.statsTraceCtx.inboundMessageRead(outboundSeqNo, -1, -1); + statsTraceCtx.outboundUncompressedSize(messageLength); + statsTraceCtx.outboundWireSize(messageLength); + // messageLength should be same at receiver's end as no actual wire is involved. + serverStream.statsTraceCtx.inboundUncompressedSize(messageLength); + serverStream.statsTraceCtx.inboundWireSize(messageLength); outboundSeqNo++; StreamListener.MessageProducer producer = new SingleMessageProducer(message); if (serverRequested > 0) { diff --git a/inprocess/src/main/java/io/grpc/inprocess/InternalInProcess.java b/inprocess/src/main/java/io/grpc/inprocess/InternalInProcess.java index 680373533c8..9e08c2523fb 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InternalInProcess.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InternalInProcess.java @@ -62,6 +62,6 @@ public static ConnectionClientTransport createInProcessTransport( serverSchedulerPool, serverStreamTracerFactories, serverListener, - includeCauseWithStatus); + includeCauseWithStatus, -1); } } diff --git a/inprocess/src/test/java/io/grpc/inprocess/AnonymousInProcessTransportTest.java b/inprocess/src/test/java/io/grpc/inprocess/AnonymousInProcessTransportTest.java index a78a604eac3..7bf884c9ff9 100644 --- a/inprocess/src/test/java/io/grpc/inprocess/AnonymousInProcessTransportTest.java +++ b/inprocess/src/test/java/io/grpc/inprocess/AnonymousInProcessTransportTest.java @@ -52,6 +52,6 @@ protected InternalServer newServer( protected ManagedClientTransport newClientTransport(InternalServer server) { return new InProcessTransport( address, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, - testAuthority(server), USER_AGENT, eagAttrs(), false); + testAuthority(server), USER_AGENT, eagAttrs(), false, -1); } } diff --git a/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java b/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java index 420a9c4a8e7..87d7467c11f 100644 --- a/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java +++ b/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java @@ -17,6 +17,7 @@ package io.grpc.inprocess; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import io.grpc.CallOptions; @@ -34,15 +35,21 @@ import io.grpc.Status.Code; import io.grpc.StatusRuntimeException; import io.grpc.internal.AbstractTransportTest; +import io.grpc.internal.ClientStream; import io.grpc.internal.GrpcUtil; import io.grpc.internal.InternalServer; import io.grpc.internal.ManagedClientTransport; +import io.grpc.internal.ServerStream; +import io.grpc.internal.testing.TestStreamTracer; import io.grpc.stub.ClientCalls; import io.grpc.testing.GrpcCleanupRule; import io.grpc.testing.TestMethodDescriptors; +import java.io.InputStream; +import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import org.junit.Assert; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; @@ -55,10 +62,18 @@ public class InProcessTransportTest extends AbstractTransportTest { private static final String TRANSPORT_NAME = "perfect-for-testing"; private static final String AUTHORITY = "a-testing-authority"; protected static final String USER_AGENT = "a-testing-user-agent"; + private static final int TIMEOUT_MS = 5000; + private static final long TEST_MESSAGE_LENGTH = 100; @Rule public final GrpcCleanupRule grpcCleanupRule = new GrpcCleanupRule(); + @Override + protected InternalServer newServer( + int port, List streamTracerFactories) { + return newServer(streamTracerFactories); + } + @Override protected InternalServer newServer( List streamTracerFactories) { @@ -68,12 +83,6 @@ protected InternalServer newServer( return new InProcessServer(builder, streamTracerFactories); } - @Override - protected InternalServer newServer( - int port, List streamTracerFactories) { - return newServer(streamTracerFactories); - } - @Override protected String testAuthority(InternalServer server) { return AUTHORITY; @@ -83,14 +92,13 @@ protected String testAuthority(InternalServer server) { protected ManagedClientTransport newClientTransport(InternalServer server) { return new InProcessTransport( new InProcessSocketAddress(TRANSPORT_NAME), GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, - testAuthority(server), USER_AGENT, eagAttrs(), false); + testAuthority(server), USER_AGENT, eagAttrs(), false, -1); } - @Override - protected boolean sizesReported() { - // TODO(zhangkun83): InProcessTransport doesn't record metrics for now - // (https://github.com/grpc/grpc-java/issues/2284) - return false; + private ManagedClientTransport newClientTransportWithAssumedMessageSize(InternalServer server) { + return new InProcessTransport( + new InProcessSocketAddress(TRANSPORT_NAME), GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, + testAuthority(server), USER_AGENT, eagAttrs(), false, TEST_MESSAGE_LENGTH); } @Test @@ -170,11 +178,65 @@ public Listener startCall(ServerCall call, Metadata headers) { .build(); ClientCall call = channel.newCall(nonMatchMethod, CallOptions.DEFAULT); try { - ClientCalls.futureUnaryCall(call, null).get(5, TimeUnit.SECONDS); + ClientCalls.futureUnaryCall(call, null).get(TIMEOUT_MS, TimeUnit.MILLISECONDS); fail("Call should fail."); } catch (ExecutionException ex) { StatusRuntimeException s = (StatusRuntimeException)ex.getCause(); assertEquals(Code.UNIMPLEMENTED, s.getStatus().getCode()); } } + + @Test + public void basicStreamInProcess() throws Exception { + InProcessServerBuilder builder = InProcessServerBuilder + .forName(TRANSPORT_NAME) + .maxInboundMetadataSize(GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE); + server = new InProcessServer(builder, Arrays.asList(serverStreamTracerFactory)); + server.start(serverListener); + client = newClientTransportWithAssumedMessageSize(server); + startTransport(client, mockClientTransportListener); + MockServerTransportListener serverTransportListener + = serverListener.takeListenerOrFail(TIMEOUT_MS, TimeUnit.MILLISECONDS); + serverTransport = serverTransportListener.transport; + // Set up client stream + ClientStream clientStream = client.newStream( + methodDescriptor, new Metadata(), CallOptions.DEFAULT, tracers); + ClientStreamListenerBase clientStreamListener = new ClientStreamListenerBase(); + clientStream.start(clientStreamListener); + StreamCreation serverStreamCreation + = serverTransportListener.takeStreamOrFail(TIMEOUT_MS, TimeUnit.MILLISECONDS); + ServerStream serverStream = serverStreamCreation.stream; + ServerStreamListenerBase serverStreamListener = serverStreamCreation.listener; + serverStream.request(1); + assertTrue(clientStream.isReady()); + // Send message from client to server + clientStream.writeMessage(methodDescriptor.streamRequest("Hello from client")); + clientStream.flush(); + // Verify server received the message and check its size + InputStream message = + serverStreamListener.messageQueue.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertEquals("Hello from client", methodDescriptor.parseRequest(message)); + message.close(); + clientStream.halfClose(); + assertAssumedMessageSize(clientStreamTracer1, serverStreamTracer1); + + clientStream.request(1); + assertTrue(serverStream.isReady()); + serverStream.writeMessage(methodDescriptor.streamResponse("Hi from server")); + serverStream.flush(); + message = clientStreamListener.messageQueue.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertEquals("Hi from server", methodDescriptor.parseResponse(message)); + assertAssumedMessageSize(serverStreamTracer1, clientStreamTracer1); + message.close(); + Status status = Status.OK.withDescription("That was normal"); + serverStream.close(status, new Metadata()); + } + + private void assertAssumedMessageSize( + TestStreamTracer streamTracerSender, TestStreamTracer streamTracerReceiver) { + Assert.assertEquals(TEST_MESSAGE_LENGTH, streamTracerSender.getOutboundWireSize()); + Assert.assertEquals(TEST_MESSAGE_LENGTH, streamTracerSender.getOutboundUncompressedSize()); + Assert.assertEquals(TEST_MESSAGE_LENGTH, streamTracerReceiver.getInboundWireSize()); + Assert.assertEquals(TEST_MESSAGE_LENGTH, streamTracerReceiver.getInboundUncompressedSize()); + } } diff --git a/inprocess/src/test/java/io/grpc/inprocess/StandaloneInProcessTransportTest.java b/inprocess/src/test/java/io/grpc/inprocess/StandaloneInProcessTransportTest.java index b1d80d53b8b..b26a5d8498d 100644 --- a/inprocess/src/test/java/io/grpc/inprocess/StandaloneInProcessTransportTest.java +++ b/inprocess/src/test/java/io/grpc/inprocess/StandaloneInProcessTransportTest.java @@ -83,13 +83,6 @@ protected ManagedClientTransport newClientTransport(InternalServer server) { false); } - @Override - protected boolean sizesReported() { - // TODO(zhangkun83): InProcessTransport doesn't record metrics for now - // (https://github.com/grpc/grpc-java/issues/2284) - return false; - } - @Test @Ignore @Override diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java index 6128323154d..6003599668b 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java @@ -37,6 +37,7 @@ import io.grpc.ClientInterceptor; import io.grpc.ClientInterceptors; import io.grpc.ClientStreamTracer; +import io.grpc.KnownLength; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.ServerCall; @@ -53,6 +54,7 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule; +import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Map; @@ -107,7 +109,7 @@ public class OpenTelemetryMetricsModuleTest { { 0L, 1024L, 2048L, 4096L, 16384L, 65536L, 262144L, 1048576L, 4194304L, 16777216L, 67108864L, 268435456L, 1073741824L, 4294967296L }; - private static final class StringInputStream extends InputStream { + private static final class StringInputStream extends InputStream implements KnownLength { final String string; StringInputStream(String string) { @@ -118,6 +120,11 @@ private static final class StringInputStream extends InputStream { public int read() { throw new UnsupportedOperationException("should not be called"); } + + @Override + public int available() throws IOException { + return string == null ? 0 : string.length(); + } } private static final MethodDescriptor.Marshaller MARSHALLER = diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java index 89e79d55d25..b4486bcf2e4 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java @@ -39,6 +39,7 @@ import io.grpc.ClientInterceptor; import io.grpc.ClientInterceptors; import io.grpc.ClientStreamTracer; +import io.grpc.KnownLength; import io.grpc.ManagedChannel; import io.grpc.Metadata; import io.grpc.MethodDescriptor; @@ -73,6 +74,7 @@ import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule; import io.opentelemetry.sdk.trace.data.EventData; import io.opentelemetry.sdk.trace.data.SpanData; +import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.List; @@ -102,7 +104,7 @@ public class OpenTelemetryTracingModuleTest { private static final CallOptions CALL_OPTIONS = CallOptions.DEFAULT.withOption(CUSTOM_OPTION, "customvalue"); - private static class StringInputStream extends InputStream { + private static class StringInputStream extends InputStream implements KnownLength { final String string; StringInputStream(String string) { @@ -111,10 +113,15 @@ private static class StringInputStream extends InputStream { @Override public int read() { - // InProcessTransport doesn't actually read bytes from the InputStream. The InputStream is + // InProcessTransport doesn't actually read bytes from the InputStream. The InputStream is // passed to the InProcess server and consumed by MARSHALLER.parse(). throw new UnsupportedOperationException("Should not be called"); } + + @Override + public int available() throws IOException { + return string == null ? 0 : string.length(); + } } private static final MethodDescriptor.Marshaller MARSHALLER = From 36e29abf4168646b4e3866061c00b5cbea6fcead Mon Sep 17 00:00:00 2001 From: jiangyuan <469391363@qq.com> Date: Tue, 15 Oct 2024 03:33:06 +0800 Subject: [PATCH 048/591] fix XdsTestServer/TestServiceServer listenAddresses conflict (#11612) --- .../grpc/testing/integration/TestServiceServer.java | 12 ++++++++---- .../io/grpc/testing/integration/XdsTestServer.java | 13 ++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceServer.java b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceServer.java index 01cccf98044..fc4cdf9178f 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceServer.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceServer.java @@ -180,18 +180,22 @@ void start() throws Exception { break; case IPV4: SocketAddress v4Address = Util.getV4Address(port); + InetSocketAddress localV4Address = new InetSocketAddress("127.0.0.1", port); serverBuilder = - NettyServerBuilder.forAddress(new InetSocketAddress("127.0.0.1", port), serverCreds); - if (v4Address == null) { + NettyServerBuilder.forAddress(localV4Address, serverCreds); + if (v4Address != null && !v4Address.equals(localV4Address)) { ((NettyServerBuilder) serverBuilder).addListenAddress(v4Address); } break; case IPV6: List v6Addresses = Util.getV6Addresses(port); + InetSocketAddress localV6Address = new InetSocketAddress("::1", port); serverBuilder = - NettyServerBuilder.forAddress(new InetSocketAddress("::1", port), serverCreds); + NettyServerBuilder.forAddress(localV6Address, serverCreds); for (SocketAddress address : v6Addresses) { - ((NettyServerBuilder)serverBuilder).addListenAddress(address); + if (!address.equals(localV6Address)) { + ((NettyServerBuilder) serverBuilder).addListenAddress(address); + } } break; default: diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java b/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java index 8c61f2eb2ad..2f1625d1581 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java @@ -242,18 +242,21 @@ void start() throws Exception { break; case IPV4: SocketAddress v4Address = Util.getV4Address(port); + InetSocketAddress localV4Address = new InetSocketAddress("127.0.0.1", port); serverBuilder = NettyServerBuilder.forAddress( - new InetSocketAddress("127.0.0.1", port), insecureServerCreds); - if (v4Address != null) { + localV4Address, insecureServerCreds); + if (v4Address != null && !v4Address.equals(localV4Address) ) { ((NettyServerBuilder) serverBuilder).addListenAddress(v4Address); } break; case IPV6: List v6Addresses = Util.getV6Addresses(port); - serverBuilder = NettyServerBuilder.forAddress( - new InetSocketAddress("::1", port), insecureServerCreds); + InetSocketAddress localV6Address = new InetSocketAddress("::1", port); + serverBuilder = NettyServerBuilder.forAddress(localV6Address, insecureServerCreds); for (SocketAddress address : v6Addresses) { - ((NettyServerBuilder)serverBuilder).addListenAddress(address); + if (!address.equals(localV6Address)) { + ((NettyServerBuilder) serverBuilder).addListenAddress(address); + } } break; default: From 99f86835ed8a1c960d532e58b5fd51ec1bb99825 Mon Sep 17 00:00:00 2001 From: Naveen Prasanna V <80662475+NaveenPrasannaV@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:53:22 +0530 Subject: [PATCH 049/591] stub: Ignore unary response on server if status is not OK Fixes #5969 --- .../main/java/io/grpc/stub/ServerCalls.java | 24 +++++++-- .../java/io/grpc/stub/ServerCallsTest.java | 54 +++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/stub/src/main/java/io/grpc/stub/ServerCalls.java b/stub/src/main/java/io/grpc/stub/ServerCalls.java index 7990a5b34c0..6c444551530 100644 --- a/stub/src/main/java/io/grpc/stub/ServerCalls.java +++ b/stub/src/main/java/io/grpc/stub/ServerCalls.java @@ -335,6 +335,7 @@ private static final class ServerCallStreamObserverImpl private boolean aborted = false; private boolean completed = false; private Runnable onCloseHandler; + private RespT unaryResponse; // Non private to avoid synthetic class ServerCallStreamObserverImpl(ServerCall call, boolean serverStreamingOrBidi) { @@ -373,15 +374,22 @@ public void onNext(RespT response) { } checkState(!aborted, "Stream was terminated by error, no further calls are allowed"); checkState(!completed, "Stream is already completed, no further calls are allowed"); - if (!sentHeaders) { - call.sendHeaders(new Metadata()); - sentHeaders = true; + if (serverStreamingOrBidi) { + if (!sentHeaders) { + call.sendHeaders(new Metadata()); + sentHeaders = true; + } + call.sendMessage(response); + } else { + unaryResponse = response; } - call.sendMessage(response); } @Override public void onError(Throwable t) { + if (!serverStreamingOrBidi) { + unaryResponse = null; + } Metadata metadata = Status.trailersFromThrowable(t); if (metadata == null) { metadata = new Metadata(); @@ -392,6 +400,14 @@ public void onError(Throwable t) { @Override public void onCompleted() { + if (!serverStreamingOrBidi && unaryResponse != null) { + if (!sentHeaders) { + call.sendHeaders(new Metadata()); + sentHeaders = true; + } + call.sendMessage(unaryResponse); + unaryResponse = null; + } call.close(Status.OK, new Metadata()); completed = true; } diff --git a/stub/src/test/java/io/grpc/stub/ServerCallsTest.java b/stub/src/test/java/io/grpc/stub/ServerCallsTest.java index 1e51ac10110..8d65be19e20 100644 --- a/stub/src/test/java/io/grpc/stub/ServerCallsTest.java +++ b/stub/src/test/java/io/grpc/stub/ServerCallsTest.java @@ -33,6 +33,7 @@ import io.grpc.ServerServiceDefinition; import io.grpc.ServiceDescriptor; import io.grpc.Status; +import io.grpc.Status.Code; import io.grpc.StatusRuntimeException; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; @@ -620,6 +621,59 @@ public void onClose(Status status, Metadata trailers) { assertArrayEquals(new int[]{0, 1, 1, 2, 2, 2}, receivedMessages); } + @Test + public void serverUnaryResponseMsgWithOkStatus() { + ServerCallHandler serverCallHandler = + ServerCalls.asyncUnaryCall( + new ServerCalls.UnaryMethod() { + @Override + public void invoke(Integer request, StreamObserver responseObserver) { + responseObserver.onNext(request); + responseObserver.onCompleted(); + } + }); + ServerCall.Listener callListener = + serverCallHandler.startCall(serverCall, new Metadata()); + serverCall.isReady = true; + serverCall.isCancelled = false; + callListener.onReady(); + callListener.onMessage(1); + callListener.onHalfClose(); + + assertThat(serverCall.status.getCode()).isEqualTo(Code.OK); + assertThat(serverCall.responses).containsExactly(1); + } + + @Test + public void serverUnaryResponseMsgWithNotOkStatus() { + ServerCallHandler serverCallHandler = + ServerCalls.asyncUnaryCall( + new ServerCalls.UnaryMethod() { + @Override + public void invoke(Integer request, StreamObserver responseObserver) { + responseObserver.onNext(request); + responseObserver.onError( + Status.INTERNAL + .withDescription("Response message is null for unary call") + .asRuntimeException()); + } + }); + + ServerCall.Listener callListener = + serverCallHandler.startCall(serverCall, new Metadata()); + + serverCall.isReady = true; + serverCall.isCancelled = false; + callListener.onReady(); + callListener.onMessage(1); + callListener.onHalfClose(); + + assertThat(serverCall.status.getCode()).isEqualTo(Code.INTERNAL); + assertThat(serverCall.status.getDescription()) + .isEqualTo("Response message is null for unary call"); + assertThat(serverCall.responses).isEmpty(); + } + public static class IntegerMarshaller implements MethodDescriptor.Marshaller { @Override public InputStream stream(Integer value) { From b692b9d26ebd5fc9df711ce9412bba8956b07969 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 15 Oct 2024 12:50:38 -0700 Subject: [PATCH 050/591] core: Handle NR/LB exceptions when panicking If a panic is followed a panic, we'd ignore the second. But if an exception happens while entering panic mode we may fail to update the picker with the first error. This is "fine" from a correctness standpoint; all bets are off when panicking and we've already logged the first error. But failing RPCs can often be more easily seen than just the log. Noticed because of http://yaqs/8493785598685872128 --- .../io/grpc/internal/ManagedChannelImpl.java | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index cda4299acec..b89a126517f 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -779,30 +779,16 @@ void panic(final Throwable t) { return; } panicMode = true; - cancelIdleTimer(/* permanent= */ true); - shutdownNameResolverAndLoadBalancer(false); - final class PanicSubchannelPicker extends SubchannelPicker { - private final PickResult panicPickResult = - PickResult.withDrop( - Status.INTERNAL.withDescription("Panic! This is a bug!").withCause(t)); - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return panicPickResult; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(PanicSubchannelPicker.class) - .add("panicPickResult", panicPickResult) - .toString(); - } + try { + cancelIdleTimer(/* permanent= */ true); + shutdownNameResolverAndLoadBalancer(false); + } finally { + updateSubchannelPicker(new LoadBalancer.FixedResultPicker(PickResult.withDrop( + Status.INTERNAL.withDescription("Panic! This is a bug!").withCause(t)))); + realChannel.updateConfigSelector(null); + channelLogger.log(ChannelLogLevel.ERROR, "PANIC! Entering TRANSIENT_FAILURE"); + channelStateManager.gotoState(TRANSIENT_FAILURE); } - - updateSubchannelPicker(new PanicSubchannelPicker()); - realChannel.updateConfigSelector(null); - channelLogger.log(ChannelLogLevel.ERROR, "PANIC! Entering TRANSIENT_FAILURE"); - channelStateManager.gotoState(TRANSIENT_FAILURE); } @VisibleForTesting @@ -1404,7 +1390,7 @@ public void updateBalancingState( final class UpdateBalancingState implements Runnable { @Override public void run() { - if (LbHelperImpl.this != lbHelper) { + if (LbHelperImpl.this != lbHelper || panicMode) { return; } updateSubchannelPicker(newPicker); From 84d30afad6fb966ab9b59fff0b381cc57129d958 Mon Sep 17 00:00:00 2001 From: Vindhya Ningegowda Date: Wed, 16 Oct 2024 16:12:27 -0700 Subject: [PATCH 051/591] Get mesh_id local label from "CSM_MESH_ID" environment variable, rather than parsing from bootstrap file (#11621) --- .../csm/observability/MetadataExchanger.java | 37 +---------- .../observability/CsmObservabilityTest.java | 65 +++++-------------- .../observability/MetadataExchangerTest.java | 59 ++--------------- 3 files changed, 27 insertions(+), 134 deletions(-) diff --git a/gcp-csm-observability/src/main/java/io/grpc/gcp/csm/observability/MetadataExchanger.java b/gcp-csm-observability/src/main/java/io/grpc/gcp/csm/observability/MetadataExchanger.java index bf76c2532bc..5f05d52c7e7 100644 --- a/gcp-csm-observability/src/main/java/io/grpc/gcp/csm/observability/MetadataExchanger.java +++ b/gcp-csm-observability/src/main/java/io/grpc/gcp/csm/observability/MetadataExchanger.java @@ -16,7 +16,6 @@ package io.grpc.gcp.csm.observability; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.io.BaseEncoding; import com.google.protobuf.Struct; @@ -29,12 +28,9 @@ import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Status; -import io.grpc.internal.JsonParser; -import io.grpc.internal.JsonUtil; import io.grpc.opentelemetry.InternalOpenTelemetryPlugin; import io.grpc.protobuf.ProtoUtils; import io.grpc.xds.ClusterImplLoadBalancerProvider; -import io.grpc.xds.InternalGrpcBootstrapperImpl; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; @@ -43,8 +39,6 @@ import java.net.URI; import java.util.Map; import java.util.function.Consumer; -import java.util.logging.Level; -import java.util.logging.Logger; /** * OpenTelemetryPlugin implementing metadata-based workload property exchange for both client and @@ -52,7 +46,6 @@ * and remote details to metrics. */ final class MetadataExchanger implements InternalOpenTelemetryPlugin { - private static final Logger logger = Logger.getLogger(MetadataExchanger.class.getName()); private static final AttributeKey CLOUD_PLATFORM = AttributeKey.stringKey("cloud.platform"); @@ -89,11 +82,10 @@ final class MetadataExchanger implements InternalOpenTelemetryPlugin { public MetadataExchanger() { this( addOtelResourceAttributes(new GCPResourceProvider().getAttributes()), - System::getenv, - InternalGrpcBootstrapperImpl::getJsonContent); + System::getenv); } - MetadataExchanger(Attributes platformAttributes, Lookup env, Supplier xdsBootstrap) { + MetadataExchanger(Attributes platformAttributes, Lookup env) { String type = platformAttributes.get(CLOUD_PLATFORM); String canonicalService = env.get("CSM_CANONICAL_SERVICE_NAME"); Struct.Builder struct = Struct.newBuilder(); @@ -121,7 +113,7 @@ public MetadataExchanger() { localMetadata = BaseEncoding.base64().encode(struct.build().toByteArray()); localAttributes = Attributes.builder() - .put("csm.mesh_id", nullIsUnknown(getMeshId(xdsBootstrap))) + .put("csm.mesh_id", nullIsUnknown(env.get("CSM_MESH_ID"))) .put("csm.workload_canonical_service", nullIsUnknown(canonicalService)) .build(); } @@ -162,29 +154,6 @@ private static Attributes addOtelResourceAttributes(Attributes platformAttribute return builder.build(); } - @VisibleForTesting - static String getMeshId(Supplier xdsBootstrap) { - try { - @SuppressWarnings("unchecked") - Map rawBootstrap = (Map) JsonParser.parse(xdsBootstrap.get()); - Map node = JsonUtil.getObject(rawBootstrap, "node"); - String id = JsonUtil.getString(node, "id"); - Preconditions.checkNotNull(id, "id"); - String[] parts = id.split("/", 6); - if (!(parts.length == 6 - && parts[0].equals("projects") - && parts[2].equals("networks") - && parts[3].startsWith("mesh:") - && parts[4].equals("nodes"))) { - throw new Exception("node id didn't match mesh format: " + id); - } - return parts[3].substring("mesh:".length()); - } catch (Exception e) { - logger.log(Level.INFO, "Failed to determine mesh ID for CSM", e); - return null; - } - } - private void addLabels(AttributesBuilder to, Struct struct) { to.putAll(localAttributes); Map remote = struct.getFieldsMap(); diff --git a/gcp-csm-observability/src/test/java/io/grpc/gcp/csm/observability/CsmObservabilityTest.java b/gcp-csm-observability/src/test/java/io/grpc/gcp/csm/observability/CsmObservabilityTest.java index 878bf30ce34..aba2c43c44f 100644 --- a/gcp-csm-observability/src/test/java/io/grpc/gcp/csm/observability/CsmObservabilityTest.java +++ b/gcp-csm-observability/src/test/java/io/grpc/gcp/csm/observability/CsmObservabilityTest.java @@ -77,17 +77,14 @@ public void tearDown() { @Test public void unknownDataExchange() throws Exception { - String xdsBootstrap = ""; MetadataExchanger clientExchanger = new MetadataExchanger( Attributes.builder().build(), - ImmutableMap.of()::get, - () -> xdsBootstrap); + ImmutableMap.of()::get); CsmObservability.Builder clientCsmBuilder = new CsmObservability.Builder(clientExchanger) .sdk(openTelemetryTesting.getOpenTelemetry()); MetadataExchanger serverExchanger = new MetadataExchanger( Attributes.builder().build(), - ImmutableMap.of()::get, - () -> xdsBootstrap); + ImmutableMap.of()::get); CsmObservability.Builder serverCsmBuilder = new CsmObservability.Builder(serverExchanger) .sdk(openTelemetryTesting.getOpenTelemetry()); @@ -140,11 +137,9 @@ public void unknownDataExchange() throws Exception { @Test public void nonCsmServer() throws Exception { - String xdsBootstrap = ""; MetadataExchanger clientExchanger = new MetadataExchanger( Attributes.builder().build(), - ImmutableMap.of()::get, - () -> xdsBootstrap); + ImmutableMap.of()::get); CsmObservability.Builder clientCsmBuilder = new CsmObservability.Builder(clientExchanger) .sdk(openTelemetryTesting.getOpenTelemetry()); @@ -205,19 +200,16 @@ public void nonCsmServer() throws Exception { @Test public void nonCsmClient() throws Exception { - String xdsBootstrap = ""; MetadataExchanger clientExchanger = new MetadataExchanger( Attributes.builder() .put(stringKey("cloud.platform"), "gcp_kubernetes_engine") .build(), - ImmutableMap.of()::get, - () -> xdsBootstrap); + ImmutableMap.of()::get); CsmObservability.Builder clientCsmBuilder = new CsmObservability.Builder(clientExchanger) .sdk(openTelemetryTesting.getOpenTelemetry()); MetadataExchanger serverExchanger = new MetadataExchanger( Attributes.builder().build(), - ImmutableMap.of()::get, - () -> xdsBootstrap); + ImmutableMap.of()::get); CsmObservability.Builder serverCsmBuilder = new CsmObservability.Builder(serverExchanger) .sdk(openTelemetryTesting.getOpenTelemetry()); @@ -262,11 +254,6 @@ public void nonCsmClient() throws Exception { @Test public void k8sExchange() throws Exception { - // Purposefully use a different project ID in the bootstrap than the resource, as the mesh could - // be in a different project than the running account. - String clientBootstrap = "{\"node\": {" - + "\"id\": \"projects/12/networks/mesh:mymesh/nodes/a6420022-cbc5-4e10-808c-507e3fc95f2e\"" - + "}}"; MetadataExchanger clientExchanger = new MetadataExchanger( Attributes.builder() .put(stringKey("cloud.platform"), "gcp_kubernetes_engine") @@ -277,13 +264,10 @@ public void k8sExchange() throws Exception { .build(), ImmutableMap.of( "CSM_CANONICAL_SERVICE_NAME", "canon-service-is-a-client", - "CSM_WORKLOAD_NAME", "best-client")::get, - () -> clientBootstrap); + "CSM_WORKLOAD_NAME", "best-client", + "CSM_MESH_ID", "mymesh")::get); CsmObservability.Builder clientCsmBuilder = new CsmObservability.Builder(clientExchanger) .sdk(openTelemetryTesting.getOpenTelemetry()); - String serverBootstrap = "{\"node\": {" - + "\"id\": \"projects/34/networks/mesh:meshhh/nodes/4969ef19-24b6-44c0-baf3-86d188ff5967\"" - + "}}"; MetadataExchanger serverExchanger = new MetadataExchanger( Attributes.builder() .put(stringKey("cloud.platform"), "gcp_kubernetes_engine") @@ -295,8 +279,8 @@ public void k8sExchange() throws Exception { .build(), ImmutableMap.of( "CSM_CANONICAL_SERVICE_NAME", "server-has-a-single-name", - "CSM_WORKLOAD_NAME", "fast-server")::get, - () -> serverBootstrap); + "CSM_WORKLOAD_NAME", "fast-server", + "CSM_MESH_ID", "meshhh")::get); CsmObservability.Builder serverCsmBuilder = new CsmObservability.Builder(serverExchanger) .sdk(openTelemetryTesting.getOpenTelemetry()); @@ -366,11 +350,6 @@ public void k8sExchange() throws Exception { @Test public void gceExchange() throws Exception { - // Purposefully use a different project ID in the bootstrap than the resource, as the mesh could - // be in a different project than the running account. - String clientBootstrap = "{\"node\": {" - + "\"id\": \"projects/12/networks/mesh:mymesh/nodes/a6420022-cbc5-4e10-808c-507e3fc95f2e\"" - + "}}"; MetadataExchanger clientExchanger = new MetadataExchanger( Attributes.builder() .put(stringKey("cloud.platform"), "gcp_compute_engine") @@ -379,13 +358,10 @@ public void gceExchange() throws Exception { .build(), ImmutableMap.of( "CSM_CANONICAL_SERVICE_NAME", "canon-service-is-a-client", - "CSM_WORKLOAD_NAME", "best-client")::get, - () -> clientBootstrap); + "CSM_WORKLOAD_NAME", "best-client", + "CSM_MESH_ID", "mymesh")::get); CsmObservability.Builder clientCsmBuilder = new CsmObservability.Builder(clientExchanger) .sdk(openTelemetryTesting.getOpenTelemetry()); - String serverBootstrap = "{\"node\": {" - + "\"id\": \"projects/34/networks/mesh:meshhh/nodes/4969ef19-24b6-44c0-baf3-86d188ff5967\"" - + "}}"; MetadataExchanger serverExchanger = new MetadataExchanger( Attributes.builder() .put(stringKey("cloud.platform"), "gcp_compute_engine") @@ -395,8 +371,8 @@ public void gceExchange() throws Exception { .build(), ImmutableMap.of( "CSM_CANONICAL_SERVICE_NAME", "server-has-a-single-name", - "CSM_WORKLOAD_NAME", "fast-server")::get, - () -> serverBootstrap); + "CSM_WORKLOAD_NAME", "fast-server", + "CSM_MESH_ID", "meshhh")::get); CsmObservability.Builder serverCsmBuilder = new CsmObservability.Builder(serverExchanger) .sdk(openTelemetryTesting.getOpenTelemetry()); @@ -456,9 +432,6 @@ public void gceExchange() throws Exception { @Test public void trailersOnly() throws Exception { - String clientBootstrap = "{\"node\": {" - + "\"id\": \"projects/12/networks/mesh:mymesh/nodes/a6420022-cbc5-4e10-808c-507e3fc95f2e\"" - + "}}"; MetadataExchanger clientExchanger = new MetadataExchanger( Attributes.builder() .put(stringKey("cloud.platform"), "gcp_compute_engine") @@ -467,13 +440,11 @@ public void trailersOnly() throws Exception { .build(), ImmutableMap.of( "CSM_CANONICAL_SERVICE_NAME", "canon-service-is-a-client", - "CSM_WORKLOAD_NAME", "best-client")::get, - () -> clientBootstrap); + "CSM_WORKLOAD_NAME", "best-client", + "CSM_MESH_ID", "mymesh")::get); CsmObservability.Builder clientCsmBuilder = new CsmObservability.Builder(clientExchanger) .sdk(openTelemetryTesting.getOpenTelemetry()); - String serverBootstrap = "{\"node\": {" - + "\"id\": \"projects/34/networks/mesh:meshhh/nodes/4969ef19-24b6-44c0-baf3-86d188ff5967\"" - + "}}"; + MetadataExchanger serverExchanger = new MetadataExchanger( Attributes.builder() .put(stringKey("cloud.platform"), "gcp_compute_engine") @@ -483,8 +454,8 @@ public void trailersOnly() throws Exception { .build(), ImmutableMap.of( "CSM_CANONICAL_SERVICE_NAME", "server-has-a-single-name", - "CSM_WORKLOAD_NAME", "fast-server")::get, - () -> serverBootstrap); + "CSM_WORKLOAD_NAME", "fast-server", + "CSM_MESH_ID", "meshhh")::get); CsmObservability.Builder serverCsmBuilder = new CsmObservability.Builder(serverExchanger) .sdk(openTelemetryTesting.getOpenTelemetry()); diff --git a/gcp-csm-observability/src/test/java/io/grpc/gcp/csm/observability/MetadataExchangerTest.java b/gcp-csm-observability/src/test/java/io/grpc/gcp/csm/observability/MetadataExchangerTest.java index 20665e502f7..cc3472be182 100644 --- a/gcp-csm-observability/src/test/java/io/grpc/gcp/csm/observability/MetadataExchangerTest.java +++ b/gcp-csm-observability/src/test/java/io/grpc/gcp/csm/observability/MetadataExchangerTest.java @@ -33,56 +33,11 @@ /** Tests for {@link MetadataExchanger}. */ @RunWith(JUnit4.class) public final class MetadataExchangerTest { - @Test - public void getMeshId_findsMeshId() { - assertThat(MetadataExchanger.getMeshId(() -> - "{\"node\":{\"id\":\"projects/12/networks/mesh:mine/nodes/uu-id\"}}")) - .isEqualTo("mine"); - assertThat(MetadataExchanger.getMeshId(() -> - "{\"node\":{\"id\":\"projects/1234567890/networks/mesh:mine/nodes/uu-id\", " - + "\"unknown\": \"\"}, \"unknown\": \"\"}")) - .isEqualTo("mine"); - } - - @Test - public void getMeshId_returnsNullOnBadMeshId() { - assertThat(MetadataExchanger.getMeshId( - () -> "[\"node\"]")) - .isNull(); - assertThat(MetadataExchanger.getMeshId( - () -> "{\"node\":[\"id\"]}}")) - .isNull(); - assertThat(MetadataExchanger.getMeshId( - () -> "{\"node\":{\"id\":[\"projects/12/networks/mesh:mine/nodes/uu-id\"]}}")) - .isNull(); - - assertThat(MetadataExchanger.getMeshId( - () -> "{\"NODE\":{\"id\":\"projects/12/networks/mesh:mine/nodes/uu-id\"}}")) - .isNull(); - assertThat(MetadataExchanger.getMeshId( - () -> "{\"node\":{\"ID\":\"projects/12/networks/mesh:mine/nodes/uu-id\"}}")) - .isNull(); - assertThat(MetadataExchanger.getMeshId( - () -> "{\"node\":{\"id\":\"projects/12/networks/mesh:mine\"}}")) - .isNull(); - assertThat(MetadataExchanger.getMeshId( - () -> "{\"node\":{\"id\":\"PROJECTS/12/networks/mesh:mine/nodes/uu-id\"}}")) - .isNull(); - assertThat(MetadataExchanger.getMeshId( - () -> "{\"node\":{\"id\":\"projects/12/NETWORKS/mesh:mine/nodes/uu-id\"}}")) - .isNull(); - assertThat(MetadataExchanger.getMeshId( - () -> "{\"node\":{\"id\":\"projects/12/networks/MESH:mine/nodes/uu-id\"}}")) - .isNull(); - assertThat(MetadataExchanger.getMeshId( - () -> "{\"node\":{\"id\":\"projects/12/networks/mesh:mine/NODES/uu-id\"}}")) - .isNull(); - } @Test public void enablePluginForChannel_matches() { MetadataExchanger exchanger = - new MetadataExchanger(Attributes.builder().build(), (name) -> null, () -> ""); + new MetadataExchanger(Attributes.builder().build(), (name) -> null); assertThat(exchanger.enablePluginForChannel("xds:///testing")).isTrue(); assertThat(exchanger.enablePluginForChannel("xds:/testing")).isTrue(); assertThat(exchanger.enablePluginForChannel( @@ -92,7 +47,7 @@ public void enablePluginForChannel_matches() { @Test public void enablePluginForChannel_doesNotMatch() { MetadataExchanger exchanger = - new MetadataExchanger(Attributes.builder().build(), (name) -> null, () -> ""); + new MetadataExchanger(Attributes.builder().build(), (name) -> null); assertThat(exchanger.enablePluginForChannel("dns:///localhost")).isFalse(); assertThat(exchanger.enablePluginForChannel("xds:///[]")).isFalse(); assertThat(exchanger.enablePluginForChannel("xds://my-xds-server/testing")).isFalse(); @@ -101,7 +56,7 @@ public void enablePluginForChannel_doesNotMatch() { @Test public void addLabels_receivedWrongType() { MetadataExchanger exchanger = - new MetadataExchanger(Attributes.builder().build(), (name) -> null, () -> ""); + new MetadataExchanger(Attributes.builder().build(), (name) -> null); Metadata metadata = new Metadata(); metadata.put(Metadata.Key.of("x-envoy-peer-metadata", Metadata.ASCII_STRING_MARSHALLER), BaseEncoding.base64().encode(Struct.newBuilder() @@ -122,7 +77,7 @@ public void addLabels_receivedWrongType() { @Test public void addLabelsFromExchange_unknownGcpType() { MetadataExchanger exchanger = - new MetadataExchanger(Attributes.builder().build(), (name) -> null, () -> ""); + new MetadataExchanger(Attributes.builder().build(), (name) -> null); Metadata metadata = new Metadata(); metadata.put(Metadata.Key.of("x-envoy-peer-metadata", Metadata.ASCII_STRING_MARSHALLER), BaseEncoding.base64().encode(Struct.newBuilder() @@ -153,8 +108,7 @@ public void addMetadata_k8s() throws Exception { .build(), ImmutableMap.of( "CSM_CANONICAL_SERVICE_NAME", "myservice1", - "CSM_WORKLOAD_NAME", "myworkload1")::get, - () -> ""); + "CSM_WORKLOAD_NAME", "myworkload1")::get); Metadata metadata = new Metadata(); exchanger.newClientCallPlugin().addMetadata(metadata); @@ -182,8 +136,7 @@ public void addMetadata_gce() throws Exception { .build(), ImmutableMap.of( "CSM_CANONICAL_SERVICE_NAME", "myservice1", - "CSM_WORKLOAD_NAME", "myworkload1")::get, - () -> ""); + "CSM_WORKLOAD_NAME", "myworkload1")::get); Metadata metadata = new Metadata(); exchanger.newClientCallPlugin().addMetadata(metadata); From 23ebf364d46205bfbc1e25d7d1e57e475731ad97 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 8 Oct 2024 13:41:55 -0700 Subject: [PATCH 052/591] inprocess: Delete "standalone" internal transport This had been used for a time with a combined inprocess+binder server. However, just having multiple servers worked fine and this is no longer used/needed. --- .../io/grpc/inprocess/InProcessTransport.java | 46 +---- .../io/grpc/inprocess/InternalInProcess.java | 67 ------- .../StandaloneInProcessTransportTest.java | 164 ------------------ 3 files changed, 9 insertions(+), 268 deletions(-) delete mode 100644 inprocess/src/main/java/io/grpc/inprocess/InternalInProcess.java delete mode 100644 inprocess/src/test/java/io/grpc/inprocess/StandaloneInProcessTransportTest.java diff --git a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java index ae597e34af0..0d173b96711 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java @@ -21,7 +21,6 @@ import static java.lang.Math.max; import com.google.common.base.MoreObjects; -import com.google.common.base.Optional; import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; @@ -54,7 +53,6 @@ import io.grpc.internal.ManagedClientTransport; import io.grpc.internal.NoopClientStream; import io.grpc.internal.ObjectPool; -import io.grpc.internal.ServerListener; import io.grpc.internal.ServerStream; import io.grpc.internal.ServerStreamListener; import io.grpc.internal.ServerTransport; @@ -90,7 +88,6 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans private final int clientMaxInboundMetadataSize; private final String authority; private final String userAgent; - private final Optional optionalServerListener; private int serverMaxInboundMetadataSize; private final boolean includeCauseWithStatus; private ObjectPool serverSchedulerPool; @@ -139,8 +136,8 @@ protected void handleNotInUse() { } }; - private InProcessTransport(SocketAddress address, int maxInboundMetadataSize, String authority, - String userAgent, Attributes eagAttrs, Optional optionalServerListener, + public InProcessTransport(SocketAddress address, int maxInboundMetadataSize, String authority, + String userAgent, Attributes eagAttrs, boolean includeCauseWithStatus, long assumedMessageSize) { this.address = address; this.clientMaxInboundMetadataSize = maxInboundMetadataSize; @@ -153,48 +150,23 @@ private InProcessTransport(SocketAddress address, int maxInboundMetadataSize, St .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, address) .set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, address) .build(); - this.optionalServerListener = optionalServerListener; logId = InternalLogId.allocate(getClass(), address.toString()); this.includeCauseWithStatus = includeCauseWithStatus; this.assumedMessageSize = assumedMessageSize; } - public InProcessTransport( - SocketAddress address, int maxInboundMetadataSize, String authority, String userAgent, - Attributes eagAttrs, boolean includeCauseWithStatus, long assumedMessageSize) { - this(address, maxInboundMetadataSize, authority, userAgent, eagAttrs, - Optional.absent(), includeCauseWithStatus, assumedMessageSize); - } - - InProcessTransport( - String name, int maxInboundMetadataSize, String authority, String userAgent, - Attributes eagAttrs, ObjectPool serverSchedulerPool, - List serverStreamTracerFactories, - ServerListener serverListener, boolean includeCauseWithStatus, long assumedMessageSize) { - this(new InProcessSocketAddress(name), maxInboundMetadataSize, authority, userAgent, eagAttrs, - Optional.of(serverListener), includeCauseWithStatus, assumedMessageSize); - this.serverMaxInboundMetadataSize = maxInboundMetadataSize; - this.serverSchedulerPool = serverSchedulerPool; - this.serverStreamTracerFactories = serverStreamTracerFactories; - } - @CheckReturnValue @Override public synchronized Runnable start(ManagedClientTransport.Listener listener) { this.clientTransportListener = listener; - if (optionalServerListener.isPresent()) { + InProcessServer server = InProcessServer.findServer(address); + if (server != null) { + serverMaxInboundMetadataSize = server.getMaxInboundMetadataSize(); + serverSchedulerPool = server.getScheduledExecutorServicePool(); serverScheduler = serverSchedulerPool.getObject(); - serverTransportListener = optionalServerListener.get().transportCreated(this); - } else { - InProcessServer server = InProcessServer.findServer(address); - if (server != null) { - serverMaxInboundMetadataSize = server.getMaxInboundMetadataSize(); - serverSchedulerPool = server.getScheduledExecutorServicePool(); - serverScheduler = serverSchedulerPool.getObject(); - serverStreamTracerFactories = server.getStreamTracerFactories(); - // Must be semi-initialized; past this point, can begin receiving requests - serverTransportListener = server.register(this); - } + serverStreamTracerFactories = server.getStreamTracerFactories(); + // Must be semi-initialized; past this point, can begin receiving requests + serverTransportListener = server.register(this); } if (serverTransportListener == null) { shutdownStatus = Status.UNAVAILABLE.withDescription("Could not find server: " + address); diff --git a/inprocess/src/main/java/io/grpc/inprocess/InternalInProcess.java b/inprocess/src/main/java/io/grpc/inprocess/InternalInProcess.java deleted file mode 100644 index 9e08c2523fb..00000000000 --- a/inprocess/src/main/java/io/grpc/inprocess/InternalInProcess.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.inprocess; - -import io.grpc.Attributes; -import io.grpc.Internal; -import io.grpc.ServerStreamTracer; -import io.grpc.internal.ConnectionClientTransport; -import io.grpc.internal.ObjectPool; -import io.grpc.internal.ServerListener; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; - -/** - * Internal {@link InProcessTransport} accessor. - * - *

This is intended for use by io.grpc.internal, and the specifically - * supported transport packages. - */ -@Internal -public final class InternalInProcess { - - private InternalInProcess() {} - - /** - * Creates a new InProcessTransport. - * - *

When started, the transport will be registered with the given - * {@link ServerListener}. - */ - @Internal - public static ConnectionClientTransport createInProcessTransport( - String name, - int maxInboundMetadataSize, - String authority, - String userAgent, - Attributes eagAttrs, - ObjectPool serverSchedulerPool, - List serverStreamTracerFactories, - ServerListener serverListener, - boolean includeCauseWithStatus) { - return new InProcessTransport( - name, - maxInboundMetadataSize, - authority, - userAgent, - eagAttrs, - serverSchedulerPool, - serverStreamTracerFactories, - serverListener, - includeCauseWithStatus, -1); - } -} diff --git a/inprocess/src/test/java/io/grpc/inprocess/StandaloneInProcessTransportTest.java b/inprocess/src/test/java/io/grpc/inprocess/StandaloneInProcessTransportTest.java deleted file mode 100644 index b26a5d8498d..00000000000 --- a/inprocess/src/test/java/io/grpc/inprocess/StandaloneInProcessTransportTest.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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.inprocess; - -import io.grpc.InternalChannelz.SocketStats; -import io.grpc.InternalInstrumented; -import io.grpc.ServerStreamTracer; -import io.grpc.internal.AbstractTransportTest; -import io.grpc.internal.GrpcUtil; -import io.grpc.internal.InternalServer; -import io.grpc.internal.ManagedClientTransport; -import io.grpc.internal.ObjectPool; -import io.grpc.internal.ServerListener; -import io.grpc.internal.ServerTransport; -import io.grpc.internal.ServerTransportListener; -import io.grpc.internal.SharedResourcePool; -import java.io.IOException; -import java.net.SocketAddress; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; -import javax.annotation.Nullable; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Unit tests for {@link InProcessTransport} when used with a separate {@link InternalServer}. */ -@RunWith(JUnit4.class) -public final class StandaloneInProcessTransportTest extends AbstractTransportTest { - private static final String TRANSPORT_NAME = "perfect-for-testing"; - private static final String AUTHORITY = "a-testing-authority"; - private static final String USER_AGENT = "a-testing-user-agent"; - - private final ObjectPool schedulerPool = - SharedResourcePool.forResource(GrpcUtil.TIMER_SERVICE); - - private TestServer currentServer; - - @Override - protected InternalServer newServer( - List streamTracerFactories) { - return new TestServer(streamTracerFactories); - } - - @Override - protected InternalServer newServer( - int port, List streamTracerFactories) { - return newServer(streamTracerFactories); - } - - @Override - protected String testAuthority(InternalServer server) { - return AUTHORITY; - } - - @Override - protected ManagedClientTransport newClientTransport(InternalServer server) { - TestServer testServer = (TestServer) server; - return InternalInProcess.createInProcessTransport( - TRANSPORT_NAME, - GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, - testAuthority(server), - USER_AGENT, - eagAttrs(), - schedulerPool, - testServer.streamTracerFactories, - testServer.serverListener, - false); - } - - @Test - @Ignore - @Override - public void socketStats() throws Exception { - // test does not apply to in-process - } - - /** An internalserver just for this test. */ - private final class TestServer implements InternalServer { - - final List streamTracerFactories; - ServerListener serverListener; - - TestServer(List streamTracerFactories) { - this.streamTracerFactories = streamTracerFactories; - } - - @Override - public void start(ServerListener serverListener) throws IOException { - if (currentServer != null) { - throw new IOException("Server already present"); - } - currentServer = this; - this.serverListener = new ServerListenerWrapper(serverListener); - } - - @Override - public void shutdown() { - currentServer = null; - serverListener.serverShutdown(); - } - - @Override - public SocketAddress getListenSocketAddress() { - return new SocketAddress() {}; - } - - @Override - public List getListenSocketAddresses() { - return Collections.singletonList(getListenSocketAddress()); - } - - @Override - @Nullable - public InternalInstrumented getListenSocketStats() { - return null; - } - - @Override - @Nullable - public List> getListenSocketStatsList() { - return null; - } - } - - /** Wraps the server listener to ensure we don't accept new transports after shutdown. */ - private static final class ServerListenerWrapper implements ServerListener { - private final ServerListener delegateListener; - private boolean shutdown; - - ServerListenerWrapper(ServerListener delegateListener) { - this.delegateListener = delegateListener; - } - - @Override - public ServerTransportListener transportCreated(ServerTransport transport) { - if (shutdown) { - return null; - } - return delegateListener.transportCreated(transport); - } - - @Override - public void serverShutdown() { - shutdown = true; - delegateListener.serverShutdown(); - } - } -} From 1e0928fb7927bef13e1dbc2b47ef8971d16673cb Mon Sep 17 00:00:00 2001 From: Eng Zer Jun Date: Sun, 29 Sep 2024 23:48:41 +0800 Subject: [PATCH 053/591] api: fix javadoc of CallCredentials.applyRequestMetadata It is the `Executor appExecutor` that should be given an asynchronous task, not `CallCredentials.MetadataApplier applier`. Signed-off-by: Eng Zer Jun --- api/src/main/java/io/grpc/CallCredentials.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/io/grpc/CallCredentials.java b/api/src/main/java/io/grpc/CallCredentials.java index 31b68b22dae..eb92a6f15fa 100644 --- a/api/src/main/java/io/grpc/CallCredentials.java +++ b/api/src/main/java/io/grpc/CallCredentials.java @@ -43,7 +43,7 @@ public abstract class CallCredentials { *

It is called for each individual RPC, within the {@link Context} of the call, before the * stream is about to be created on a transport. Implementations should not block in this * method. If metadata is not immediately available, e.g., needs to be fetched from network, the - * implementation may give the {@code applier} to an asynchronous task which will eventually call + * implementation may give the {@code appExecutor} an asynchronous task which will eventually call * the {@code applier}. The RPC proceeds only after the {@code applier} is called. * * @param requestInfo request-related information From 4be69e3f8a9c70ec36234d6373c9854eab6e9027 Mon Sep 17 00:00:00 2001 From: erm-g <110920239+erm-g@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:11:07 -0400 Subject: [PATCH 054/591] core: SpiffeUtil API for extracting Spiffe URI and loading TrustBundles (#11575) Additional API for SpiffeUtil: - extract Spiffe URI from certificate chain - load Spiffe Trust Bundle from filesystem [json spec][] [JWK spec][] JsonParser was changed to reject duplicate keys in objects. [json spec]: https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md [JWK spec]: https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#61-publishing-spiffe-bundle-elements --- .../java/io/grpc/internal/JsonParser.java | 7 +- .../java/io/grpc/internal/SpiffeUtil.java | 190 +++++++++++++++++- .../java/io/grpc/internal/JsonParserTest.java | 9 +- .../java/io/grpc/internal/SpiffeUtilTest.java | 172 +++++++++++++++- .../io/grpc/internal/spiffebundle.json | 115 +++++++++++ .../internal/spiffebundle_corrupted_cert.json | 14 ++ .../internal/spiffebundle_duplicates.json | 23 +++ .../grpc/internal/spiffebundle_malformed.json | 4 + .../grpc/internal/spiffebundle_wrong_kid.json | 15 ++ .../grpc/internal/spiffebundle_wrong_kty.json | 12 ++ .../spiffebundle_wrong_multi_certs.json | 67 ++++++ .../internal/spiffebundle_wrong_root.json | 6 + .../internal/spiffebundle_wrong_seq_type.json | 12 ++ .../grpc/internal/spiffebundle_wrong_use.json | 13 ++ .../ObservabilityConfigImplTest.java | 3 +- .../main/resources/certs/spiffe-openssl.cnf | 12 ++ .../src/main/resources/certs/spiffe_cert.pem | 33 +++ .../certs/spiffe_multi_uri_san_cert.pem | 25 +++ 18 files changed, 725 insertions(+), 7 deletions(-) create mode 100644 core/src/test/resources/io/grpc/internal/spiffebundle.json create mode 100644 core/src/test/resources/io/grpc/internal/spiffebundle_corrupted_cert.json create mode 100644 core/src/test/resources/io/grpc/internal/spiffebundle_duplicates.json create mode 100644 core/src/test/resources/io/grpc/internal/spiffebundle_malformed.json create mode 100644 core/src/test/resources/io/grpc/internal/spiffebundle_wrong_kid.json create mode 100644 core/src/test/resources/io/grpc/internal/spiffebundle_wrong_kty.json create mode 100644 core/src/test/resources/io/grpc/internal/spiffebundle_wrong_multi_certs.json create mode 100644 core/src/test/resources/io/grpc/internal/spiffebundle_wrong_root.json create mode 100644 core/src/test/resources/io/grpc/internal/spiffebundle_wrong_seq_type.json create mode 100644 core/src/test/resources/io/grpc/internal/spiffebundle_wrong_use.json create mode 100644 testing/src/main/resources/certs/spiffe-openssl.cnf create mode 100644 testing/src/main/resources/certs/spiffe_cert.pem create mode 100644 testing/src/main/resources/certs/spiffe_multi_uri_san_cert.pem diff --git a/core/src/main/java/io/grpc/internal/JsonParser.java b/core/src/main/java/io/grpc/internal/JsonParser.java index 384d29754f0..14f78c09e72 100644 --- a/core/src/main/java/io/grpc/internal/JsonParser.java +++ b/core/src/main/java/io/grpc/internal/JsonParser.java @@ -16,6 +16,7 @@ package io.grpc.internal; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import com.google.gson.stream.JsonReader; @@ -41,7 +42,8 @@ private JsonParser() {} /** * Parses a json string, returning either a {@code Map}, {@code List}, - * {@code String}, {@code Double}, {@code Boolean}, or {@code null}. + * {@code String}, {@code Double}, {@code Boolean}, or {@code null}. Fails if duplicate names + * found. */ public static Object parse(String raw) throws IOException { JsonReader jr = new JsonReader(new StringReader(raw)); @@ -81,6 +83,7 @@ private static Object parseRecursive(JsonReader jr) throws IOException { Map obj = new LinkedHashMap<>(); while (jr.hasNext()) { String name = jr.nextName(); + checkArgument(!obj.containsKey(name), "Duplicate key found: %s", name); Object value = parseRecursive(jr); obj.put(name, value); } @@ -105,4 +108,4 @@ private static Void parseJsonNull(JsonReader jr) throws IOException { jr.nextNull(); return null; } -} +} \ No newline at end of file diff --git a/core/src/main/java/io/grpc/internal/SpiffeUtil.java b/core/src/main/java/io/grpc/internal/SpiffeUtil.java index bddce3d035e..57e201d19ba 100644 --- a/core/src/main/java/io/grpc/internal/SpiffeUtil.java +++ b/core/src/main/java/io/grpc/internal/SpiffeUtil.java @@ -19,15 +19,42 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.base.Optional; import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; /** - * Helper utility to work with SPIFFE URIs. + * Provides utilities to manage SPIFFE bundles, extract SPIFFE IDs from X.509 certificate chains, + * and parse SPIFFE IDs. * @see Standard */ public final class SpiffeUtil { + private static final Integer URI_SAN_TYPE = 6; + private static final String USE_PARAMETER_VALUE = "x509-svid"; + private static final String KTY_PARAMETER_VALUE = "RSA"; + private static final String CERTIFICATE_PREFIX = "-----BEGIN CERTIFICATE-----\n"; + private static final String CERTIFICATE_SUFFIX = "-----END CERTIFICATE-----"; private static final String PREFIX = "spiffe://"; private SpiffeUtil() {} @@ -96,6 +123,137 @@ private static void validatePathSegment(String pathSegment) { + " ([a-zA-Z0-9.-_])"); } + /** + * Returns the SPIFFE ID from the leaf certificate, if present. + * + * @param certChain certificate chain to extract SPIFFE ID from + */ + public static Optional extractSpiffeId(X509Certificate[] certChain) + throws CertificateParsingException { + checkArgument(checkNotNull(certChain, "certChain").length > 0, "certChain can't be empty"); + Collection> subjectAltNames = certChain[0].getSubjectAlternativeNames(); + if (subjectAltNames == null) { + return Optional.absent(); + } + String uri = null; + // Search for the unique URI SAN. + for (List altName : subjectAltNames) { + if (altName.size() < 2 ) { + continue; + } + if (URI_SAN_TYPE.equals(altName.get(0))) { + if (uri != null) { + throw new IllegalArgumentException("Multiple URI SAN values found in the leaf cert."); + } + uri = (String) altName.get(1); + } + } + if (uri == null) { + return Optional.absent(); + } + return Optional.of(parse(uri)); + } + + /** + * Loads a SPIFFE trust bundle from a file, parsing it from the JSON format. + * In case of success, returns {@link SpiffeBundle}. + * If any element of the JSON content is invalid or unsupported, an + * {@link IllegalArgumentException} is thrown and the entire Bundle is considered invalid. + * + * @param trustBundleFile the file path to the JSON file containing the trust bundle + * @see JSON format + * @see JWK entry format + * @see x5c (certificate) parameter + */ + public static SpiffeBundle loadTrustBundleFromFile(String trustBundleFile) throws IOException { + Map trustDomainsNode = readTrustDomainsFromFile(trustBundleFile); + Map> trustBundleMap = new HashMap<>(); + Map sequenceNumbers = new HashMap<>(); + for (String trustDomainName : trustDomainsNode.keySet()) { + Map domainNode = JsonUtil.getObject(trustDomainsNode, trustDomainName); + if (domainNode.size() == 0) { + trustBundleMap.put(trustDomainName, Collections.emptyList()); + continue; + } + Long sequenceNumber = JsonUtil.getNumberAsLong(domainNode, "spiffe_sequence"); + sequenceNumbers.put(trustDomainName, sequenceNumber == null ? -1L : sequenceNumber); + List> keysNode = JsonUtil.getListOfObjects(domainNode, "keys"); + if (keysNode == null || keysNode.size() == 0) { + trustBundleMap.put(trustDomainName, Collections.emptyList()); + continue; + } + trustBundleMap.put(trustDomainName, extractCert(keysNode, trustDomainName)); + } + return new SpiffeBundle(sequenceNumbers, trustBundleMap); + } + + private static Map readTrustDomainsFromFile(String filePath) throws IOException { + Path path = Paths.get(checkNotNull(filePath, "trustBundleFile")); + String json = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + Object jsonObject = JsonParser.parse(json); + if (!(jsonObject instanceof Map)) { + throw new IllegalArgumentException( + "SPIFFE Trust Bundle should be a JSON object. Found: " + + (jsonObject == null ? null : jsonObject.getClass())); + } + @SuppressWarnings("unchecked") + Map root = (Map)jsonObject; + Map trustDomainsNode = JsonUtil.getObject(root, "trust_domains"); + checkNotNull(trustDomainsNode, "Mandatory trust_domains element is missing"); + checkArgument(trustDomainsNode.size() > 0, "Mandatory trust_domains element is missing"); + return trustDomainsNode; + } + + private static void checkJwkEntry(Map jwkNode, String trustDomainName) { + String kty = JsonUtil.getString(jwkNode, "kty"); + if (kty == null || !kty.equals(KTY_PARAMETER_VALUE)) { + throw new IllegalArgumentException(String.format("'kty' parameter must be '%s' but '%s' " + + "found. Certificate loading for trust domain '%s' failed.", KTY_PARAMETER_VALUE, + kty, trustDomainName)); + } + if (jwkNode.containsKey("kid")) { + throw new IllegalArgumentException(String.format("'kid' parameter must not be set. " + + "Certificate loading for trust domain '%s' failed.", trustDomainName)); + } + String use = JsonUtil.getString(jwkNode, "use"); + if (use == null || !use.equals(USE_PARAMETER_VALUE)) { + throw new IllegalArgumentException(String.format("'use' parameter must be '%s' but '%s' " + + "found. Certificate loading for trust domain '%s' failed.", USE_PARAMETER_VALUE, + use, trustDomainName)); + } + } + + private static List extractCert(List> keysNode, + String trustDomainName) { + List result = new ArrayList<>(); + for (Map keyNode : keysNode) { + checkJwkEntry(keyNode, trustDomainName); + List rawCerts = JsonUtil.getListOfStrings(keyNode, "x5c"); + if (rawCerts == null) { + break; + } + if (rawCerts.size() != 1) { + throw new IllegalArgumentException(String.format("Exactly 1 certificate is expected, but " + + "%s found. Certificate loading for trust domain '%s' failed.", rawCerts.size(), + trustDomainName)); + } + InputStream stream = new ByteArrayInputStream((CERTIFICATE_PREFIX + rawCerts.get(0) + "\n" + + CERTIFICATE_SUFFIX) + .getBytes(StandardCharsets.UTF_8)); + try { + Collection certs = CertificateFactory.getInstance("X509") + .generateCertificates(stream); + X509Certificate[] certsArray = certs.toArray(new X509Certificate[0]); + assert certsArray.length == 1; + result.add(certsArray[0]); + } catch (CertificateException e) { + throw new IllegalArgumentException(String.format("Certificate can't be parsed. Certificate " + + "loading for trust domain '%s' failed.", trustDomainName), e); + } + } + return result; + } + /** * Represents a SPIFFE ID as defined in the SPIFFE standard. * @see Standard @@ -119,4 +277,34 @@ public String getPath() { } } + /** + * Represents a SPIFFE trust bundle; that is, a map from trust domain to set of trusted + * certificates. Only trust domain's sequence numbers and x509 certificates are supported. + * @see Standard + */ + public static final class SpiffeBundle { + + private final ImmutableMap sequenceNumbers; + + private final ImmutableMap> bundleMap; + + private SpiffeBundle(Map sequenceNumbers, + Map> trustDomainMap) { + this.sequenceNumbers = ImmutableMap.copyOf(sequenceNumbers); + ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (Map.Entry> entry : trustDomainMap.entrySet()) { + builder.put(entry.getKey(), ImmutableList.copyOf(entry.getValue())); + } + this.bundleMap = builder.build(); + } + + public ImmutableMap getSequenceNumbers() { + return sequenceNumbers; + } + + public ImmutableMap> getBundleMap() { + return bundleMap; + } + } + } diff --git a/core/src/test/java/io/grpc/internal/JsonParserTest.java b/core/src/test/java/io/grpc/internal/JsonParserTest.java index 1e74c753d4d..cfee566fa4a 100644 --- a/core/src/test/java/io/grpc/internal/JsonParserTest.java +++ b/core/src/test/java/io/grpc/internal/JsonParserTest.java @@ -123,4 +123,11 @@ public void objectStringName() throws IOException { assertEquals(expected, JsonParser.parse("{\"hi\": 2}")); } -} + + @Test + public void duplicate() throws IOException { + thrown.expect(IllegalArgumentException.class); + + JsonParser.parse("{\"hi\": 2, \"hi\": 3}"); + } +} \ No newline at end of file diff --git a/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java b/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java index c3a98ce33e0..244539501a6 100644 --- a/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java +++ b/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java @@ -17,12 +17,28 @@ package io.grpc.internal; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import com.google.common.base.Optional; +import io.grpc.internal.SpiffeUtil.SpiffeBundle; +import io.grpc.internal.SpiffeUtil.SpiffeId; +import io.grpc.testing.TlsTesting; +import io.grpc.util.CertificateUtils; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collection; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; @@ -159,7 +175,8 @@ public void spiffeTrustDomainFormatTest() { SpiffeUtil.parse(longTrustDomain.toString())); assertEquals("Trust Domain maximum length is 255 characters", iae.getMessage()); - StringBuilder longSpiffe = new StringBuilder(String.format("spiffe://mydomain%scom/", "%21")); + @SuppressWarnings("OrphanedFormatString") + StringBuilder longSpiffe = new StringBuilder("spiffe://mydomain%21com/"); for (int i = 0; i < 405; i++) { longSpiffe.append("qwert"); } @@ -193,4 +210,157 @@ public void spiffePathFormatTest() { + "underscores ([a-zA-Z0-9.-_])", iae.getMessage()); } } + + public static class CertificateApiTest { + private static final String SPIFFE_PEM_FILE = "spiffe_cert.pem"; + private static final String MULTI_URI_SAN_PEM_FILE = "spiffe_multi_uri_san_cert.pem"; + private static final String SERVER_0_PEM_FILE = "server0.pem"; + private static final String TEST_DIRECTORY_PREFIX = "io/grpc/internal/"; + private static final String SPIFFE_TRUST_BUNDLE = "spiffebundle.json"; + private static final String SPIFFE_TRUST_BUNDLE_MALFORMED = "spiffebundle_malformed.json"; + private static final String SPIFFE_TRUST_BUNDLE_CORRUPTED_CERT = + "spiffebundle_corrupted_cert.json"; + private static final String SPIFFE_TRUST_BUNDLE_WRONG_KTY = "spiffebundle_wrong_kty.json"; + private static final String SPIFFE_TRUST_BUNDLE_WRONG_KID = "spiffebundle_wrong_kid.json"; + private static final String SPIFFE_TRUST_BUNDLE_WRONG_USE = "spiffebundle_wrong_use.json"; + private static final String SPIFFE_TRUST_BUNDLE_WRONG_MULTI_CERTS = + "spiffebundle_wrong_multi_certs.json"; + private static final String SPIFFE_TRUST_BUNDLE_DUPLICATES = "spiffebundle_duplicates.json"; + private static final String SPIFFE_TRUST_BUNDLE_WRONG_ROOT = "spiffebundle_wrong_root.json"; + private static final String SPIFFE_TRUST_BUNDLE_WRONG_SEQ = "spiffebundle_wrong_seq_type.json"; + private static final String DOMAIN_ERROR_MESSAGE = + " Certificate loading for trust domain 'google.com' failed."; + + + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + + private X509Certificate[] spiffeCert; + private X509Certificate[] multipleUriSanCert; + private X509Certificate[] serverCert0; + + @Before + public void setUp() throws Exception { + spiffeCert = CertificateUtils.getX509Certificates(TlsTesting.loadCert(SPIFFE_PEM_FILE)); + multipleUriSanCert = CertificateUtils.getX509Certificates(TlsTesting + .loadCert(MULTI_URI_SAN_PEM_FILE)); + serverCert0 = CertificateUtils.getX509Certificates(TlsTesting.loadCert(SERVER_0_PEM_FILE)); + } + + private String copyFileToTmp(String fileName) throws Exception { + Path tempFilePath = tempFolder.newFile(fileName).toPath(); + try (InputStream resourceStream = SpiffeUtilTest.class.getClassLoader() + .getResourceAsStream(TEST_DIRECTORY_PREFIX + fileName)) { + Files.copy(resourceStream, tempFilePath, StandardCopyOption.REPLACE_EXISTING); + } + return tempFilePath.toString(); + } + + @Test + public void extractSpiffeIdSuccessTest() throws Exception { + Optional spiffeId = SpiffeUtil.extractSpiffeId(spiffeCert); + assertTrue(spiffeId.isPresent()); + assertEquals("foo.bar.com", spiffeId.get().getTrustDomain()); + assertEquals("/client/workload/1", spiffeId.get().getPath()); + } + + @Test + public void extractSpiffeIdFailureTest() throws Exception { + Optional spiffeId = SpiffeUtil.extractSpiffeId(serverCert0); + assertFalse(spiffeId.isPresent()); + IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil + .extractSpiffeId(multipleUriSanCert)); + assertEquals("Multiple URI SAN values found in the leaf cert.", iae.getMessage()); + + } + + @Test + public void extractSpiffeIdFromChainTest() throws Exception { + // Check that the SPIFFE ID is extracted only from the leaf cert in the chain (spiffeCert + // contains it, but serverCert0 does not). + X509Certificate[] leafWithSpiffeChain = new X509Certificate[]{spiffeCert[0], serverCert0[0]}; + assertTrue(SpiffeUtil.extractSpiffeId(leafWithSpiffeChain).isPresent()); + X509Certificate[] leafWithoutSpiffeChain = + new X509Certificate[]{serverCert0[0], spiffeCert[0]}; + assertFalse(SpiffeUtil.extractSpiffeId(leafWithoutSpiffeChain).isPresent()); + } + + @Test + public void extractSpiffeIdParameterValidityTest() { + NullPointerException npe = assertThrows(NullPointerException.class, () -> SpiffeUtil + .extractSpiffeId(null)); + assertEquals("certChain", npe.getMessage()); + IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil + .extractSpiffeId(new X509Certificate[]{})); + assertEquals("certChain can't be empty", iae.getMessage()); + } + + @Test + public void loadTrustBundleFromFileSuccessTest() throws Exception { + SpiffeBundle tb = SpiffeUtil.loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE)); + assertEquals(2, tb.getSequenceNumbers().size()); + assertEquals(12035488L, (long) tb.getSequenceNumbers().get("example.com")); + assertEquals(-1L, (long) tb.getSequenceNumbers().get("test.example.com")); + assertEquals(3, tb.getBundleMap().size()); + assertEquals(0, tb.getBundleMap().get("test.google.com.au").size()); + assertEquals(1, tb.getBundleMap().get("example.com").size()); + assertEquals(2, tb.getBundleMap().get("test.example.com").size()); + Optional spiffeId = SpiffeUtil.extractSpiffeId(tb.getBundleMap().get("example.com") + .toArray(new X509Certificate[0])); + assertTrue(spiffeId.isPresent()); + assertEquals("foo.bar.com", spiffeId.get().getTrustDomain()); + } + + @Test + public void loadTrustBundleFromFileFailureTest() { + // Check the exception if JSON root element is different from 'trust_domains' + NullPointerException npe = assertThrows(NullPointerException.class, () -> SpiffeUtil + .loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_ROOT))); + assertEquals("Mandatory trust_domains element is missing", npe.getMessage()); + // Check the exception if JSON root element is different from 'trust_domains' + ClassCastException cce = assertThrows(ClassCastException.class, () -> SpiffeUtil + .loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_SEQ))); + assertTrue(cce.getMessage().contains("Number expected to be long")); + // Check the exception if JSON file doesn't contain an object + IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil + .loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_MALFORMED))); + assertTrue(iae.getMessage().contains("SPIFFE Trust Bundle should be a JSON object.")); + // Check the exception if JSON contains duplicates + iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil + .loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_DUPLICATES))); + assertEquals("Duplicate key found: google.com", iae.getMessage()); + // Check the exception if 'x5c' value cannot be parsed + iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil + .loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_CORRUPTED_CERT))); + assertEquals("Certificate can't be parsed." + DOMAIN_ERROR_MESSAGE, iae.getMessage()); + // Check the exception if 'kty' value differs from 'RSA' + iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil + .loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_KTY))); + assertEquals("'kty' parameter must be 'RSA' but 'null' found." + DOMAIN_ERROR_MESSAGE, + iae.getMessage()); + // Check the exception if 'kid' has a value + iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil + .loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_KID))); + assertEquals("'kid' parameter must not be set." + DOMAIN_ERROR_MESSAGE, iae.getMessage()); + // Check the exception if 'use' value differs from 'x509-svid' + iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil + .loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_USE))); + assertEquals("'use' parameter must be 'x509-svid' but 'i_am_not_x509-svid' found." + + DOMAIN_ERROR_MESSAGE, iae.getMessage()); + // Check the exception if multiple certs are provided for 'x5c' + iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil + .loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_MULTI_CERTS))); + assertEquals("Exactly 1 certificate is expected, but 2 found." + DOMAIN_ERROR_MESSAGE, + iae.getMessage()); + } + + @Test + public void loadTrustBundleFromFileParameterValidityTest() { + NullPointerException npe = assertThrows(NullPointerException.class, () -> SpiffeUtil + .loadTrustBundleFromFile(null)); + assertEquals("trustBundleFile", npe.getMessage()); + NoSuchFileException nsfe = assertThrows(NoSuchFileException.class, () -> SpiffeUtil + .loadTrustBundleFromFile("i_do_not_exist")); + assertEquals("i_do_not_exist", nsfe.getMessage()); + } + } } \ No newline at end of file diff --git a/core/src/test/resources/io/grpc/internal/spiffebundle.json b/core/src/test/resources/io/grpc/internal/spiffebundle.json new file mode 100644 index 00000000000..f968f730d94 --- /dev/null +++ b/core/src/test/resources/io/grpc/internal/spiffebundle.json @@ -0,0 +1,115 @@ +{ + "trust_domains": { + "test.google.com.au": {}, + "example.com": { + "spiffe_sequence": 12035488, + "keys": [ + { + "kty": "RSA", + "use": "x509-svid", + "x5c": ["MIIFsjCCA5qgAwIBAgIURygVMMzdr+Q7rsUaz189JozyHMwwDQYJKoZIhvcNAQEL + BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL + BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMTEyMjMxODQy + NTJaFw0zMTEyMjExODQyNTJaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM + MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu + dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ4AqpGetyVSqGUuBJ + LVFla+7bEfca7UYzfVSSZLZ/X+JDmWIVN8UIPuFib5jhMEc3XaUnFXUmM7zEtz/Z + G5hapwLwOb2C3ZxOP6PQjYCJxbkLie+b43UQrFu1xxd3vMhVJgcj/AIxEpmszuqO + a6kUrkYifjJADQ+64kZgl66bsTdXMCzpxyFl9xUfff59L8OX+HUfAcoZz3emjg3Z + JPYURQEmjdZTOau1EjFilwHgd989Jt7NKgx30NXoHmw7nusVBIY94fL2VKN3f1XV + m0dHu5NI279Q6zr0ZBU7k5T3IeHnzsUesQS4NGlklDWoVTKk73Uv9Pna8yQsSW75 + 7PEbHOGp9Knu4bnoGPOlsG81yIPipO6hTgGFK24pF97M9kpGbWqYX4+2vLlrCAfc + msHqaUPmQlYeRVTT6vw7ctYo2kyUYGtnODXk76LqewRBVvkzx75QUhfjAyb740Yc + DmIenc56Tq6gebJHjhEmVSehR6xIpXP7SVeurTyhPsEQnpJHtgs4dcwWOZp7BvPN + zHXmJqfr7vsshie3vS5kQ0u1e1yqAqXgyDjqKXOkx+dpgUTehSJHhPNHvTc5LXRs + vvXKYz6FrwR/DZ8t7BNEvPeLjFgxpH7QVJFLCvCbXs5K6yYbsnLfxFIBPRnrbJkI + sK+sQwnRdnsiUdPsTkG5B2lQfQIDAQABo4GHMIGEMB0GA1UdDgQWBBQ2lBp0PiRH + HvQ5IRURm8aHsj4RETAfBgNVHSMEGDAWgBQ2lBp0PiRHHvQ5IRURm8aHsj4RETAP + BgNVHRMBAf8EBTADAQH/MDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29t + L2NsaWVudC93b3JrbG9hZC8xMA0GCSqGSIb3DQEBCwUAA4ICAQA1mSkgRclAl+E/ + aS9zJ7t8+Y4n3T24nOKKveSIjxXm/zjhWqVsLYBI6kglWtih2+PELvU8JdPqNZK3 + 4Kl0Q6FWpVSGDdWN1i6NyORt2ocggL3ke3iXxRk3UpUKJmqwz81VhA2KUHnMlyE0 + IufFfZNwNWWHBv13uJfRbjeQpKPhU+yf4DeXrsWcvrZlGvAET+mcplafUzCp7Iv+ + PcISJtUerbxbVtuHVeZCLlgDXWkLAWJN8rf0dIG4x060LJ+j6j9uRVhb9sZn1HJV + +j4XdIYm1VKilluhOtNwP2d3Ox/JuTBxf7hFHXZPfMagQE5k5PzmxRaCAEMJ1l2D + vUbZw+shJfSNoWcBo2qadnUaWT3BmmJRBDh7ZReib/RQ1Rd4ygOyzP3E0vkV4/gq + yjLdApXh5PZP8KLQZ+1JN/sdWt7VfIt9wYOpkIqujdll51ESHzwQeAK9WVCB4UvV + z6zdhItB9CRbXPreWC+wCB1xDovIzFKOVsLs5+Gqs1m7VinG2LxbDqaKyo/FB0Hx + x0acBNzezLWoDwXYQrN0T0S4pnqhKD1CYPpdArBkNezUYAjS725FkApuK+mnBX3U + 0msBffEaUEOkcyar1EW2m/33vpetD/k3eQQkmvQf4Hbiu9AF+9cNDm/hMuXEw5EX + GA91fn0891b5eEW8BJHXX0jri0aN8g=="], + "n": "", + "e": "AQAB" + } + ] + }, + "test.example.com": { + "keys": [ + { + "kty": "RSA", + "use": "x509-svid", + "x5c": ["MIIFsjCCA5qgAwIBAgIURygVMMzdr+Q7rsUaz189JozyHMwwDQYJKoZIhvcNAQEL + BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL + BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMTEyMjMxODQy + NTJaFw0zMTEyMjExODQyNTJaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM + MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu + dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ4AqpGetyVSqGUuBJ + LVFla+7bEfca7UYzfVSSZLZ/X+JDmWIVN8UIPuFib5jhMEc3XaUnFXUmM7zEtz/Z + G5hapwLwOb2C3ZxOP6PQjYCJxbkLie+b43UQrFu1xxd3vMhVJgcj/AIxEpmszuqO + a6kUrkYifjJADQ+64kZgl66bsTdXMCzpxyFl9xUfff59L8OX+HUfAcoZz3emjg3Z + JPYURQEmjdZTOau1EjFilwHgd989Jt7NKgx30NXoHmw7nusVBIY94fL2VKN3f1XV + m0dHu5NI279Q6zr0ZBU7k5T3IeHnzsUesQS4NGlklDWoVTKk73Uv9Pna8yQsSW75 + 7PEbHOGp9Knu4bnoGPOlsG81yIPipO6hTgGFK24pF97M9kpGbWqYX4+2vLlrCAfc + msHqaUPmQlYeRVTT6vw7ctYo2kyUYGtnODXk76LqewRBVvkzx75QUhfjAyb740Yc + DmIenc56Tq6gebJHjhEmVSehR6xIpXP7SVeurTyhPsEQnpJHtgs4dcwWOZp7BvPN + zHXmJqfr7vsshie3vS5kQ0u1e1yqAqXgyDjqKXOkx+dpgUTehSJHhPNHvTc5LXRs + vvXKYz6FrwR/DZ8t7BNEvPeLjFgxpH7QVJFLCvCbXs5K6yYbsnLfxFIBPRnrbJkI + sK+sQwnRdnsiUdPsTkG5B2lQfQIDAQABo4GHMIGEMB0GA1UdDgQWBBQ2lBp0PiRH + HvQ5IRURm8aHsj4RETAfBgNVHSMEGDAWgBQ2lBp0PiRHHvQ5IRURm8aHsj4RETAP + BgNVHRMBAf8EBTADAQH/MDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29t + L2NsaWVudC93b3JrbG9hZC8xMA0GCSqGSIb3DQEBCwUAA4ICAQA1mSkgRclAl+E/ + aS9zJ7t8+Y4n3T24nOKKveSIjxXm/zjhWqVsLYBI6kglWtih2+PELvU8JdPqNZK3 + 4Kl0Q6FWpVSGDdWN1i6NyORt2ocggL3ke3iXxRk3UpUKJmqwz81VhA2KUHnMlyE0 + IufFfZNwNWWHBv13uJfRbjeQpKPhU+yf4DeXrsWcvrZlGvAET+mcplafUzCp7Iv+ + PcISJtUerbxbVtuHVeZCLlgDXWkLAWJN8rf0dIG4x060LJ+j6j9uRVhb9sZn1HJV + +j4XdIYm1VKilluhOtNwP2d3Ox/JuTBxf7hFHXZPfMagQE5k5PzmxRaCAEMJ1l2D + vUbZw+shJfSNoWcBo2qadnUaWT3BmmJRBDh7ZReib/RQ1Rd4ygOyzP3E0vkV4/gq + yjLdApXh5PZP8KLQZ+1JN/sdWt7VfIt9wYOpkIqujdll51ESHzwQeAK9WVCB4UvV + z6zdhItB9CRbXPreWC+wCB1xDovIzFKOVsLs5+Gqs1m7VinG2LxbDqaKyo/FB0Hx + x0acBNzezLWoDwXYQrN0T0S4pnqhKD1CYPpdArBkNezUYAjS725FkApuK+mnBX3U + 0msBffEaUEOkcyar1EW2m/33vpetD/k3eQQkmvQf4Hbiu9AF+9cNDm/hMuXEw5EX + GA91fn0891b5eEW8BJHXX0jri0aN8g=="], + "n": "", + "e": "AQAB" + }, + { + "kty": "RSA", + "use": "x509-svid", + "x5c": ["MIIELTCCAxWgAwIBAgIUVXGlXjNENtOZbI12epjgIhMaShEwDQYJKoZIhvcNAQEL + BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM + GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTI0 + MDkxNzE2MTk0NFoXDTM0MDkxNTE2MTk0NFowTjELMAkGA1UEBhMCVVMxCzAJBgNV + BAgMAkNBMQwwCgYDVQQHDANTVkwxDTALBgNVBAoMBGdSUEMxFTATBgNVBAMMDHRl + c3QtY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOcTjjcS + SfG/EGrr6G+f+3T2GXyHHfroQFi9mZUz80L7uKBdECOImID+YhoK8vcxLQjPmEEv + FIYgJT5amugDcYIgUhMjBx/8RPJaP/nGmBngAqsuuNCaZfyaHBRqN8XdS/AwmsI5 + Wo+nru0+0/7aQFdqqtd2+e9dHjUWwgHxXvMgC4hkHpsdCGIZWVzWyBliwTYQYb1Y + yYe1LzqqQA5OMbZfKOY9MYDCEYOliRiunOn30iIOHj9V5qLzWGfSyxCRuvLRdEP8 + iDeNweHbdaKuI80nQmxuBdRIspE9k5sD1WA4vLZpeg3zggxp4rfLL5zBJgb/33D3 + d9Rkm14xfDPihhkCAwEAAaOB+jCB9zBZBgNVHREEUjBQhiZzcGlmZmU6Ly9mb28u + YmFyLmNvbS9jbGllbnQvd29ya2xvYWQvMYYmc3BpZmZlOi8vZm9vLmJhci5jb20v + Y2xpZW50L3dvcmtsb2FkLzIwHQYDVR0OBBYEFG9GkBgdBg/p0U9/lXv8zIJ+2c2N + MHsGA1UdIwR0MHKhWqRYMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0 + YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMM + BnRlc3RjYYIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQELBQADggEB + AJ4Cbxv+02SpUgkEu4hP/1+8DtSBXUxNxI0VG4e3Ap2+Rhjm3YiFeS/UeaZhNrrw + UEjkSTPFODyXR7wI7UO9OO1StyD6CMkp3SEvevU5JsZtGL6mTiTLTi3Qkywa91Bt + GlyZdVMghA1bBJLBMwiD5VT5noqoJBD7hDy6v9yNmt1Sw2iYBJPqI3Gnf5bMjR3s + UICaxmFyqaMCZsPkfJh0DmZpInGJys3m4QqGz6ZE2DWgcSr1r/ML7/5bSPjjr8j4 + WFFSqFR3dMu8CbGnfZTCTXa4GTX/rARXbAO67Z/oJbJBK7VKayskL+PzKuohb9ox + jGL772hQMbwtFCOFXu5VP0s="] + } + ] + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/io/grpc/internal/spiffebundle_corrupted_cert.json b/core/src/test/resources/io/grpc/internal/spiffebundle_corrupted_cert.json new file mode 100644 index 00000000000..9ca51733ff3 --- /dev/null +++ b/core/src/test/resources/io/grpc/internal/spiffebundle_corrupted_cert.json @@ -0,0 +1,14 @@ +{ + "trust_domains": { + "google.com": { + "spiffe_sequence": 123, + "keys": [ + { + "kty": "RSA", + "use": "x509-svid", + "x5c": ["UNPARSABLE_CERTIFICATE"] + } + ] + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/io/grpc/internal/spiffebundle_duplicates.json b/core/src/test/resources/io/grpc/internal/spiffebundle_duplicates.json new file mode 100644 index 00000000000..3f015bd1568 --- /dev/null +++ b/core/src/test/resources/io/grpc/internal/spiffebundle_duplicates.json @@ -0,0 +1,23 @@ +{ + "trust_domains": { + "google.com": { + "spiffe_sequence": 123, + "keys": [ + { + "x5c": "VALUE_DOESN'T_MATTER" + } + ] + }, + "google.com": { + "spiffe_sequence": 123, + "keys": [ + { + "use": "x509-svid", + "kid": "some_value", + "x5c": "VALUE_DOESN'T_MATTER" + } + ] + }, + "test.google.com.au": {} + } +} \ No newline at end of file diff --git a/core/src/test/resources/io/grpc/internal/spiffebundle_malformed.json b/core/src/test/resources/io/grpc/internal/spiffebundle_malformed.json new file mode 100644 index 00000000000..a2488eeb3cd --- /dev/null +++ b/core/src/test/resources/io/grpc/internal/spiffebundle_malformed.json @@ -0,0 +1,4 @@ +[ + "test.google.com", + "test.google.com.au" +] \ No newline at end of file diff --git a/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_kid.json b/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_kid.json new file mode 100644 index 00000000000..f93af634a54 --- /dev/null +++ b/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_kid.json @@ -0,0 +1,15 @@ +{ + "trust_domains": { + "google.com": { + "spiffe_sequence": 123, + "keys": [ + { + "kty": "RSA", + "use": "x509-svid", + "kid": "some_value", + "x5c": "VALUE_DOESN'T_MATTER" + } + ] + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_kty.json b/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_kty.json new file mode 100644 index 00000000000..384da03fd6f --- /dev/null +++ b/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_kty.json @@ -0,0 +1,12 @@ +{ + "trust_domains": { + "google.com": { + "spiffe_sequence": 123, + "keys": [ + { + "x5c": "VALUE_DOESN'T_MATTER" + } + ] + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_multi_certs.json b/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_multi_certs.json new file mode 100644 index 00000000000..5e85635bb02 --- /dev/null +++ b/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_multi_certs.json @@ -0,0 +1,67 @@ +{ + "trust_domains": { + "google.com": { + "spiffe_sequence": 123, + "keys": [ + { + "kty": "RSA", + "use": "x509-svid", + "x5c": ["MIIFsjCCA5qgAwIBAgIURygVMMzdr+Q7rsUaz189JozyHMwwDQYJKoZIhvcNAQEL + BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL + BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMTEyMjMxODQy + NTJaFw0zMTEyMjExODQyNTJaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM + MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu + dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ4AqpGetyVSqGUuBJ + LVFla+7bEfca7UYzfVSSZLZ/X+JDmWIVN8UIPuFib5jhMEc3XaUnFXUmM7zEtz/Z + G5hapwLwOb2C3ZxOP6PQjYCJxbkLie+b43UQrFu1xxd3vMhVJgcj/AIxEpmszuqO + a6kUrkYifjJADQ+64kZgl66bsTdXMCzpxyFl9xUfff59L8OX+HUfAcoZz3emjg3Z + JPYURQEmjdZTOau1EjFilwHgd989Jt7NKgx30NXoHmw7nusVBIY94fL2VKN3f1XV + m0dHu5NI279Q6zr0ZBU7k5T3IeHnzsUesQS4NGlklDWoVTKk73Uv9Pna8yQsSW75 + 7PEbHOGp9Knu4bnoGPOlsG81yIPipO6hTgGFK24pF97M9kpGbWqYX4+2vLlrCAfc + msHqaUPmQlYeRVTT6vw7ctYo2kyUYGtnODXk76LqewRBVvkzx75QUhfjAyb740Yc + DmIenc56Tq6gebJHjhEmVSehR6xIpXP7SVeurTyhPsEQnpJHtgs4dcwWOZp7BvPN + zHXmJqfr7vsshie3vS5kQ0u1e1yqAqXgyDjqKXOkx+dpgUTehSJHhPNHvTc5LXRs + vvXKYz6FrwR/DZ8t7BNEvPeLjFgxpH7QVJFLCvCbXs5K6yYbsnLfxFIBPRnrbJkI + sK+sQwnRdnsiUdPsTkG5B2lQfQIDAQABo4GHMIGEMB0GA1UdDgQWBBQ2lBp0PiRH + HvQ5IRURm8aHsj4RETAfBgNVHSMEGDAWgBQ2lBp0PiRHHvQ5IRURm8aHsj4RETAP + BgNVHRMBAf8EBTADAQH/MDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29t + L2NsaWVudC93b3JrbG9hZC8xMA0GCSqGSIb3DQEBCwUAA4ICAQA1mSkgRclAl+E/ + aS9zJ7t8+Y4n3T24nOKKveSIjxXm/zjhWqVsLYBI6kglWtih2+PELvU8JdPqNZK3 + 4Kl0Q6FWpVSGDdWN1i6NyORt2ocggL3ke3iXxRk3UpUKJmqwz81VhA2KUHnMlyE0 + IufFfZNwNWWHBv13uJfRbjeQpKPhU+yf4DeXrsWcvrZlGvAET+mcplafUzCp7Iv+ + PcISJtUerbxbVtuHVeZCLlgDXWkLAWJN8rf0dIG4x060LJ+j6j9uRVhb9sZn1HJV + +j4XdIYm1VKilluhOtNwP2d3Ox/JuTBxf7hFHXZPfMagQE5k5PzmxRaCAEMJ1l2D + vUbZw+shJfSNoWcBo2qadnUaWT3BmmJRBDh7ZReib/RQ1Rd4ygOyzP3E0vkV4/gq + yjLdApXh5PZP8KLQZ+1JN/sdWt7VfIt9wYOpkIqujdll51ESHzwQeAK9WVCB4UvV + z6zdhItB9CRbXPreWC+wCB1xDovIzFKOVsLs5+Gqs1m7VinG2LxbDqaKyo/FB0Hx + x0acBNzezLWoDwXYQrN0T0S4pnqhKD1CYPpdArBkNezUYAjS725FkApuK+mnBX3U + 0msBffEaUEOkcyar1EW2m/33vpetD/k3eQQkmvQf4Hbiu9AF+9cNDm/hMuXEw5EX + GA91fn0891b5eEW8BJHXX0jri0aN8g==", + "MIIELTCCAxWgAwIBAgIUVXGlXjNENtOZbI12epjgIhMaShEwDQYJKoZIhvcNAQEL + BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM + GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTI0 + MDkxNzE2MTk0NFoXDTM0MDkxNTE2MTk0NFowTjELMAkGA1UEBhMCVVMxCzAJBgNV + BAgMAkNBMQwwCgYDVQQHDANTVkwxDTALBgNVBAoMBGdSUEMxFTATBgNVBAMMDHRl + c3QtY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOcTjjcS + SfG/EGrr6G+f+3T2GXyHHfroQFi9mZUz80L7uKBdECOImID+YhoK8vcxLQjPmEEv + FIYgJT5amugDcYIgUhMjBx/8RPJaP/nGmBngAqsuuNCaZfyaHBRqN8XdS/AwmsI5 + Wo+nru0+0/7aQFdqqtd2+e9dHjUWwgHxXvMgC4hkHpsdCGIZWVzWyBliwTYQYb1Y + yYe1LzqqQA5OMbZfKOY9MYDCEYOliRiunOn30iIOHj9V5qLzWGfSyxCRuvLRdEP8 + iDeNweHbdaKuI80nQmxuBdRIspE9k5sD1WA4vLZpeg3zggxp4rfLL5zBJgb/33D3 + d9Rkm14xfDPihhkCAwEAAaOB+jCB9zBZBgNVHREEUjBQhiZzcGlmZmU6Ly9mb28u + YmFyLmNvbS9jbGllbnQvd29ya2xvYWQvMYYmc3BpZmZlOi8vZm9vLmJhci5jb20v + Y2xpZW50L3dvcmtsb2FkLzIwHQYDVR0OBBYEFG9GkBgdBg/p0U9/lXv8zIJ+2c2N + MHsGA1UdIwR0MHKhWqRYMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0 + YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMM + BnRlc3RjYYIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQELBQADggEB + AJ4Cbxv+02SpUgkEu4hP/1+8DtSBXUxNxI0VG4e3Ap2+Rhjm3YiFeS/UeaZhNrrw + UEjkSTPFODyXR7wI7UO9OO1StyD6CMkp3SEvevU5JsZtGL6mTiTLTi3Qkywa91Bt + GlyZdVMghA1bBJLBMwiD5VT5noqoJBD7hDy6v9yNmt1Sw2iYBJPqI3Gnf5bMjR3s + UICaxmFyqaMCZsPkfJh0DmZpInGJys3m4QqGz6ZE2DWgcSr1r/ML7/5bSPjjr8j4 + WFFSqFR3dMu8CbGnfZTCTXa4GTX/rARXbAO67Z/oJbJBK7VKayskL+PzKuohb9ox + jGL772hQMbwtFCOFXu5VP0s="] + } + ] + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_root.json b/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_root.json new file mode 100644 index 00000000000..90d2847dc05 --- /dev/null +++ b/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_root.json @@ -0,0 +1,6 @@ +{ + "trustDomains": { + "test.google.com": {}, + "test.google.com.au": {} + } +} \ No newline at end of file diff --git a/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_seq_type.json b/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_seq_type.json new file mode 100644 index 00000000000..4e0aeacc89f --- /dev/null +++ b/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_seq_type.json @@ -0,0 +1,12 @@ +{ + "trust_domains": { + "google.com": { + "spiffe_sequence": 123.5, + "keys": [ + { + "x5c": "VALUE_DOESN'T_MATTER" + } + ] + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_use.json b/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_use.json new file mode 100644 index 00000000000..166be04846c --- /dev/null +++ b/core/src/test/resources/io/grpc/internal/spiffebundle_wrong_use.json @@ -0,0 +1,13 @@ +{ + "trust_domains": { + "google.com": { + "keys": [ + { + "kty": "RSA", + "use": "i_am_not_x509-svid", + "x5c": "VALUE_DOESN'T_MATTER" + } + ] + } + } +} \ No newline at end of file diff --git a/gcp-observability/src/test/java/io/grpc/gcp/observability/ObservabilityConfigImplTest.java b/gcp-observability/src/test/java/io/grpc/gcp/observability/ObservabilityConfigImplTest.java index a9e0d6e2235..f409a149bf1 100644 --- a/gcp-observability/src/test/java/io/grpc/gcp/observability/ObservabilityConfigImplTest.java +++ b/gcp-observability/src/test/java/io/grpc/gcp/observability/ObservabilityConfigImplTest.java @@ -108,8 +108,7 @@ public class ObservabilityConfigImplTest { private static final String PROJECT_ID = "{\n" + " \"project_id\": \"grpc-testing\",\n" - + " \"cloud_logging\": {},\n" - + " \"project_id\": \"grpc-testing\"\n" + + " \"cloud_logging\": {}\n" + "}"; private static final String EMPTY_CONFIG = "{}"; diff --git a/testing/src/main/resources/certs/spiffe-openssl.cnf b/testing/src/main/resources/certs/spiffe-openssl.cnf new file mode 100644 index 00000000000..40c473da7e5 --- /dev/null +++ b/testing/src/main/resources/certs/spiffe-openssl.cnf @@ -0,0 +1,12 @@ +[spiffe_client] +subjectAltName = @alt_names + +[spiffe_client_multi] +subjectAltName = @alt_names_multi + +[alt_names] +URI = spiffe://foo.bar.com/client/workload/1 + +[alt_names_multi] +URI.1 = spiffe://foo.bar.com/client/workload/1 +URI.2 = spiffe://foo.bar.com/client/workload/2 \ No newline at end of file diff --git a/testing/src/main/resources/certs/spiffe_cert.pem b/testing/src/main/resources/certs/spiffe_cert.pem new file mode 100644 index 00000000000..bc070042f69 --- /dev/null +++ b/testing/src/main/resources/certs/spiffe_cert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFsjCCA5qgAwIBAgIURygVMMzdr+Q7rsUaz189JozyHMwwDQYJKoZIhvcNAQEL +BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL +BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMTEyMjMxODQy +NTJaFw0zMTEyMjExODQyNTJaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM +MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu +dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ4AqpGetyVSqGUuBJ +LVFla+7bEfca7UYzfVSSZLZ/X+JDmWIVN8UIPuFib5jhMEc3XaUnFXUmM7zEtz/Z +G5hapwLwOb2C3ZxOP6PQjYCJxbkLie+b43UQrFu1xxd3vMhVJgcj/AIxEpmszuqO +a6kUrkYifjJADQ+64kZgl66bsTdXMCzpxyFl9xUfff59L8OX+HUfAcoZz3emjg3Z +JPYURQEmjdZTOau1EjFilwHgd989Jt7NKgx30NXoHmw7nusVBIY94fL2VKN3f1XV +m0dHu5NI279Q6zr0ZBU7k5T3IeHnzsUesQS4NGlklDWoVTKk73Uv9Pna8yQsSW75 +7PEbHOGp9Knu4bnoGPOlsG81yIPipO6hTgGFK24pF97M9kpGbWqYX4+2vLlrCAfc +msHqaUPmQlYeRVTT6vw7ctYo2kyUYGtnODXk76LqewRBVvkzx75QUhfjAyb740Yc +DmIenc56Tq6gebJHjhEmVSehR6xIpXP7SVeurTyhPsEQnpJHtgs4dcwWOZp7BvPN +zHXmJqfr7vsshie3vS5kQ0u1e1yqAqXgyDjqKXOkx+dpgUTehSJHhPNHvTc5LXRs +vvXKYz6FrwR/DZ8t7BNEvPeLjFgxpH7QVJFLCvCbXs5K6yYbsnLfxFIBPRnrbJkI +sK+sQwnRdnsiUdPsTkG5B2lQfQIDAQABo4GHMIGEMB0GA1UdDgQWBBQ2lBp0PiRH +HvQ5IRURm8aHsj4RETAfBgNVHSMEGDAWgBQ2lBp0PiRHHvQ5IRURm8aHsj4RETAP +BgNVHRMBAf8EBTADAQH/MDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29t +L2NsaWVudC93b3JrbG9hZC8xMA0GCSqGSIb3DQEBCwUAA4ICAQA1mSkgRclAl+E/ +aS9zJ7t8+Y4n3T24nOKKveSIjxXm/zjhWqVsLYBI6kglWtih2+PELvU8JdPqNZK3 +4Kl0Q6FWpVSGDdWN1i6NyORt2ocggL3ke3iXxRk3UpUKJmqwz81VhA2KUHnMlyE0 +IufFfZNwNWWHBv13uJfRbjeQpKPhU+yf4DeXrsWcvrZlGvAET+mcplafUzCp7Iv+ +PcISJtUerbxbVtuHVeZCLlgDXWkLAWJN8rf0dIG4x060LJ+j6j9uRVhb9sZn1HJV ++j4XdIYm1VKilluhOtNwP2d3Ox/JuTBxf7hFHXZPfMagQE5k5PzmxRaCAEMJ1l2D +vUbZw+shJfSNoWcBo2qadnUaWT3BmmJRBDh7ZReib/RQ1Rd4ygOyzP3E0vkV4/gq +yjLdApXh5PZP8KLQZ+1JN/sdWt7VfIt9wYOpkIqujdll51ESHzwQeAK9WVCB4UvV +z6zdhItB9CRbXPreWC+wCB1xDovIzFKOVsLs5+Gqs1m7VinG2LxbDqaKyo/FB0Hx +x0acBNzezLWoDwXYQrN0T0S4pnqhKD1CYPpdArBkNezUYAjS725FkApuK+mnBX3U +0msBffEaUEOkcyar1EW2m/33vpetD/k3eQQkmvQf4Hbiu9AF+9cNDm/hMuXEw5EX +GA91fn0891b5eEW8BJHXX0jri0aN8g== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/testing/src/main/resources/certs/spiffe_multi_uri_san_cert.pem b/testing/src/main/resources/certs/spiffe_multi_uri_san_cert.pem new file mode 100644 index 00000000000..eb5c879abf8 --- /dev/null +++ b/testing/src/main/resources/certs/spiffe_multi_uri_san_cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELTCCAxWgAwIBAgIUVXGlXjNENtOZbI12epjgIhMaShEwDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTI0 +MDkxNzE2MTk0NFoXDTM0MDkxNTE2MTk0NFowTjELMAkGA1UEBhMCVVMxCzAJBgNV +BAgMAkNBMQwwCgYDVQQHDANTVkwxDTALBgNVBAoMBGdSUEMxFTATBgNVBAMMDHRl +c3QtY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOcTjjcS +SfG/EGrr6G+f+3T2GXyHHfroQFi9mZUz80L7uKBdECOImID+YhoK8vcxLQjPmEEv +FIYgJT5amugDcYIgUhMjBx/8RPJaP/nGmBngAqsuuNCaZfyaHBRqN8XdS/AwmsI5 +Wo+nru0+0/7aQFdqqtd2+e9dHjUWwgHxXvMgC4hkHpsdCGIZWVzWyBliwTYQYb1Y +yYe1LzqqQA5OMbZfKOY9MYDCEYOliRiunOn30iIOHj9V5qLzWGfSyxCRuvLRdEP8 +iDeNweHbdaKuI80nQmxuBdRIspE9k5sD1WA4vLZpeg3zggxp4rfLL5zBJgb/33D3 +d9Rkm14xfDPihhkCAwEAAaOB+jCB9zBZBgNVHREEUjBQhiZzcGlmZmU6Ly9mb28u +YmFyLmNvbS9jbGllbnQvd29ya2xvYWQvMYYmc3BpZmZlOi8vZm9vLmJhci5jb20v +Y2xpZW50L3dvcmtsb2FkLzIwHQYDVR0OBBYEFG9GkBgdBg/p0U9/lXv8zIJ+2c2N +MHsGA1UdIwR0MHKhWqRYMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0 +YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMM +BnRlc3RjYYIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQELBQADggEB +AJ4Cbxv+02SpUgkEu4hP/1+8DtSBXUxNxI0VG4e3Ap2+Rhjm3YiFeS/UeaZhNrrw +UEjkSTPFODyXR7wI7UO9OO1StyD6CMkp3SEvevU5JsZtGL6mTiTLTi3Qkywa91Bt +GlyZdVMghA1bBJLBMwiD5VT5noqoJBD7hDy6v9yNmt1Sw2iYBJPqI3Gnf5bMjR3s +UICaxmFyqaMCZsPkfJh0DmZpInGJys3m4QqGz6ZE2DWgcSr1r/ML7/5bSPjjr8j4 +WFFSqFR3dMu8CbGnfZTCTXa4GTX/rARXbAO67Z/oJbJBK7VKayskL+PzKuohb9ox +jGL772hQMbwtFCOFXu5VP0s= +-----END CERTIFICATE----- \ No newline at end of file From 00c8bc78dd5a338a1b5b1476dd8ea86b4290a8bf Mon Sep 17 00:00:00 2001 From: Lucas Mirelmann Date: Fri, 18 Oct 2024 07:59:35 +0200 Subject: [PATCH 055/591] Minor grammar fix in Javadoc (#11609) --- api/src/main/java/io/grpc/StatusRuntimeException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/io/grpc/StatusRuntimeException.java b/api/src/main/java/io/grpc/StatusRuntimeException.java index 70c4d10f0b2..9465e4c38cd 100644 --- a/api/src/main/java/io/grpc/StatusRuntimeException.java +++ b/api/src/main/java/io/grpc/StatusRuntimeException.java @@ -30,7 +30,7 @@ public class StatusRuntimeException extends RuntimeException { private final Metadata trailers; /** - * Constructs the exception with both a status. See also {@link Status#asRuntimeException()}. + * Constructs the exception with a status. See also {@link Status#asRuntimeException()}. * * @since 1.0.0 */ From 62f409810d48bbae9fdd111217e7b2b85d377e60 Mon Sep 17 00:00:00 2001 From: hlx502 <73785906+hlx502@users.noreply.github.com> Date: Wed, 23 Oct 2024 03:17:39 +0800 Subject: [PATCH 056/591] netty: Avoid TCP_USER_TIMEOUT warning when not using epoll (#11564) In NettyClientTransport, the TCP_USER_TIMEOUT attribute can be set only if the channel is of the AbstractEpollStreamChannel. Fixes #11517 --- .../io/grpc/netty/NettyClientTransport.java | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/NettyClientTransport.java b/netty/src/main/java/io/grpc/netty/NettyClientTransport.java index a82470cacb4..426f3c0d583 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientTransport.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientTransport.java @@ -241,13 +241,6 @@ public Runnable start(Listener transportListener) { b.channelFactory(channelFactory); // For non-socket based channel, the option will be ignored. b.option(SO_KEEPALIVE, true); - // For non-epoll based channel, the option will be ignored. - if (keepAliveTimeNanos != KEEPALIVE_TIME_NANOS_DISABLED) { - ChannelOption tcpUserTimeout = Utils.maybeGetTcpUserTimeoutOption(); - if (tcpUserTimeout != null) { - b.option(tcpUserTimeout, (int) TimeUnit.NANOSECONDS.toMillis(keepAliveTimeoutNanos)); - } - } for (Map.Entry, ?> entry : channelOptions.entrySet()) { // Every entry in the map is obtained from // NettyChannelBuilder#withOption(ChannelOption option, T value) @@ -286,6 +279,20 @@ public void run() { }; } channel = regFuture.channel(); + // For non-epoll based channel, the option will be ignored. + try { + if (keepAliveTimeNanos != KEEPALIVE_TIME_NANOS_DISABLED + && Class.forName("io.netty.channel.epoll.AbstractEpollChannel").isInstance(channel)) { + ChannelOption tcpUserTimeout = Utils.maybeGetTcpUserTimeoutOption(); + if (tcpUserTimeout != null) { + int tcpUserTimeoutMs = (int) TimeUnit.NANOSECONDS.toMillis(keepAliveTimeoutNanos); + channel.config().setOption(tcpUserTimeout, tcpUserTimeoutMs); + } + } + } catch (ClassNotFoundException ignored) { + // JVM did not load AbstractEpollChannel, so the current channel will not be of epoll type, + // so there is no need to set TCP_USER_TIMEOUT + } // Start the write queue as soon as the channel is constructed handler.startWriteQueue(channel); // This write will have no effect, yet it will only complete once the negotiationHandler From b65cbf508195fb86b3642b75185c2b150c700e31 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Thu, 24 Oct 2024 01:22:41 +0530 Subject: [PATCH 057/591] inprocess: Support tracing message sizes guarded by flag (#11629) --- .../grpc/internal/AbstractTransportTest.java | 45 +++++++--- .../inprocess/InProcessChannelBuilder.java | 4 + .../io/grpc/inprocess/InProcessTransport.java | 88 +++++++++++-------- .../inprocess/InProcessTransportTest.java | 10 ++- 4 files changed, 92 insertions(+), 55 deletions(-) diff --git a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java index 75ea2678709..69d8e65955e 100644 --- a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java +++ b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java @@ -96,6 +96,9 @@ public abstract class AbstractTransportTest { private static final int TIMEOUT_MS = 5000; + protected static final String GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES = + "GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES"; + private static final Attributes.Key ADDITIONAL_TRANSPORT_ATTR_KEY = Attributes.Key.create("additional-attr"); @@ -238,6 +241,13 @@ protected void advanceClock(long offset, TimeUnit unit) { throw new UnsupportedOperationException(); } + /** + * Returns true if env var is set. + */ + protected static boolean isEnabledSupportTracingMessageSizes() { + return GrpcUtil.getFlag(GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES, false); + } + /** * Returns the current time, for tests that rely on the clock. */ @@ -850,16 +860,21 @@ public void basicStream() throws Exception { message.close(); assertThat(clientStreamTracer1.nextOutboundEvent()) .matches("outboundMessageSent\\(0, -?[0-9]+, -?[0-9]+\\)"); - assertThat(clientStreamTracer1.getOutboundWireSize()).isGreaterThan(0L); - assertThat(clientStreamTracer1.getOutboundUncompressedSize()).isGreaterThan(0L); + if (isEnabledSupportTracingMessageSizes()) { + assertThat(clientStreamTracer1.getOutboundWireSize()).isGreaterThan(0L); + assertThat(clientStreamTracer1.getOutboundUncompressedSize()).isGreaterThan(0L); + } + assertThat(serverStreamTracer1.nextInboundEvent()).isEqualTo("inboundMessage(0)"); assertNull("no additional message expected", serverStreamListener.messageQueue.poll()); clientStream.halfClose(); assertTrue(serverStreamListener.awaitHalfClosed(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - assertThat(serverStreamTracer1.getInboundWireSize()).isGreaterThan(0L); - assertThat(serverStreamTracer1.getInboundUncompressedSize()).isGreaterThan(0L); + if (isEnabledSupportTracingMessageSizes()) { + assertThat(serverStreamTracer1.getInboundWireSize()).isGreaterThan(0L); + assertThat(serverStreamTracer1.getInboundUncompressedSize()).isGreaterThan(0L); + } assertThat(serverStreamTracer1.nextInboundEvent()) .matches("inboundMessageRead\\(0, -?[0-9]+, -?[0-9]+\\)"); @@ -890,15 +905,19 @@ public void basicStream() throws Exception { assertNotNull("message expected", message); assertThat(serverStreamTracer1.nextOutboundEvent()) .matches("outboundMessageSent\\(0, -?[0-9]+, -?[0-9]+\\)"); - assertThat(serverStreamTracer1.getOutboundWireSize()).isGreaterThan(0L); - assertThat(serverStreamTracer1.getOutboundUncompressedSize()).isGreaterThan(0L); + if (isEnabledSupportTracingMessageSizes()) { + assertThat(serverStreamTracer1.getOutboundWireSize()).isGreaterThan(0L); + assertThat(serverStreamTracer1.getOutboundUncompressedSize()).isGreaterThan(0L); + } assertTrue(clientStreamTracer1.getInboundHeaders()); assertThat(clientStreamTracer1.nextInboundEvent()).isEqualTo("inboundMessage(0)"); assertEquals("Hi. Who are you?", methodDescriptor.parseResponse(message)); assertThat(clientStreamTracer1.nextInboundEvent()) .matches("inboundMessageRead\\(0, -?[0-9]+, -?[0-9]+\\)"); - assertThat(clientStreamTracer1.getInboundWireSize()).isGreaterThan(0L); - assertThat(clientStreamTracer1.getInboundUncompressedSize()).isGreaterThan(0L); + if (isEnabledSupportTracingMessageSizes()) { + assertThat(clientStreamTracer1.getInboundWireSize()).isGreaterThan(0L); + assertThat(clientStreamTracer1.getInboundUncompressedSize()).isGreaterThan(0L); + } message.close(); assertNull("no additional message expected", clientStreamListener.messageQueue.poll()); @@ -1258,10 +1277,12 @@ public void onReady() { serverStream.close(Status.OK, new Metadata()); assertTrue(clientStreamTracer1.getOutboundHeaders()); assertTrue(clientStreamTracer1.getInboundHeaders()); - assertThat(clientStreamTracer1.getInboundWireSize()).isGreaterThan(0L); - assertThat(clientStreamTracer1.getInboundUncompressedSize()).isGreaterThan(0L); - assertThat(serverStreamTracer1.getOutboundWireSize()).isGreaterThan(0L); - assertThat(serverStreamTracer1.getOutboundUncompressedSize()).isGreaterThan(0L); + if (isEnabledSupportTracingMessageSizes()) { + assertThat(clientStreamTracer1.getInboundWireSize()).isGreaterThan(0L); + assertThat(clientStreamTracer1.getInboundUncompressedSize()).isGreaterThan(0L); + assertThat(serverStreamTracer1.getOutboundWireSize()).isGreaterThan(0L); + assertThat(serverStreamTracer1.getOutboundUncompressedSize()).isGreaterThan(0L); + } assertNull(clientStreamTracer1.getInboundTrailers()); assertSame(status, clientStreamTracer1.getStatus()); // There is a race between client cancelling and server closing. The final status seen by the diff --git a/inprocess/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java index 4e711a94004..9b33b3d3618 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.inprocess.InProcessTransport.isEnabledSupportTracingMessageSizes; import com.google.errorprone.annotations.DoNotCall; import io.grpc.ChannelCredentials; @@ -118,6 +119,9 @@ public ClientTransportFactory buildClientTransportFactory() { managedChannelImplBuilder.setStatsRecordStartedRpcs(false); managedChannelImplBuilder.setStatsRecordFinishedRpcs(false); managedChannelImplBuilder.setStatsRecordRetryMetrics(false); + if (!isEnabledSupportTracingMessageSizes) { + managedChannelImplBuilder.disableRetry(); + } } @Internal diff --git a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java index 0d173b96711..4212d96e9fb 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java @@ -82,6 +82,8 @@ @ThreadSafe final class InProcessTransport implements ServerTransport, ConnectionClientTransport { private static final Logger log = Logger.getLogger(InProcessTransport.class.getName()); + static boolean isEnabledSupportTracingMessageSizes = + GrpcUtil.getFlag("GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES", false); private final InternalLogId logId; private final SocketAddress address; @@ -485,22 +487,25 @@ private void clientCancelled(Status status) { @Override public void writeMessage(InputStream message) { - long messageLength; - try { - if (assumedMessageSize != -1) { - messageLength = assumedMessageSize; - } else if (message instanceof KnownLength || message instanceof ByteArrayInputStream) { - messageLength = message.available(); - } else { - InputStream oldMessage = message; - byte[] payload = ByteStreams.toByteArray(message); - messageLength = payload.length; - message = new ByteArrayInputStream(payload); - oldMessage.close(); + long messageLength = 0; + if (isEnabledSupportTracingMessageSizes) { + try { + if (assumedMessageSize != -1) { + messageLength = assumedMessageSize; + } else if (message instanceof KnownLength || message instanceof ByteArrayInputStream) { + messageLength = message.available(); + } else { + InputStream oldMessage = message; + byte[] payload = ByteStreams.toByteArray(message); + messageLength = payload.length; + message = new ByteArrayInputStream(payload); + oldMessage.close(); + } + } catch (Exception e) { + throw new RuntimeException("Error processing the message length", e); } - } catch (Exception e) { - throw new RuntimeException("Error processing the message length", e); } + synchronized (this) { if (closed) { return; @@ -509,11 +514,13 @@ public void writeMessage(InputStream message) { statsTraceCtx.outboundMessageSent(outboundSeqNo, -1, -1); clientStream.statsTraceCtx.inboundMessage(outboundSeqNo); clientStream.statsTraceCtx.inboundMessageRead(outboundSeqNo, -1, -1); - statsTraceCtx.outboundUncompressedSize(messageLength); - statsTraceCtx.outboundWireSize(messageLength); - // messageLength should be same at receiver's end as no actual wire is involved. - clientStream.statsTraceCtx.inboundUncompressedSize(messageLength); - clientStream.statsTraceCtx.inboundWireSize(messageLength); + if (isEnabledSupportTracingMessageSizes) { + statsTraceCtx.outboundUncompressedSize(messageLength); + statsTraceCtx.outboundWireSize(messageLength); + // messageLength should be same at receiver's end as no actual wire is involved. + clientStream.statsTraceCtx.inboundUncompressedSize(messageLength); + clientStream.statsTraceCtx.inboundWireSize(messageLength); + } outboundSeqNo++; StreamListener.MessageProducer producer = new SingleMessageProducer(message); if (clientRequested > 0) { @@ -523,7 +530,6 @@ public void writeMessage(InputStream message) { clientReceiveQueue.add(producer); } } - syncContext.drain(); } @@ -777,21 +783,23 @@ private void serverClosed(Status serverListenerStatus, Status serverTracerStatus @Override public void writeMessage(InputStream message) { - long messageLength; - try { - if (assumedMessageSize != -1) { - messageLength = assumedMessageSize; - } else if (message instanceof KnownLength || message instanceof ByteArrayInputStream) { - messageLength = message.available(); - } else { - InputStream oldMessage = message; - byte[] payload = ByteStreams.toByteArray(message); - messageLength = payload.length; - message = new ByteArrayInputStream(payload); - oldMessage.close(); + long messageLength = 0; + if (isEnabledSupportTracingMessageSizes) { + try { + if (assumedMessageSize != -1) { + messageLength = assumedMessageSize; + } else if (message instanceof KnownLength || message instanceof ByteArrayInputStream) { + messageLength = message.available(); + } else { + InputStream oldMessage = message; + byte[] payload = ByteStreams.toByteArray(message); + messageLength = payload.length; + message = new ByteArrayInputStream(payload); + oldMessage.close(); + } + } catch (Exception e) { + throw new RuntimeException("Error processing the message length", e); } - } catch (Exception e) { - throw new RuntimeException("Error processing the message length", e); } synchronized (this) { if (closed) { @@ -801,11 +809,13 @@ public void writeMessage(InputStream message) { statsTraceCtx.outboundMessageSent(outboundSeqNo, -1, -1); serverStream.statsTraceCtx.inboundMessage(outboundSeqNo); serverStream.statsTraceCtx.inboundMessageRead(outboundSeqNo, -1, -1); - statsTraceCtx.outboundUncompressedSize(messageLength); - statsTraceCtx.outboundWireSize(messageLength); - // messageLength should be same at receiver's end as no actual wire is involved. - serverStream.statsTraceCtx.inboundUncompressedSize(messageLength); - serverStream.statsTraceCtx.inboundWireSize(messageLength); + if (isEnabledSupportTracingMessageSizes) { + statsTraceCtx.outboundUncompressedSize(messageLength); + statsTraceCtx.outboundWireSize(messageLength); + // messageLength should be same at receiver's end as no actual wire is involved. + serverStream.statsTraceCtx.inboundUncompressedSize(messageLength); + serverStream.statsTraceCtx.inboundWireSize(messageLength); + } outboundSeqNo++; StreamListener.MessageProducer producer = new SingleMessageProducer(message); if (serverRequested > 0) { diff --git a/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java b/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java index 87d7467c11f..3ed8dd24ca9 100644 --- a/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java +++ b/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java @@ -234,9 +234,11 @@ public void basicStreamInProcess() throws Exception { private void assertAssumedMessageSize( TestStreamTracer streamTracerSender, TestStreamTracer streamTracerReceiver) { - Assert.assertEquals(TEST_MESSAGE_LENGTH, streamTracerSender.getOutboundWireSize()); - Assert.assertEquals(TEST_MESSAGE_LENGTH, streamTracerSender.getOutboundUncompressedSize()); - Assert.assertEquals(TEST_MESSAGE_LENGTH, streamTracerReceiver.getInboundWireSize()); - Assert.assertEquals(TEST_MESSAGE_LENGTH, streamTracerReceiver.getInboundUncompressedSize()); + if (isEnabledSupportTracingMessageSizes()) { + Assert.assertEquals(TEST_MESSAGE_LENGTH, streamTracerSender.getOutboundWireSize()); + Assert.assertEquals(TEST_MESSAGE_LENGTH, streamTracerSender.getOutboundUncompressedSize()); + Assert.assertEquals(TEST_MESSAGE_LENGTH, streamTracerReceiver.getInboundWireSize()); + Assert.assertEquals(TEST_MESSAGE_LENGTH, streamTracerReceiver.getInboundUncompressedSize()); + } } } From 46c1b387fa547038dbfb2f6ecd236ed3e4b757e3 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 24 Oct 2024 10:52:44 -0700 Subject: [PATCH 058/591] Update binderDied() error description to spell out the possibilities for those unfamiliar with Android internals. (#11628) Callers are frequently confused by this message and waste time looking for problems in the client when the root cause is simply a server crash. See b/371447460 for more context. --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 81eecf975bf..8c428fac98c 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -299,7 +299,7 @@ protected boolean setOutgoingBinder(OneWayBinderProxy binder) { @Override public synchronized void binderDied() { - shutdownInternal(Status.UNAVAILABLE.withDescription("binderDied"), true); + shutdownInternal(Status.UNAVAILABLE.withDescription("Peer process crashed, exited or was killed (binderDied)"), true); } @GuardedBy("this") From 31dad6af4996170a55f1bd0a99f0d892e6bbcd84 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 24 Oct 2024 10:07:53 -0700 Subject: [PATCH 059/591] Start 1.69.0 development cycle --- MODULE.bazel | 2 +- build.gradle | 2 +- .../src/test/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/test/golden/TestService.java.txt | 2 +- core/src/main/java/io/grpc/internal/GrpcUtil.java | 2 +- examples/MODULE.bazel | 2 +- examples/android/clientcache/app/build.gradle | 10 +++++----- examples/android/helloworld/app/build.gradle | 8 ++++---- examples/android/routeguide/app/build.gradle | 8 ++++---- examples/android/strictmode/app/build.gradle | 8 ++++---- examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 4 ++-- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 4 ++-- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 4 ++-- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 4 ++-- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 4 ++-- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- 34 files changed, 55 insertions(+), 55 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 81fa8795e68..f58d2daa40a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "grpc-java", compatibility_level = 0, repo_name = "io_grpc_grpc_java", - version = "1.68.0-SNAPSHOT", # CURRENT_GRPC_VERSION + version = "1.69.0-SNAPSHOT", # CURRENT_GRPC_VERSION ) # GRPC_DEPS_START diff --git a/build.gradle b/build.gradle index 6afe010de4d..f647acdb382 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.68.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.69.0-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index 04a7f2406b3..adfe415c845 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.68.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.69.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index d69abad7cbb..438d067ac0d 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.68.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.69.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index a1fe34c2edc..f519b502c1f 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -219,7 +219,7 @@ public byte[] parseAsciiString(byte[] serialized) { public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); - public static final String IMPLEMENTATION_VERSION = "1.68.0-SNAPSHOT"; // CURRENT_GRPC_VERSION + public static final String IMPLEMENTATION_VERSION = "1.69.0-SNAPSHOT"; // CURRENT_GRPC_VERSION /** * The default timeout in nanos for a keepalive ping request. diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index 60bed40f349..e7b65724a5b 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,5 +1,5 @@ bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") -bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.66.0-SNAPSHOT") # CURRENT_GRPC_VERSION +bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.69.0-SNAPSHOT") # CURRENT_GRPC_VERSION bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") bazel_dep(name = "rules_jvm_external", version = "6.0") diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index 6b5b966e7f6..271d29cfb4b 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -34,7 +34,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -54,12 +54,12 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.5' - testImplementation 'io.grpc:grpc-testing:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION + testImplementation 'io.grpc:grpc-testing:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index 4edbcb14612..e51176cc4f6 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index 4a08f40e4ee..3209937ba70 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index 9f41994e3c2..1fdf43d1f3f 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -33,7 +33,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -53,8 +53,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/build.gradle b/examples/build.gradle index 24f6d2b04f8..83877cbb8d9 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index 6b1f0ded169..e0d565a41f2 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index 97bca5f91b6..7926d53138b 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -25,7 +25,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index dc399a78c9d..d08ec7787eb 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -6,13 +6,13 @@ jar - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT example-debug https://github.com/grpc/grpc-java UTF-8 - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index 0c45e6de7cf..ca9ad9a60b8 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -25,7 +25,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index a8e32b347e5..94691b1d1d2 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -6,13 +6,13 @@ jar - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT example-dualstack https://github.com/grpc/grpc-java UTF-8 - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 86edd557206..98841c20ef9 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index 89478a2dfa6..6a1b11d5e20 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,13 +6,13 @@ jar - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index 179ab3a74d9..7a72ebcdcd9 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -25,7 +25,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 1a6f4719460..3365b844895 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -25,7 +25,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 17817e2a374..7ade8da565a 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index 991174af382..baebfc6fc50 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,13 +6,13 @@ jar - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index c31486095f3..0e14f369ebe 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index 10330d955ed..d20358f9266 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,13 +7,13 @@ jar - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index fe21efcf0db..ee46d69cdaf 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index 072bd957dcf..fe48c0db6ee 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -7,13 +7,13 @@ jar - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT example-oauth https://github.com/grpc/grpc-java UTF-8 - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index f08f9d492a5..ccdcf93ebfd 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index 4ca9343f887..43df71e5ed7 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -18,7 +18,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index e1efc8ee050..769e9bddee8 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -18,7 +18,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index ebd14674578..e4a5128898f 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index 8a16a902b72..1eb51182309 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index 972976ecdf1..de32a5f9807 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,13 +6,13 @@ jar - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT example-tls https://github.com/grpc/grpc-java UTF-8 - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index a3e23a19601..a77e71b253c 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.68.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/pom.xml b/examples/pom.xml index 554c70a35ce..475747a5b5e 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,13 +6,13 @@ jar - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT examples https://github.com/grpc/grpc-java UTF-8 - 1.68.0-SNAPSHOT + 1.69.0-SNAPSHOT 3.25.5 3.25.5 From ba8ab796e78e420d2d81b6dd479434a1bc548584 Mon Sep 17 00:00:00 2001 From: Luwei Ge Date: Thu, 24 Oct 2024 15:18:53 -0700 Subject: [PATCH 060/591] alts: support altsCallCredentials in GoogleDefaultChannelCredentials (#11634) --- .../java/io/grpc/alts/AltsContextUtil.java | 18 ++- .../io/grpc/alts/DualCallCredentials.java | 46 ++++++++ .../alts/GoogleDefaultChannelCredentials.java | 34 ++++-- .../io/grpc/alts/DualCallCredentialsTest.java | 109 ++++++++++++++++++ 4 files changed, 191 insertions(+), 16 deletions(-) create mode 100644 alts/src/main/java/io/grpc/alts/DualCallCredentials.java create mode 100644 alts/src/test/java/io/grpc/alts/DualCallCredentialsTest.java diff --git a/alts/src/main/java/io/grpc/alts/AltsContextUtil.java b/alts/src/main/java/io/grpc/alts/AltsContextUtil.java index 91b06756dc3..5f2ce353761 100644 --- a/alts/src/main/java/io/grpc/alts/AltsContextUtil.java +++ b/alts/src/main/java/io/grpc/alts/AltsContextUtil.java @@ -14,9 +14,9 @@ * limitations under the License. */ - package io.grpc.alts; +import io.grpc.Attributes; import io.grpc.ExperimentalApi; import io.grpc.ServerCall; import io.grpc.alts.internal.AltsInternalContext; @@ -35,7 +35,7 @@ private AltsContextUtil() {} * @return the created {@link AltsContext} * @throws IllegalArgumentException if the {@link ServerCall} has no ALTS information. */ - public static AltsContext createFrom(ServerCall call) { + public static AltsContext createFrom(ServerCall call) { Object authContext = call.getAttributes().get(AltsProtocolNegotiator.AUTH_CONTEXT_KEY); if (!(authContext instanceof AltsInternalContext)) { throw new IllegalArgumentException("No ALTS context information found"); @@ -49,8 +49,18 @@ public static AltsContext createFrom(ServerCall call) { * @param call the {@link ServerCall} to check * @return true, if the {@link ServerCall} contains ALTS information and false otherwise. */ - public static boolean check(ServerCall call) { - Object authContext = call.getAttributes().get(AltsProtocolNegotiator.AUTH_CONTEXT_KEY); + public static boolean check(ServerCall call) { + return check(call.getAttributes()); + } + + /** + * Checks if the {@link Attributes} contains ALTS information. + * + * @param attributes the {@link Attributes} to check + * @return true, if the {@link Attributes} contains ALTS information and false otherwise. + */ + public static boolean check(Attributes attributes) { + Object authContext = attributes.get(AltsProtocolNegotiator.AUTH_CONTEXT_KEY); return authContext instanceof AltsInternalContext; } } diff --git a/alts/src/main/java/io/grpc/alts/DualCallCredentials.java b/alts/src/main/java/io/grpc/alts/DualCallCredentials.java new file mode 100644 index 00000000000..08104712e65 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/DualCallCredentials.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import io.grpc.CallCredentials; +import java.util.concurrent.Executor; + +/** + * {@code CallCredentials} that will pick the right credentials based on whether the established + * connection is ALTS or TLS. + */ +final class DualCallCredentials extends CallCredentials { + private final CallCredentials tlsCallCredentials; + private final CallCredentials altsCallCredentials; + + public DualCallCredentials(CallCredentials tlsCallCreds, CallCredentials altsCallCreds) { + tlsCallCredentials = tlsCallCreds; + altsCallCredentials = altsCallCreds; + } + + @Override + public void applyRequestMetadata( + CallCredentials.RequestInfo requestInfo, + Executor appExecutor, + CallCredentials.MetadataApplier applier) { + if (AltsContextUtil.check(requestInfo.getTransportAttrs())) { + altsCallCredentials.applyRequestMetadata(requestInfo, appExecutor, applier); + } else { + tlsCallCredentials.applyRequestMetadata(requestInfo, appExecutor, applier); + } + } +} diff --git a/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelCredentials.java b/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelCredentials.java index d9c2ddaaed7..1b5880120a4 100644 --- a/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelCredentials.java +++ b/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelCredentials.java @@ -63,6 +63,7 @@ public static Builder newBuilder() { */ public static final class Builder { private CallCredentials callCredentials; + private CallCredentials altsCallCredentials; private Builder() {} @@ -72,23 +73,32 @@ public Builder callCredentials(CallCredentials callCreds) { return this; } + /** Constructs GoogleDefaultChannelCredentials with an ALTS-specific call credential. */ + public Builder altsCallCredentials(CallCredentials callCreds) { + altsCallCredentials = callCreds; + return this; + } + /** Builds a GoogleDefaultChannelCredentials instance. */ public ChannelCredentials build() { ChannelCredentials nettyCredentials = InternalNettyChannelCredentials.create(createClientFactory()); - if (callCredentials != null) { - return CompositeChannelCredentials.create(nettyCredentials, callCredentials); - } - CallCredentials callCreds; - try { - callCreds = MoreCallCredentials.from(GoogleCredentials.getApplicationDefault()); - } catch (IOException e) { - callCreds = - new FailingCallCredentials( - Status.UNAUTHENTICATED - .withDescription("Failed to get Google default credentials") - .withCause(e)); + CallCredentials tlsCallCreds = callCredentials; + if (tlsCallCreds == null) { + try { + tlsCallCreds = MoreCallCredentials.from(GoogleCredentials.getApplicationDefault()); + } catch (IOException e) { + tlsCallCreds = + new FailingCallCredentials( + Status.UNAUTHENTICATED + .withDescription("Failed to get Google default credentials") + .withCause(e)); + } } + CallCredentials callCreds = + altsCallCredentials == null + ? tlsCallCreds + : new DualCallCredentials(tlsCallCreds, altsCallCredentials); return CompositeChannelCredentials.create(nettyCredentials, callCreds); } diff --git a/alts/src/test/java/io/grpc/alts/DualCallCredentialsTest.java b/alts/src/test/java/io/grpc/alts/DualCallCredentialsTest.java new file mode 100644 index 00000000000..29646191be1 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/DualCallCredentialsTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.alts; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import io.grpc.Attributes; +import io.grpc.CallCredentials; +import io.grpc.CallCredentials.RequestInfo; +import io.grpc.MethodDescriptor; +import io.grpc.SecurityLevel; +import io.grpc.alts.internal.AltsInternalContext; +import io.grpc.alts.internal.AltsProtocolNegotiator; +import io.grpc.testing.TestMethodDescriptors; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit tests for {@link DualCallCredentials}. */ +@RunWith(JUnit4.class) +public class DualCallCredentialsTest { + + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + @Mock CallCredentials tlsCallCredentials; + + @Mock CallCredentials altsCallCredentials; + + private static final String AUTHORITY = "testauthority"; + private static final SecurityLevel SECURITY_LEVEL = SecurityLevel.PRIVACY_AND_INTEGRITY; + + @Test + public void invokeTlsCallCredentials() { + DualCallCredentials callCredentials = + new DualCallCredentials(tlsCallCredentials, altsCallCredentials); + RequestInfo requestInfo = new RequestInfoImpl(false); + callCredentials.applyRequestMetadata(requestInfo, null, null); + + verify(altsCallCredentials, never()).applyRequestMetadata(any(), any(), any()); + verify(tlsCallCredentials, times(1)).applyRequestMetadata(requestInfo, null, null); + } + + @Test + public void invokeAltsCallCredentials() { + DualCallCredentials callCredentials = + new DualCallCredentials(tlsCallCredentials, altsCallCredentials); + RequestInfo requestInfo = new RequestInfoImpl(true); + callCredentials.applyRequestMetadata(requestInfo, null, null); + + verify(altsCallCredentials, times(1)).applyRequestMetadata(requestInfo, null, null); + verify(tlsCallCredentials, never()).applyRequestMetadata(any(), any(), any()); + } + + private static final class RequestInfoImpl extends CallCredentials.RequestInfo { + private Attributes attrs; + + RequestInfoImpl(boolean hasAltsContext) { + attrs = + hasAltsContext + ? Attributes.newBuilder() + .set( + AltsProtocolNegotiator.AUTH_CONTEXT_KEY, + AltsInternalContext.getDefaultInstance()) + .build() + : Attributes.EMPTY; + } + + @Override + public MethodDescriptor getMethodDescriptor() { + return TestMethodDescriptors.voidMethod(); + } + + @Override + public SecurityLevel getSecurityLevel() { + return SECURITY_LEVEL; + } + + @Override + public String getAuthority() { + return AUTHORITY; + } + + @Override + public Attributes getTransportAttrs() { + return attrs; + } + } +} From 370e7ce27c815034e6b028953c0d9e734fb0390f Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 24 Oct 2024 23:39:22 -0700 Subject: [PATCH 061/591] Revert "stub: Ignore unary response on server if status is not OK" (#11636) This reverts commit 99f86835ed8a1c960d532e58b5fd51ec1bb99825. The change doesn't handle `null` messages, which don't happen with protobuf, but can happen with other marshallers, especially in tests. See cl/689445172 This will reopen #5969. --- .../main/java/io/grpc/stub/ServerCalls.java | 24 ++------- .../java/io/grpc/stub/ServerCallsTest.java | 54 ------------------- 2 files changed, 4 insertions(+), 74 deletions(-) diff --git a/stub/src/main/java/io/grpc/stub/ServerCalls.java b/stub/src/main/java/io/grpc/stub/ServerCalls.java index 6c444551530..7990a5b34c0 100644 --- a/stub/src/main/java/io/grpc/stub/ServerCalls.java +++ b/stub/src/main/java/io/grpc/stub/ServerCalls.java @@ -335,7 +335,6 @@ private static final class ServerCallStreamObserverImpl private boolean aborted = false; private boolean completed = false; private Runnable onCloseHandler; - private RespT unaryResponse; // Non private to avoid synthetic class ServerCallStreamObserverImpl(ServerCall call, boolean serverStreamingOrBidi) { @@ -374,22 +373,15 @@ public void onNext(RespT response) { } checkState(!aborted, "Stream was terminated by error, no further calls are allowed"); checkState(!completed, "Stream is already completed, no further calls are allowed"); - if (serverStreamingOrBidi) { - if (!sentHeaders) { - call.sendHeaders(new Metadata()); - sentHeaders = true; - } - call.sendMessage(response); - } else { - unaryResponse = response; + if (!sentHeaders) { + call.sendHeaders(new Metadata()); + sentHeaders = true; } + call.sendMessage(response); } @Override public void onError(Throwable t) { - if (!serverStreamingOrBidi) { - unaryResponse = null; - } Metadata metadata = Status.trailersFromThrowable(t); if (metadata == null) { metadata = new Metadata(); @@ -400,14 +392,6 @@ public void onError(Throwable t) { @Override public void onCompleted() { - if (!serverStreamingOrBidi && unaryResponse != null) { - if (!sentHeaders) { - call.sendHeaders(new Metadata()); - sentHeaders = true; - } - call.sendMessage(unaryResponse); - unaryResponse = null; - } call.close(Status.OK, new Metadata()); completed = true; } diff --git a/stub/src/test/java/io/grpc/stub/ServerCallsTest.java b/stub/src/test/java/io/grpc/stub/ServerCallsTest.java index 8d65be19e20..1e51ac10110 100644 --- a/stub/src/test/java/io/grpc/stub/ServerCallsTest.java +++ b/stub/src/test/java/io/grpc/stub/ServerCallsTest.java @@ -33,7 +33,6 @@ import io.grpc.ServerServiceDefinition; import io.grpc.ServiceDescriptor; import io.grpc.Status; -import io.grpc.Status.Code; import io.grpc.StatusRuntimeException; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; @@ -621,59 +620,6 @@ public void onClose(Status status, Metadata trailers) { assertArrayEquals(new int[]{0, 1, 1, 2, 2, 2}, receivedMessages); } - @Test - public void serverUnaryResponseMsgWithOkStatus() { - ServerCallHandler serverCallHandler = - ServerCalls.asyncUnaryCall( - new ServerCalls.UnaryMethod() { - @Override - public void invoke(Integer request, StreamObserver responseObserver) { - responseObserver.onNext(request); - responseObserver.onCompleted(); - } - }); - ServerCall.Listener callListener = - serverCallHandler.startCall(serverCall, new Metadata()); - serverCall.isReady = true; - serverCall.isCancelled = false; - callListener.onReady(); - callListener.onMessage(1); - callListener.onHalfClose(); - - assertThat(serverCall.status.getCode()).isEqualTo(Code.OK); - assertThat(serverCall.responses).containsExactly(1); - } - - @Test - public void serverUnaryResponseMsgWithNotOkStatus() { - ServerCallHandler serverCallHandler = - ServerCalls.asyncUnaryCall( - new ServerCalls.UnaryMethod() { - @Override - public void invoke(Integer request, StreamObserver responseObserver) { - responseObserver.onNext(request); - responseObserver.onError( - Status.INTERNAL - .withDescription("Response message is null for unary call") - .asRuntimeException()); - } - }); - - ServerCall.Listener callListener = - serverCallHandler.startCall(serverCall, new Metadata()); - - serverCall.isReady = true; - serverCall.isCancelled = false; - callListener.onReady(); - callListener.onMessage(1); - callListener.onHalfClose(); - - assertThat(serverCall.status.getCode()).isEqualTo(Code.INTERNAL); - assertThat(serverCall.status.getDescription()) - .isEqualTo("Response message is null for unary call"); - assertThat(serverCall.responses).isEmpty(); - } - public static class IntegerMarshaller implements MethodDescriptor.Marshaller { @Override public InputStream stream(Integer value) { From 0b2c17d0da84e92fbb104bd46ccb2f61860db04b Mon Sep 17 00:00:00 2001 From: Kannan J Date: Fri, 25 Oct 2024 14:36:27 +0530 Subject: [PATCH 062/591] Xds: Implement using system root trust CA for TLS server authentication (#11470) Allow using system root certs for server cert validation rather than CA root certs provided by the control plane when the validation context provided by the control plane specifies so. --- .../io/grpc/xds/EnvoyServerProtoData.java | 10 +- .../java/io/grpc/xds/XdsClusterResource.java | 11 +- .../ClientSslContextProviderFactory.java | 18 +-- .../security/CommonTlsContextUtil.java | 13 +- .../CertProviderClientSslContextProvider.java | 19 +-- ...oviderClientSslContextProviderFactory.java | 21 ++- .../CertProviderSslContextProvider.java | 9 +- .../grpc/xds/GrpcXdsClientImplDataTest.java | 93 +++++++++++- .../grpc/xds/GrpcXdsClientImplTestBase.java | 3 +- .../grpc/xds/XdsSecurityClientServerTest.java | 143 +++++++++++++++++- .../ClientSslContextProviderFactoryTest.java | 18 --- .../security/CommonTlsContextTestsUtil.java | 44 +++--- ...tProviderClientSslContextProviderTest.java | 26 +++- 13 files changed, 342 insertions(+), 86 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java index 978e6663cbe..4a6213277e7 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java @@ -16,6 +16,8 @@ package io.grpc.xds; +import static com.google.common.base.Preconditions.checkNotNull; + import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -41,13 +43,13 @@ private EnvoyServerProtoData() { } public abstract static class BaseTlsContext { - @Nullable protected final CommonTlsContext commonTlsContext; + protected final CommonTlsContext commonTlsContext; - protected BaseTlsContext(@Nullable CommonTlsContext commonTlsContext) { - this.commonTlsContext = commonTlsContext; + protected BaseTlsContext(CommonTlsContext commonTlsContext) { + this.commonTlsContext = checkNotNull(commonTlsContext, "commonTlsContext cannot be null."); } - @Nullable public CommonTlsContext getCommonTlsContext() { + public CommonTlsContext getCommonTlsContext() { return commonTlsContext; } diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index c6340156d49..c7789f3d7dd 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -39,6 +39,7 @@ import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; import io.grpc.LoadBalancerRegistry; import io.grpc.NameResolver; +import io.grpc.internal.GrpcUtil; import io.grpc.internal.ServiceConfigUtil; import io.grpc.internal.ServiceConfigUtil.LbConfig; import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; @@ -46,6 +47,7 @@ import io.grpc.xds.XdsClusterResource.CdsUpdate; import io.grpc.xds.client.XdsClient.ResourceUpdate; import io.grpc.xds.client.XdsResourceType; +import io.grpc.xds.internal.security.CommonTlsContextUtil; import java.util.List; import java.util.Locale; import java.util.Set; @@ -57,6 +59,9 @@ class XdsClusterResource extends XdsResourceType { !Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST")) ? Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST")) : Boolean.parseBoolean(System.getProperty("io.grpc.xds.experimentalEnableLeastRequest")); + @VisibleForTesting + public static boolean enableSystemRootCerts = + GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_SYSTEM_ROOT_CERTS", false); @VisibleForTesting static final String AGGREGATE_CLUSTER_TYPE_NAME = "envoy.clusters.aggregate"; @@ -430,9 +435,11 @@ static void validateCommonTlsContext( } String rootCaInstanceName = getRootCertInstanceName(commonTlsContext); if (rootCaInstanceName == null) { - if (!server) { + if (!server && (!enableSystemRootCerts + || !CommonTlsContextUtil.isUsingSystemRootCerts(commonTlsContext))) { throw new ResourceInvalidException( - "ca_certificate_provider_instance is required in upstream-tls-context"); + "ca_certificate_provider_instance or system_root_certs is required in " + + "upstream-tls-context"); } } else { if (certProviderInstances == null || !certProviderInstances.contains(rootCaInstanceName)) { diff --git a/xds/src/main/java/io/grpc/xds/internal/security/ClientSslContextProviderFactory.java b/xds/src/main/java/io/grpc/xds/internal/security/ClientSslContextProviderFactory.java index 90202b4820a..37d289c1c47 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/ClientSslContextProviderFactory.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/ClientSslContextProviderFactory.java @@ -16,8 +16,6 @@ package io.grpc.xds.internal.security; -import static com.google.common.base.Preconditions.checkNotNull; - import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.internal.security.ReferenceCountingMap.ValueFactory; @@ -44,17 +42,9 @@ final class ClientSslContextProviderFactory /** Creates an SslContextProvider from the given UpstreamTlsContext. */ @Override public SslContextProvider create(UpstreamTlsContext upstreamTlsContext) { - checkNotNull(upstreamTlsContext, "upstreamTlsContext"); - checkNotNull( - upstreamTlsContext.getCommonTlsContext(), - "upstreamTlsContext should have CommonTlsContext"); - if (CommonTlsContextUtil.hasCertProviderInstance( - upstreamTlsContext.getCommonTlsContext())) { - return certProviderClientSslContextProviderFactory.getProvider( - upstreamTlsContext, - bootstrapInfo.node().toEnvoyProtoNode(), - bootstrapInfo.certProviders()); - } - throw new UnsupportedOperationException("Unsupported configurations in UpstreamTlsContext!"); + return certProviderClientSslContextProviderFactory.getProvider( + upstreamTlsContext, + bootstrapInfo.node().toEnvoyProtoNode(), + bootstrapInfo.certProviders()); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/CommonTlsContextUtil.java b/xds/src/main/java/io/grpc/xds/internal/security/CommonTlsContextUtil.java index d3003b4a792..e5a8c115361 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/CommonTlsContextUtil.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/CommonTlsContextUtil.java @@ -25,7 +25,7 @@ public final class CommonTlsContextUtil { private CommonTlsContextUtil() {} - static boolean hasCertProviderInstance(CommonTlsContext commonTlsContext) { + public static boolean hasCertProviderInstance(CommonTlsContext commonTlsContext) { if (commonTlsContext == null) { return false; } @@ -65,4 +65,15 @@ public static CommonTlsContext.CertificateProviderInstance convert( .setInstanceName(pluginInstance.getInstanceName()) .setCertificateName(pluginInstance.getCertificateName()).build(); } + + public static boolean isUsingSystemRootCerts(CommonTlsContext commonTlsContext) { + if (commonTlsContext.hasCombinedValidationContext()) { + return commonTlsContext.getCombinedValidationContext().getDefaultValidationContext() + .hasSystemRootCerts(); + } + if (commonTlsContext.hasValidationContext()) { + return commonTlsContext.getValidationContext().hasSystemRootCerts(); + } + return false; + } } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java index d4080101c1a..8aa74b2b50f 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java @@ -16,8 +16,6 @@ package io.grpc.xds.internal.security.certprovider; -import static com.google.common.base.Preconditions.checkNotNull; - import io.envoyproxy.envoy.config.core.v3.Node; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; @@ -46,7 +44,7 @@ final class CertProviderClientSslContextProvider extends CertProviderSslContextP node, certProviders, certInstance, - checkNotNull(rootCertInstance, "Client SSL requires rootCertInstance"), + rootCertInstance, staticCertValidationContext, upstreamTlsContext, certificateProviderStore); @@ -56,12 +54,15 @@ final class CertProviderClientSslContextProvider extends CertProviderSslContextP protected final SslContextBuilder getSslContextBuilder( CertificateValidationContext certificateValidationContextdationContext) throws CertStoreException { - SslContextBuilder sslContextBuilder = - GrpcSslContexts.forClient() - .trustManager( - new XdsTrustManagerFactory( - savedTrustedRoots.toArray(new X509Certificate[0]), - certificateValidationContextdationContext)); + SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient(); + // Null rootCertInstance implies hasSystemRootCerts because of the check in + // CertProviderClientSslContextProviderFactory. + if (rootCertInstance != null) { + sslContextBuilder.trustManager( + new XdsTrustManagerFactory( + savedTrustedRoots.toArray(new X509Certificate[0]), + certificateValidationContextdationContext)); + } if (isMtls()) { sslContextBuilder.keyManager(savedKey, savedCertChain); } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderFactory.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderFactory.java index 21782741c2c..6205c1c3a63 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderFactory.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderFactory.java @@ -25,6 +25,7 @@ import io.grpc.Internal; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.client.Bootstrapper.CertificateProviderInfo; +import io.grpc.xds.internal.security.CommonTlsContextUtil; import io.grpc.xds.internal.security.SslContextProvider; import java.util.Map; import javax.annotation.Nullable; @@ -64,13 +65,17 @@ public SslContextProvider getProvider( = CertProviderSslContextProvider.getRootCertProviderInstance(commonTlsContext); CommonTlsContext.CertificateProviderInstance certInstance = CertProviderSslContextProvider.getCertProviderInstance(commonTlsContext); - return new CertProviderClientSslContextProvider( - node, - certProviders, - certInstance, - rootCertInstance, - staticCertValidationContext, - upstreamTlsContext, - certificateProviderStore); + if (CommonTlsContextUtil.hasCertProviderInstance(upstreamTlsContext.getCommonTlsContext()) + || CommonTlsContextUtil.isUsingSystemRootCerts(upstreamTlsContext.getCommonTlsContext())) { + return new CertProviderClientSslContextProvider( + node, + certProviders, + certInstance, + rootCertInstance, + staticCertValidationContext, + upstreamTlsContext, + certificateProviderStore); + } + throw new UnsupportedOperationException("Unsupported configurations in UpstreamTlsContext!"); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java index 6570c619913..b68f705fedb 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java @@ -37,10 +37,11 @@ abstract class CertProviderSslContextProvider extends DynamicSslContextProvider @Nullable private final CertificateProviderStore.Handle certHandle; @Nullable private final CertificateProviderStore.Handle rootCertHandle; @Nullable private final CertificateProviderInstance certInstance; - @Nullable private final CertificateProviderInstance rootCertInstance; + @Nullable protected final CertificateProviderInstance rootCertInstance; @Nullable protected PrivateKey savedKey; @Nullable protected List savedCertChain; @Nullable protected List savedTrustedRoots; + private final boolean isUsingSystemRootCerts; protected CertProviderSslContextProvider( Node node, @@ -83,6 +84,8 @@ protected CertProviderSslContextProvider( } else { rootCertHandle = null; } + this.isUsingSystemRootCerts = rootCertInstance == null + && CommonTlsContextUtil.isUsingSystemRootCerts(tlsContext.getCommonTlsContext()); } private static CertificateProviderInfo getCertProviderConfig( @@ -151,7 +154,7 @@ public final void updateTrustedRoots(List trustedRoots) { private void updateSslContextWhenReady() { if (isMtls()) { - if (savedKey != null && savedTrustedRoots != null) { + if (savedKey != null && (savedTrustedRoots != null || isUsingSystemRootCerts)) { updateSslContext(); clearKeysAndCerts(); } @@ -175,7 +178,7 @@ private void clearKeysAndCerts() { } protected final boolean isMtls() { - return certInstance != null && rootCertInstance != null; + return certInstance != null && (rootCertInstance != null || isUsingSystemRootCerts); } protected final boolean isClientSideTls() { diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 3c159ba7055..a23e7177c29 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -173,11 +173,13 @@ public class GrpcXdsClientImplDataTest { private final FilterRegistry filterRegistry = FilterRegistry.getDefaultRegistry(); private boolean originalEnableRouteLookup; private boolean originalEnableLeastRequest; + private boolean originalEnableUseSystemRootCerts; @Before public void setUp() { originalEnableRouteLookup = XdsRouteConfigureResource.enableRouteLookup; originalEnableLeastRequest = XdsClusterResource.enableLeastRequest; + originalEnableUseSystemRootCerts = XdsClusterResource.enableSystemRootCerts; assertThat(originalEnableLeastRequest).isFalse(); } @@ -185,6 +187,7 @@ public void setUp() { public void tearDown() { XdsRouteConfigureResource.enableRouteLookup = originalEnableRouteLookup; XdsClusterResource.enableLeastRequest = originalEnableLeastRequest; + XdsClusterResource.enableSystemRootCerts = originalEnableUseSystemRootCerts; } @Test @@ -2503,7 +2506,8 @@ public void validateCommonTlsContext_validationContext() throws ResourceInvalidE .setValidationContext(CertificateValidationContext.getDefaultInstance()) .build(); thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("ca_certificate_provider_instance is required in upstream-tls-context"); + thrown.expectMessage("ca_certificate_provider_instance or system_root_certs is required " + + "in upstream-tls-context"); XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); } @@ -2613,6 +2617,87 @@ public void validateCommonTlsContext_validationContextProviderInstance() .validateCommonTlsContext(commonTlsContext, ImmutableSet.of("name1", "name2"), false); } + @Test + public void + validateCommonTlsContext_combinedValidationContextSystemRootCerts_envVarNotSet_throws() { + XdsClusterResource.enableSystemRootCerts = false; + CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() + .setCombinedValidationContext( + CommonTlsContext.CombinedCertificateValidationContext.newBuilder() + .setDefaultValidationContext( + CertificateValidationContext.newBuilder() + .setSystemRootCerts( + CertificateValidationContext.SystemRootCerts.newBuilder().build()) + .build() + ) + .build()) + .build(); + try { + XdsClusterResource + .validateCommonTlsContext(commonTlsContext, ImmutableSet.of(), false); + fail("Expected exception"); + } catch (ResourceInvalidException ex) { + assertThat(ex.getMessage()).isEqualTo( + "ca_certificate_provider_instance or system_root_certs is required in" + + " upstream-tls-context"); + } + } + + @Test + public void validateCommonTlsContext_combinedValidationContextSystemRootCerts() + throws ResourceInvalidException { + XdsClusterResource.enableSystemRootCerts = true; + CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() + .setCombinedValidationContext( + CommonTlsContext.CombinedCertificateValidationContext.newBuilder() + .setDefaultValidationContext( + CertificateValidationContext.newBuilder() + .setSystemRootCerts( + CertificateValidationContext.SystemRootCerts.newBuilder().build()) + .build() + ) + .build()) + .build(); + XdsClusterResource + .validateCommonTlsContext(commonTlsContext, ImmutableSet.of(), false); + } + + @Test + public void validateCommonTlsContext_validationContextSystemRootCerts_envVarNotSet_throws() { + XdsClusterResource.enableSystemRootCerts = false; + CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() + .setValidationContext( + CertificateValidationContext.newBuilder() + .setSystemRootCerts( + CertificateValidationContext.SystemRootCerts.newBuilder().build()) + .build()) + .build(); + try { + XdsClusterResource + .validateCommonTlsContext(commonTlsContext, ImmutableSet.of(), false); + fail("Expected exception"); + } catch (ResourceInvalidException ex) { + assertThat(ex.getMessage()).isEqualTo( + "ca_certificate_provider_instance or system_root_certs is required in " + + "upstream-tls-context"); + } + } + + @Test + public void validateCommonTlsContext_validationContextSystemRootCerts() + throws ResourceInvalidException { + XdsClusterResource.enableSystemRootCerts = true; + CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() + .setValidationContext( + CertificateValidationContext.newBuilder() + .setSystemRootCerts( + CertificateValidationContext.SystemRootCerts.newBuilder().build()) + .build()) + .build(); + XdsClusterResource + .validateCommonTlsContext(commonTlsContext, ImmutableSet.of(), false); + } + @Test @SuppressWarnings("deprecation") public void validateCommonTlsContext_validationContextProviderInstance_absentInBootstrapFile() @@ -2674,7 +2759,8 @@ public void validateCommonTlsContext_combinedValidationContext_isRequiredForClie CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .build(); thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("ca_certificate_provider_instance is required in upstream-tls-context"); + thrown.expectMessage("ca_certificate_provider_instance or system_root_certs is required " + + "in upstream-tls-context"); XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); } @@ -2687,7 +2773,8 @@ public void validateCommonTlsContext_combinedValidationContextWithoutCertProvide .build(); thrown.expect(ResourceInvalidException.class); thrown.expectMessage( - "ca_certificate_provider_instance is required in upstream-tls-context"); + "ca_certificate_provider_instance or system_root_certs is required in " + + "upstream-tls-context"); XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index a9fda599183..f516dc87e98 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -2217,7 +2217,8 @@ public void cdsResponseErrorHandling_badUpstreamTlsContext() { String errorMsg = "CDS response Cluster 'cluster.googleapis.com' validation error: " + "Cluster cluster.googleapis.com: malformed UpstreamTlsContext: " + "io.grpc.xds.client.XdsResourceType$ResourceInvalidException: " - + "ca_certificate_provider_instance is required in upstream-tls-context"; + + "ca_certificate_provider_instance or system_root_certs is required in " + + "upstream-tls-context"; call.verifyRequestNack(CDS, CDS_RESOURCE, "", "0000", NODE, ImmutableList.of(errorMsg)); verify(cdsResourceWatcher).onError(errorCaptor.capture()); verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); diff --git a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java index c6b8e7515b2..f56a7c367bb 100644 --- a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java @@ -31,6 +31,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.SettableFuture; +import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.grpc.Attributes; import io.grpc.EquivalentAddressGroup; import io.grpc.Grpc; @@ -67,10 +68,21 @@ import io.grpc.xds.internal.security.SslContextProviderSupplier; import io.grpc.xds.internal.security.TlsContextManagerImpl; import io.netty.handler.ssl.NotSslRecordException; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.net.Inet4Address; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -104,7 +116,7 @@ public class XdsSecurityClientServerTest { private static final String OVERRIDE_AUTHORITY = "foo.test.google.fr"; @After - public void tearDown() { + public void tearDown() throws IOException { if (fakeNameResolverFactory != null) { NameResolverRegistry.getDefaultRegistry().deregister(fakeNameResolverFactory); } @@ -147,6 +159,99 @@ public void tlsClientServer_noClientAuthentication() throws Exception { assertThat(unaryRpc(/* requestMessage= */ "buddy", blockingStub)).isEqualTo("Hello buddy"); } + /** + * Use system root ca cert for TLS channel - no mTLS. + * Uses common_tls_context.combined_validation_context in upstream_tls_context. + */ + @Test + public void tlsClientServer_useSystemRootCerts_useCombinedValidationContext() throws Exception { + Path trustStoreFilePath = getCacertFilePathForTestCa(); + try { + setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); + DownstreamTlsContext downstreamTlsContext = + setBootstrapInfoAndBuildDownstreamTlsContext(null, null, null, null, false, false); + buildServerWithTlsContext(downstreamTlsContext); + + UpstreamTlsContext upstreamTlsContext = + setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, + CLIENT_PEM_FILE, true); + + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = + getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); + assertThat(unaryRpc(/* requestMessage= */ "buddy", blockingStub)).isEqualTo("Hello buddy"); + } finally { + Files.deleteIfExists(trustStoreFilePath); + clearTrustStoreSystemProperties(); + } + } + + /** + * Use system root ca cert for TLS channel - no mTLS. + * Uses common_tls_context.validation_context in upstream_tls_context. + */ + @Test + public void tlsClientServer_useSystemRootCerts_validationContext() throws Exception { + Path trustStoreFilePath = getCacertFilePathForTestCa().toAbsolutePath(); + try { + setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); + DownstreamTlsContext downstreamTlsContext = + setBootstrapInfoAndBuildDownstreamTlsContext(null, null, null, null, false, false); + buildServerWithTlsContext(downstreamTlsContext); + + UpstreamTlsContext upstreamTlsContext = + setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, + CLIENT_PEM_FILE, false); + + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = + getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); + assertThat(unaryRpc(/* requestMessage= */ "buddy", blockingStub)).isEqualTo("Hello buddy"); + } finally { + Files.deleteIfExists(trustStoreFilePath.toAbsolutePath()); + clearTrustStoreSystemProperties(); + } + } + + /** + * Use system root ca cert for TLS channel - mTLS. + * Uses common_tls_context.combined_validation_context in upstream_tls_context. + */ + @Test + public void tlsClientServer_useSystemRootCerts_requireClientAuth() throws Exception { + Path trustStoreFilePath = getCacertFilePathForTestCa().toAbsolutePath(); + try { + setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); + DownstreamTlsContext downstreamTlsContext = + setBootstrapInfoAndBuildDownstreamTlsContext(null, null, null, null, false, false); + buildServerWithTlsContext(downstreamTlsContext); + + UpstreamTlsContext upstreamTlsContext = + setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, + CLIENT_PEM_FILE, true); + + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = + getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); + assertThat(unaryRpc(/* requestMessage= */ "buddy", blockingStub)).isEqualTo("Hello buddy"); + } finally { + Files.deleteIfExists(trustStoreFilePath.toAbsolutePath()); + clearTrustStoreSystemProperties(); + } + } + + private Path getCacertFilePathForTestCa() + throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(null, null); + InputStream caCertStream = getClass().getResource("/certs/ca.pem").openStream(); + keystore.setCertificateEntry("testca", CertificateFactory.getInstance("X.509") + .generateCertificate(caCertStream)); + caCertStream.close(); + File trustStoreFile = File.createTempFile("testca-truststore", "jks"); + FileOutputStream out = new FileOutputStream(trustStoreFile); + keystore.store(out, "changeit".toCharArray()); + out.close(); + return trustStoreFile.toPath(); + } + @Test public void requireClientAuth_noClientCert_expectException() throws Exception { @@ -323,6 +428,30 @@ private UpstreamTlsContext setBootstrapInfoAndBuildUpstreamTlsContext(String cli .buildUpstreamTlsContext("google_cloud_private_spiffe-client", hasIdentityCert); } + private UpstreamTlsContext setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts( + String clientKeyFile, + String clientPemFile, + boolean useCombinedValidationContext) { + bootstrapInfoForClient = CommonBootstrapperTestUtils + .buildBootstrapInfo("google_cloud_private_spiffe-client", clientKeyFile, clientPemFile, + CA_PEM_FILE, null, null, null, null); + if (useCombinedValidationContext) { + return CommonTlsContextTestsUtil.buildUpstreamTlsContextForCertProviderInstance( + "google_cloud_private_spiffe-client", "ROOT", null, + null, null, + CertificateValidationContext.newBuilder() + .setSystemRootCerts( + CertificateValidationContext.SystemRootCerts.newBuilder().build()) + .build()); + } + return CommonTlsContextTestsUtil.buildNewUpstreamTlsContextForCertProviderInstance( + "google_cloud_private_spiffe-client", "ROOT", null, + null, null, CertificateValidationContext.newBuilder() + .setSystemRootCerts( + CertificateValidationContext.SystemRootCerts.newBuilder().build()) + .build()); + } + private void buildServerWithTlsContext(DownstreamTlsContext downstreamTlsContext) throws Exception { buildServerWithTlsContext(downstreamTlsContext, InsecureServerCredentials.create()); @@ -450,6 +579,18 @@ public void run() { return settableFuture; } + private void setTrustStoreSystemProperties(String trustStoreFilePath) { + System.setProperty("javax.net.ssl.trustStore", trustStoreFilePath); + System.setProperty("javax.net.ssl.trustStorePassword", "changeit"); + System.setProperty("javax.net.ssl.trustStoreType", "JKS"); + } + + private void clearTrustStoreSystemProperties() { + System.clearProperty("javax.net.ssl.trustStore"); + System.clearProperty("javax.net.ssl.trustStorePassword"); + System.clearProperty("javax.net.ssl.trustStoreType"); + } + private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase { @Override diff --git a/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java index 4de881c710e..23e30883307 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java @@ -38,8 +38,6 @@ import io.grpc.xds.internal.security.certprovider.CertificateProviderRegistry; import io.grpc.xds.internal.security.certprovider.CertificateProviderStore; import io.grpc.xds.internal.security.certprovider.TestCertificateProvider; -import java.io.IOException; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -285,22 +283,6 @@ public void createNewCertProviderClientSslContextProvider_onlyRootCert() { verifyWatcher(sslContextProvider, watcherCaptor[0]); } - @Test - public void createNullCommonTlsContext_exception() throws IOException { - clientSslContextProviderFactory = - new ClientSslContextProviderFactory( - null, certProviderClientSslContextProviderFactory); - UpstreamTlsContext upstreamTlsContext = new UpstreamTlsContext(null); - try { - clientSslContextProviderFactory.create(upstreamTlsContext); - Assert.fail("no exception thrown"); - } catch (NullPointerException expected) { - assertThat(expected) - .hasMessageThat() - .isEqualTo("upstreamTlsContext should have CommonTlsContext"); - } - } - static void createAndRegisterProviderProvider( CertificateProviderRegistry certificateProviderRegistry, final CertificateProvider.DistributorWatcher[] watcherCaptor, diff --git a/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java b/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java index 8a04a3d02a7..db82a8682e0 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java @@ -250,22 +250,28 @@ private static CommonTlsContext.Builder addCertificateValidationContext( String rootInstanceName, String rootCertName, CertificateValidationContext staticCertValidationContext) { + CertificateProviderInstance providerInstance = null; if (rootInstanceName != null) { - CertificateProviderInstance providerInstance = - CertificateProviderInstance.newBuilder() - .setInstanceName(rootInstanceName) - .setCertificateName(rootCertName) - .build(); - if (staticCertValidationContext != null) { - CombinedCertificateValidationContext combined = - CombinedCertificateValidationContext.newBuilder() - .setDefaultValidationContext(staticCertValidationContext) - .setValidationContextCertificateProviderInstance(providerInstance) - .build(); - return builder.setCombinedValidationContext(combined); - } + providerInstance = CertificateProviderInstance.newBuilder() + .setInstanceName(rootInstanceName) + .setCertificateName(rootCertName) + .build(); + } + if (providerInstance != null) { builder = builder.setValidationContextCertificateProviderInstance(providerInstance); } + CombinedCertificateValidationContext.Builder combined = + CombinedCertificateValidationContext.newBuilder(); + if (providerInstance != null) { + combined = combined.setValidationContextCertificateProviderInstance(providerInstance); + } + if (staticCertValidationContext != null) { + combined = combined.setDefaultValidationContext(staticCertValidationContext); + } + if (combined.hasValidationContextCertificateProviderInstance() + || combined.hasDefaultValidationContext()) { + builder = builder.setCombinedValidationContext(combined.build()); + } return builder; } @@ -274,19 +280,19 @@ private static CommonTlsContext.Builder addNewCertificateValidationContext( String rootInstanceName, String rootCertName, CertificateValidationContext staticCertValidationContext) { + CertificateValidationContext.Builder validationContextBuilder = + staticCertValidationContext != null ? staticCertValidationContext.toBuilder() + : CertificateValidationContext.newBuilder(); if (rootInstanceName != null) { CertificateProviderPluginInstance providerInstance = CertificateProviderPluginInstance.newBuilder() .setInstanceName(rootInstanceName) .setCertificateName(rootCertName) .build(); - CertificateValidationContext.Builder validationContextBuilder = - staticCertValidationContext != null ? staticCertValidationContext.toBuilder() - : CertificateValidationContext.newBuilder(); - return builder.setValidationContext( - validationContextBuilder.setCaCertificateProviderInstance(providerInstance)); + validationContextBuilder = validationContextBuilder.setCaCertificateProviderInstance( + providerInstance); } - return builder; + return builder.setValidationContext(validationContextBuilder); } /** Helper method to build UpstreamTlsContext for CertProvider tests. */ diff --git a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java index 5925c5f03b1..7c300c88297 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java @@ -338,7 +338,8 @@ public void testProviderForClient_sslContextException_onError() throws Exception } @Test - public void testProviderForClient_rootInstanceNull_expectError() throws Exception { + public void testProviderForClient_rootInstanceNull_and_notUsingSystemRootCerts_expectError() + throws Exception { final CertificateProvider.DistributorWatcher[] watcherCaptor = new CertificateProvider.DistributorWatcher[1]; TestCertificateProvider.createAndRegisterProviderProvider( @@ -351,11 +352,30 @@ public void testProviderForClient_rootInstanceNull_expectError() throws Exceptio /* alpnProtocols= */ null, /* staticCertValidationContext= */ null); fail("exception expected"); - } catch (NullPointerException expected) { - assertThat(expected).hasMessageThat().contains("Client SSL requires rootCertInstance"); + } catch (UnsupportedOperationException expected) { + assertThat(expected).hasMessageThat().contains("Unsupported configurations in " + + "UpstreamTlsContext!"); } } + @Test + public void testProviderForClient_rootInstanceNull_but_isUsingSystemRootCerts_valid() + throws Exception { + final CertificateProvider.DistributorWatcher[] watcherCaptor = + new CertificateProvider.DistributorWatcher[1]; + TestCertificateProvider.createAndRegisterProviderProvider( + certificateProviderRegistry, watcherCaptor, "testca", 0); + getSslContextProvider( + /* certInstanceName= */ null, + /* rootInstanceName= */ null, + CommonBootstrapperTestUtils.getTestBootstrapInfo(), + /* alpnProtocols= */ null, + CertificateValidationContext.newBuilder() + .setSystemRootCerts( + CertificateValidationContext.SystemRootCerts.newBuilder().build()) + .build()); + } + static class QueuedExecutor implements Executor { /** A list of Runnables to be run in order. */ @VisibleForTesting final Queue runQueue = new ConcurrentLinkedQueue<>(); From fe350cfd506b426107f413818974cb1d1f23d13c Mon Sep 17 00:00:00 2001 From: John Cormie Date: Fri, 25 Oct 2024 14:41:03 -0700 Subject: [PATCH 063/591] Update error codes doc for new "Safer Intent" rules. (#11639) --- .../android-binderchannel-status-codes.md | 72 ++++++++++--------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/documentation/android-binderchannel-status-codes.md b/documentation/android-binderchannel-status-codes.md index 28bdd8907c1..fae4ef406af 100644 --- a/documentation/android-binderchannel-status-codes.md +++ b/documentation/android-binderchannel-status-codes.md @@ -27,9 +27,9 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ Server app not visible. - bindService() returns false + bindService() returns false -

UNIMPLEMENTED

“The operation is not implemented or is not supported / enabled in this service.” +

UNIMPLEMENTED

“The operation is not implemented or is not supported / enabled in this service.” Give up - This is an error in the client manifest. @@ -37,7 +37,8 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ 1 - Server app not installed + + Safer Intents violation. Direct the user to install/reinstall the server app. @@ -45,37 +46,43 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ 2 - Old version of the server app doesn’t declare the target android.app.Service in its manifest. + Server app not installed 3 - Target android.app.Service is disabled + Old version of the server app doesn’t declare the target android.app.Service in its manifest. 4 - The whole server app is disabled + Target android.app.Service is disabled 5 - Server app predates the Android M permissions model and the user must review and approve some newly requested permissions before it can run. + The whole server app is disabled 6 + Server app predates the Android M permissions model and the user must review and approve some newly requested permissions before it can run. + + + + 7 + Target android.app.Service doesn’t recognize grpc binding Intent (old version of server app?) onNullBinding() ServiceConnection callback - 7 + 8 Method not found on the io.grpc.Server (old version of server app?) @@ -83,13 +90,13 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ - 8 + 9 Request cardinality violation (old version of server app expects unary rather than streaming, say) - 9 + 10 Old version of the server app exposes target android.app.Service but doesn’t android:export it. @@ -102,7 +109,7 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ - 10 + 11 Target android.app.Service requires an <android:permission> that client doesn’t hold. @@ -110,7 +117,7 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ - 11 + 12 Violations of the security policy for miscellaneous Android features like android:isolatedProcess, android:externalService, android:singleUser, instant apps, BIND_TREAT_LIKE_ACTIVITY, etc, @@ -118,7 +125,7 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ - 12 + 13 Calling Android UID not allowed by ServerSecurityPolicy @@ -126,13 +133,13 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ - 13 + 14 Server Android UID not allowed by client’s SecurityPolicy - 14 + 15 Server process crashed or killed with request in flight. @@ -154,7 +161,7 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ - 15 + 16 Server app is currently being upgraded to a new version @@ -162,13 +169,13 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ - 16 + 17 The whole server app or the target android.app.Service was disabled - 17 + 18 Binder transaction buffer overflow @@ -176,7 +183,7 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ - 18 + 19 Source Context for bindService() is destroyed with a request in flight @@ -188,11 +195,11 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ Give up for now.

-(Re. 18: The caller can try again later when the user opens the source Activity or restarts the source Service) +(Re. 19: The caller can try again later when the user opens the source Activity or restarts the source Service) - 19 + 20 Client application cancelled the request @@ -200,7 +207,7 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ - 19 + 21 Bug in Android itself or the way the io.grpc.binder transport uses it. @@ -218,7 +225,7 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ - 20 + 22 Flow-control protocol violation @@ -226,7 +233,7 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ - 21 + 23 Can’t parse request/response proto @@ -236,27 +243,27 @@ Consider the table that follows as an BinderChannel-specific addendum to the “ ### Ambiguity -We say a status code is ambiguous if it maps to two error cases that reasonable clients want to handle differently. For instance, a client may have good reasons to handle error cases 9 and 10 above differently. But they can’t do so based on status code alone because those error cases map to the same one. +We say a status code is ambiguous if it maps to two error cases that reasonable clients want to handle differently. For instance, a client may have good reasons to handle error cases 10 and 11 above differently. But they can’t do so based on status code alone because those error cases map to the same one. -In contrast, for example, even though error case 18 and 19 both map to the status code (`CANCELLED`), they are not ambiguous because we see no reason that clients would want to distinguish them. In both cases, clients will simply give up on the request. +In contrast, for example, even though error case 19 and 20 both map to the status code (`CANCELLED`), they are not ambiguous because we see no reason that clients would want to distinguish them. In both cases, clients will simply give up on the request. #### Ambiguity of PERMISSION_DENIED and Mitigations The mapping above has only one apparently ambiguous status code: `PERMISSION_DENIED`. However, this isn’t so bad because of the following: -The use of ``s for inter-app IPC access control (error case 10) is uncommon. Instead, we recommend that server apps only allow IPC from a limited set of client apps known in advance and identified by signature. +The use of ``s for inter-app IPC access control (error case 11) is uncommon. Instead, we recommend that server apps only allow IPC from a limited set of client apps known in advance and identified by signature. -However, there may be gRPC server apps that want to use custom <android:permission>’s to let the end user decide which arbitrary other apps can make use of its gRPC services. In that case, clients should preempt error case 10 simply by [checking whether they hold the required permissions](https://developer.android.com/training/permissions/requesting) before sending a request. +However, there may be gRPC server apps that want to use custom <android:permission>’s to let the end user decide which arbitrary other apps can make use of its gRPC services. In that case, clients should preempt error case 11 simply by [checking whether they hold the required permissions](https://developer.android.com/training/permissions/requesting) before sending a request. -Server apps can avoid error case 9 by never reusing an android.app.Service as a gRPC host if it has ever been android:exported=false in some previous app version. Instead they should simply create a new android.app.Service for this purpose. +Server apps can avoid error case 10 by never reusing an android.app.Service as a gRPC host if it has ever been android:exported=false in some previous app version. Instead they should simply create a new android.app.Service for this purpose. -Only error cases 11 - 13 remain, making `PERMISSION_DENIED` unambiguous for the purpose of error handling. Reasonable client apps can handle it in a generic way by displaying an error message and/or proceeding with degraded functionality. +Only error cases 12 - 14 remain, making `PERMISSION_DENIED` unambiguous for the purpose of error handling. Reasonable client apps can handle it in a generic way by displaying an error message and/or proceeding with degraded functionality. #### Non-Ambiguity of UNIMPLEMENTED -The `UNIMPLEMENTED` status code corresponds to quite a few different problems with the server app: It’s either not installed, too old, or disabled in whole or in part. Despite the diversity of underlying error cases, we believe most client apps will and should handle `UNIMPLEMENTED` in the same way: by sending the user to the app store to (re)install the server app. Reinstalling might be overkill for the disabled cases but most end users don't know what it means to enable/disable an app and there’s neither enough space in a UI dialog nor enough reader attention to explain it. Reinstalling is something users likely already understand and very likely to cure problems 1-8. +The `UNIMPLEMENTED` status code corresponds to quite a few different problems with the server app: It’s either not installed, too old, misconfigured, or disabled in whole or in part. Despite the diversity of underlying error cases, we believe most client apps will and should handle `UNIMPLEMENTED` in the same way: by sending the user to the app store to (re)install the server app. Reinstalling might be overkill for the disabled cases but most end users don't know what it means to enable/disable an app and there’s neither enough space in a UI dialog nor enough reader attention to explain it. Reinstalling is something users likely already understand and likely to cure problems 0-9 (once a fixed version of the server is available). ## Detailed Discussion of Binder Failure Modes @@ -326,6 +333,7 @@ According to a review of the AOSP source code, there are in fact several cases: 2. The target package is installed but does not declare the target Service in its manifest. 3. The target package requests dangerous permissions but targets sdk <= M and therefore requires a permissions review, but the caller is not running in the foreground and so it would be inappropriate to launch the review UI. 4. The target package is not visible to the client due to [Android 11 package visibility rules](https://developer.android.com/training/package-visibility). +5. One of the new [Safer Intents](https://developer.android.com/about/versions/15/behavior-changes-15#safer-intents) rules is violated. Most commonly, the bind `Intent` specifies a `ComponentName` explicitly but doesn't match any of its <intent-filter>s. Status code mapping: **UNIMPLEMENTED** @@ -333,7 +341,7 @@ Status code mapping: **UNIMPLEMENTED** Unfortunately `UNIMPLEMENTED` doesn’t capture (3) but none of the other canonical status codes do either and we expect this case to be extremely rare. -(4) is intentially indistinguishable from (1) by Android design so we can't handle it differently. However, as a client manifest error, it's not something reasonable apps would handle at runtime anyway. +(4) and (5) are intentially indistinguishable from (1) by Android design so we can't handle them differently. However, as an error in its own manifest, (4) isn't something a reasonable client would handle at runtime anyway. (5) is an error in the server manifest and so, just like the other cases, the best practice for handling it is to send the user to the app store in the hope that the server can be updated with a fix. ### bindService() throws SecurityException From 735b3f3fe6c6b9d8fc6019e8c89f81c065d9b0d6 Mon Sep 17 00:00:00 2001 From: Ran Date: Mon, 28 Oct 2024 10:25:17 -0700 Subject: [PATCH 064/591] netty: add soft Metadata size limit enforcement. (#11603) --- .../io/grpc/netty/AbstractNettyHandler.java | 9 +- .../io/grpc/netty/NettyChannelBuilder.java | 130 +++++-- .../io/grpc/netty/NettyClientHandler.java | 48 ++- .../io/grpc/netty/NettyClientTransport.java | 58 ++-- .../main/java/io/grpc/netty/NettyServer.java | 58 ++-- .../io/grpc/netty/NettyServerBuilder.java | 69 +++- .../io/grpc/netty/NettyServerHandler.java | 37 +- .../io/grpc/netty/NettyServerTransport.java | 4 + netty/src/main/java/io/grpc/netty/Utils.java | 60 +++- .../io/grpc/netty/NettyClientHandlerTest.java | 33 ++ .../grpc/netty/NettyClientTransportTest.java | 137 ++++++-- .../io/grpc/netty/NettyServerBuilderTest.java | 16 + .../io/grpc/netty/NettyServerHandlerTest.java | 2 + .../java/io/grpc/netty/NettyServerTest.java | 325 ++++++++++-------- 14 files changed, 722 insertions(+), 264 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/AbstractNettyHandler.java b/netty/src/main/java/io/grpc/netty/AbstractNettyHandler.java index 7f088509c04..6743b4fd5f7 100644 --- a/netty/src/main/java/io/grpc/netty/AbstractNettyHandler.java +++ b/netty/src/main/java/io/grpc/netty/AbstractNettyHandler.java @@ -42,7 +42,8 @@ abstract class AbstractNettyHandler extends GrpcHttp2ConnectionHandler { private final int initialConnectionWindow; private final FlowControlPinger flowControlPing; - + protected final int maxHeaderListSize; + protected final int softLimitHeaderListSize; private boolean autoTuneFlowControlOn; private ChannelHandlerContext ctx; private boolean initialWindowSent = false; @@ -58,7 +59,9 @@ abstract class AbstractNettyHandler extends GrpcHttp2ConnectionHandler { ChannelLogger negotiationLogger, boolean autoFlowControl, PingLimiter pingLimiter, - Ticker ticker) { + Ticker ticker, + int maxHeaderListSize, + int softLimitHeaderListSize) { super(channelUnused, decoder, encoder, initialSettings, negotiationLogger); // During a graceful shutdown, wait until all streams are closed. @@ -73,6 +76,8 @@ abstract class AbstractNettyHandler extends GrpcHttp2ConnectionHandler { } this.flowControlPing = new FlowControlPinger(pingLimiter); this.ticker = checkNotNull(ticker, "ticker"); + this.maxHeaderListSize = maxHeaderListSize; + this.softLimitHeaderListSize = softLimitHeaderListSize; } @Override diff --git a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java index d1d9810b485..fe226ec2ba9 100644 --- a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java +++ b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java @@ -104,6 +104,7 @@ public final class NettyChannelBuilder extends ForwardingChannelBuilder2 0, "maxInboundMetadataSize must be > 0"); this.maxHeaderListSize = bytes; + // Clear the soft limit setting, by setting soft limit to maxInboundMetadataSize. The + // maxInboundMetadataSize will take precedence be applied before soft limit check. + this.softLimitHeaderListSize = bytes; + return this; + } + + /** + * Sets the size of metadata that clients are advised to not exceed. When a metadata with size + * larger than the soft limit is encountered there will be a probability the RPC will fail. The + * chance of failing increases as the metadata size approaches the hard limit. + * {@code Integer.MAX_VALUE} disables the enforcement. The default is implementation-dependent, + * but is not generally less than 8 KiB and may be unlimited. + * + *

This is cumulative size of the metadata. The precise calculation is + * implementation-dependent, but implementations are encouraged to follow the calculation used + * for + * HTTP/2's + * SETTINGS_MAX_HEADER_LIST_SIZE. It sums the bytes from each entry's key and value, plus 32 + * bytes of overhead per entry. + * + * @param soft the soft size limit of received metadata + * @param max the hard size limit of received metadata + * @return this + * @throws IllegalArgumentException if soft and/or max is non-positive, or max smaller than + * soft + * @since 1.68.0 + */ + @CanIgnoreReturnValue + public NettyChannelBuilder maxInboundMetadataSize(int soft, int max) { + checkArgument(soft > 0, "softLimitHeaderListSize must be > 0"); + checkArgument(max > soft, + "maxInboundMetadataSize must be greater than softLimitHeaderListSize"); + this.softLimitHeaderListSize = soft; + this.maxHeaderListSize = max; return this; } @@ -573,10 +608,22 @@ ClientTransportFactory buildTransportFactory() { ProtocolNegotiator negotiator = protocolNegotiatorFactory.newNegotiator(); return new NettyTransportFactory( - negotiator, channelFactory, channelOptions, - eventLoopGroupPool, autoFlowControl, flowControlWindow, maxInboundMessageSize, - maxHeaderListSize, keepAliveTimeNanos, keepAliveTimeoutNanos, keepAliveWithoutCalls, - transportTracerFactory, localSocketPicker, useGetForSafeMethods, transportSocketType); + negotiator, + channelFactory, + channelOptions, + eventLoopGroupPool, + autoFlowControl, + flowControlWindow, + maxInboundMessageSize, + maxHeaderListSize, + softLimitHeaderListSize, + keepAliveTimeNanos, + keepAliveTimeoutNanos, + keepAliveWithoutCalls, + transportTracerFactory, + localSocketPicker, + useGetForSafeMethods, + transportSocketType); } @VisibleForTesting @@ -710,6 +757,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto private final int flowControlWindow; private final int maxMessageSize; private final int maxHeaderListSize; + private final int softLimitHeaderListSize; private final long keepAliveTimeNanos; private final AtomicBackoff keepAliveBackoff; private final long keepAliveTimeoutNanos; @@ -724,11 +772,20 @@ private static final class NettyTransportFactory implements ClientTransportFacto NettyTransportFactory( ProtocolNegotiator protocolNegotiator, ChannelFactory channelFactory, - Map, ?> channelOptions, ObjectPool groupPool, - boolean autoFlowControl, int flowControlWindow, int maxMessageSize, int maxHeaderListSize, - long keepAliveTimeNanos, long keepAliveTimeoutNanos, boolean keepAliveWithoutCalls, - TransportTracer.Factory transportTracerFactory, LocalSocketPicker localSocketPicker, - boolean useGetForSafeMethods, Class transportSocketType) { + Map, ?> channelOptions, + ObjectPool groupPool, + boolean autoFlowControl, + int flowControlWindow, + int maxMessageSize, + int maxHeaderListSize, + int softLimitHeaderListSize, + long keepAliveTimeNanos, + long keepAliveTimeoutNanos, + boolean keepAliveWithoutCalls, + TransportTracer.Factory transportTracerFactory, + LocalSocketPicker localSocketPicker, + boolean useGetForSafeMethods, + Class transportSocketType) { this.protocolNegotiator = checkNotNull(protocolNegotiator, "protocolNegotiator"); this.channelFactory = channelFactory; this.channelOptions = new HashMap, Object>(channelOptions); @@ -738,6 +795,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto this.flowControlWindow = flowControlWindow; this.maxMessageSize = maxMessageSize; this.maxHeaderListSize = maxHeaderListSize; + this.softLimitHeaderListSize = softLimitHeaderListSize; this.keepAliveTimeNanos = keepAliveTimeNanos; this.keepAliveBackoff = new AtomicBackoff("keepalive time nanos", keepAliveTimeNanos); this.keepAliveTimeoutNanos = keepAliveTimeoutNanos; @@ -774,13 +832,30 @@ public void run() { }; // TODO(carl-mastrangelo): Pass channelLogger in. - NettyClientTransport transport = new NettyClientTransport( - serverAddress, channelFactory, channelOptions, group, - localNegotiator, autoFlowControl, flowControlWindow, - maxMessageSize, maxHeaderListSize, keepAliveTimeNanosState.get(), keepAliveTimeoutNanos, - keepAliveWithoutCalls, options.getAuthority(), options.getUserAgent(), - tooManyPingsRunnable, transportTracerFactory.create(), options.getEagAttributes(), - localSocketPicker, channelLogger, useGetForSafeMethods, Ticker.systemTicker()); + NettyClientTransport transport = + new NettyClientTransport( + serverAddress, + channelFactory, + channelOptions, + group, + localNegotiator, + autoFlowControl, + flowControlWindow, + maxMessageSize, + maxHeaderListSize, + softLimitHeaderListSize, + keepAliveTimeNanosState.get(), + keepAliveTimeoutNanos, + keepAliveWithoutCalls, + options.getAuthority(), + options.getUserAgent(), + tooManyPingsRunnable, + transportTracerFactory.create(), + options.getEagAttributes(), + localSocketPicker, + channelLogger, + useGetForSafeMethods, + Ticker.systemTicker()); return transport; } @@ -796,11 +871,24 @@ public SwapChannelCredentialsResult swapChannelCredentials(ChannelCredentials ch if (result.error != null) { return null; } - ClientTransportFactory factory = new NettyTransportFactory( - result.negotiator.newNegotiator(), channelFactory, channelOptions, groupPool, - autoFlowControl, flowControlWindow, maxMessageSize, maxHeaderListSize, keepAliveTimeNanos, - keepAliveTimeoutNanos, keepAliveWithoutCalls, transportTracerFactory, localSocketPicker, - useGetForSafeMethods, transportSocketType); + ClientTransportFactory factory = + new NettyTransportFactory( + result.negotiator.newNegotiator(), + channelFactory, + channelOptions, + groupPool, + autoFlowControl, + flowControlWindow, + maxMessageSize, + maxHeaderListSize, + softLimitHeaderListSize, + keepAliveTimeNanos, + keepAliveTimeoutNanos, + keepAliveWithoutCalls, + transportTracerFactory, + localSocketPicker, + useGetForSafeMethods, + transportSocketType); return new SwapChannelCredentialsResult(factory, result.callCredentials); } diff --git a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java index eb4dbf8cc66..194decb1120 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java @@ -142,6 +142,7 @@ static NettyClientHandler newHandler( boolean autoFlowControl, int flowControlWindow, int maxHeaderListSize, + int softLimitHeaderListSize, Supplier stopwatchFactory, Runnable tooManyPingsRunnable, TransportTracer transportTracer, @@ -171,6 +172,7 @@ static NettyClientHandler newHandler( autoFlowControl, flowControlWindow, maxHeaderListSize, + softLimitHeaderListSize, stopwatchFactory, tooManyPingsRunnable, transportTracer, @@ -190,6 +192,7 @@ static NettyClientHandler newHandler( boolean autoFlowControl, int flowControlWindow, int maxHeaderListSize, + int softLimitHeaderListSize, Supplier stopwatchFactory, Runnable tooManyPingsRunnable, TransportTracer transportTracer, @@ -202,6 +205,8 @@ static NettyClientHandler newHandler( Preconditions.checkNotNull(lifecycleManager, "lifecycleManager"); Preconditions.checkArgument(flowControlWindow > 0, "flowControlWindow must be positive"); Preconditions.checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be positive"); + Preconditions.checkArgument(softLimitHeaderListSize > 0, + "softLimitHeaderListSize must be positive"); Preconditions.checkNotNull(stopwatchFactory, "stopwatchFactory"); Preconditions.checkNotNull(tooManyPingsRunnable, "tooManyPingsRunnable"); Preconditions.checkNotNull(eagAttributes, "eagAttributes"); @@ -247,7 +252,9 @@ static NettyClientHandler newHandler( authority, autoFlowControl, pingCounter, - ticker); + ticker, + maxHeaderListSize, + softLimitHeaderListSize); } private NettyClientHandler( @@ -264,9 +271,20 @@ private NettyClientHandler( String authority, boolean autoFlowControl, PingLimiter pingLimiter, - Ticker ticker) { - super(/* channelUnused= */ null, decoder, encoder, settings, - negotiationLogger, autoFlowControl, pingLimiter, ticker); + Ticker ticker, + int maxHeaderListSize, + int softLimitHeaderListSize) { + super( + /* channelUnused= */ null, + decoder, + encoder, + settings, + negotiationLogger, + autoFlowControl, + pingLimiter, + ticker, + maxHeaderListSize, + softLimitHeaderListSize); this.lifecycleManager = lifecycleManager; this.keepAliveManager = keepAliveManager; this.stopwatchFactory = stopwatchFactory; @@ -380,6 +398,28 @@ private void onHeadersRead(int streamId, Http2Headers headers, boolean endStream if (streamId != Http2CodecUtil.HTTP_UPGRADE_STREAM_ID) { NettyClientStream.TransportState stream = clientStream(requireHttp2Stream(streamId)); PerfMark.event("NettyClientHandler.onHeadersRead", stream.tag()); + // check metadata size vs soft limit + int h2HeadersSize = Utils.getH2HeadersSize(headers); + boolean shouldFail = + Utils.shouldRejectOnMetadataSizeSoftLimitExceeded( + h2HeadersSize, softLimitHeaderListSize, maxHeaderListSize); + if (shouldFail && endStream) { + stream.transportReportStatus(Status.RESOURCE_EXHAUSTED + .withDescription( + String.format( + "Server Status + Trailers of size %d exceeded Metadata size soft limit: %d", + h2HeadersSize, + softLimitHeaderListSize)), true, new Metadata()); + return; + } else if (shouldFail) { + stream.transportReportStatus(Status.RESOURCE_EXHAUSTED + .withDescription( + String.format( + "Server Headers of size %d exceeded Metadata size soft limit: %d", + h2HeadersSize, + softLimitHeaderListSize)), true, new Metadata()); + return; + } stream.transportHeadersReceived(headers, endStream); } diff --git a/netty/src/main/java/io/grpc/netty/NettyClientTransport.java b/netty/src/main/java/io/grpc/netty/NettyClientTransport.java index 426f3c0d583..54d1641a7ed 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientTransport.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientTransport.java @@ -83,6 +83,7 @@ class NettyClientTransport implements ConnectionClientTransport { private final int flowControlWindow; private final int maxMessageSize; private final int maxHeaderListSize; + private final int softLimitHeaderListSize; private KeepAliveManager keepAliveManager; private final long keepAliveTimeNanos; private final long keepAliveTimeoutNanos; @@ -106,15 +107,28 @@ class NettyClientTransport implements ConnectionClientTransport { private final Ticker ticker; NettyClientTransport( - SocketAddress address, ChannelFactory channelFactory, - Map, ?> channelOptions, EventLoopGroup group, - ProtocolNegotiator negotiator, boolean autoFlowControl, int flowControlWindow, - int maxMessageSize, int maxHeaderListSize, - long keepAliveTimeNanos, long keepAliveTimeoutNanos, - boolean keepAliveWithoutCalls, String authority, @Nullable String userAgent, - Runnable tooManyPingsRunnable, TransportTracer transportTracer, Attributes eagAttributes, - LocalSocketPicker localSocketPicker, ChannelLogger channelLogger, - boolean useGetForSafeMethods, Ticker ticker) { + SocketAddress address, + ChannelFactory channelFactory, + Map, ?> channelOptions, + EventLoopGroup group, + ProtocolNegotiator negotiator, + boolean autoFlowControl, + int flowControlWindow, + int maxMessageSize, + int maxHeaderListSize, + int softLimitHeaderListSize, + long keepAliveTimeNanos, + long keepAliveTimeoutNanos, + boolean keepAliveWithoutCalls, + String authority, + @Nullable String userAgent, + Runnable tooManyPingsRunnable, + TransportTracer transportTracer, + Attributes eagAttributes, + LocalSocketPicker localSocketPicker, + ChannelLogger channelLogger, + boolean useGetForSafeMethods, + Ticker ticker) { this.negotiator = Preconditions.checkNotNull(negotiator, "negotiator"); this.negotiationScheme = this.negotiator.scheme(); @@ -126,6 +140,7 @@ class NettyClientTransport implements ConnectionClientTransport { this.flowControlWindow = flowControlWindow; this.maxMessageSize = maxMessageSize; this.maxHeaderListSize = maxHeaderListSize; + this.softLimitHeaderListSize = softLimitHeaderListSize; this.keepAliveTimeNanos = keepAliveTimeNanos; this.keepAliveTimeoutNanos = keepAliveTimeoutNanos; this.keepAliveWithoutCalls = keepAliveWithoutCalls; @@ -220,18 +235,19 @@ public Runnable start(Listener transportListener) { } handler = NettyClientHandler.newHandler( - lifecycleManager, - keepAliveManager, - autoFlowControl, - flowControlWindow, - maxHeaderListSize, - GrpcUtil.STOPWATCH_SUPPLIER, - tooManyPingsRunnable, - transportTracer, - eagAttributes, - authorityString, - channelLogger, - ticker); + lifecycleManager, + keepAliveManager, + autoFlowControl, + flowControlWindow, + maxHeaderListSize, + softLimitHeaderListSize, + GrpcUtil.STOPWATCH_SUPPLIER, + tooManyPingsRunnable, + transportTracer, + eagAttributes, + authorityString, + channelLogger, + ticker); ChannelHandler negotiationHandler = negotiator.newHandler(handler); diff --git a/netty/src/main/java/io/grpc/netty/NettyServer.java b/netty/src/main/java/io/grpc/netty/NettyServer.java index 2960604e5b5..1cf67ea25ca 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServer.java +++ b/netty/src/main/java/io/grpc/netty/NettyServer.java @@ -92,6 +92,7 @@ class NettyServer implements InternalServer, InternalWithLogId { private final int flowControlWindow; private final int maxMessageSize; private final int maxHeaderListSize; + private final int softLimitHeaderListSize; private final long keepAliveTimeInNanos; private final long keepAliveTimeoutInNanos; private final long maxConnectionIdleInNanos; @@ -123,9 +124,14 @@ class NettyServer implements InternalServer, InternalWithLogId { ProtocolNegotiator protocolNegotiator, List streamTracerFactories, TransportTracer.Factory transportTracerFactory, - int maxStreamsPerConnection, boolean autoFlowControl, int flowControlWindow, - int maxMessageSize, int maxHeaderListSize, - long keepAliveTimeInNanos, long keepAliveTimeoutInNanos, + int maxStreamsPerConnection, + boolean autoFlowControl, + int flowControlWindow, + int maxMessageSize, + int maxHeaderListSize, + int softLimitHeaderListSize, + long keepAliveTimeInNanos, + long keepAliveTimeoutInNanos, long maxConnectionIdleInNanos, long maxConnectionAgeInNanos, long maxConnectionAgeGraceInNanos, boolean permitKeepAliveWithoutCalls, long permitKeepAliveTimeInNanos, @@ -152,6 +158,7 @@ class NettyServer implements InternalServer, InternalWithLogId { this.flowControlWindow = flowControlWindow; this.maxMessageSize = maxMessageSize; this.maxHeaderListSize = maxHeaderListSize; + this.softLimitHeaderListSize = softLimitHeaderListSize; this.keepAliveTimeInNanos = keepAliveTimeInNanos; this.keepAliveTimeoutInNanos = keepAliveTimeoutInNanos; this.maxConnectionIdleInNanos = maxConnectionIdleInNanos; @@ -243,28 +250,29 @@ public void initChannel(Channel ch) { (long) ((.9D + Math.random() * .2D) * maxConnectionAgeInNanos); } - NettyServerTransport transport = - new NettyServerTransport( - ch, - channelDone, - protocolNegotiator, - streamTracerFactories, - transportTracerFactory.create(), - maxStreamsPerConnection, - autoFlowControl, - flowControlWindow, - maxMessageSize, - maxHeaderListSize, - keepAliveTimeInNanos, - keepAliveTimeoutInNanos, - maxConnectionIdleInNanos, - maxConnectionAgeInNanos, - maxConnectionAgeGraceInNanos, - permitKeepAliveWithoutCalls, - permitKeepAliveTimeInNanos, - maxRstCount, - maxRstPeriodNanos, - eagAttributes); + NettyServerTransport transport = + new NettyServerTransport( + ch, + channelDone, + protocolNegotiator, + streamTracerFactories, + transportTracerFactory.create(), + maxStreamsPerConnection, + autoFlowControl, + flowControlWindow, + maxMessageSize, + maxHeaderListSize, + softLimitHeaderListSize, + keepAliveTimeInNanos, + keepAliveTimeoutInNanos, + maxConnectionIdleInNanos, + maxConnectionAgeInNanos, + maxConnectionAgeGraceInNanos, + permitKeepAliveWithoutCalls, + permitKeepAliveTimeInNanos, + maxRstCount, + maxRstPeriodNanos, + eagAttributes); ServerTransportListener transportListener; // This is to order callbacks on the listener, not to guard access to channel. synchronized (NettyServer.this) { diff --git a/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java b/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java index 3b82b193f61..eb3a6d9b538 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java @@ -105,6 +105,7 @@ public final class NettyServerBuilder extends ForwardingServerBuilder 0, "maxInboundMetadataSize must be positive: %s", bytes); this.maxHeaderListSize = bytes; + // Clear the soft limit setting, by setting soft limit to maxInboundMetadataSize. The + // maxInboundMetadataSize will take precedence over soft limit check. + this.softLimitHeaderListSize = bytes; + return this; + } + + /** + * Sets the size of metadata that clients are advised to not exceed. When a metadata with size + * larger than the soft limit is encountered there will be a probability the RPC will fail. The + * chance of failing increases as the metadata size approaches the hard limit. + * {@code Integer.MAX_VALUE} disables the enforcement. The default is implementation-dependent, + * but is not generally less than 8 KiB and may be unlimited. + * + *

This is cumulative size of the metadata. The precise calculation is + * implementation-dependent, but implementations are encouraged to follow the calculation used + * for + * HTTP/2's + * SETTINGS_MAX_HEADER_LIST_SIZE. It sums the bytes from each entry's key and value, plus 32 + * bytes of overhead per entry. + * + * @param soft the soft size limit of received metadata + * @param max the hard size limit of received metadata + * @return this + * @throws IllegalArgumentException if soft and/or max is non-positive, or max smaller than soft + * @since 1.68.0 + */ + @CanIgnoreReturnValue + public NettyServerBuilder maxInboundMetadataSize(int soft, int max) { + checkArgument(soft > 0, "softLimitHeaderListSize must be positive: %s", soft); + checkArgument(max > soft, + "maxInboundMetadataSize: %s must be greater than softLimitHeaderListSize: %s", max, soft); + this.softLimitHeaderListSize = soft; + this.maxHeaderListSize = max; return this; } @@ -677,14 +711,33 @@ NettyServer buildTransportServers( this.serverImplBuilder.getExecutorPool()); return new NettyServer( - listenAddresses, channelFactory, channelOptions, childChannelOptions, - bossEventLoopGroupPool, workerEventLoopGroupPool, forceHeapBuffer, negotiator, - streamTracerFactories, transportTracerFactory, maxConcurrentCallsPerConnection, - autoFlowControl, flowControlWindow, maxMessageSize, maxHeaderListSize, - keepAliveTimeInNanos, keepAliveTimeoutInNanos, - maxConnectionIdleInNanos, maxConnectionAgeInNanos, - maxConnectionAgeGraceInNanos, permitKeepAliveWithoutCalls, permitKeepAliveTimeInNanos, - maxRstCount, maxRstPeriodNanos, eagAttributes, this.serverImplBuilder.getChannelz()); + listenAddresses, + channelFactory, + channelOptions, + childChannelOptions, + bossEventLoopGroupPool, + workerEventLoopGroupPool, + forceHeapBuffer, + negotiator, + streamTracerFactories, + transportTracerFactory, + maxConcurrentCallsPerConnection, + autoFlowControl, + flowControlWindow, + maxMessageSize, + maxHeaderListSize, + softLimitHeaderListSize, + keepAliveTimeInNanos, + keepAliveTimeoutInNanos, + maxConnectionIdleInNanos, + maxConnectionAgeInNanos, + maxConnectionAgeGraceInNanos, + permitKeepAliveWithoutCalls, + permitKeepAliveTimeInNanos, + maxRstCount, + maxRstPeriodNanos, + eagAttributes, + this.serverImplBuilder.getChannelz()); } @VisibleForTesting diff --git a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java index a6e855a199d..85a4886b766 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java @@ -152,7 +152,6 @@ class NettyServerHandler extends AbstractNettyHandler { private int rstCount; private long lastRstNanoTime; - static NettyServerHandler newHandler( ServerTransportListener transportListener, ChannelPromise channelUnused, @@ -162,6 +161,7 @@ static NettyServerHandler newHandler( boolean autoFlowControl, int flowControlWindow, int maxHeaderListSize, + int softLimitHeaderListSize, int maxMessageSize, long keepAliveTimeInNanos, long keepAliveTimeoutInNanos, @@ -192,6 +192,7 @@ static NettyServerHandler newHandler( autoFlowControl, flowControlWindow, maxHeaderListSize, + softLimitHeaderListSize, maxMessageSize, keepAliveTimeInNanos, keepAliveTimeoutInNanos, @@ -217,6 +218,7 @@ static NettyServerHandler newHandler( boolean autoFlowControl, int flowControlWindow, int maxHeaderListSize, + int softLimitHeaderListSize, int maxMessageSize, long keepAliveTimeInNanos, long keepAliveTimeoutInNanos, @@ -234,6 +236,9 @@ static NettyServerHandler newHandler( flowControlWindow); Preconditions.checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be positive: %s", maxHeaderListSize); + Preconditions.checkArgument( + softLimitHeaderListSize > 0, "softLimitHeaderListSize must be positive: %s", + softLimitHeaderListSize); Preconditions.checkArgument(maxMessageSize > 0, "maxMessageSize must be positive: %s", maxMessageSize); @@ -273,7 +278,10 @@ static NettyServerHandler newHandler( transportTracer, decoder, encoder, settings, maxMessageSize, - keepAliveTimeInNanos, keepAliveTimeoutInNanos, + maxHeaderListSize, + softLimitHeaderListSize, + keepAliveTimeInNanos, + keepAliveTimeoutInNanos, maxConnectionIdleInNanos, maxConnectionAgeInNanos, maxConnectionAgeGraceInNanos, keepAliveEnforcer, @@ -293,6 +301,8 @@ private NettyServerHandler( Http2ConnectionEncoder encoder, Http2Settings settings, int maxMessageSize, + int maxHeaderListSize, + int softLimitHeaderListSize, long keepAliveTimeInNanos, long keepAliveTimeoutInNanos, long maxConnectionIdleInNanos, @@ -304,8 +314,17 @@ private NettyServerHandler( long maxRstPeriodNanos, Attributes eagAttributes, Ticker ticker) { - super(channelUnused, decoder, encoder, settings, new ServerChannelLogger(), - autoFlowControl, null, ticker); + super( + channelUnused, + decoder, + encoder, + settings, + new ServerChannelLogger(), + autoFlowControl, + null, + ticker, + maxHeaderListSize, + softLimitHeaderListSize); final MaxConnectionIdleManager maxConnectionIdleManager; if (maxConnectionIdleInNanos == MAX_CONNECTION_IDLE_NANOS_DISABLED) { @@ -470,6 +489,16 @@ private void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers return; } + int h2HeadersSize = Utils.getH2HeadersSize(headers); + if (Utils.shouldRejectOnMetadataSizeSoftLimitExceeded( + h2HeadersSize, softLimitHeaderListSize, maxHeaderListSize)) { + respondWithHttpError(ctx, streamId, 431, Status.Code.RESOURCE_EXHAUSTED, String.format( + "Client Headers of size %d exceeded Metadata size soft limit: %d", + h2HeadersSize, + softLimitHeaderListSize)); + return; + } + if (!teWarningLogged && !TE_TRAILERS.contentEquals(headers.get(TE_HEADER))) { logger.warning(String.format("Expected header TE: %s, but %s is received. This means " + "some intermediate proxy may not support trailers", diff --git a/netty/src/main/java/io/grpc/netty/NettyServerTransport.java b/netty/src/main/java/io/grpc/netty/NettyServerTransport.java index 9511927a09f..758ffeee5b1 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerTransport.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerTransport.java @@ -70,6 +70,7 @@ class NettyServerTransport implements ServerTransport { private final int flowControlWindow; private final int maxMessageSize; private final int maxHeaderListSize; + private final int softLimitHeaderListSize; private final long keepAliveTimeInNanos; private final long keepAliveTimeoutInNanos; private final long maxConnectionIdleInNanos; @@ -94,6 +95,7 @@ class NettyServerTransport implements ServerTransport { int flowControlWindow, int maxMessageSize, int maxHeaderListSize, + int softLimitHeaderListSize, long keepAliveTimeInNanos, long keepAliveTimeoutInNanos, long maxConnectionIdleInNanos, @@ -115,6 +117,7 @@ class NettyServerTransport implements ServerTransport { this.flowControlWindow = flowControlWindow; this.maxMessageSize = maxMessageSize; this.maxHeaderListSize = maxHeaderListSize; + this.softLimitHeaderListSize = softLimitHeaderListSize; this.keepAliveTimeInNanos = keepAliveTimeInNanos; this.keepAliveTimeoutInNanos = keepAliveTimeoutInNanos; this.maxConnectionIdleInNanos = maxConnectionIdleInNanos; @@ -275,6 +278,7 @@ private NettyServerHandler createHandler( autoFlowControl, flowControlWindow, maxHeaderListSize, + softLimitHeaderListSize, maxMessageSize, keepAliveTimeInNanos, keepAliveTimeoutInNanos, diff --git a/netty/src/main/java/io/grpc/netty/Utils.java b/netty/src/main/java/io/grpc/netty/Utils.java index 96f19aab5e3..ba405637af5 100644 --- a/netty/src/main/java/io/grpc/netty/Utils.java +++ b/netty/src/main/java/io/grpc/netty/Utils.java @@ -23,6 +23,7 @@ import static io.netty.channel.ChannelOption.SO_LINGER; import static io.netty.channel.ChannelOption.SO_TIMEOUT; import static io.netty.util.CharsetUtil.UTF_8; +import static java.nio.charset.StandardCharsets.US_ASCII; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -91,7 +92,9 @@ class Utils { = new DefaultEventLoopGroupResource(1, "grpc-nio-boss-ELG", EventLoopGroupType.NIO); public static final Resource NIO_WORKER_EVENT_LOOP_GROUP = new DefaultEventLoopGroupResource(0, "grpc-nio-worker-ELG", EventLoopGroupType.NIO); - + private static final int HEADER_ENTRY_OVERHEAD = 32; + private static final byte[] binaryHeaderSuffixBytes = + Metadata.BINARY_HEADER_SUFFIX.getBytes(US_ASCII); public static final Resource DEFAULT_BOSS_EVENT_LOOP_GROUP; public static final Resource DEFAULT_WORKER_EVENT_LOOP_GROUP; @@ -195,6 +198,61 @@ public static Metadata convertHeaders(Http2Headers http2Headers) { return InternalMetadata.newMetadata(convertHeadersToArray(http2Headers)); } + public static int getH2HeadersSize(Http2Headers http2Headers) { + if (http2Headers instanceof GrpcHttp2InboundHeaders) { + GrpcHttp2InboundHeaders h = (GrpcHttp2InboundHeaders) http2Headers; + int size = 0; + for (int i = 0; i < h.numHeaders(); i++) { + size += h.namesAndValues()[2 * i].length; + size += + maybeAddBinaryHeaderOverhead(h.namesAndValues()[2 * i], h.namesAndValues()[2 * i + 1]); + size += HEADER_ENTRY_OVERHEAD; + } + return size; + } + + // the binary header is not decoded yet, no need to add overhead. + int size = 0; + for (Map.Entry entry : http2Headers) { + size += entry.getKey().length(); + size += entry.getValue().length(); + size += HEADER_ENTRY_OVERHEAD; + } + return size; + } + + private static int maybeAddBinaryHeaderOverhead(byte[] name, byte[] value) { + if (endsWith(name, binaryHeaderSuffixBytes)) { + return value.length * 4 / 3; + } + return value.length; + } + + private static boolean endsWith(byte[] bytes, byte[] suffix) { + if (bytes == null || suffix == null || bytes.length < suffix.length) { + return false; + } + + for (int i = 0; i < suffix.length; i++) { + if (bytes[bytes.length - suffix.length + i] != suffix[i]) { + return false; + } + } + + return true; + } + + public static boolean shouldRejectOnMetadataSizeSoftLimitExceeded( + int h2HeadersSize, int softLimitHeaderListSize, int maxHeaderListSize) { + if (h2HeadersSize < softLimitHeaderListSize) { + return false; + } + double failProbability = + (double) (h2HeadersSize - softLimitHeaderListSize) / (double) (maxHeaderListSize + - softLimitHeaderListSize); + return Math.random() < failProbability; + } + @CheckReturnValue private static byte[][] convertHeadersToArray(Http2Headers http2Headers) { // The Netty AsciiString class is really just a wrapper around a byte[] and supports diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java index 73988f773cb..6c5dd6b18bc 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java @@ -47,6 +47,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import com.google.common.base.Stopwatch; +import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.base.Ticker; import com.google.common.collect.ImmutableList; @@ -122,6 +123,7 @@ public class NettyClientHandlerTest extends NettyHandlerTestBase statusArgumentCaptor = ArgumentCaptor.forClass(Status.class); + verify(streamListener).closed(statusArgumentCaptor.capture(), eq(PROCESSED), + any(Metadata.class)); + assertThat(statusArgumentCaptor.getValue().getCode()).isEqualTo(Status.Code.RESOURCE_EXHAUSTED); + assertThat(statusArgumentCaptor.getValue().getDescription()).contains( + "exceeded Metadata size soft limit"); + } + @Test public void cancelBufferedStreamShouldChangeClientStreamStatus() throws Exception { // Force the stream to be buffered. @@ -946,6 +978,7 @@ public Stopwatch get() { false, flowControlWindow, maxHeaderListSize, + softLimitHeaderListSize, stopwatchSupplier, tooManyPingsRunnable, transportTracer, diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index 7cb269b7d62..b7a3ff13a59 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -205,12 +205,30 @@ public void setSoLingerChannelOption() throws IOException { // set SO_LINGER option int soLinger = 123; channelOptions.put(ChannelOption.SO_LINGER, soLinger); - NettyClientTransport transport = new NettyClientTransport( - address, new ReflectiveChannelFactory<>(NioSocketChannel.class), channelOptions, group, - newNegotiator(), false, DEFAULT_WINDOW_SIZE, DEFAULT_MAX_MESSAGE_SIZE, - GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, KEEPALIVE_TIME_NANOS_DISABLED, 1L, false, authority, - null /* user agent */, tooManyPingsRunnable, new TransportTracer(), Attributes.EMPTY, - new SocketPicker(), new FakeChannelLogger(), false, Ticker.systemTicker()); + NettyClientTransport transport = + new NettyClientTransport( + address, + new ReflectiveChannelFactory<>(NioSocketChannel.class), + channelOptions, + group, + newNegotiator(), + false, + DEFAULT_WINDOW_SIZE, + DEFAULT_MAX_MESSAGE_SIZE, + GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, + GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, + KEEPALIVE_TIME_NANOS_DISABLED, + 1L, + false, + authority, + null /* user agent */, + tooManyPingsRunnable, + new TransportTracer(), + Attributes.EMPTY, + new SocketPicker(), + new FakeChannelLogger(), + false, + Ticker.systemTicker()); transports.add(transport); callMeMaybe(transport.start(clientTransportListener)); @@ -454,13 +472,30 @@ private static class CantConstructChannelError extends Error {} public void failingToConstructChannelShouldFailGracefully() throws Exception { address = TestUtils.testServerAddress(new InetSocketAddress(12345)); authority = GrpcUtil.authorityFromHostAndPort(address.getHostString(), address.getPort()); - NettyClientTransport transport = new NettyClientTransport( - address, new ReflectiveChannelFactory<>(CantConstructChannel.class), - new HashMap, Object>(), group, - newNegotiator(), false, DEFAULT_WINDOW_SIZE, DEFAULT_MAX_MESSAGE_SIZE, - GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, KEEPALIVE_TIME_NANOS_DISABLED, 1, false, authority, - null, tooManyPingsRunnable, new TransportTracer(), Attributes.EMPTY, new SocketPicker(), - new FakeChannelLogger(), false, Ticker.systemTicker()); + NettyClientTransport transport = + new NettyClientTransport( + address, + new ReflectiveChannelFactory<>(CantConstructChannel.class), + new HashMap, Object>(), + group, + newNegotiator(), + false, + DEFAULT_WINDOW_SIZE, + DEFAULT_MAX_MESSAGE_SIZE, + GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, + GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, + KEEPALIVE_TIME_NANOS_DISABLED, + 1, + false, + authority, + null, + tooManyPingsRunnable, + new TransportTracer(), + Attributes.EMPTY, + new SocketPicker(), + new FakeChannelLogger(), + false, + Ticker.systemTicker()); transports.add(transport); // Should not throw @@ -814,13 +849,30 @@ private NettyClientTransport newTransport(ProtocolNegotiator negotiator, int max if (!enableKeepAlive) { keepAliveTimeNano = KEEPALIVE_TIME_NANOS_DISABLED; } - NettyClientTransport transport = new NettyClientTransport( - address, channelFactory, new HashMap, Object>(), group, - negotiator, false, DEFAULT_WINDOW_SIZE, maxMsgSize, maxHeaderListSize, - keepAliveTimeNano, keepAliveTimeoutNano, - false, authority, userAgent, tooManyPingsRunnable, - new TransportTracer(), eagAttributes, new SocketPicker(), new FakeChannelLogger(), false, - Ticker.systemTicker()); + NettyClientTransport transport = + new NettyClientTransport( + address, + channelFactory, + new HashMap, Object>(), + group, + negotiator, + false, + DEFAULT_WINDOW_SIZE, + maxMsgSize, + maxHeaderListSize, + maxHeaderListSize, + keepAliveTimeNano, + keepAliveTimeoutNano, + false, + authority, + userAgent, + tooManyPingsRunnable, + new TransportTracer(), + eagAttributes, + new SocketPicker(), + new FakeChannelLogger(), + false, + Ticker.systemTicker()); transports.add(transport); return transport; } @@ -830,22 +882,35 @@ private void startServer() throws IOException { } private void startServer(int maxStreamsPerConnection, int maxHeaderListSize) throws IOException { - server = new NettyServer( - TestUtils.testServerAddresses(new InetSocketAddress(0)), - new ReflectiveChannelFactory<>(NioServerSocketChannel.class), - new HashMap, Object>(), - new HashMap, Object>(), - new FixedObjectPool<>(group), new FixedObjectPool<>(group), false, negotiator, - Collections.emptyList(), - TransportTracer.getDefaultFactory(), - maxStreamsPerConnection, - false, - DEFAULT_WINDOW_SIZE, DEFAULT_MAX_MESSAGE_SIZE, maxHeaderListSize, - DEFAULT_SERVER_KEEPALIVE_TIME_NANOS, DEFAULT_SERVER_KEEPALIVE_TIMEOUT_NANOS, - MAX_CONNECTION_IDLE_NANOS_DISABLED, - MAX_CONNECTION_AGE_NANOS_DISABLED, MAX_CONNECTION_AGE_GRACE_NANOS_INFINITE, true, 0, - MAX_RST_COUNT_DISABLED, 0, Attributes.EMPTY, - channelz); + server = + new NettyServer( + TestUtils.testServerAddresses(new InetSocketAddress(0)), + new ReflectiveChannelFactory<>(NioServerSocketChannel.class), + new HashMap, Object>(), + new HashMap, Object>(), + new FixedObjectPool<>(group), + new FixedObjectPool<>(group), + false, + negotiator, + Collections.emptyList(), + TransportTracer.getDefaultFactory(), + maxStreamsPerConnection, + false, + DEFAULT_WINDOW_SIZE, + DEFAULT_MAX_MESSAGE_SIZE, + maxHeaderListSize, + maxHeaderListSize, + DEFAULT_SERVER_KEEPALIVE_TIME_NANOS, + DEFAULT_SERVER_KEEPALIVE_TIMEOUT_NANOS, + MAX_CONNECTION_IDLE_NANOS_DISABLED, + MAX_CONNECTION_AGE_NANOS_DISABLED, + MAX_CONNECTION_AGE_GRACE_NANOS_INFINITE, + true, + 0, + MAX_RST_COUNT_DISABLED, + 0, + Attributes.EMPTY, + channelz); server.start(serverListener); address = TestUtils.testServerAddress((InetSocketAddress) server.getListenSocketAddress()); authority = GrpcUtil.authorityFromHostAndPort(address.getHostString(), address.getPort()); diff --git a/netty/src/test/java/io/grpc/netty/NettyServerBuilderTest.java b/netty/src/test/java/io/grpc/netty/NettyServerBuilderTest.java index 6d8192322aa..48af23b78a8 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerBuilderTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerBuilderTest.java @@ -100,6 +100,22 @@ public void failIfMaxInboundMetadataSizeNonPositive() { builder.maxInboundMetadataSize(0); } + @Test + public void failIfSoftInboundMetadataSizeNonPositive() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("softLimitHeaderListSize must be positive"); + + builder.maxInboundMetadataSize(0, 100); + } + + @Test + public void failIfMaxInboundMetadataSizeSmallerThanSoft() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("must be greater than softLimitHeaderListSize"); + + builder.maxInboundMetadataSize(100, 80); + } + @Test public void failIfMaxConnectionIdleNegative() { thrown.expect(IllegalArgumentException.class); diff --git a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java index 541490847c0..308079ff62f 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java @@ -141,6 +141,7 @@ public class NettyServerHandlerTest extends NettyHandlerTestBase(NioServerSocketChannel.class), - new HashMap, Object>(), - new HashMap, Object>(), - new FixedObjectPool<>(eventLoop), - new FixedObjectPool<>(eventLoop), - false, - protocolNegotiator, - Collections.emptyList(), - TransportTracer.getDefaultFactory(), - 1, // ignore - false, // ignore - 1, // ignore - 1, // ignore - 1, // ignore - 1, // ignore - 1, 1, // ignore - 1, 1, // ignore - true, 0, // ignore - 0, 0, // ignore - Attributes.EMPTY, - channelz); + NettyServer ns = + new NettyServer( + Arrays.asList(addr), + new ReflectiveChannelFactory<>(NioServerSocketChannel.class), + new HashMap, Object>(), + new HashMap, Object>(), + new FixedObjectPool<>(eventLoop), + new FixedObjectPool<>(eventLoop), + false, + protocolNegotiator, + Collections.emptyList(), + TransportTracer.getDefaultFactory(), + 1, // ignore + false, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, + 1, // ignore + 1, + 1, // ignore + true, + 0, // ignore + 0, + 0, // ignore + Attributes.EMPTY, + channelz); final SettableFuture serverShutdownCalled = SettableFuture.create(); ns.start(new ServerListener() { @Override @@ -184,29 +190,35 @@ public void multiPortStartStopGet() throws Exception { InetSocketAddress addr1 = new InetSocketAddress(0); InetSocketAddress addr2 = new InetSocketAddress(0); - NettyServer ns = new NettyServer( - Arrays.asList(addr1, addr2), - new ReflectiveChannelFactory<>(NioServerSocketChannel.class), - new HashMap, Object>(), - new HashMap, Object>(), - new FixedObjectPool<>(eventLoop), - new FixedObjectPool<>(eventLoop), - false, - ProtocolNegotiators.plaintext(), - Collections.emptyList(), - TransportTracer.getDefaultFactory(), - 1, // ignore - false, // ignore - 1, // ignore - 1, // ignore - 1, // ignore - 1, // ignore - 1, 1, // ignore - 1, 1, // ignore - true, 0, // ignore - 0, 0, // ignore - Attributes.EMPTY, - channelz); + NettyServer ns = + new NettyServer( + Arrays.asList(addr1, addr2), + new ReflectiveChannelFactory<>(NioServerSocketChannel.class), + new HashMap, Object>(), + new HashMap, Object>(), + new FixedObjectPool<>(eventLoop), + new FixedObjectPool<>(eventLoop), + false, + ProtocolNegotiators.plaintext(), + Collections.emptyList(), + TransportTracer.getDefaultFactory(), + 1, // ignore + false, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, + 1, // ignore + 1, + 1, // ignore + true, + 0, // ignore + 0, + 0, // ignore + Attributes.EMPTY, + channelz); final SettableFuture shutdownCompleted = SettableFuture.create(); ns.start(new ServerListener() { @Override @@ -258,29 +270,35 @@ public void multiPortConnections() throws Exception { InetSocketAddress addr2 = new InetSocketAddress(0); final CountDownLatch allPortsConnectedCountDown = new CountDownLatch(2); - NettyServer ns = new NettyServer( - Arrays.asList(addr1, addr2), - new ReflectiveChannelFactory<>(NioServerSocketChannel.class), - new HashMap, Object>(), - new HashMap, Object>(), - new FixedObjectPool<>(eventLoop), - new FixedObjectPool<>(eventLoop), - false, - ProtocolNegotiators.plaintext(), - Collections.emptyList(), - TransportTracer.getDefaultFactory(), - 1, // ignore - false, // ignore - 1, // ignore - 1, // ignore - 1, // ignore - 1, // ignore - 1, 1, // ignore - 1, 1, // ignore - true, 0, // ignore - 0, 0, // ignore - Attributes.EMPTY, - channelz); + NettyServer ns = + new NettyServer( + Arrays.asList(addr1, addr2), + new ReflectiveChannelFactory<>(NioServerSocketChannel.class), + new HashMap, Object>(), + new HashMap, Object>(), + new FixedObjectPool<>(eventLoop), + new FixedObjectPool<>(eventLoop), + false, + ProtocolNegotiators.plaintext(), + Collections.emptyList(), + TransportTracer.getDefaultFactory(), + 1, // ignore + false, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, + 1, // ignore + 1, + 1, // ignore + true, + 0, // ignore + 0, + 0, // ignore + Attributes.EMPTY, + channelz); final SettableFuture shutdownCompleted = SettableFuture.create(); ns.start(new ServerListener() { @Override @@ -320,29 +338,35 @@ public void run() {} public void getPort_notStarted() { InetSocketAddress addr = new InetSocketAddress(0); List addresses = Collections.singletonList(addr); - NettyServer ns = new NettyServer( - addresses, - new ReflectiveChannelFactory<>(NioServerSocketChannel.class), - new HashMap, Object>(), - new HashMap, Object>(), - new FixedObjectPool<>(eventLoop), - new FixedObjectPool<>(eventLoop), - false, - ProtocolNegotiators.plaintext(), - Collections.emptyList(), - TransportTracer.getDefaultFactory(), - 1, // ignore - false, // ignore - 1, // ignore - 1, // ignore - 1, // ignore - 1, // ignore - 1, 1, // ignore - 1, 1, // ignore - true, 0, // ignore - 0, 0, // ignore - Attributes.EMPTY, - channelz); + NettyServer ns = + new NettyServer( + addresses, + new ReflectiveChannelFactory<>(NioServerSocketChannel.class), + new HashMap, Object>(), + new HashMap, Object>(), + new FixedObjectPool<>(eventLoop), + new FixedObjectPool<>(eventLoop), + false, + ProtocolNegotiators.plaintext(), + Collections.emptyList(), + TransportTracer.getDefaultFactory(), + 1, // ignore + false, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, + 1, // ignore + 1, + 1, // ignore + true, + 0, // ignore + 0, + 0, // ignore + Attributes.EMPTY, + channelz); assertThat(ns.getListenSocketAddress()).isEqualTo(addr); assertThat(ns.getListenSocketAddresses()).isEqualTo(addresses); @@ -395,29 +419,35 @@ class TestProtocolNegotiator implements ProtocolNegotiator { .build(); TestProtocolNegotiator protocolNegotiator = new TestProtocolNegotiator(); InetSocketAddress addr = new InetSocketAddress(0); - NettyServer ns = new NettyServer( - Arrays.asList(addr), - new ReflectiveChannelFactory<>(NioServerSocketChannel.class), - new HashMap, Object>(), - childChannelOptions, - new FixedObjectPool<>(eventLoop), - new FixedObjectPool<>(eventLoop), - false, - protocolNegotiator, - Collections.emptyList(), - TransportTracer.getDefaultFactory(), - 1, // ignore - false, // ignore - 1, // ignore - 1, // ignore - 1, // ignore - 1, // ignore - 1, 1, // ignore - 1, 1, // ignore - true, 0, // ignore - 0, 0, // ignore - eagAttributes, - channelz); + NettyServer ns = + new NettyServer( + Arrays.asList(addr), + new ReflectiveChannelFactory<>(NioServerSocketChannel.class), + new HashMap, Object>(), + childChannelOptions, + new FixedObjectPool<>(eventLoop), + new FixedObjectPool<>(eventLoop), + false, + protocolNegotiator, + Collections.emptyList(), + TransportTracer.getDefaultFactory(), + 1, // ignore + false, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, + 1, // ignore + 1, + 1, // ignore + true, + 0, // ignore + 0, + 0, // ignore + eagAttributes, + channelz); ns.start(new ServerListener() { @Override public ServerTransportListener transportCreated(ServerTransport transport) { @@ -443,29 +473,35 @@ public void serverShutdown() {} @Test public void channelzListenSocket() throws Exception { InetSocketAddress addr = new InetSocketAddress(0); - NettyServer ns = new NettyServer( - Arrays.asList(addr), - new ReflectiveChannelFactory<>(NioServerSocketChannel.class), - new HashMap, Object>(), - new HashMap, Object>(), - new FixedObjectPool<>(eventLoop), - new FixedObjectPool<>(eventLoop), - false, - ProtocolNegotiators.plaintext(), - Collections.emptyList(), - TransportTracer.getDefaultFactory(), - 1, // ignore - false, // ignore - 1, // ignore - 1, // ignore - 1, // ignore - 1, // ignore - 1, 1, // ignore - 1, 1, // ignore - true, 0, // ignore - 0, 0, // ignore - Attributes.EMPTY, - channelz); + NettyServer ns = + new NettyServer( + Arrays.asList(addr), + new ReflectiveChannelFactory<>(NioServerSocketChannel.class), + new HashMap, Object>(), + new HashMap, Object>(), + new FixedObjectPool<>(eventLoop), + new FixedObjectPool<>(eventLoop), + false, + ProtocolNegotiators.plaintext(), + Collections.emptyList(), + TransportTracer.getDefaultFactory(), + 1, // ignore + false, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, // ignore + 1, + 1, // ignore + 1, + 1, // ignore + true, + 0, // ignore + 0, + 0, // ignore + Attributes.EMPTY, + channelz); final SettableFuture shutdownCompleted = SettableFuture.create(); ns.start(new ServerListener() { @Override @@ -603,10 +639,15 @@ private NettyServer getServer(List addr, EventLoopGroup ev) { 1, // ignore 1, // ignore 1, // ignore - 1, 1, // ignore - 1, 1, // ignore - true, 0, // ignore - 0, 0, // ignore + 1, // ignore + 1, + 1, // ignore + 1, + 1, // ignore + true, + 0, // ignore + 0, + 0, // ignore Attributes.EMPTY, channelz); } From 9176b55286c478cc522ec7349311f29fa73d7c18 Mon Sep 17 00:00:00 2001 From: vinodhabib <47808007+vinodhabib@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:19:47 +0530 Subject: [PATCH 065/591] core: Make timestamp usage in Channelz use nanos from Java.time.Instant when available (#11604) When java.time.Instant is available use the timestamp from this class in nano precision rather than using System.currentTimeInMillis and converting it to nanos. Fixes #5494. --- .../grpc/internal/ConcurrentTimeProvider.java | 32 +++++++++++ .../io/grpc/internal/InstantTimeProvider.java | 54 +++++++++++++++++++ .../java/io/grpc/internal/TimeProvider.java | 9 +--- .../internal/TimeProviderResolverFactory.java | 32 +++++++++++ .../internal/ConcurrentTimeProviderTest.java | 48 +++++++++++++++++ .../internal/InstantTimeProviderTest.java | 50 +++++++++++++++++ 6 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 core/src/main/java/io/grpc/internal/ConcurrentTimeProvider.java create mode 100644 core/src/main/java/io/grpc/internal/InstantTimeProvider.java create mode 100644 core/src/main/java/io/grpc/internal/TimeProviderResolverFactory.java create mode 100644 core/src/test/java/io/grpc/internal/ConcurrentTimeProviderTest.java create mode 100644 core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java diff --git a/core/src/main/java/io/grpc/internal/ConcurrentTimeProvider.java b/core/src/main/java/io/grpc/internal/ConcurrentTimeProvider.java new file mode 100644 index 00000000000..c82a68222b4 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/ConcurrentTimeProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import java.util.concurrent.TimeUnit; + +/** + * {@link ConcurrentTimeProvider} resolves ConcurrentTimeProvider which implements + * {@link TimeProvider}. + */ + +final class ConcurrentTimeProvider implements TimeProvider { + + @Override + public long currentTimeNanos() { + return TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + } +} diff --git a/core/src/main/java/io/grpc/internal/InstantTimeProvider.java b/core/src/main/java/io/grpc/internal/InstantTimeProvider.java new file mode 100644 index 00000000000..38c840d2594 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/InstantTimeProvider.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import static com.google.common.math.LongMath.saturatedAdd; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; + +/** + * {@link InstantTimeProvider} resolves InstantTimeProvider which implements {@link TimeProvider}. + */ +final class InstantTimeProvider implements TimeProvider { + private Method now; + private Method getNano; + private Method getEpochSecond; + + public InstantTimeProvider(Class instantClass) { + try { + this.now = instantClass.getMethod("now"); + this.getNano = instantClass.getMethod("getNano"); + this.getEpochSecond = instantClass.getMethod("getEpochSecond"); + } catch (NoSuchMethodException ex) { + throw new AssertionError(ex); + } + } + + @Override + public long currentTimeNanos() { + try { + Object instant = now.invoke(null); + int nanos = (int) getNano.invoke(instant); + long epochSeconds = (long) getEpochSecond.invoke(instant); + return saturatedAdd(TimeUnit.SECONDS.toNanos(epochSeconds), nanos); + } catch (IllegalAccessException | InvocationTargetException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/core/src/main/java/io/grpc/internal/TimeProvider.java b/core/src/main/java/io/grpc/internal/TimeProvider.java index b0ea147ada1..3bd052ab3e0 100644 --- a/core/src/main/java/io/grpc/internal/TimeProvider.java +++ b/core/src/main/java/io/grpc/internal/TimeProvider.java @@ -16,8 +16,6 @@ package io.grpc.internal; -import java.util.concurrent.TimeUnit; - /** * Time source representing the current system time in nanos. Used to inject a fake clock * into unit tests. @@ -26,10 +24,5 @@ public interface TimeProvider { /** Returns the current nano time. */ long currentTimeNanos(); - TimeProvider SYSTEM_TIME_PROVIDER = new TimeProvider() { - @Override - public long currentTimeNanos() { - return TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); - } - }; + TimeProvider SYSTEM_TIME_PROVIDER = TimeProviderResolverFactory.resolveTimeProvider(); } diff --git a/core/src/main/java/io/grpc/internal/TimeProviderResolverFactory.java b/core/src/main/java/io/grpc/internal/TimeProviderResolverFactory.java new file mode 100644 index 00000000000..d88d9bb9eb5 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/TimeProviderResolverFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +/** + * {@link TimeProviderResolverFactory} resolves Time providers. + */ + +final class TimeProviderResolverFactory { + static TimeProvider resolveTimeProvider() { + try { + Class instantClass = Class.forName("java.time.Instant"); + return new InstantTimeProvider(instantClass); + } catch (ClassNotFoundException ex) { + return new ConcurrentTimeProvider(); + } + } +} diff --git a/core/src/test/java/io/grpc/internal/ConcurrentTimeProviderTest.java b/core/src/test/java/io/grpc/internal/ConcurrentTimeProviderTest.java new file mode 100644 index 00000000000..a02cb6a6e42 --- /dev/null +++ b/core/src/test/java/io/grpc/internal/ConcurrentTimeProviderTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import static com.google.common.truth.Truth.assertThat; + +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@link ConcurrentTimeProvider}. + */ +@RunWith(JUnit4.class) +public class ConcurrentTimeProviderTest { + @Test + public void testConcurrentCurrentTimeNanos() { + + ConcurrentTimeProvider concurrentTimeProvider = new ConcurrentTimeProvider(); + // Get the current time from the ConcurrentTimeProvider + long actualTimeNanos = concurrentTimeProvider.currentTimeNanos(); + + // Get the current time from Instant for comparison + Instant instantNow = Instant.now(); + long expectedTimeNanos = TimeUnit.SECONDS.toNanos(instantNow.getEpochSecond()) + + instantNow.getNano(); + + // Validate the time returned is close to the expected value within a tolerance + // (i,e 10 millisecond tolerance in nanoseconds). + assertThat(actualTimeNanos).isWithin(10_000_000L).of(expectedTimeNanos); + } +} diff --git a/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java b/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java new file mode 100644 index 00000000000..ef97d374b10 --- /dev/null +++ b/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import static com.google.common.truth.Truth.assertThat; + +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@link InstantTimeProvider}. + */ +@RunWith(JUnit4.class) +public class InstantTimeProviderTest { + @Test + public void testInstantCurrentTimeNanos() throws Exception { + + InstantTimeProvider instantTimeProvider = new InstantTimeProvider( + Class.forName("java.time.Instant")); + + // Get the current time from the InstantTimeProvider + long actualTimeNanos = instantTimeProvider.currentTimeNanos(); + + // Get the current time from Instant for comparison + Instant instantNow = Instant.now(); + long expectedTimeNanos = TimeUnit.SECONDS.toNanos(instantNow.getEpochSecond()) + + instantNow.getNano(); + + // Validate the time returned is close to the expected value within a tolerance + // (i,e 10 millisecond tolerance in nanoseconds). + assertThat(actualTimeNanos).isWithin(10_000_000L).of(expectedTimeNanos); + } +} From a431e3664be54a40dca12d62f81d22656f8d2057 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 28 Oct 2024 14:56:46 -0700 Subject: [PATCH 066/591] binder: Remove unnecessary uses of LooperMode(PAUSED) PAUSED Looper mode has been the default for many years, maybe around robolectric 4.5 (9ae9f0b6a6). Explicitly specifying PAUSED Looper mode is not necessary. cl/690684542 --- .../io/grpc/binder/internal/BinderServerTransportTest.java | 3 --- .../test/java/io/grpc/binder/internal/ServiceBindingTest.java | 3 --- 2 files changed, 6 deletions(-) diff --git a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java index e56d860c091..d3c13d6c89e 100644 --- a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java @@ -23,7 +23,6 @@ import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; -import static org.robolectric.annotation.LooperMode.Mode.PAUSED; import android.os.IBinder; import android.os.Looper; @@ -44,14 +43,12 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.LooperMode; /** * Low-level server-side transport tests for binder channel. Like BinderChannelSmokeTest, this * convers edge cases not exercised by AbstractTransportTest, but it deals with the * binderTransport.BinderServerTransport directly. */ -@LooperMode(PAUSED) @RunWith(RobolectricTestRunner.class) public final class BinderServerTransportTest { diff --git a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java index b44692f560d..b0ad35e6806 100644 --- a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java @@ -21,7 +21,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.robolectric.Shadows.shadowOf; -import static org.robolectric.annotation.LooperMode.Mode.PAUSED; import android.app.Application; import android.app.admin.DevicePolicyManager; @@ -48,11 +47,9 @@ import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import org.robolectric.annotation.LooperMode; import org.robolectric.shadows.ShadowApplication; import org.robolectric.shadows.ShadowDevicePolicyManager; -@LooperMode(PAUSED) @RunWith(RobolectricTestRunner.class) public final class ServiceBindingTest { From 1612536f866645e000ea2d5825eb4560bc1a08c7 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 28 Oct 2024 16:11:16 -0700 Subject: [PATCH 067/591] Update README etc to reference 1.68.1 --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index f56aebfb3ac..97b2bd6d5f9 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.67.1/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.67.1/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.68.1/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.68.1/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.67.1 + 1.68.1 runtime io.grpc grpc-protobuf - 1.67.1 + 1.68.1 io.grpc grpc-stub - 1.67.1 + 1.68.1 org.apache.tomcat @@ -79,18 +79,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.67.1' -implementation 'io.grpc:grpc-protobuf:1.67.1' -implementation 'io.grpc:grpc-stub:1.67.1' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.68.1' +implementation 'io.grpc:grpc-protobuf:1.68.1' +implementation 'io.grpc:grpc-stub:1.68.1' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.67.1' -implementation 'io.grpc:grpc-protobuf-lite:1.67.1' -implementation 'io.grpc:grpc-stub:1.67.1' +implementation 'io.grpc:grpc-okhttp:1.68.1' +implementation 'io.grpc:grpc-protobuf-lite:1.68.1' +implementation 'io.grpc:grpc-stub:1.68.1' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` @@ -99,7 +99,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.67.1 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.68.1 Development snapshots are available in [Sonatypes's snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/). @@ -129,9 +129,9 @@ For protobuf-based codegen integrated with the Maven build system, you can use protobuf-maven-plugin 0.6.1 - com.google.protobuf:protoc:3.25.3:exe:${os.detected.classifier} + com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.67.1:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.68.1:exe:${os.detected.classifier} @@ -157,11 +157,11 @@ plugins { protobuf { protoc { - artifact = "com.google.protobuf:protoc:3.25.3" + artifact = "com.google.protobuf:protoc:3.25.5" } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.67.1' + artifact = 'io.grpc:protoc-gen-grpc-java:1.68.1' } } generateProtoTasks { @@ -190,11 +190,11 @@ plugins { protobuf { protoc { - artifact = "com.google.protobuf:protoc:3.25.3" + artifact = "com.google.protobuf:protoc:3.25.5" } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.67.1' + artifact = 'io.grpc:protoc-gen-grpc-java:1.68.1' } } generateProtoTasks { From b5ef09c5483d23ccdd7888e879d45f4054ad63f7 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 29 Oct 2024 22:29:03 -0700 Subject: [PATCH 068/591] RELEASING.md: Fix interop_matrix image name (#11653) --- RELEASING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 0add8991746..120b0ea6ac1 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -167,14 +167,14 @@ Tagging the Release generation instructions][gcr-image]. Summary: ```bash # If you haven't previously configured docker: - gcloud auth configure-docker + gcloud auth configure-docker us-docker.pkg.dev # In main grpc repo, add the new version to matrix ${EDITOR:-nano -w} tools/interop_matrix/client_matrix.py tools/interop_matrix/create_matrix_images.py --git_checkout --release=v$MAJOR.$MINOR.$PATCH \ --upload_images --language java - docker pull gcr.io/grpc-testing/grpc_interop_java:v$MAJOR.$MINOR.$PATCH - docker_image=gcr.io/grpc-testing/grpc_interop_java:v$MAJOR.$MINOR.$PATCH \ + docker pull us-docker.pkg.dev/grpc-testing/testing-images-public/grpc_interop_java:v$MAJOR.$MINOR.$PATCH + docker_image=us-docker.pkg.dev/grpc-testing/testing-images-public/grpc_interop_java:v$MAJOR.$MINOR.$PATCH \ tools/interop_matrix/testcases/java__master # Commit the changes From 766b92379b5eac82d64bf2fd913d1e10a6960600 Mon Sep 17 00:00:00 2001 From: SreeramdasLavanya <76050406+SreeramdasLavanya@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:19:53 +0000 Subject: [PATCH 069/591] api: Add java.time.Duration overloads to CallOptions, AbstractStub taking TimeUnit and a time value (#11562) --- api/src/main/java/io/grpc/CallOptions.java | 7 +++ .../main/java/io/grpc/InternalTimeUtils.java | 26 ++++++++ api/src/main/java/io/grpc/LoadBalancer.java | 2 +- .../java/io/grpc/SynchronizationContext.java | 18 +++++- api/src/main/java/io/grpc/TimeUtils.java | 31 ++++++++++ .../test/java/io/grpc/CallOptionsTest.java | 9 +++ .../io/grpc/SynchronizationContextTest.java | 48 ++++++++++++++- api/src/test/java/io/grpc/TimeUtilsTest.java | 59 +++++++++++++++++++ .../main/java/io/grpc/stub/AbstractStub.java | 7 +++ .../java/io/grpc/stub/AbstractStubTest.java | 22 ++++++- 10 files changed, 224 insertions(+), 5 deletions(-) create mode 100644 api/src/main/java/io/grpc/InternalTimeUtils.java create mode 100644 api/src/main/java/io/grpc/TimeUtils.java create mode 100644 api/src/test/java/io/grpc/TimeUtilsTest.java diff --git a/api/src/main/java/io/grpc/CallOptions.java b/api/src/main/java/io/grpc/CallOptions.java index 25c4df386a1..a1b8984c48b 100644 --- a/api/src/main/java/io/grpc/CallOptions.java +++ b/api/src/main/java/io/grpc/CallOptions.java @@ -17,9 +17,11 @@ package io.grpc; import static com.google.common.base.Preconditions.checkArgument; +import static io.grpc.TimeUtils.convertToNanos; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -176,6 +178,11 @@ public CallOptions withDeadlineAfter(long duration, TimeUnit unit) { return withDeadline(Deadline.after(duration, unit)); } + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657") + public CallOptions withDeadlineAfter(Duration duration) { + return withDeadlineAfter(convertToNanos(duration), TimeUnit.NANOSECONDS); + } + /** * Returns the deadline or {@code null} if the deadline is not set. */ diff --git a/api/src/main/java/io/grpc/InternalTimeUtils.java b/api/src/main/java/io/grpc/InternalTimeUtils.java new file mode 100644 index 00000000000..ef8022f53c5 --- /dev/null +++ b/api/src/main/java/io/grpc/InternalTimeUtils.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import java.time.Duration; + +@Internal +public final class InternalTimeUtils { + public static long convert(Duration duration) { + return TimeUtils.convertToNanos(duration); + } +} diff --git a/api/src/main/java/io/grpc/LoadBalancer.java b/api/src/main/java/io/grpc/LoadBalancer.java index 6d74006b396..52d37f8677d 100644 --- a/api/src/main/java/io/grpc/LoadBalancer.java +++ b/api/src/main/java/io/grpc/LoadBalancer.java @@ -212,7 +212,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { * * @since 1.21.0 */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1771") + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657") public static final class ResolvedAddresses { private final List addresses; @NameResolver.ResolutionResultAttr diff --git a/api/src/main/java/io/grpc/SynchronizationContext.java b/api/src/main/java/io/grpc/SynchronizationContext.java index 5a7677ac15f..94916a1b473 100644 --- a/api/src/main/java/io/grpc/SynchronizationContext.java +++ b/api/src/main/java/io/grpc/SynchronizationContext.java @@ -18,8 +18,10 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static io.grpc.TimeUtils.convertToNanos; import java.lang.Thread.UncaughtExceptionHandler; +import java.time.Duration; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; @@ -162,6 +164,12 @@ public String toString() { return new ScheduledHandle(runnable, future); } + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657") + public final ScheduledHandle schedule( + final Runnable task, Duration delay, ScheduledExecutorService timerService) { + return schedule(task, convertToNanos(delay), TimeUnit.NANOSECONDS, timerService); + } + /** * Schedules a task to be added and run via {@link #execute} after an initial delay and then * repeated after the delay until cancelled. @@ -193,6 +201,14 @@ public String toString() { return new ScheduledHandle(runnable, future); } + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657") + public final ScheduledHandle scheduleWithFixedDelay( + final Runnable task, Duration initialDelay, Duration delay, + ScheduledExecutorService timerService) { + return scheduleWithFixedDelay(task, convertToNanos(initialDelay), convertToNanos(delay), + TimeUnit.NANOSECONDS, timerService); + } + private static class ManagedRunnable implements Runnable { final Runnable task; @@ -246,4 +262,4 @@ public boolean isPending() { return !(runnable.hasStarted || runnable.isCancelled); } } -} +} \ No newline at end of file diff --git a/api/src/main/java/io/grpc/TimeUtils.java b/api/src/main/java/io/grpc/TimeUtils.java new file mode 100644 index 00000000000..c3031f13d94 --- /dev/null +++ b/api/src/main/java/io/grpc/TimeUtils.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import java.time.Duration; + +final class TimeUtils { + private TimeUtils() {} + + static long convertToNanos(Duration duration) { + try { + return duration.toNanos(); + } catch (ArithmeticException tooBig) { + return duration.isNegative() ? Long.MIN_VALUE : Long.MAX_VALUE; + } + } +} diff --git a/api/src/test/java/io/grpc/CallOptionsTest.java b/api/src/test/java/io/grpc/CallOptionsTest.java index cc90a9799d7..d74c74ccd66 100644 --- a/api/src/test/java/io/grpc/CallOptionsTest.java +++ b/api/src/test/java/io/grpc/CallOptionsTest.java @@ -32,6 +32,7 @@ import com.google.common.base.Objects; import io.grpc.ClientStreamTracer.StreamInfo; import io.grpc.internal.SerializingExecutor; +import java.time.Duration; import java.util.concurrent.Executor; import org.junit.Test; import org.junit.runner.RunWith; @@ -150,6 +151,14 @@ public void withDeadlineAfter() { assertAbout(deadline()).that(actual).isWithin(10, MILLISECONDS).of(expected); } + @Test + public void withDeadlineAfterDuration() { + Deadline actual = CallOptions.DEFAULT.withDeadlineAfter(Duration.ofMinutes(1L)).getDeadline(); + Deadline expected = Deadline.after(1, MINUTES); + + assertAbout(deadline()).that(actual).isWithin(10, MILLISECONDS).of(expected); + } + @Test public void toStringMatches_noDeadline_default() { String actual = allSet diff --git a/api/src/test/java/io/grpc/SynchronizationContextTest.java b/api/src/test/java/io/grpc/SynchronizationContextTest.java index 3d5e7fa42b9..f0797df227e 100644 --- a/api/src/test/java/io/grpc/SynchronizationContextTest.java +++ b/api/src/test/java/io/grpc/SynchronizationContextTest.java @@ -27,6 +27,7 @@ import com.google.common.util.concurrent.testing.TestingExecutors; import io.grpc.SynchronizationContext.ScheduledHandle; +import java.time.Duration; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; @@ -72,7 +73,7 @@ public void uncaughtException(Thread t, Throwable e) { @Mock private Runnable task3; - + @After public void tearDown() { assertThat(uncaughtErrors).isEmpty(); } @@ -246,6 +247,41 @@ public void schedule() { verify(task1).run(); } + @Test + public void scheduleDuration() { + MockScheduledExecutorService executorService = new MockScheduledExecutorService(); + ScheduledHandle handle = + syncContext.schedule(task1, Duration.ofSeconds(10), executorService); + + assertThat(executorService.delay) + .isEqualTo(executorService.unit.convert(10, TimeUnit.SECONDS)); + assertThat(handle.isPending()).isTrue(); + verify(task1, never()).run(); + + executorService.command.run(); + + assertThat(handle.isPending()).isFalse(); + verify(task1).run(); + } + + @Test + public void scheduleWithFixedDelayDuration() { + MockScheduledExecutorService executorService = new MockScheduledExecutorService(); + ScheduledHandle handle = + syncContext.scheduleWithFixedDelay(task1, Duration.ofSeconds(10), + Duration.ofSeconds(10), executorService); + + assertThat(executorService.delay) + .isEqualTo(executorService.unit.convert(10, TimeUnit.SECONDS)); + assertThat(handle.isPending()).isTrue(); + verify(task1, never()).run(); + + executorService.command.run(); + + assertThat(handle.isPending()).isFalse(); + verify(task1).run(); + } + @Test public void scheduleDueImmediately() { MockScheduledExecutorService executorService = new MockScheduledExecutorService(); @@ -357,5 +393,13 @@ static class MockScheduledExecutorService extends ForwardingScheduledExecutorSer this.unit = unit; return future = super.schedule(command, delay, unit); } + + @Override public ScheduledFuture scheduleWithFixedDelay(Runnable command, long intialDelay, + long delay, TimeUnit unit) { + this.command = command; + this.delay = delay; + this.unit = unit; + return future = super.scheduleWithFixedDelay(command, intialDelay, delay, unit); + } } -} +} \ No newline at end of file diff --git a/api/src/test/java/io/grpc/TimeUtilsTest.java b/api/src/test/java/io/grpc/TimeUtilsTest.java new file mode 100644 index 00000000000..4faaa9cbf6d --- /dev/null +++ b/api/src/test/java/io/grpc/TimeUtilsTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import static org.junit.Assert.assertEquals; + +import java.time.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link TimeUtils}. */ +@RunWith(JUnit4.class) +public class TimeUtilsTest { + + @Test + public void testConvertNormalDuration() { + Duration duration = Duration.ofSeconds(10); + long expected = 10 * 1_000_000_000L; + + assertEquals(expected, TimeUtils.convertToNanos(duration)); + } + + @Test + public void testConvertNegativeDuration() { + Duration duration = Duration.ofSeconds(-3); + long expected = -3 * 1_000_000_000L; + + assertEquals(expected, TimeUtils.convertToNanos(duration)); + } + + @Test + public void testConvertTooLargeDuration() { + Duration duration = Duration.ofSeconds(Long.MAX_VALUE / 1_000_000_000L + 1); + + assertEquals(Long.MAX_VALUE, TimeUtils.convertToNanos(duration)); + } + + @Test + public void testConvertTooLargeNegativeDuration() { + Duration duration = Duration.ofSeconds(Long.MIN_VALUE / 1_000_000_000L - 1); + + assertEquals(Long.MIN_VALUE, TimeUtils.convertToNanos(duration)); + } +} \ No newline at end of file diff --git a/stub/src/main/java/io/grpc/stub/AbstractStub.java b/stub/src/main/java/io/grpc/stub/AbstractStub.java index 0b6f86f2acf..06dd55ff466 100644 --- a/stub/src/main/java/io/grpc/stub/AbstractStub.java +++ b/stub/src/main/java/io/grpc/stub/AbstractStub.java @@ -17,6 +17,7 @@ package io.grpc.stub; import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.InternalTimeUtils.convert; import io.grpc.CallCredentials; import io.grpc.CallOptions; @@ -26,6 +27,7 @@ import io.grpc.Deadline; import io.grpc.ExperimentalApi; import io.grpc.ManagedChannelBuilder; +import java.time.Duration; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import javax.annotation.CheckReturnValue; @@ -149,6 +151,11 @@ public final S withDeadlineAfter(long duration, TimeUnit unit) { return build(channel, callOptions.withDeadlineAfter(duration, unit)); } + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657") + public final S withDeadlineAfter(Duration duration) { + return withDeadlineAfter(convert(duration), TimeUnit.NANOSECONDS); + } + /** * Returns a new stub with the given executor that is to be used instead of the default one * specified with {@link ManagedChannelBuilder#executor}. Note that setting this option may not diff --git a/stub/src/test/java/io/grpc/stub/AbstractStubTest.java b/stub/src/test/java/io/grpc/stub/AbstractStubTest.java index 9006b8679e4..a167c735160 100644 --- a/stub/src/test/java/io/grpc/stub/AbstractStubTest.java +++ b/stub/src/test/java/io/grpc/stub/AbstractStubTest.java @@ -16,12 +16,18 @@ package io.grpc.stub; +import static com.google.common.truth.Truth.assertAbout; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.testing.DeadlineSubject.deadline; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; import io.grpc.CallOptions; import io.grpc.Channel; +import io.grpc.Deadline; import io.grpc.stub.AbstractStub.StubFactory; import io.grpc.stub.AbstractStubTest.NoopStub; +import java.time.Duration; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -47,8 +53,22 @@ public NoopStub newStub(Channel channel, CallOptions callOptions) { .isNull(); } - class NoopStub extends AbstractStub { + @Test + public void testDuration() { + NoopStub stub = NoopStub.newStub(new StubFactory() { + @Override + public NoopStub newStub(Channel channel, CallOptions callOptions) { + return create(channel, callOptions); + } + }, channel, CallOptions.DEFAULT); + NoopStub stubInstance = stub.withDeadlineAfter(Duration.ofMinutes(1L)); + Deadline actual = stubInstance.getCallOptions().getDeadline(); + Deadline expected = Deadline.after(1, MINUTES); + assertAbout(deadline()).that(actual).isWithin(10, MILLISECONDS).of(expected); + } + + class NoopStub extends AbstractStub { NoopStub(Channel channel, CallOptions options) { super(channel, options); } From 3562380da543778b9048e0557e71d59bef288118 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 29 Oct 2024 14:16:45 -0700 Subject: [PATCH 070/591] Upgrade Gradle to 8.10.2 and upgrade plugins com.github.johnrengelman.shadow is now com.gradleup.shadow (note the redirect) https://github.com/johnrengelman/shadow/releases/tag/8.3.0 --- alts/build.gradle | 2 +- authz/build.gradle | 2 +- build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- netty/shaded/build.gradle | 2 +- s2a/build.gradle | 2 +- settings.gradle | 10 ++++++---- xds/build.gradle | 2 +- 10 files changed, 15 insertions(+), 13 deletions(-) diff --git a/alts/build.gradle b/alts/build.gradle index 9477e2540af..de93c90546f 100644 --- a/alts/build.gradle +++ b/alts/build.gradle @@ -2,8 +2,8 @@ plugins { id "java-library" id "maven-publish" - id "com.github.johnrengelman.shadow" id "com.google.protobuf" + id "com.gradleup.shadow" id "ru.vyarus.animalsniffer" } diff --git a/authz/build.gradle b/authz/build.gradle index 491e8f32a74..60c86ca0dba 100644 --- a/authz/build.gradle +++ b/authz/build.gradle @@ -2,8 +2,8 @@ plugins { id "java-library" id "maven-publish" - id "com.github.johnrengelman.shadow" id "com.google.protobuf" + id "com.gradleup.shadow" id "ru.vyarus.animalsniffer" } diff --git a/build.gradle b/build.gradle index f647acdb382..1013a2a4386 100644 --- a/build.gradle +++ b/build.gradle @@ -311,7 +311,7 @@ subprojects { } } - plugins.withId("com.github.johnrengelman.shadow") { + plugins.withId("com.gradleup.shadow") { tasks.named("shadowJar").configure { // Do a dance to remove Class-Path. This needs to run after the doFirst() from the // shadow plugin that adds Class-Path and before the core jar action. Using doFirst will diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 7ade8da565a..54407994dab 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -3,7 +3,7 @@ plugins { id 'java' id "com.google.protobuf" version "0.9.4" - id 'com.google.cloud.tools.jib' version '3.4.3' // For releasing to Docker Hub + id 'com.google.cloud.tools.jib' version '3.4.4' // For releasing to Docker Hub } repositories { diff --git a/examples/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/wrapper/gradle-wrapper.properties index 0d1842103b1..1e2fbf0d458 100644 --- a/examples/gradle/wrapper/gradle-wrapper.properties +++ b/examples/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4413138c96..df97d72b8b9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/netty/shaded/build.gradle b/netty/shaded/build.gradle index a1151ca2427..25682d60b75 100644 --- a/netty/shaded/build.gradle +++ b/netty/shaded/build.gradle @@ -9,7 +9,7 @@ plugins { id "java" id "maven-publish" - id "com.github.johnrengelman.shadow" + id "com.gradleup.shadow" id "ru.vyarus.animalsniffer" } diff --git a/s2a/build.gradle b/s2a/build.gradle index 900283d9a07..572c48a0c5b 100644 --- a/s2a/build.gradle +++ b/s2a/build.gradle @@ -2,9 +2,9 @@ plugins { id "java-library" id "maven-publish" - id "com.github.johnrengelman.shadow" id "com.google.osdetector" id "com.google.protobuf" + id "com.gradleup.shadow" id "ru.vyarus.animalsniffer" } diff --git a/settings.gradle b/settings.gradle index b451242f7c1..1d8600cbe8a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,24 +4,26 @@ pluginManagement { // 8+ has many changes: https://github.com/grpc/grpc-java/issues/10152 id "com.android.application" version "7.4.1" id "com.android.library" version "7.4.1" - // https://github.com/johnrengelman/shadow/releases - id "com.github.johnrengelman.shadow" version "8.1.1" // https://github.com/kt3k/coveralls-gradle-plugin/tags id "com.github.kt3k.coveralls" version "2.12.2" // https://github.com/GoogleCloudPlatform/appengine-plugins/releases id "com.google.cloud.tools.appengine" version "2.8.0" // https://github.com/GoogleContainerTools/jib/blob/master/jib-gradle-plugin/CHANGELOG.md - id "com.google.cloud.tools.jib" version "3.4.3" + id "com.google.cloud.tools.jib" version "3.4.4" // https://github.com/google/osdetector-gradle-plugin/tags id "com.google.osdetector" version "1.7.3" // https://github.com/google/protobuf-gradle-plugin/releases id "com.google.protobuf" version "0.9.4" + // https://github.com/GradleUp/shadow/releases + // 8.3.2+ requires Java 11+ + // 8.3.1 breaks apache imports for netty/shaded, fixed in 8.3.2 + id "com.gradleup.shadow" version "8.3.0" // https://github.com/melix/japicmp-gradle-plugin/blob/master/CHANGELOG.txt id "me.champeau.gradle.japicmp" version "0.4.2" // https://github.com/melix/jmh-gradle-plugin/releases id "me.champeau.jmh" version "0.7.2" // https://github.com/tbroyer/gradle-errorprone-plugin/releases - id "net.ltgt.errorprone" version "4.0.1" + id "net.ltgt.errorprone" version "4.1.0" // https://github.com/xvik/gradle-animalsniffer-plugin/releases id "ru.vyarus.animalsniffer" version "1.7.1" } diff --git a/xds/build.gradle b/xds/build.gradle index a738145a2a0..e09b42d06a9 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -4,8 +4,8 @@ plugins { id "java" id "maven-publish" - id "com.github.johnrengelman.shadow" id "com.google.protobuf" + id "com.gradleup.shadow" id "ru.vyarus.animalsniffer" } From c167ead85120267a7a85086e5ba2b5c644592250 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Wed, 30 Oct 2024 21:11:41 +0530 Subject: [PATCH 071/591] xds: Per-rpc rewriting of the authority header based on the selected route. (#11631) Implementation of A81. --- api/src/main/java/io/grpc/LoadBalancer.java | 33 ++ .../grpc/internal/DelayedClientTransport.java | 12 +- .../java/io/grpc/internal/DelayedStream.java | 1 - .../internal/DelayedClientTransportTest.java | 86 +++++ .../io/grpc/internal/DelayedStreamTest.java | 6 - .../io/grpc/xds/ClusterImplLoadBalancer.java | 27 +- .../grpc/xds/ClusterResolverLoadBalancer.java | 10 +- xds/src/main/java/io/grpc/xds/Endpoints.java | 10 +- .../io/grpc/xds/InternalXdsAttributes.java | 5 + .../main/java/io/grpc/xds/VirtualHost.java | 23 +- .../java/io/grpc/xds/XdsEndpointResource.java | 3 +- .../java/io/grpc/xds/XdsListenerResource.java | 24 +- .../java/io/grpc/xds/XdsNameResolver.java | 9 +- .../grpc/xds/XdsRouteConfigureResource.java | 36 ++- .../java/io/grpc/xds/client/Bootstrapper.java | 9 +- .../io/grpc/xds/client/BootstrapperImpl.java | 5 +- .../grpc/xds/ClusterImplLoadBalancerTest.java | 131 +++++++- .../xds/ClusterResolverLoadBalancerTest.java | 125 +++----- .../java/io/grpc/xds/ControlPlaneRule.java | 14 +- .../java/io/grpc/xds/CsdsServiceTest.java | 1 - .../test/java/io/grpc/xds/DataPlaneRule.java | 14 +- .../FakeControlPlaneXdsIntegrationTest.java | 84 +++-- .../io/grpc/xds/GrpcBootstrapperImplTest.java | 22 ++ .../grpc/xds/GrpcXdsClientImplDataTest.java | 297 ++++++++++++++---- .../grpc/xds/GrpcXdsClientImplTestBase.java | 36 ++- .../io/grpc/xds/GrpcXdsClientImplV3Test.java | 5 +- .../java/io/grpc/xds/XdsNameResolverTest.java | 153 ++++++--- .../io/grpc/xds/XdsServerWrapperTest.java | 3 +- 28 files changed, 875 insertions(+), 309 deletions(-) diff --git a/api/src/main/java/io/grpc/LoadBalancer.java b/api/src/main/java/io/grpc/LoadBalancer.java index 52d37f8677d..97ead20ed36 100644 --- a/api/src/main/java/io/grpc/LoadBalancer.java +++ b/api/src/main/java/io/grpc/LoadBalancer.java @@ -552,6 +552,7 @@ public static final class PickResult { private final Status status; // True if the result is created by withDrop() private final boolean drop; + @Nullable private final String authorityOverride; private PickResult( @Nullable Subchannel subchannel, @Nullable ClientStreamTracer.Factory streamTracerFactory, @@ -560,6 +561,17 @@ private PickResult( this.streamTracerFactory = streamTracerFactory; this.status = checkNotNull(status, "status"); this.drop = drop; + this.authorityOverride = null; + } + + private PickResult( + @Nullable Subchannel subchannel, @Nullable ClientStreamTracer.Factory streamTracerFactory, + Status status, boolean drop, @Nullable String authorityOverride) { + this.subchannel = subchannel; + this.streamTracerFactory = streamTracerFactory; + this.status = checkNotNull(status, "status"); + this.drop = drop; + this.authorityOverride = authorityOverride; } /** @@ -639,6 +651,19 @@ public static PickResult withSubchannel( false); } + /** + * Same as {@code withSubchannel(subchannel, streamTracerFactory)} but with an authority name + * to override in the host header. + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11656") + public static PickResult withSubchannel( + Subchannel subchannel, @Nullable ClientStreamTracer.Factory streamTracerFactory, + @Nullable String authorityOverride) { + return new PickResult( + checkNotNull(subchannel, "subchannel"), streamTracerFactory, Status.OK, + false, authorityOverride); + } + /** * Equivalent to {@code withSubchannel(subchannel, null)}. * @@ -682,6 +707,13 @@ public static PickResult withNoResult() { return NO_RESULT; } + /** Returns the authority override if any. */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11656") + @Nullable + public String getAuthorityOverride() { + return authorityOverride; + } + /** * The Subchannel if this result was created by {@link #withSubchannel withSubchannel()}, or * null otherwise. @@ -736,6 +768,7 @@ public String toString() { .add("streamTracerFactory", streamTracerFactory) .add("status", status) .add("drop", drop) + .add("authority-override", authorityOverride) .toString(); } diff --git a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java index 6eebfdd0fae..ae173f4ac26 100644 --- a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java +++ b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java @@ -131,11 +131,17 @@ public final ClientStream newStream( } if (state.lastPicker != null) { PickResult pickResult = state.lastPicker.pickSubchannel(args); + callOptions = args.getCallOptions(); + // User code provided authority takes precedence over the LB provided one. + if (callOptions.getAuthority() == null + && pickResult.getAuthorityOverride() != null) { + callOptions = callOptions.withAuthority(pickResult.getAuthorityOverride()); + } ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult, callOptions.isWaitForReady()); if (transport != null) { return transport.newStream( - args.getMethodDescriptor(), args.getHeaders(), args.getCallOptions(), + args.getMethodDescriptor(), args.getHeaders(), callOptions, tracers); } } @@ -281,6 +287,10 @@ final void reprocess(@Nullable SubchannelPicker picker) { for (final PendingStream stream : toProcess) { PickResult pickResult = picker.pickSubchannel(stream.args); CallOptions callOptions = stream.args.getCallOptions(); + // User code provided authority takes precedence over the LB provided one. + if (callOptions.getAuthority() == null && pickResult.getAuthorityOverride() != null) { + stream.setAuthority(pickResult.getAuthorityOverride()); + } final ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult, callOptions.isWaitForReady()); if (transport != null) { diff --git a/core/src/main/java/io/grpc/internal/DelayedStream.java b/core/src/main/java/io/grpc/internal/DelayedStream.java index cc9dd0effc7..c94986a3458 100644 --- a/core/src/main/java/io/grpc/internal/DelayedStream.java +++ b/core/src/main/java/io/grpc/internal/DelayedStream.java @@ -208,7 +208,6 @@ private void delayOrExecute(Runnable runnable) { @Override public void setAuthority(final String authority) { - checkState(listener == null, "May only be called before start"); checkNotNull(authority, "authority"); preStartPendingCalls.add(new Runnable() { @Override diff --git a/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java b/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java index c7ae8c8b4be..a5160552a9e 100644 --- a/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java +++ b/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java @@ -502,6 +502,43 @@ public void uncaughtException(Thread t, Throwable e) { verify(transportListener).transportTerminated(); } + @Test + public void reprocess_authorityOverridePresentInCallOptions_authorityOverrideFromLbIsIgnored() { + DelayedStream delayedStream = (DelayedStream) delayedTransport.newStream( + method, headers, callOptions, tracers); + delayedStream.start(mock(ClientStreamListener.class)); + SubchannelPicker picker = mock(SubchannelPicker.class); + PickResult pickResult = PickResult.withSubchannel( + mockSubchannel, null, "authority-override-hostname-from-lb"); + when(picker.pickSubchannel(any(PickSubchannelArgs.class))).thenReturn(pickResult); + + delayedTransport.reprocess(picker); + fakeExecutor.runDueTasks(); + + verify(mockRealStream, never()).setAuthority("authority-override-hostname-from-lb"); + } + + @Test + public void + reprocess_authorityOverrideNotInCallOptions_authorityOverrideFromLbIsSetIntoStream() { + DelayedStream delayedStream = (DelayedStream) delayedTransport.newStream( + method, headers, callOptions.withAuthority(null), tracers); + delayedStream.start(mock(ClientStreamListener.class)); + SubchannelPicker picker = mock(SubchannelPicker.class); + PickResult pickResult = PickResult.withSubchannel( + mockSubchannel, null, "authority-override-hostname-from-lb"); + when(picker.pickSubchannel(any(PickSubchannelArgs.class))).thenReturn(pickResult); + when(mockRealTransport.newStream( + same(method), same(headers), any(CallOptions.class), + ArgumentMatchers.any())) + .thenReturn(mockRealStream); + + delayedTransport.reprocess(picker); + fakeExecutor.runDueTasks(); + + verify(mockRealStream).setAuthority("authority-override-hostname-from-lb"); + } + @Test public void reprocess_NoPendingStream() { SubchannelPicker picker = mock(SubchannelPicker.class); @@ -525,6 +562,55 @@ public void reprocess_NoPendingStream() { assertSame(mockRealStream, stream); } + @Test + public void newStream_assignsTransport_authorityFromCallOptionsSupersedesAuthorityFromLB() { + SubchannelPicker picker = mock(SubchannelPicker.class); + AbstractSubchannel subchannel = mock(AbstractSubchannel.class); + when(subchannel.getInternalSubchannel()).thenReturn(mockInternalSubchannel); + PickResult pickResult = PickResult.withSubchannel( + subchannel, null, "authority-override-hostname-from-lb"); + when(picker.pickSubchannel(any(PickSubchannelArgs.class))).thenReturn(pickResult); + ArgumentCaptor callOptionsArgumentCaptor = + ArgumentCaptor.forClass(CallOptions.class); + when(mockRealTransport.newStream( + any(MethodDescriptor.class), any(Metadata.class), callOptionsArgumentCaptor.capture(), + ArgumentMatchers.any())) + .thenReturn(mockRealStream); + delayedTransport.reprocess(picker); + verifyNoMoreInteractions(picker); + verifyNoMoreInteractions(transportListener); + + CallOptions callOptions = + CallOptions.DEFAULT.withAuthority("authority-override-hosstname-from-calloptions"); + delayedTransport.newStream(method, headers, callOptions, tracers); + assertThat(callOptionsArgumentCaptor.getValue().getAuthority()).isEqualTo( + "authority-override-hosstname-from-calloptions"); + } + + @Test + public void newStream_assignsTransport_authorityFromLB() { + SubchannelPicker picker = mock(SubchannelPicker.class); + AbstractSubchannel subchannel = mock(AbstractSubchannel.class); + when(subchannel.getInternalSubchannel()).thenReturn(mockInternalSubchannel); + PickResult pickResult = PickResult.withSubchannel( + subchannel, null, "authority-override-hostname-from-lb"); + when(picker.pickSubchannel(any(PickSubchannelArgs.class))).thenReturn(pickResult); + ArgumentCaptor callOptionsArgumentCaptor = + ArgumentCaptor.forClass(CallOptions.class); + when(mockRealTransport.newStream( + any(MethodDescriptor.class), any(Metadata.class), callOptionsArgumentCaptor.capture(), + ArgumentMatchers.any())) + .thenReturn(mockRealStream); + delayedTransport.reprocess(picker); + verifyNoMoreInteractions(picker); + verifyNoMoreInteractions(transportListener); + + CallOptions callOptions = CallOptions.DEFAULT; + delayedTransport.newStream(method, headers, callOptions, tracers); + assertThat(callOptionsArgumentCaptor.getValue().getAuthority()).isEqualTo( + "authority-override-hostname-from-lb"); + } + @Test public void reprocess_newStreamRacesWithReprocess() throws Exception { final CyclicBarrier barrier = new CyclicBarrier(2); diff --git a/core/src/test/java/io/grpc/internal/DelayedStreamTest.java b/core/src/test/java/io/grpc/internal/DelayedStreamTest.java index e39e8d420a2..a47bea9f4ab 100644 --- a/core/src/test/java/io/grpc/internal/DelayedStreamTest.java +++ b/core/src/test/java/io/grpc/internal/DelayedStreamTest.java @@ -84,12 +84,6 @@ public void setStream_setAuthority() { inOrder.verify(realStream).start(any(ClientStreamListener.class)); } - @Test(expected = IllegalStateException.class) - public void setAuthority_afterStart() { - stream.start(listener); - stream.setAuthority("notgonnawork"); - } - @Test(expected = IllegalStateException.class) public void start_afterStart() { stream.start(listener); diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index 2a9435aa72f..e4bb28a1b6a 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -34,6 +34,7 @@ import io.grpc.Metadata; import io.grpc.Status; import io.grpc.internal.ForwardingClientStreamTracer; +import io.grpc.internal.GrpcUtil; import io.grpc.internal.ObjectPool; import io.grpc.services.MetricReport; import io.grpc.util.ForwardingLoadBalancerHelper; @@ -231,10 +232,16 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { args.getAddresses().get(0).getAttributes()); AtomicReference localityAtomicReference = new AtomicReference<>( clusterLocality); - Attributes attrs = args.getAttributes().toBuilder() - .set(ATTR_CLUSTER_LOCALITY, localityAtomicReference) - .build(); - args = args.toBuilder().setAddresses(addresses).setAttributes(attrs).build(); + Attributes.Builder attrsBuilder = args.getAttributes().toBuilder() + .set(ATTR_CLUSTER_LOCALITY, localityAtomicReference); + if (GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE", false)) { + String hostname = args.getAddresses().get(0).getAttributes() + .get(InternalXdsAttributes.ATTR_ADDRESS_NAME); + if (hostname != null) { + attrsBuilder.set(InternalXdsAttributes.ATTR_ADDRESS_NAME, hostname); + } + } + args = args.toBuilder().setAddresses(addresses).setAttributes(attrsBuilder.build()).build(); final Subchannel subchannel = delegate().createSubchannel(args); return new ForwardingSubchannel() { @@ -389,7 +396,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { Status.UNAVAILABLE.withDescription("Dropped: " + dropOverload.category())); } } - final PickResult result = delegate.pickSubchannel(args); + PickResult result = delegate.pickSubchannel(args); if (result.getStatus().isOk() && result.getSubchannel() != null) { if (enableCircuitBreaking) { if (inFlights.get() >= maxConcurrentRequests) { @@ -415,9 +422,17 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { stats, inFlights, result.getStreamTracerFactory()); ClientStreamTracer.Factory orcaTracerFactory = OrcaPerRequestUtil.getInstance() .newOrcaClientStreamTracerFactory(tracerFactory, new OrcaPerRpcListener(stats)); - return PickResult.withSubchannel(result.getSubchannel(), orcaTracerFactory); + result = PickResult.withSubchannel(result.getSubchannel(), + orcaTracerFactory); } } + if (args.getCallOptions().getOption(XdsNameResolver.AUTO_HOST_REWRITE_KEY) != null + && args.getCallOptions().getOption(XdsNameResolver.AUTO_HOST_REWRITE_KEY)) { + result = PickResult.withSubchannel(result.getSubchannel(), + result.getStreamTracerFactory(), + result.getSubchannel().getAttributes().get( + InternalXdsAttributes.ATTR_ADDRESS_NAME)); + } } return result; } diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index 875a6c45020..3ef79699b10 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -428,6 +428,7 @@ public void run() { .set(InternalXdsAttributes.ATTR_LOCALITY_WEIGHT, localityLbInfo.localityWeight()) .set(InternalXdsAttributes.ATTR_SERVER_WEIGHT, weight) + .set(InternalXdsAttributes.ATTR_ADDRESS_NAME, endpoint.hostname()) .build(); EquivalentAddressGroup eag = new EquivalentAddressGroup( endpoint.eag().getAddresses(), attr); @@ -567,7 +568,7 @@ void start() { handleEndpointResolutionError(); return; } - resolver.start(new NameResolverListener()); + resolver.start(new NameResolverListener(dnsHostName)); } void refresh() { @@ -606,6 +607,12 @@ public void run() { } private class NameResolverListener extends NameResolver.Listener2 { + private final String dnsHostName; + + NameResolverListener(String dnsHostName) { + this.dnsHostName = dnsHostName; + } + @Override public void onResult(final ResolutionResult resolutionResult) { class NameResolved implements Runnable { @@ -625,6 +632,7 @@ public void run() { Attributes attr = eag.getAttributes().toBuilder() .set(InternalXdsAttributes.ATTR_LOCALITY, LOGICAL_DNS_CLUSTER_LOCALITY) .set(InternalXdsAttributes.ATTR_LOCALITY_NAME, localityName) + .set(InternalXdsAttributes.ATTR_ADDRESS_NAME, dnsHostName) .build(); eag = new EquivalentAddressGroup(eag.getAddresses(), attr); eag = AddressFilter.setPathFilter(eag, Arrays.asList(priorityName, localityName)); diff --git a/xds/src/main/java/io/grpc/xds/Endpoints.java b/xds/src/main/java/io/grpc/xds/Endpoints.java index 8b1715731df..7d7aa3e386d 100644 --- a/xds/src/main/java/io/grpc/xds/Endpoints.java +++ b/xds/src/main/java/io/grpc/xds/Endpoints.java @@ -61,17 +61,19 @@ abstract static class LbEndpoint { // Whether the endpoint is healthy. abstract boolean isHealthy(); + abstract String hostname(); + static LbEndpoint create(EquivalentAddressGroup eag, int loadBalancingWeight, - boolean isHealthy) { - return new AutoValue_Endpoints_LbEndpoint(eag, loadBalancingWeight, isHealthy); + boolean isHealthy, String hostname) { + return new AutoValue_Endpoints_LbEndpoint(eag, loadBalancingWeight, isHealthy, hostname); } // Only for testing. @VisibleForTesting static LbEndpoint create( - String address, int port, int loadBalancingWeight, boolean isHealthy) { + String address, int port, int loadBalancingWeight, boolean isHealthy, String hostname) { return LbEndpoint.create(new EquivalentAddressGroup(new InetSocketAddress(address, port)), - loadBalancingWeight, isHealthy); + loadBalancingWeight, isHealthy, hostname); } } diff --git a/xds/src/main/java/io/grpc/xds/InternalXdsAttributes.java b/xds/src/main/java/io/grpc/xds/InternalXdsAttributes.java index aaaeb198d21..575bda73f0c 100644 --- a/xds/src/main/java/io/grpc/xds/InternalXdsAttributes.java +++ b/xds/src/main/java/io/grpc/xds/InternalXdsAttributes.java @@ -91,6 +91,11 @@ public final class InternalXdsAttributes { static final Attributes.Key ATTR_SERVER_WEIGHT = Attributes.Key.create("io.grpc.xds.InternalXdsAttributes.serverWeight"); + /** Name associated with individual address, if available (e.g., DNS name). */ + @EquivalentAddressGroup.Attr + static final Attributes.Key ATTR_ADDRESS_NAME = + Attributes.Key.create("io.grpc.xds.InternalXdsAttributes.addressName"); + /** * Filter chain match for network filters. */ diff --git a/xds/src/main/java/io/grpc/xds/VirtualHost.java b/xds/src/main/java/io/grpc/xds/VirtualHost.java index d9f93dd3a07..7dfa0b34a35 100644 --- a/xds/src/main/java/io/grpc/xds/VirtualHost.java +++ b/xds/src/main/java/io/grpc/xds/VirtualHost.java @@ -166,29 +166,34 @@ abstract static class RouteAction { @Nullable abstract RetryPolicy retryPolicy(); + abstract boolean autoHostRewrite(); + static RouteAction forCluster( String cluster, List hashPolicies, @Nullable Long timeoutNano, - @Nullable RetryPolicy retryPolicy) { + @Nullable RetryPolicy retryPolicy, boolean autoHostRewrite) { checkNotNull(cluster, "cluster"); - return RouteAction.create(hashPolicies, timeoutNano, cluster, null, null, retryPolicy); + return RouteAction.create(hashPolicies, timeoutNano, cluster, null, null, retryPolicy, + autoHostRewrite); } static RouteAction forWeightedClusters( List weightedClusters, List hashPolicies, - @Nullable Long timeoutNano, @Nullable RetryPolicy retryPolicy) { + @Nullable Long timeoutNano, @Nullable RetryPolicy retryPolicy, boolean autoHostRewrite) { checkNotNull(weightedClusters, "weightedClusters"); checkArgument(!weightedClusters.isEmpty(), "empty cluster list"); return RouteAction.create( - hashPolicies, timeoutNano, null, weightedClusters, null, retryPolicy); + hashPolicies, timeoutNano, null, weightedClusters, null, retryPolicy, autoHostRewrite); } static RouteAction forClusterSpecifierPlugin( NamedPluginConfig namedConfig, List hashPolicies, @Nullable Long timeoutNano, - @Nullable RetryPolicy retryPolicy) { + @Nullable RetryPolicy retryPolicy, + boolean autoHostRewrite) { checkNotNull(namedConfig, "namedConfig"); - return RouteAction.create(hashPolicies, timeoutNano, null, null, namedConfig, retryPolicy); + return RouteAction.create(hashPolicies, timeoutNano, null, null, namedConfig, retryPolicy, + autoHostRewrite); } private static RouteAction create( @@ -197,14 +202,16 @@ private static RouteAction create( @Nullable String cluster, @Nullable List weightedClusters, @Nullable NamedPluginConfig namedConfig, - @Nullable RetryPolicy retryPolicy) { + @Nullable RetryPolicy retryPolicy, + boolean autoHostRewrite) { return new AutoValue_VirtualHost_Route_RouteAction( ImmutableList.copyOf(hashPolicies), timeoutNano, cluster, weightedClusters == null ? null : ImmutableList.copyOf(weightedClusters), namedConfig, - retryPolicy); + retryPolicy, + autoHostRewrite); } @AutoValue diff --git a/xds/src/main/java/io/grpc/xds/XdsEndpointResource.java b/xds/src/main/java/io/grpc/xds/XdsEndpointResource.java index 3ed68ac9b75..6a3cd35bd59 100644 --- a/xds/src/main/java/io/grpc/xds/XdsEndpointResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsEndpointResource.java @@ -213,7 +213,8 @@ static StructOrError parseLocalityLbEndpoints( || (endpoint.getHealthStatus() == HealthStatus.UNKNOWN); endpoints.add(Endpoints.LbEndpoint.create( new EquivalentAddressGroup(addresses), - endpoint.getLoadBalancingWeight().getValue(), isHealthy)); + endpoint.getLoadBalancingWeight().getValue(), isHealthy, + endpoint.getEndpoint().getHostname())); } return StructOrError.fromStruct(Endpoints.LocalityLbEndpoints.create( endpoints, proto.getLoadBalancingWeight().getValue(), proto.getPriority())); diff --git a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java index af77d128ae7..141580af73d 100644 --- a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java @@ -108,13 +108,13 @@ protected LdsUpdate doParse(Args args, Message unpackedMessage) Listener listener = (Listener) unpackedMessage; if (listener.hasApiListener()) { - return processClientSideListener(listener); + return processClientSideListener(listener, args); } else { return processServerSideListener(listener, args); } } - private LdsUpdate processClientSideListener(Listener listener) + private LdsUpdate processClientSideListener(Listener listener, XdsResourceType.Args args) throws ResourceInvalidException { // Unpack HttpConnectionManager from the Listener. HttpConnectionManager hcm; @@ -127,10 +127,10 @@ private LdsUpdate processClientSideListener(Listener listener) "Could not parse HttpConnectionManager config from ApiListener", e); } return LdsUpdate.forApiListener( - parseHttpConnectionManager(hcm, filterRegistry, true /* isForClient */)); + parseHttpConnectionManager(hcm, filterRegistry, true /* isForClient */, args)); } - private LdsUpdate processServerSideListener(Listener proto, Args args) + private LdsUpdate processServerSideListener(Listener proto, XdsResourceType.Args args) throws ResourceInvalidException { Set certProviderInstances = null; if (args.getBootstrapInfo() != null && args.getBootstrapInfo().certProviders() != null) { @@ -138,13 +138,13 @@ private LdsUpdate processServerSideListener(Listener proto, Args args) } return LdsUpdate.forTcpListener(parseServerSideListener(proto, (TlsContextManager) args.getSecurityConfig(), - filterRegistry, certProviderInstances)); + filterRegistry, certProviderInstances, args)); } @VisibleForTesting static EnvoyServerProtoData.Listener parseServerSideListener( Listener proto, TlsContextManager tlsContextManager, - FilterRegistry filterRegistry, Set certProviderInstances) + FilterRegistry filterRegistry, Set certProviderInstances, XdsResourceType.Args args) throws ResourceInvalidException { if (!proto.getTrafficDirection().equals(TrafficDirection.INBOUND) && !proto.getTrafficDirection().equals(TrafficDirection.UNSPECIFIED)) { @@ -182,13 +182,13 @@ static EnvoyServerProtoData.Listener parseServerSideListener( for (io.envoyproxy.envoy.config.listener.v3.FilterChain fc : proto.getFilterChainsList()) { filterChains.add( parseFilterChain(fc, tlsContextManager, filterRegistry, uniqueSet, - certProviderInstances)); + certProviderInstances, args)); } FilterChain defaultFilterChain = null; if (proto.hasDefaultFilterChain()) { defaultFilterChain = parseFilterChain( proto.getDefaultFilterChain(), tlsContextManager, filterRegistry, - null, certProviderInstances); + null, certProviderInstances, args); } return EnvoyServerProtoData.Listener.create( @@ -199,7 +199,7 @@ static EnvoyServerProtoData.Listener parseServerSideListener( static FilterChain parseFilterChain( io.envoyproxy.envoy.config.listener.v3.FilterChain proto, TlsContextManager tlsContextManager, FilterRegistry filterRegistry, - Set uniqueSet, Set certProviderInstances) + Set uniqueSet, Set certProviderInstances, XdsResourceType.Args args) throws ResourceInvalidException { if (proto.getFiltersCount() != 1) { throw new ResourceInvalidException("FilterChain " + proto.getName() @@ -226,7 +226,7 @@ static FilterChain parseFilterChain( + filter.getName() + " failed to unpack message", e); } io.grpc.xds.HttpConnectionManager httpConnectionManager = parseHttpConnectionManager( - hcmProto, filterRegistry, false /* isForClient */); + hcmProto, filterRegistry, false /* isForClient */, args); EnvoyServerProtoData.DownstreamTlsContext downstreamTlsContext = null; if (proto.hasTransportSocket()) { @@ -458,7 +458,7 @@ private static FilterChainMatch parseFilterChainMatch( @VisibleForTesting static io.grpc.xds.HttpConnectionManager parseHttpConnectionManager( HttpConnectionManager proto, FilterRegistry filterRegistry, - boolean isForClient) throws ResourceInvalidException { + boolean isForClient, XdsResourceType.Args args) throws ResourceInvalidException { if (proto.getXffNumTrustedHops() != 0) { throw new ResourceInvalidException( "HttpConnectionManager with xff_num_trusted_hops unsupported"); @@ -515,7 +515,7 @@ static io.grpc.xds.HttpConnectionManager parseHttpConnectionManager( // Parse inlined RouteConfiguration or RDS. if (proto.hasRouteConfig()) { List virtualHosts = extractVirtualHosts( - proto.getRouteConfig(), filterRegistry); + proto.getRouteConfig(), filterRegistry, args); return io.grpc.xds.HttpConnectionManager.forVirtualHosts( maxStreamDuration, virtualHosts, filterConfigs); } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index ca73b7d8451..0beb6dc2483 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -96,6 +96,8 @@ final class XdsNameResolver extends NameResolver { CallOptions.Key.create("io.grpc.xds.CLUSTER_SELECTION_KEY"); static final CallOptions.Key RPC_HASH_KEY = CallOptions.Key.create("io.grpc.xds.RPC_HASH_KEY"); + static final CallOptions.Key AUTO_HOST_REWRITE_KEY = + CallOptions.Key.create("io.grpc.xds.AUTO_HOST_REWRITE_KEY"); @VisibleForTesting static boolean enableTimeout = Strings.isNullOrEmpty(System.getenv("GRPC_XDS_EXPERIMENTAL_ENABLE_TIMEOUT")) @@ -217,6 +219,7 @@ public void start(Listener2 listener) { ldsResourceName = XdsClient.canonifyResourceName(ldsResourceName); callCounterProvider = SharedCallCounterMap.getInstance(); resolveState = new ResolveState(ldsResourceName); + resolveState.start(); } @@ -465,14 +468,18 @@ public Result selectConfig(PickSubchannelArgs args) { } final String finalCluster = cluster; final long hash = generateHash(selectedRoute.routeAction().hashPolicies(), headers); + Route finalSelectedRoute = selectedRoute; class ClusterSelectionInterceptor implements ClientInterceptor { @Override public ClientCall interceptCall( final MethodDescriptor method, CallOptions callOptions, final Channel next) { - final CallOptions callOptionsForCluster = + CallOptions callOptionsForCluster = callOptions.withOption(CLUSTER_SELECTION_KEY, finalCluster) .withOption(RPC_HASH_KEY, hash); + if (finalSelectedRoute.routeAction().autoHostRewrite()) { + callOptionsForCluster = callOptionsForCluster.withOption(AUTO_HOST_REWRITE_KEY, true); + } return new SimpleForwardingClientCall( next.newCall(method, callOptionsForCluster)) { @Override diff --git a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java index 0a3d1406dac..0e065c6ba9a 100644 --- a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java @@ -68,6 +68,9 @@ import javax.annotation.Nullable; class XdsRouteConfigureResource extends XdsResourceType { + + private static final String GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE = + "GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE"; @VisibleForTesting static boolean enableRouteLookup = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_RLS_LB", true); @@ -128,17 +131,17 @@ protected RdsUpdate doParse(XdsResourceType.Args args, Message unpackedMessage) throw new ResourceInvalidException("Invalid message type: " + unpackedMessage.getClass()); } return processRouteConfiguration( - (RouteConfiguration) unpackedMessage, FilterRegistry.getDefaultRegistry()); + (RouteConfiguration) unpackedMessage, FilterRegistry.getDefaultRegistry(), args); } private static RdsUpdate processRouteConfiguration( - RouteConfiguration routeConfig, FilterRegistry filterRegistry) + RouteConfiguration routeConfig, FilterRegistry filterRegistry, XdsResourceType.Args args) throws ResourceInvalidException { - return new RdsUpdate(extractVirtualHosts(routeConfig, filterRegistry)); + return new RdsUpdate(extractVirtualHosts(routeConfig, filterRegistry, args)); } static List extractVirtualHosts( - RouteConfiguration routeConfig, FilterRegistry filterRegistry) + RouteConfiguration routeConfig, FilterRegistry filterRegistry, XdsResourceType.Args args) throws ResourceInvalidException { Map pluginConfigMap = new HashMap<>(); ImmutableSet.Builder optionalPlugins = ImmutableSet.builder(); @@ -164,7 +167,7 @@ static List extractVirtualHosts( : routeConfig.getVirtualHostsList()) { StructOrError virtualHost = parseVirtualHost(virtualHostProto, filterRegistry, pluginConfigMap, - optionalPlugins.build()); + optionalPlugins.build(), args); if (virtualHost.getErrorDetail() != null) { throw new ResourceInvalidException( "RouteConfiguration contains invalid virtual host: " + virtualHost.getErrorDetail()); @@ -177,12 +180,12 @@ static List extractVirtualHosts( private static StructOrError parseVirtualHost( io.envoyproxy.envoy.config.route.v3.VirtualHost proto, FilterRegistry filterRegistry, Map pluginConfigMap, - Set optionalPlugins) { + Set optionalPlugins, XdsResourceType.Args args) { String name = proto.getName(); List routes = new ArrayList<>(proto.getRoutesCount()); for (io.envoyproxy.envoy.config.route.v3.Route routeProto : proto.getRoutesList()) { StructOrError route = parseRoute( - routeProto, filterRegistry, pluginConfigMap, optionalPlugins); + routeProto, filterRegistry, pluginConfigMap, optionalPlugins, args); if (route == null) { continue; } @@ -264,7 +267,7 @@ static StructOrError> parseOverrideFilterConfigs( static StructOrError parseRoute( io.envoyproxy.envoy.config.route.v3.Route proto, FilterRegistry filterRegistry, Map pluginConfigMap, - Set optionalPlugins) { + Set optionalPlugins, XdsResourceType.Args args) { StructOrError routeMatch = parseRouteMatch(proto.getMatch()); if (routeMatch == null) { return null; @@ -288,7 +291,7 @@ static StructOrError parseRoute( case ROUTE: StructOrError routeAction = parseRouteAction(proto.getRoute(), filterRegistry, pluginConfigMap, - optionalPlugins); + optionalPlugins, args); if (routeAction == null) { return null; } @@ -414,7 +417,7 @@ static StructOrError parseHeaderMatcher( static StructOrError parseRouteAction( io.envoyproxy.envoy.config.route.v3.RouteAction proto, FilterRegistry filterRegistry, Map pluginConfigMap, - Set optionalPlugins) { + Set optionalPlugins, XdsResourceType.Args args) { Long timeoutNano = null; if (proto.hasMaxStreamDuration()) { io.envoyproxy.envoy.config.route.v3.RouteAction.MaxStreamDuration maxStreamDuration @@ -470,7 +473,9 @@ static StructOrError parseRouteAction( switch (proto.getClusterSpecifierCase()) { case CLUSTER: return StructOrError.fromStruct(RouteAction.forCluster( - proto.getCluster(), hashPolicies, timeoutNano, retryPolicy)); + proto.getCluster(), hashPolicies, timeoutNano, retryPolicy, + GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, false) + && args.getServerInfo().isTrustedXdsServer() && proto.getAutoHostRewrite().getValue())); case CLUSTER_HEADER: return null; case WEIGHTED_CLUSTERS: @@ -502,7 +507,9 @@ static StructOrError parseRouteAction( UnsignedInteger.MAX_VALUE.longValue(), clusterWeightSum)); } return StructOrError.fromStruct(VirtualHost.Route.RouteAction.forWeightedClusters( - weightedClusters, hashPolicies, timeoutNano, retryPolicy)); + weightedClusters, hashPolicies, timeoutNano, retryPolicy, + GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, false) + && args.getServerInfo().isTrustedXdsServer() && proto.getAutoHostRewrite().getValue())); case CLUSTER_SPECIFIER_PLUGIN: if (enableRouteLookup) { String pluginName = proto.getClusterSpecifierPlugin(); @@ -517,7 +524,10 @@ static StructOrError parseRouteAction( } NamedPluginConfig namedPluginConfig = NamedPluginConfig.create(pluginName, pluginConfig); return StructOrError.fromStruct(VirtualHost.Route.RouteAction.forClusterSpecifierPlugin( - namedPluginConfig, hashPolicies, timeoutNano, retryPolicy)); + namedPluginConfig, hashPolicies, timeoutNano, retryPolicy, + GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, false) + && args.getServerInfo().isTrustedXdsServer() + && proto.getAutoHostRewrite().getValue())); } else { return null; } diff --git a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java index fe0c0050b52..90babd1e8d0 100644 --- a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java @@ -61,16 +61,19 @@ public abstract static class ServerInfo { public abstract boolean ignoreResourceDeletion(); + public abstract boolean isTrustedXdsServer(); + @VisibleForTesting public static ServerInfo create(String target, @Nullable Object implSpecificConfig) { - return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, false); + return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, false, false); } @VisibleForTesting public static ServerInfo create( - String target, Object implSpecificConfig, boolean ignoreResourceDeletion) { + String target, Object implSpecificConfig, boolean ignoreResourceDeletion, + boolean isTrustedXdsServer) { return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, - ignoreResourceDeletion); + ignoreResourceDeletion, isTrustedXdsServer); } } diff --git a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java index 7ef739c8048..9930417348b 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -50,6 +50,7 @@ public abstract class BootstrapperImpl extends Bootstrapper { // Server features. private static final String SERVER_FEATURE_IGNORE_RESOURCE_DELETION = "ignore_resource_deletion"; + private static final String SERVER_FEATURE_TRUSTED_XDS_SERVER = "trusted_xds_server"; protected final XdsLogger logger; @@ -240,7 +241,9 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo ignoreResourceDeletion = serverFeatures.contains(SERVER_FEATURE_IGNORE_RESOURCE_DELETION); } servers.add( - ServerInfo.create(serverUri, implSpecificConfig, ignoreResourceDeletion)); + ServerInfo.create(serverUri, implSpecificConfig, ignoreResourceDeletion, + serverFeatures != null + && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER))); } return servers.build(); } diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index 7eba43ce278..0d18af0b04a 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static io.grpc.xds.XdsNameResolver.AUTO_HOST_REWRITE_KEY; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -36,6 +37,7 @@ import io.grpc.InsecureChannelCredentials; import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.CreateSubchannelArgs; +import io.grpc.LoadBalancer.FixedResultPicker; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancer.PickDetailsConsumer; import io.grpc.LoadBalancer.PickResult; @@ -748,6 +750,106 @@ public void endpointAddressesAttachedWithClusterName() { } } + @Test + public void + endpointsWithAuthorityHostname_autoHostRewriteEnabled_pickResultHasAuthorityHostname() { + System.setProperty("GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE", "true"); + try { + LoadBalancerProvider weightedTargetProvider = new WeightedTargetLoadBalancerProvider(); + WeightedTargetConfig weightedTargetConfig = + buildWeightedTargetConfig(ImmutableMap.of(locality, 10)); + ClusterImplConfig config = new ClusterImplConfig(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, + null, Collections.emptyList(), + GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + weightedTargetProvider, weightedTargetConfig), + null, Collections.emptyMap()); + EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr1", locality, + "authority-host-name"); + deliverAddressesAndConfig(Arrays.asList(endpoint1), config); + assertThat(downstreamBalancers).hasSize(1); // one leaf balancer + FakeLoadBalancer leafBalancer = Iterables.getOnlyElement(downstreamBalancers); + assertThat(leafBalancer.name).isEqualTo("round_robin"); + + // Simulates leaf load balancer creating subchannels. + CreateSubchannelArgs args = + CreateSubchannelArgs.newBuilder() + .setAddresses(leafBalancer.addresses) + .build(); + Subchannel subchannel = leafBalancer.helper.createSubchannel(args); + subchannel.start(infoObject -> { + if (infoObject.getState() == ConnectivityState.READY) { + helper.updateBalancingState( + ConnectivityState.READY, + new FixedResultPicker(PickResult.withSubchannel(subchannel))); + } + }); + assertThat(subchannel.getAttributes().get(InternalXdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo( + "authority-host-name"); + for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) { + assertThat(eag.getAttributes().get(InternalXdsAttributes.ATTR_ADDRESS_NAME)) + .isEqualTo("authority-host-name"); + } + + leafBalancer.deliverSubchannelState(subchannel, ConnectivityState.READY); + assertThat(currentState).isEqualTo(ConnectivityState.READY); + PickDetailsConsumer detailsConsumer = mock(PickDetailsConsumer.class); + pickSubchannelArgs = new PickSubchannelArgsImpl( + TestMethodDescriptors.voidMethod(), new Metadata(), + CallOptions.DEFAULT.withOption(AUTO_HOST_REWRITE_KEY, true), detailsConsumer); + PickResult result = currentPicker.pickSubchannel(pickSubchannelArgs); + assertThat(result.getAuthorityOverride()).isEqualTo("authority-host-name"); + } finally { + System.clearProperty("GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE"); + } + } + + @Test + public void + endpointWithAuthorityHostname_autoHostRewriteNotEnabled_pickResultNoAuthorityHostname() { + LoadBalancerProvider weightedTargetProvider = new WeightedTargetLoadBalancerProvider(); + WeightedTargetConfig weightedTargetConfig = + buildWeightedTargetConfig(ImmutableMap.of(locality, 10)); + ClusterImplConfig config = new ClusterImplConfig(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, + null, Collections.emptyList(), + GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + weightedTargetProvider, weightedTargetConfig), + null, Collections.emptyMap()); + EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr1", locality, + "authority-host-name"); + deliverAddressesAndConfig(Arrays.asList(endpoint1), config); + assertThat(downstreamBalancers).hasSize(1); // one leaf balancer + FakeLoadBalancer leafBalancer = Iterables.getOnlyElement(downstreamBalancers); + assertThat(leafBalancer.name).isEqualTo("round_robin"); + + // Simulates leaf load balancer creating subchannels. + CreateSubchannelArgs args = + CreateSubchannelArgs.newBuilder() + .setAddresses(leafBalancer.addresses) + .build(); + Subchannel subchannel = leafBalancer.helper.createSubchannel(args); + subchannel.start(infoObject -> { + if (infoObject.getState() == ConnectivityState.READY) { + helper.updateBalancingState( + ConnectivityState.READY, + new FixedResultPicker(PickResult.withSubchannel(subchannel))); + } + }); + // Sub Channel wrapper args won't have the address name although addresses will. + assertThat(subchannel.getAttributes().get(InternalXdsAttributes.ATTR_ADDRESS_NAME)).isNull(); + for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) { + assertThat(eag.getAttributes().get(InternalXdsAttributes.ATTR_ADDRESS_NAME)) + .isEqualTo("authority-host-name"); + } + + leafBalancer.deliverSubchannelState(subchannel, ConnectivityState.READY); + assertThat(currentState).isEqualTo(ConnectivityState.READY); + PickDetailsConsumer detailsConsumer = mock(PickDetailsConsumer.class); + pickSubchannelArgs = new PickSubchannelArgsImpl( + TestMethodDescriptors.voidMethod(), new Metadata(), CallOptions.DEFAULT, detailsConsumer); + PickResult result = currentPicker.pickSubchannel(pickSubchannelArgs); + assertThat(result.getAuthorityOverride()).isNull(); + } + @Test public void endpointAddressesAttachedWithTlsConfig_securityEnabledByDefault() { UpstreamTlsContext upstreamTlsContext = @@ -848,6 +950,11 @@ private WeightedTargetConfig buildWeightedTargetConfig(Map lo * Create a locality-labeled address. */ private static EquivalentAddressGroup makeAddress(final String name, Locality locality) { + return makeAddress(name, locality, null); + } + + private static EquivalentAddressGroup makeAddress(final String name, Locality locality, + String authorityHostname) { class FakeSocketAddress extends SocketAddress { private final String name; @@ -878,12 +985,15 @@ public String toString() { } } + Attributes.Builder attributes = Attributes.newBuilder() + .set(InternalXdsAttributes.ATTR_LOCALITY, locality) + // Unique but arbitrary string + .set(InternalXdsAttributes.ATTR_LOCALITY_NAME, locality.toString()); + if (authorityHostname != null) { + attributes.set(InternalXdsAttributes.ATTR_ADDRESS_NAME, authorityHostname); + } EquivalentAddressGroup eag = new EquivalentAddressGroup(new FakeSocketAddress(name), - Attributes.newBuilder() - .set(InternalXdsAttributes.ATTR_LOCALITY, locality) - // Unique but arbitrary string - .set(InternalXdsAttributes.ATTR_LOCALITY_NAME, locality.toString()) - .build()); + attributes.build()); return AddressFilter.setPathFilter(eag, Collections.singletonList(locality.toString())); } @@ -948,6 +1058,16 @@ public void shutdown() { downstreamBalancers.remove(this); } + void deliverSubchannelState(final Subchannel subchannel, ConnectivityState state) { + SubchannelPicker picker = new SubchannelPicker() { + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + return PickResult.withSubchannel(subchannel); + } + }; + helper.updateBalancingState(state, picker); + } + Subchannel createSubChannel() { Subchannel subchannel = helper.createSubchannel( CreateSubchannelArgs.newBuilder().setAddresses(addresses).build()); @@ -1078,6 +1198,7 @@ public void setConnectedEagIndex(int eagIndex) { } private final class FakeXdsClient extends XdsClient { + @Override public ClusterDropStats addClusterDropStats( ServerInfo lrsServerInfo, String clusterName, @Nullable String edsServiceName) { diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index 0ecd77b12cb..29c46963da3 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -61,7 +61,6 @@ import io.grpc.internal.ObjectPool; import io.grpc.util.GracefulSwitchLoadBalancer; import io.grpc.util.GracefulSwitchLoadBalancerAccessor; -import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig; import io.grpc.util.OutlierDetectionLoadBalancerProvider; import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; @@ -253,13 +252,13 @@ public void edsClustersWithRingHashEndpointLbPolicy() { LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( Arrays.asList( - LbEndpoint.create(endpoint1, 0 /* loadBalancingWeight */, true), - LbEndpoint.create(endpoint2, 0 /* loadBalancingWeight */, true)), + LbEndpoint.create(endpoint1, 0 /* loadBalancingWeight */, true, "hostname1"), + LbEndpoint.create(endpoint2, 0 /* loadBalancingWeight */, true, "hostname2")), 10 /* localityWeight */, 1 /* priority */); LocalityLbEndpoints localityLbEndpoints2 = LocalityLbEndpoints.create( Collections.singletonList( - LbEndpoint.create(endpoint3, 60 /* loadBalancingWeight */, true)), + LbEndpoint.create(endpoint3, 60 /* loadBalancingWeight */, true, "hostname3")), 50 /* localityWeight */, 1 /* priority */); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, @@ -313,7 +312,7 @@ public void edsClustersWithLeastRequestEndpointLbPolicy() { LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( Arrays.asList( - LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true)), + LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, "hostname1")), 100 /* localityWeight */, 1 /* priority */); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, @@ -329,7 +328,7 @@ public void edsClustersWithLeastRequestEndpointLbPolicy() { PriorityChildConfig priorityChildConfig = Iterables.getOnlyElement(priorityLbConfig.childConfigs.values()); assertThat(GracefulSwitchLoadBalancerAccessor.getChildProvider(priorityChildConfig.childConfig) - .getPolicyName()) + .getPolicyName()) .isEqualTo(CLUSTER_IMPL_POLICY_NAME); ClusterImplConfig clusterImplConfig = (ClusterImplConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig.childConfig); @@ -347,7 +346,7 @@ public void edsClustersWithLeastRequestEndpointLbPolicy() { } @Test - public void edsClustersWithOutlierDetection() { + public void edsClustersEndpointHostname_addedToAddressAttribute() { ClusterResolverConfig config = new ClusterResolverConfig( Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection), leastRequest); deliverLbConfig(config); @@ -359,76 +358,17 @@ public void edsClustersWithOutlierDetection() { LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( Arrays.asList( - LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true)), + LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, "hostname1")), 100 /* localityWeight */, 1 /* priority */); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, ImmutableMap.of(locality1, localityLbEndpoints)); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(childBalancer.addresses).hasSize(1); - EquivalentAddressGroup addr = childBalancer.addresses.get(0); - assertThat(addr.getAddresses()).isEqualTo(endpoint.getAddresses()); - assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); - PriorityLbConfig priorityLbConfig = (PriorityLbConfig) childBalancer.config; - assertThat(priorityLbConfig.priorities).containsExactly(CLUSTER1 + "[child1]"); - PriorityChildConfig priorityChildConfig = - Iterables.getOnlyElement(priorityLbConfig.childConfigs.values()); - - // The child config for priority should be outlier detection. - assertThat(GracefulSwitchLoadBalancerAccessor.getChildProvider(priorityChildConfig.childConfig) - .getPolicyName()) - .isEqualTo("outlier_detection_experimental"); - OutlierDetectionLoadBalancerConfig outlierDetectionConfig = (OutlierDetectionLoadBalancerConfig) - GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig.childConfig); - - // The outlier detection config should faithfully represent what came down from xDS. - assertThat(outlierDetectionConfig.intervalNanos).isEqualTo(outlierDetection.intervalNanos()); - assertThat(outlierDetectionConfig.baseEjectionTimeNanos).isEqualTo( - outlierDetection.baseEjectionTimeNanos()); - assertThat(outlierDetectionConfig.baseEjectionTimeNanos).isEqualTo( - outlierDetection.baseEjectionTimeNanos()); - assertThat(outlierDetectionConfig.maxEjectionTimeNanos).isEqualTo( - outlierDetection.maxEjectionTimeNanos()); - assertThat(outlierDetectionConfig.maxEjectionPercent).isEqualTo( - outlierDetection.maxEjectionPercent()); - - OutlierDetectionLoadBalancerConfig.SuccessRateEjection successRateEjection - = outlierDetectionConfig.successRateEjection; - assertThat(successRateEjection.stdevFactor).isEqualTo( - outlierDetection.successRateEjection().stdevFactor()); - assertThat(successRateEjection.enforcementPercentage).isEqualTo( - outlierDetection.successRateEjection().enforcementPercentage()); - assertThat(successRateEjection.minimumHosts).isEqualTo( - outlierDetection.successRateEjection().minimumHosts()); - assertThat(successRateEjection.requestVolume).isEqualTo( - outlierDetection.successRateEjection().requestVolume()); - - OutlierDetectionLoadBalancerConfig.FailurePercentageEjection failurePercentageEjection - = outlierDetectionConfig.failurePercentageEjection; - assertThat(failurePercentageEjection.threshold).isEqualTo( - outlierDetection.failurePercentageEjection().threshold()); - assertThat(failurePercentageEjection.enforcementPercentage).isEqualTo( - outlierDetection.failurePercentageEjection().enforcementPercentage()); - assertThat(failurePercentageEjection.minimumHosts).isEqualTo( - outlierDetection.failurePercentageEjection().minimumHosts()); - assertThat(failurePercentageEjection.requestVolume).isEqualTo( - outlierDetection.failurePercentageEjection().requestVolume()); - - // The wrapped configuration should not have been tampered with. - ClusterImplConfig clusterImplConfig = (ClusterImplConfig) - GracefulSwitchLoadBalancerAccessor.getChildConfig(outlierDetectionConfig.childConfig); - assertClusterImplConfig(clusterImplConfig, CLUSTER1, EDS_SERVICE_NAME1, LRS_SERVER_INFO, 100L, - tlsContext, Collections.emptyList(), WRR_LOCALITY_POLICY_NAME); - WrrLocalityConfig wrrLocalityConfig = (WrrLocalityConfig) - GracefulSwitchLoadBalancerAccessor.getChildConfig(clusterImplConfig.childConfig); - LoadBalancerProvider childProvider = - GracefulSwitchLoadBalancerAccessor.getChildProvider(wrrLocalityConfig.childConfig); - assertThat(childProvider.getPolicyName()).isEqualTo("least_request_experimental"); assertThat( childBalancer.addresses.get(0).getAttributes() - .get(InternalXdsAttributes.ATTR_LOCALITY_WEIGHT)).isEqualTo(100); + .get(InternalXdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo("hostname1"); } @@ -449,16 +389,16 @@ public void onlyEdsClusters_receivedEndpoints() { LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( Arrays.asList( - LbEndpoint.create(endpoint1, 100, true), - LbEndpoint.create(endpoint2, 100, true)), + LbEndpoint.create(endpoint1, 100, true, "hostname1"), + LbEndpoint.create(endpoint2, 100, true, "hostname1")), 70 /* localityWeight */, 1 /* priority */); LocalityLbEndpoints localityLbEndpoints2 = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint3, 100, true)), + Collections.singletonList(LbEndpoint.create(endpoint3, 100, true, "hostname2")), 10 /* localityWeight */, 1 /* priority */); LocalityLbEndpoints localityLbEndpoints3 = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint4, 100, true)), + Collections.singletonList(LbEndpoint.create(endpoint4, 100, true, "hostname3")), 20 /* localityWeight */, 2 /* priority */); String priority1 = CLUSTER2 + "[child1]"; String priority2 = CLUSTER2 + "[child2]"; @@ -613,8 +553,8 @@ locality2, createEndpoints(1) private LocalityLbEndpoints createEndpoints(int priority) { return LocalityLbEndpoints.create( Arrays.asList( - LbEndpoint.create(makeAddress("endpoint-addr-1"), 100, true), - LbEndpoint.create(makeAddress("endpoint-addr-2"), 100, true)), + LbEndpoint.create(makeAddress("endpoint-addr-1"), 100, true, "hostname1"), + LbEndpoint.create(makeAddress("endpoint-addr-2"), 100, true, "hostname2")), 70 /* localityWeight */, priority /* priority */); } @@ -652,11 +592,11 @@ public void onlyEdsClusters_allResourcesRevoked_shutDownChildLbPolicy() { EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint1, 100, true)), + Collections.singletonList(LbEndpoint.create(endpoint1, 100, true, "hostname1")), 10 /* localityWeight */, 1 /* priority */); LocalityLbEndpoints localityLbEndpoints2 = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint2, 100, true)), + Collections.singletonList(LbEndpoint.create(endpoint2, 100, true, "hostname2")), 20 /* localityWeight */, 2 /* priority */); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints1)); @@ -686,8 +626,8 @@ public void handleEdsResource_ignoreUnhealthyEndpoints() { LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( Arrays.asList( - LbEndpoint.create(endpoint1, 100, false /* isHealthy */), - LbEndpoint.create(endpoint2, 100, true /* isHealthy */)), + LbEndpoint.create(endpoint1, 100, false /* isHealthy */, "hostname1"), + LbEndpoint.create(endpoint2, 100, true /* isHealthy */, "hostname2")), 10 /* localityWeight */, 1 /* priority */); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); @@ -705,11 +645,13 @@ public void handleEdsResource_ignoreLocalitiesWithNoHealthyEndpoints() { EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint1, 100, false /* isHealthy */)), + Collections.singletonList(LbEndpoint.create(endpoint1, 100, false /* isHealthy */, + "hostname1")), 10 /* localityWeight */, 1 /* priority */); LocalityLbEndpoints localityLbEndpoints2 = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint2, 100, true /* isHealthy */)), + Collections.singletonList(LbEndpoint.create(endpoint2, 100, true /* isHealthy */, + "hostname2")), 10 /* localityWeight */, 1 /* priority */); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, @@ -730,11 +672,13 @@ public void handleEdsResource_ignorePrioritiesWithNoHealthyEndpoints() { EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint1, 100, false /* isHealthy */)), + Collections.singletonList(LbEndpoint.create(endpoint1, 100, false /* isHealthy */, + "hostname1")), 10 /* localityWeight */, 1 /* priority */); LocalityLbEndpoints localityLbEndpoints2 = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint2, 200, true /* isHealthy */)), + Collections.singletonList(LbEndpoint.create(endpoint2, 200, true /* isHealthy */, + "hostname2")), 10 /* localityWeight */, 2 /* priority */); String priority2 = CLUSTER1 + "[child2]"; xdsClient.deliverClusterLoadAssignment( @@ -753,7 +697,8 @@ public void handleEdsResource_noHealthyEndpoint() { EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint, 100, false /* isHealthy */)), + Collections.singletonList(LbEndpoint.create(endpoint, 100, false /* isHealthy */, + "hostname1")), 10 /* localityWeight */, 1 /* priority */); xdsClient.deliverClusterLoadAssignment(EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); // single endpoint, unhealthy @@ -794,6 +739,11 @@ public void onlyLogicalDnsCluster_endpointsResolved() { assertClusterImplConfig(clusterImplConfig, CLUSTER_DNS, null, LRS_SERVER_INFO, 300L, null, Collections.emptyList(), "pick_first"); assertAddressesEqual(Arrays.asList(endpoint1, endpoint2), childBalancer.addresses); + assertThat(childBalancer.addresses.get(0).getAttributes() + .get(InternalXdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME); + assertThat(childBalancer.addresses.get(1).getAttributes() + .get(InternalXdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME); + } @Test @@ -912,7 +862,7 @@ public void edsClustersAndLogicalDnsCluster_receivedEndpoints() { resolver.deliverEndpointAddresses(Arrays.asList(endpoint1, endpoint2)); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint3, 100, true)), + Collections.singletonList(LbEndpoint.create(endpoint3, 100, true, "hostname3")), 10 /* localityWeight */, 1 /* priority */); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); @@ -969,7 +919,7 @@ public void edsResourceRevoked_dnsResolutionError_shutDownChildLbPolicyAndReturn EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint, 100, true)), + Collections.singletonList(LbEndpoint.create(endpoint, 100, true, "hostname1")), 10 /* localityWeight */, 1 /* priority */); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); @@ -1000,7 +950,7 @@ public void resolutionErrorAfterChildLbCreated_propagateErrorIfAllClustersEncoun EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint, 100, true)), + Collections.singletonList(LbEndpoint.create(endpoint, 100, true, "hostname1")), 10 /* localityWeight */, 1 /* priority */); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); @@ -1093,7 +1043,7 @@ public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_fallThroug EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint1, 100, true)), + Collections.singletonList(LbEndpoint.create(endpoint1, 100, true, "hostname1")), 10 /* localityWeight */, 1 /* priority */); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); @@ -1202,6 +1152,7 @@ public String toString() { } private static final class FakeXdsClient extends XdsClient { + private final Map> watchers = new HashMap<>(); @Override diff --git a/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java b/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java index 1ddf9620434..69fde29a0a9 100644 --- a/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java +++ b/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java @@ -22,7 +22,9 @@ import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_RDS; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; import com.google.protobuf.Message; import com.google.protobuf.UInt32Value; import io.envoyproxy.envoy.config.cluster.v3.Cluster; @@ -159,7 +161,7 @@ public Server getServer() { "channel_creds", Collections.singletonList( ImmutableMap.of("type", "insecure") ), - "server_features", Collections.singletonList("xds_v3") + "server_features", Lists.newArrayList("xds_v3", "trusted_xds_server") ) ), "server_listener_resource_name_template", SERVER_LISTENER_TEMPLATE_NO_REPLACEMENT @@ -197,7 +199,9 @@ static RouteConfiguration buildRouteConfiguration(String authority) { .setMatch( RouteMatch.newBuilder().setPrefix("/").build()) .setRoute( - RouteAction.newBuilder().setCluster(CLUSTER_NAME).build()).build()).build(); + RouteAction.newBuilder().setCluster(CLUSTER_NAME) + .setAutoHostRewrite(BoolValue.newBuilder().setValue(true).build()) + .build()).build()).build(); return RouteConfiguration.newBuilder().setName(RDS_NAME).addVirtualHosts(virtualHost).build(); } @@ -223,7 +227,8 @@ static Cluster buildCluster() { /** * Builds a new default EDS configuration. */ - static ClusterLoadAssignment buildClusterLoadAssignment(String hostName, int port) { + static ClusterLoadAssignment buildClusterLoadAssignment(String hostName, String endpointHostname, + int port) { Address address = Address.newBuilder() .setSocketAddress( SocketAddress.newBuilder().setAddress(hostName).setPortValue(port).build()).build(); @@ -233,7 +238,8 @@ static ClusterLoadAssignment buildClusterLoadAssignment(String hostName, int por .addLbEndpoints( LbEndpoint.newBuilder() .setEndpoint( - Endpoint.newBuilder().setAddress(address).build()) + Endpoint.newBuilder() + .setAddress(address).setHostname(endpointHostname).build()) .setHealthStatus(HealthStatus.HEALTHY) .build()).build(); return ClusterLoadAssignment.newBuilder() diff --git a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java index 63b9cda043c..8166027033f 100644 --- a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java +++ b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java @@ -506,7 +506,6 @@ public Collection getSubscribedResources(ServerInfo serverInfo, public Map> getSubscribedResourceTypesWithTypeUrl() { return ImmutableMap.of(); } - } private static class FakeXdsClientPoolFactory implements XdsClientPoolFactory { diff --git a/xds/src/test/java/io/grpc/xds/DataPlaneRule.java b/xds/src/test/java/io/grpc/xds/DataPlaneRule.java index faa79444071..b308419d142 100644 --- a/xds/src/test/java/io/grpc/xds/DataPlaneRule.java +++ b/xds/src/test/java/io/grpc/xds/DataPlaneRule.java @@ -48,6 +48,7 @@ public class DataPlaneRule extends TestWatcher { private static final Logger logger = Logger.getLogger(DataPlaneRule.class.getName()); private static final String SERVER_HOST_NAME = "test-server"; + static final String ENDPOINT_HOST_NAME = "endpoint-host-name"; private static final String SCHEME = "test-xds"; private final ControlPlaneRule controlPlane; @@ -73,7 +74,8 @@ public Server getServer() { */ public ManagedChannel getManagedChannel() { ManagedChannel channel = Grpc.newChannelBuilder(SCHEME + ":///" + SERVER_HOST_NAME, - InsecureChannelCredentials.create()).build(); + InsecureChannelCredentials.create()) + .build(); channels.add(channel); return channel; } @@ -98,7 +100,7 @@ protected void starting(Description description) { InetSocketAddress edsInetSocketAddress = (InetSocketAddress) server.getListenSockets().get(0); controlPlane.setEdsConfig( ControlPlaneRule.buildClusterLoadAssignment(edsInetSocketAddress.getHostName(), - edsInetSocketAddress.getPort())); + ENDPOINT_HOST_NAME, edsInetSocketAddress.getPort())); } @Override @@ -124,10 +126,12 @@ protected void finished(Description description) { } private void startServer(Map bootstrapOverride) throws Exception { + final String[] authority = new String[1]; ServerInterceptor metadataInterceptor = new ServerInterceptor() { @Override public ServerCall.Listener interceptCall(ServerCall call, Metadata requestHeaders, ServerCallHandler next) { + authority[0] = call.getAuthority(); logger.fine("Received following metadata: " + requestHeaders); // Make a copy of the headers so that it can be read in a thread-safe manner when copying @@ -155,8 +159,12 @@ public void close(Status status, Metadata trailers) { @Override public void unaryRpc( SimpleRequest request, StreamObserver responseObserver) { + String responseMsg = "Hi, xDS!"; + if (authority[0] != null) { + responseMsg += " Authority= " + authority[0]; + } SimpleResponse response = - SimpleResponse.newBuilder().setResponseMessage("Hi, xDS!").build(); + SimpleResponse.newBuilder().setResponseMessage(responseMsg).build(); responseObserver.onNext(response); responseObserver.onCompleted(); } diff --git a/xds/src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java b/xds/src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java index 30c2403396e..a3106bd20ae 100644 --- a/xds/src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java +++ b/xds/src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java @@ -18,6 +18,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.xds.DataPlaneRule.ENDPOINT_HOST_NAME; import static org.junit.Assert.assertEquals; import com.github.xds.type.v3.TypedStruct; @@ -91,11 +92,29 @@ public void pingPong() throws Exception { SimpleRequest request = SimpleRequest.newBuilder() .build(); SimpleResponse goldenResponse = SimpleResponse.newBuilder() - .setResponseMessage("Hi, xDS!") + .setResponseMessage("Hi, xDS! Authority= test-server") .build(); assertEquals(goldenResponse, blockingStub.unaryRpc(request)); } + @Test + public void pingPong_edsEndpoint_authorityOverride() throws Exception { + System.setProperty("GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE", "true"); + try { + ManagedChannel channel = dataPlane.getManagedChannel(); + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = SimpleServiceGrpc.newBlockingStub( + channel); + SimpleRequest request = SimpleRequest.newBuilder() + .build(); + SimpleResponse goldenResponse = SimpleResponse.newBuilder() + .setResponseMessage("Hi, xDS! Authority= " + ENDPOINT_HOST_NAME) + .build(); + assertEquals(goldenResponse, blockingStub.unaryRpc(request)); + } finally { + System.clearProperty("GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE"); + } + } + @Test public void pingPong_metadataLoadBalancer() throws Exception { MetadataLoadBalancerProvider metadataLbProvider = new MetadataLoadBalancerProvider(); @@ -129,7 +148,7 @@ public void pingPong_metadataLoadBalancer() throws Exception { SimpleRequest request = SimpleRequest.newBuilder() .build(); SimpleResponse goldenResponse = SimpleResponse.newBuilder() - .setResponseMessage("Hi, xDS!") + .setResponseMessage("Hi, xDS! Authority= test-server") .build(); assertEquals(goldenResponse, blockingStub.unaryRpc(request)); @@ -183,38 +202,43 @@ public void pingPong_ringHash() { SimpleRequest request = SimpleRequest.newBuilder() .build(); SimpleResponse goldenResponse = SimpleResponse.newBuilder() - .setResponseMessage("Hi, xDS!") + .setResponseMessage("Hi, xDS! Authority= test-server") .build(); assertEquals(goldenResponse, blockingStub.unaryRpc(request)); } @Test - public void pingPong_logicalDns() { - InetSocketAddress serverAddress = - (InetSocketAddress) dataPlane.getServer().getListenSockets().get(0); - controlPlane.setCdsConfig( - ControlPlaneRule.buildCluster().toBuilder() - .setType(Cluster.DiscoveryType.LOGICAL_DNS) - .setLoadAssignment( - ClusterLoadAssignment.newBuilder().addEndpoints( - LocalityLbEndpoints.newBuilder().addLbEndpoints( - LbEndpoint.newBuilder().setEndpoint( - Endpoint.newBuilder().setAddress( - Address.newBuilder().setSocketAddress( - SocketAddress.newBuilder() - .setAddress("localhost") - .setPortValue(serverAddress.getPort())))))) - .build()) - .build()); - - ManagedChannel channel = dataPlane.getManagedChannel(); - SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = SimpleServiceGrpc.newBlockingStub( - channel); - SimpleRequest request = SimpleRequest.newBuilder() - .build(); - SimpleResponse goldenResponse = SimpleResponse.newBuilder() - .setResponseMessage("Hi, xDS!") - .build(); - assertEquals(goldenResponse, blockingStub.unaryRpc(request)); + public void pingPong_logicalDns_authorityOverride() { + System.setProperty("GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE", "true"); + try { + InetSocketAddress serverAddress = + (InetSocketAddress) dataPlane.getServer().getListenSockets().get(0); + controlPlane.setCdsConfig( + ControlPlaneRule.buildCluster().toBuilder() + .setType(Cluster.DiscoveryType.LOGICAL_DNS) + .setLoadAssignment( + ClusterLoadAssignment.newBuilder().addEndpoints( + LocalityLbEndpoints.newBuilder().addLbEndpoints( + LbEndpoint.newBuilder().setEndpoint( + Endpoint.newBuilder().setAddress( + Address.newBuilder().setSocketAddress( + SocketAddress.newBuilder() + .setAddress("localhost") + .setPortValue(serverAddress.getPort())))))) + .build()) + .build()); + + ManagedChannel channel = dataPlane.getManagedChannel(); + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = SimpleServiceGrpc.newBlockingStub( + channel); + SimpleRequest request = SimpleRequest.newBuilder() + .build(); + SimpleResponse goldenResponse = SimpleResponse.newBuilder() + .setResponseMessage("Hi, xDS! Authority= localhost:" + serverAddress.getPort()) + .build(); + assertEquals(goldenResponse, blockingStub.unaryRpc(request)); + } finally { + System.clearProperty("GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE"); + } } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java index 30ea76b54f2..475b6e00a07 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -627,6 +627,28 @@ public void serverFeatureIgnoreResourceDeletion() throws XdsInitializationExcept assertThat(serverInfo.ignoreResourceDeletion()).isTrue(); } + @Test + public void serverFeatureTrustedXdsServer() throws XdsInitializationException { + String rawData = "{\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\"type\": \"insecure\"}\n" + + " ],\n" + + " \"server_features\": [\"trusted_xds_server\"]\n" + + " }\n" + + " ]\n" + + "}"; + + bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); + BootstrapInfo info = bootstrapper.bootstrap(); + ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); + assertThat(serverInfo.target()).isEqualTo(SERVER_URI); + assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.isTrustedXdsServer()).isTrue(); + } + @Test public void serverFeatureIgnoreResourceDeletion_xdsV3() throws XdsInitializationException { String rawData = "{\n" diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index a23e7177c29..87e875e75f6 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -166,6 +166,8 @@ public class GrpcXdsClientImplDataTest { private static final ServerInfo LRS_SERVER_INFO = ServerInfo.create("lrs.googleapis.com", InsecureChannelCredentials.create()); + private static final String GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE = + "GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE"; @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 @Rule @@ -203,7 +205,7 @@ public void parseRoute_withRouteAction() { .setCluster("cluster-foo")) .build(); StructOrError struct = XdsRouteConfigureResource.parseRoute( - proto, filterRegistry, ImmutableMap.of(), ImmutableSet.of()); + proto, filterRegistry, ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct()) .isEqualTo( @@ -211,7 +213,7 @@ public void parseRoute_withRouteAction() { RouteMatch.create(PathMatcher.fromPath("/service/method", false), Collections.emptyList(), null), RouteAction.forCluster( - "cluster-foo", Collections.emptyList(), null, null), + "cluster-foo", Collections.emptyList(), null, null, false), ImmutableMap.of())); } @@ -226,7 +228,7 @@ public void parseRoute_withNonForwardingAction() { .setNonForwardingAction(NonForwardingAction.getDefaultInstance()) .build(); StructOrError struct = XdsRouteConfigureResource.parseRoute( - proto, filterRegistry, ImmutableMap.of(), ImmutableSet.of()); + proto, filterRegistry, ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getStruct()) .isEqualTo( Route.forNonForwardingAction( @@ -245,7 +247,8 @@ public void parseRoute_withUnsupportedActionTypes() { .setRedirect(RedirectAction.getDefaultInstance()) .build(); res = XdsRouteConfigureResource.parseRoute( - redirectRoute, filterRegistry, ImmutableMap.of(), ImmutableSet.of()); + redirectRoute, filterRegistry, ImmutableMap.of(), ImmutableSet.of(), + getXdsResourceTypeArgs(true)); assertThat(res.getStruct()).isNull(); assertThat(res.getErrorDetail()) .isEqualTo("Route [route-blade] with unknown action type: REDIRECT"); @@ -257,7 +260,8 @@ public void parseRoute_withUnsupportedActionTypes() { .setDirectResponse(DirectResponseAction.getDefaultInstance()) .build(); res = XdsRouteConfigureResource.parseRoute( - directResponseRoute, filterRegistry, ImmutableMap.of(), ImmutableSet.of()); + directResponseRoute, filterRegistry, ImmutableMap.of(), ImmutableSet.of(), + getXdsResourceTypeArgs(true)); assertThat(res.getStruct()).isNull(); assertThat(res.getErrorDetail()) .isEqualTo("Route [route-blade] with unknown action type: DIRECT_RESPONSE"); @@ -269,7 +273,8 @@ public void parseRoute_withUnsupportedActionTypes() { .setFilterAction(FilterAction.getDefaultInstance()) .build(); res = XdsRouteConfigureResource.parseRoute( - filterRoute, filterRegistry, ImmutableMap.of(), ImmutableSet.of()); + filterRoute, filterRegistry, ImmutableMap.of(), ImmutableSet.of(), + getXdsResourceTypeArgs(true)); assertThat(res.getStruct()).isNull(); assertThat(res.getErrorDetail()) .isEqualTo("Route [route-blade] with unknown action type: FILTER_ACTION"); @@ -291,7 +296,8 @@ public void parseRoute_skipRouteWithUnsupportedMatcher() { .setCluster("cluster-foo")) .build(); assertThat(XdsRouteConfigureResource.parseRoute( - proto, filterRegistry, ImmutableMap.of(), ImmutableSet.of())) + proto, filterRegistry, ImmutableMap.of(), ImmutableSet.of(), + getXdsResourceTypeArgs(true))) .isNull(); } @@ -308,7 +314,8 @@ public void parseRoute_skipRouteWithUnsupportedAction() { .setClusterHeader("cluster header")) // cluster_header action not supported .build(); assertThat(XdsRouteConfigureResource.parseRoute( - proto, filterRegistry, ImmutableMap.of(), ImmutableSet.of())) + proto, filterRegistry, ImmutableMap.of(), ImmutableSet.of(), + getXdsResourceTypeArgs(true))) .isNull(); } @@ -518,10 +525,48 @@ public void parseRouteAction_withCluster() { .build(); StructOrError struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct().cluster()).isEqualTo("cluster-foo"); assertThat(struct.getStruct().weightedClusters()).isNull(); + assertThat(struct.getStruct().autoHostRewrite()).isFalse(); + } + + @Test + public void parseRouteAction_withCluster_autoHostRewriteEnabled() { + System.setProperty(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, "true"); + try { + io.envoyproxy.envoy.config.route.v3.RouteAction proto = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setCluster("cluster-foo") + .setAutoHostRewrite(BoolValue.of(true)) + .build(); + StructOrError struct = + XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct().cluster()).isEqualTo("cluster-foo"); + assertThat(struct.getStruct().weightedClusters()).isNull(); + assertThat(struct.getStruct().autoHostRewrite()).isTrue(); + } finally { + System.clearProperty(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE); + } + } + + @Test + public void parseRouteAction_withCluster_flagDisabled_autoHostRewriteNotEnabled() { + io.envoyproxy.envoy.config.route.v3.RouteAction proto = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setCluster("cluster-foo") + .setAutoHostRewrite(BoolValue.of(true)) + .build(); + StructOrError struct = + XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct().cluster()).isEqualTo("cluster-foo"); + assertThat(struct.getStruct().weightedClusters()).isNull(); + assertThat(struct.getStruct().autoHostRewrite()).isFalse(); } @Test @@ -542,12 +587,74 @@ public void parseRouteAction_withWeightedCluster() { .build(); StructOrError struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct().cluster()).isNull(); + assertThat(struct.getStruct().weightedClusters()).containsExactly( + ClusterWeight.create("cluster-foo", 30, ImmutableMap.of()), + ClusterWeight.create("cluster-bar", 70, ImmutableMap.of())); + assertThat(struct.getStruct().autoHostRewrite()).isFalse(); + } + + @Test + public void parseRouteAction_withWeightedCluster_autoHostRewriteEnabled() { + System.setProperty(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, "true"); + try { + io.envoyproxy.envoy.config.route.v3.RouteAction proto = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setWeightedClusters( + WeightedCluster.newBuilder() + .addClusters( + WeightedCluster.ClusterWeight + .newBuilder() + .setName("cluster-foo") + .setWeight(UInt32Value.newBuilder().setValue(30))) + .addClusters(WeightedCluster.ClusterWeight + .newBuilder() + .setName("cluster-bar") + .setWeight(UInt32Value.newBuilder().setValue(70)))) + .setAutoHostRewrite(BoolValue.of(true)) + .build(); + StructOrError struct = + XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct().cluster()).isNull(); + assertThat(struct.getStruct().weightedClusters()).containsExactly( + ClusterWeight.create("cluster-foo", 30, ImmutableMap.of()), + ClusterWeight.create("cluster-bar", 70, ImmutableMap.of())); + assertThat(struct.getStruct().autoHostRewrite()).isTrue(); + } finally { + System.clearProperty(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE); + } + } + + @Test + public void parseRouteAction_withWeightedCluster_flagDisabled_autoHostRewriteDisabled() { + io.envoyproxy.envoy.config.route.v3.RouteAction proto = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setWeightedClusters( + WeightedCluster.newBuilder() + .addClusters( + WeightedCluster.ClusterWeight + .newBuilder() + .setName("cluster-foo") + .setWeight(UInt32Value.newBuilder().setValue(30))) + .addClusters(WeightedCluster.ClusterWeight + .newBuilder() + .setName("cluster-bar") + .setWeight(UInt32Value.newBuilder().setValue(70)))) + .setAutoHostRewrite(BoolValue.of(true)) + .build(); + StructOrError struct = + XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct().cluster()).isNull(); assertThat(struct.getStruct().weightedClusters()).containsExactly( ClusterWeight.create("cluster-foo", 30, ImmutableMap.of()), ClusterWeight.create("cluster-bar", 70, ImmutableMap.of())); + assertThat(struct.getStruct().autoHostRewrite()).isFalse(); } @Test @@ -568,7 +675,7 @@ public void parseRouteAction_weightedClusterSum() { .build(); StructOrError struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getErrorDetail()).isEqualTo("Sum of cluster weights should be above 0."); } @@ -584,7 +691,7 @@ public void parseRouteAction_withTimeoutByGrpcTimeoutHeaderMax() { .build(); StructOrError struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getStruct().timeoutNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L)); } @@ -599,7 +706,7 @@ public void parseRouteAction_withTimeoutByMaxStreamDuration() { .build(); StructOrError struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getStruct().timeoutNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L)); } @@ -611,7 +718,7 @@ public void parseRouteAction_withTimeoutUnset() { .build(); StructOrError struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getStruct().timeoutNano()).isNull(); } @@ -633,7 +740,7 @@ public void parseRouteAction_withRetryPolicy() { .build(); StructOrError struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); RouteAction.RetryPolicy retryPolicy = struct.getStruct().retryPolicy(); assertThat(retryPolicy.maxAttempts()).isEqualTo(4); assertThat(retryPolicy.initialBackoff()).isEqualTo(Durations.fromMillis(500)); @@ -657,7 +764,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder.build()) .build(); struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getStruct().retryPolicy()).isNotNull(); assertThat(struct.getStruct().retryPolicy().retryableStatusCodes()).isEmpty(); @@ -670,7 +777,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getErrorDetail()).isEqualTo("No base_interval specified in retry_backoff"); // max_interval unset @@ -680,7 +787,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); retryPolicy = struct.getStruct().retryPolicy(); assertThat(retryPolicy.maxBackoff()).isEqualTo(Durations.fromMillis(500 * 10)); @@ -691,7 +798,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getErrorDetail()) .isEqualTo("base_interval in retry_backoff must be positive"); @@ -704,7 +811,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getErrorDetail()) .isEqualTo("max_interval in retry_backoff cannot be less than base_interval"); @@ -717,7 +824,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getErrorDetail()) .isEqualTo("max_interval in retry_backoff cannot be less than base_interval"); @@ -730,7 +837,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getStruct().retryPolicy().initialBackoff()) .isEqualTo(Durations.fromMillis(1)); assertThat(struct.getStruct().retryPolicy().maxBackoff()) @@ -746,7 +853,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); retryPolicy = struct.getStruct().retryPolicy(); assertThat(retryPolicy.initialBackoff()).isEqualTo(Durations.fromMillis(25)); assertThat(retryPolicy.maxBackoff()).isEqualTo(Durations.fromMillis(250)); @@ -765,7 +872,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getStruct().retryPolicy().retryableStatusCodes()) .containsExactly(Code.CANCELLED); @@ -783,7 +890,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getStruct().retryPolicy().retryableStatusCodes()) .containsExactly(Code.CANCELLED); @@ -801,7 +908,7 @@ public void parseRouteAction_withRetryPolicy() { .setRetryPolicy(builder) .build(); struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getStruct().retryPolicy().retryableStatusCodes()) .containsExactly(Code.CANCELLED); } @@ -840,7 +947,7 @@ public void parseRouteAction_withHashPolicies() { .build(); StructOrError struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); List policies = struct.getStruct().hashPolicies(); assertThat(policies).hasSize(2); assertThat(policies.get(0).type()).isEqualTo(HashPolicy.Type.HEADER); @@ -860,7 +967,7 @@ public void parseRouteAction_custerSpecifierNotSet() { .build(); StructOrError struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct).isNull(); } @@ -873,10 +980,65 @@ public void parseRouteAction_clusterSpecifier_routeLookupDisabled() { .build(); StructOrError struct = XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, - ImmutableMap.of(), ImmutableSet.of()); + ImmutableMap.of(), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct).isNull(); } + @Test + public void parseRouteAction_clusterSpecifier() { + XdsRouteConfigureResource.enableRouteLookup = true; + io.envoyproxy.envoy.config.route.v3.RouteAction proto = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setClusterSpecifierPlugin(CLUSTER_SPECIFIER_PLUGIN.name()) + .build(); + StructOrError struct = + XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, + ImmutableMap.of(CLUSTER_SPECIFIER_PLUGIN.name(), RlsPluginConfig.create( + ImmutableMap.of("lookupService", "rls-cbt.googleapis.com"))), ImmutableSet.of(), + getXdsResourceTypeArgs(true)); + assertThat(struct.getStruct()).isNotNull(); + assertThat(struct.getStruct().autoHostRewrite()).isFalse(); + } + + @Test + public void parseRouteAction_clusterSpecifier_autoHostRewriteEnabled() { + System.setProperty(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, "true"); + try { + XdsRouteConfigureResource.enableRouteLookup = true; + io.envoyproxy.envoy.config.route.v3.RouteAction proto = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setClusterSpecifierPlugin(CLUSTER_SPECIFIER_PLUGIN.name()) + .setAutoHostRewrite(BoolValue.of(true)) + .build(); + StructOrError struct = + XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, + ImmutableMap.of(CLUSTER_SPECIFIER_PLUGIN.name(), RlsPluginConfig.create( + ImmutableMap.of("lookupService", "rls-cbt.googleapis.com"))), ImmutableSet.of(), + getXdsResourceTypeArgs(true)); + assertThat(struct.getStruct()).isNotNull(); + assertThat(struct.getStruct().autoHostRewrite()).isTrue(); + } finally { + System.clearProperty(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE); + } + } + + @Test + public void parseRouteAction_clusterSpecifier_flagDisabled_autoHostRewriteDisabled() { + XdsRouteConfigureResource.enableRouteLookup = true; + io.envoyproxy.envoy.config.route.v3.RouteAction proto = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setClusterSpecifierPlugin(CLUSTER_SPECIFIER_PLUGIN.name()) + .setAutoHostRewrite(BoolValue.of(true)) + .build(); + StructOrError struct = + XdsRouteConfigureResource.parseRouteAction(proto, filterRegistry, + ImmutableMap.of(CLUSTER_SPECIFIER_PLUGIN.name(), RlsPluginConfig.create( + ImmutableMap.of("lookupService", "rls-cbt.googleapis.com"))), ImmutableSet.of(), + getXdsResourceTypeArgs(true)); + assertThat(struct.getStruct()).isNotNull(); + assertThat(struct.getStruct().autoHostRewrite()).isFalse(); + } + @Test public void parseClusterWeight() { io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight proto = @@ -911,7 +1073,8 @@ public void parseLocalityLbEndpoints_withHealthyEndpoints() { assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct()).isEqualTo( LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, true)), 100, 1)); + Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, true, "")), + 100, 1)); } @Test @@ -935,7 +1098,8 @@ public void parseLocalityLbEndpoints_treatUnknownHealthAsHealthy() { assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct()).isEqualTo( LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, true)), 100, 1)); + Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, true, "")), 100, + 1)); } @Test @@ -959,7 +1123,8 @@ public void parseLocalityLbEndpoints_withUnHealthyEndpoints() { assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct()).isEqualTo( LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, false)), 100, 1)); + Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, false, "")), 100, + 1)); } @Test @@ -1020,7 +1185,7 @@ public void parseLocalityLbEndpoints_withDualStackEndpoints() { EquivalentAddressGroup expectedEag = new EquivalentAddressGroup(socketAddressList); assertThat(struct.getStruct()).isEqualTo( LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(expectedEag, 20, true)), 100, 1)); + Collections.singletonList(LbEndpoint.create(expectedEag, 20, true, "")), 100, 1)); } finally { if (originalDualStackProp != null) { System.setProperty(GRPC_EXPERIMENTAL_XDS_DUALSTACK_ENDPOINTS, originalDualStackProp); @@ -1399,7 +1564,7 @@ public void parseHttpConnectionManager_xffNumTrustedHopsUnsupported() thrown.expectMessage("HttpConnectionManager with xff_num_trusted_hops unsupported"); XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); } @Test @@ -1412,7 +1577,7 @@ public void parseHttpConnectionManager_OriginalIpDetectionExtensionsMustEmpty() thrown.expect(ResourceInvalidException.class); thrown.expectMessage("HttpConnectionManager with original_ip_detection_extensions unsupported"); XdsListenerResource.parseHttpConnectionManager( - hcm, filterRegistry, false); + hcm, filterRegistry, false, getXdsResourceTypeArgs(true)); } @Test @@ -1431,7 +1596,7 @@ public void parseHttpConnectionManager_missingRdsAndInlinedRouteConfiguration() thrown.expectMessage("HttpConnectionManager neither has inlined route_config nor RDS"); XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); } @Test @@ -1450,7 +1615,7 @@ public void parseHttpConnectionManager_duplicateHttpFilters() throws ResourceInv thrown.expectMessage("HttpConnectionManager contains duplicate HttpFilter: envoy.filter.foo"); XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); } @Test @@ -1468,7 +1633,7 @@ public void parseHttpConnectionManager_lastNotTerminal() throws ResourceInvalidE thrown.expectMessage("The last HttpFilter must be a terminal filter: envoy.filter.bar"); XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); } @Test @@ -1486,7 +1651,7 @@ public void parseHttpConnectionManager_terminalNotLast() throws ResourceInvalidE thrown.expectMessage("A terminal HttpFilter must be the last filter: terminal"); XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true); + true, getXdsResourceTypeArgs(true)); } @Test @@ -1502,7 +1667,7 @@ public void parseHttpConnectionManager_unknownFilters() throws ResourceInvalidEx thrown.expectMessage("The last HttpFilter must be a terminal filter: envoy.filter.bar"); XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); } @Test @@ -1514,7 +1679,7 @@ public void parseHttpConnectionManager_emptyFilters() throws ResourceInvalidExce thrown.expectMessage("Missing HttpFilter in HttpConnectionManager."); XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); } @Test @@ -1563,7 +1728,7 @@ public void parseHttpConnectionManager_clusterSpecifierPlugin() throws Exception io.grpc.xds.HttpConnectionManager parsedHcm = XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); VirtualHost virtualHost = Iterables.getOnlyElement(parsedHcm.virtualHosts()); Route parsedRoute = Iterables.getOnlyElement(virtualHost.routes()); @@ -1643,7 +1808,7 @@ public void parseHttpConnectionManager_duplicatePluginName() throws Exception { XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); } @Test @@ -1695,7 +1860,7 @@ public void parseHttpConnectionManager_pluginNameNotFound() throws Exception { XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); } @@ -1769,7 +1934,7 @@ public void parseHttpConnectionManager_optionalPlugin() throws ResourceInvalidEx HttpFilter.newBuilder().setName("terminal").setTypedConfig( Any.pack(Router.newBuilder().build())).setIsOptional(true)) .build(), filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); // Verify that the only route left is the one with the registered RLS plugin `rls-plugin-1`, // while the route with unregistered optional `optional-plugin-`1 has been skipped. @@ -1797,7 +1962,7 @@ public void parseHttpConnectionManager_validateRdsConfigSource() throws Exceptio .build(); XdsListenerResource.parseHttpConnectionManager( hcm1, filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); HttpConnectionManager hcm2 = HttpConnectionManager.newBuilder() @@ -1811,7 +1976,7 @@ public void parseHttpConnectionManager_validateRdsConfigSource() throws Exceptio .build(); XdsListenerResource.parseHttpConnectionManager( hcm2, filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); HttpConnectionManager hcm3 = HttpConnectionManager.newBuilder() @@ -1829,7 +1994,7 @@ public void parseHttpConnectionManager_validateRdsConfigSource() throws Exceptio "HttpConnectionManager contains invalid RDS: must specify ADS or self ConfigSource"); XdsListenerResource.parseHttpConnectionManager( hcm3, filterRegistry, - true /* does not matter */); + true /* does not matter */, getXdsResourceTypeArgs(true)); } @Test @@ -2187,7 +2352,7 @@ public void parseServerSideListener_invalidTrafficDirection() throws ResourceInv thrown.expect(ResourceInvalidException.class); thrown.expectMessage("Listener listener1 with invalid traffic direction: OUTBOUND"); XdsListenerResource.parseServerSideListener( - listener, null, filterRegistry, null); + listener, null, filterRegistry, null, getXdsResourceTypeArgs(true)); } @Test @@ -2197,7 +2362,7 @@ public void parseServerSideListener_noTrafficDirection() throws ResourceInvalidE .setName("listener1") .build(); XdsListenerResource.parseServerSideListener( - listener, null, filterRegistry, null); + listener, null, filterRegistry, null, getXdsResourceTypeArgs(true)); } @Test @@ -2211,7 +2376,7 @@ public void parseServerSideListener_listenerFiltersPresent() throws ResourceInva thrown.expect(ResourceInvalidException.class); thrown.expectMessage("Listener listener1 cannot have listener_filters"); XdsListenerResource.parseServerSideListener( - listener, null, filterRegistry, null); + listener, null, filterRegistry, null, getXdsResourceTypeArgs(true)); } @Test @@ -2225,7 +2390,7 @@ public void parseServerSideListener_useOriginalDst() throws ResourceInvalidExcep thrown.expect(ResourceInvalidException.class); thrown.expectMessage("Listener listener1 cannot have use_original_dst set to true"); XdsListenerResource.parseServerSideListener( - listener,null, filterRegistry, null); + listener,null, filterRegistry, null, getXdsResourceTypeArgs(true)); } @Test @@ -2274,7 +2439,7 @@ public void parseServerSideListener_nonUniqueFilterChainMatch() throws ResourceI thrown.expect(ResourceInvalidException.class); thrown.expectMessage("FilterChainMatch must be unique. Found duplicate:"); XdsListenerResource.parseServerSideListener( - listener, null, filterRegistry, null); + listener, null, filterRegistry, null, getXdsResourceTypeArgs(true)); } @Test @@ -2323,7 +2488,7 @@ public void parseServerSideListener_nonUniqueFilterChainMatch_sameFilter() thrown.expect(ResourceInvalidException.class); thrown.expectMessage("FilterChainMatch must be unique. Found duplicate:"); XdsListenerResource.parseServerSideListener( - listener,null, filterRegistry, null); + listener,null, filterRegistry, null, getXdsResourceTypeArgs(true)); } @Test @@ -2372,7 +2537,7 @@ public void parseServerSideListener_uniqueFilterChainMatch() throws ResourceInva .addAllFilterChains(Arrays.asList(filterChain1, filterChain2)) .build(); XdsListenerResource.parseServerSideListener( - listener, null, filterRegistry, null); + listener, null, filterRegistry, null, getXdsResourceTypeArgs(true)); } @Test @@ -2387,7 +2552,8 @@ public void parseFilterChain_noHcm() throws ResourceInvalidException { thrown.expectMessage( "FilterChain filter-chain-foo should contain exact one HttpConnectionManager filter"); XdsListenerResource.parseFilterChain( - filterChain, null, filterRegistry, null, null); + filterChain, null, filterRegistry, null, null, + getXdsResourceTypeArgs(true)); } @Test @@ -2405,7 +2571,8 @@ public void parseFilterChain_duplicateFilter() throws ResourceInvalidException { thrown.expectMessage( "FilterChain filter-chain-foo should contain exact one HttpConnectionManager filter"); XdsListenerResource.parseFilterChain( - filterChain, null, filterRegistry, null, null); + filterChain, null, filterRegistry, null, null, + getXdsResourceTypeArgs(true)); } @Test @@ -2423,7 +2590,8 @@ public void parseFilterChain_filterMissingTypedConfig() throws ResourceInvalidEx "FilterChain filter-chain-foo contains filter envoy.http_connection_manager " + "without typed_config"); XdsListenerResource.parseFilterChain( - filterChain, null, filterRegistry, null, null); + filterChain, null, filterRegistry, null, null, + getXdsResourceTypeArgs(true)); } @Test @@ -2445,7 +2613,8 @@ public void parseFilterChain_unsupportedFilter() throws ResourceInvalidException "FilterChain filter-chain-foo contains filter unsupported with unsupported " + "typed_config type unsupported-type-url"); XdsListenerResource.parseFilterChain( - filterChain, null, filterRegistry, null, null); + filterChain, null, filterRegistry, null, null, + getXdsResourceTypeArgs(true)); } @Test @@ -2473,10 +2642,10 @@ public void parseFilterChain_noName() throws ResourceInvalidException { EnvoyServerProtoData.FilterChain parsedFilterChain1 = XdsListenerResource.parseFilterChain( filterChain1, null, filterRegistry, null, - null); + null, getXdsResourceTypeArgs(true)); EnvoyServerProtoData.FilterChain parsedFilterChain2 = XdsListenerResource.parseFilterChain( filterChain2, null, filterRegistry, null, - null); + null, getXdsResourceTypeArgs(true)); assertThat(parsedFilterChain1.name()).isEqualTo(parsedFilterChain2.name()); } @@ -3044,4 +3213,10 @@ private static Filter buildHttpConnectionManagerFilter(HttpFilter... httpFilters "type.googleapis.com")) .build(); } + + private XdsResourceType.Args getXdsResourceTypeArgs(boolean isTrustedServer) { + return new XdsResourceType.Args( + ServerInfo.create("http://td", "", false, isTrustedServer), "1.0", null, null, null, null + ); + } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index f516dc87e98..8ecb40383b1 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -249,7 +249,7 @@ public long currentTimeNanos() { // EDS test resources. private final Message lbEndpointHealthy = mf.buildLocalityLbEndpoints("region1", "zone1", "subzone1", - mf.buildLbEndpoint("192.168.0.1", 8080, "healthy", 2), 1, 0); + mf.buildLbEndpoint("192.168.0.1", 8080, "healthy", 2, "endpoint-host-name"), 1, 0); // Locality with 0 endpoints private final Message lbEndpointEmpty = mf.buildLocalityLbEndpoints("region3", "zone3", "subzone3", @@ -257,7 +257,7 @@ public long currentTimeNanos() { // Locality with 0-weight endpoint private final Message lbEndpointZeroWeight = mf.buildLocalityLbEndpoints("region4", "zone4", "subzone4", - mf.buildLbEndpoint("192.168.142.5", 80, "unknown", 5), 0, 2); + mf.buildLbEndpoint("192.168.142.5", 80, "unknown", 5, "endpoint-host-name"), 0, 2); private final Any testClusterLoadAssignment = Any.pack(mf.buildClusterLoadAssignment(EDS_RESOURCE, ImmutableList.of(lbEndpointHealthy, lbEndpointEmpty, lbEndpointZeroWeight), ImmutableList.of(mf.buildDropOverload("lb", 200), mf.buildDropOverload("throttle", 1000)))); @@ -340,7 +340,8 @@ public XdsTransport create(ServerInfo serverInfo) { } }; - xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion()); + xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), + true); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -595,7 +596,8 @@ private void validateGoldenClusterLoadAssignment(EdsUpdate edsUpdate) { .containsExactly( Locality.create("region1", "zone1", "subzone1"), LocalityLbEndpoints.create( - ImmutableList.of(LbEndpoint.create("192.168.0.1", 8080, 2, true)), 1, 0), + ImmutableList.of(LbEndpoint.create("192.168.0.1", 8080, 2, true, + "endpoint-host-name")), 1, 0), Locality.create("region3", "zone3", "subzone3"), LocalityLbEndpoints.create(ImmutableList.of(), 2, 1)); } @@ -1134,7 +1136,7 @@ public void edsResourceUpdated_withXdstpResourceName_withWrongType() { edsResourceNameWithWrongType, ImmutableList.of(mf.buildLocalityLbEndpoints( "region2", "zone2", "subzone2", - mf.buildLbEndpoint("172.44.2.2", 8000, "unknown", 3), 2, 0)), + mf.buildLbEndpoint("172.44.2.2", 8000, "unknown", 3, "endpoint-host-name"), 2, 0)), ImmutableList.of())); call.sendResponse(EDS, testEdsConfig, VERSION_1, "0000"); call.verifyRequestNack( @@ -3049,7 +3051,7 @@ public void simpleFlowControl() throws Exception { // Updated EDS response. Any updatedClusterLoadAssignment = Any.pack(mf.buildClusterLoadAssignment(EDS_RESOURCE, ImmutableList.of(mf.buildLocalityLbEndpoints("region2", "zone2", "subzone2", - mf.buildLbEndpoint("172.44.2.2", 8000, "unknown", 3), 2, 0)), + mf.buildLbEndpoint("172.44.2.2", 8000, "unknown", 3, "endpoint-host-name"), 2, 0)), ImmutableList.of())); call.sendResponse(EDS, updatedClusterLoadAssignment, VERSION_2, "0001"); // message not processed due to flow control @@ -3110,7 +3112,7 @@ public void edsResourceUpdated() { // Updated EDS response. Any updatedClusterLoadAssignment = Any.pack(mf.buildClusterLoadAssignment(EDS_RESOURCE, ImmutableList.of(mf.buildLocalityLbEndpoints("region2", "zone2", "subzone2", - mf.buildLbEndpoint("172.44.2.2", 8000, "unknown", 3), 2, 0)), + mf.buildLbEndpoint("172.44.2.2", 8000, "unknown", 3, "endpoint-host-name"), 2, 0)), ImmutableList.of())); call.sendResponse(EDS, updatedClusterLoadAssignment, VERSION_2, "0001"); @@ -3123,7 +3125,7 @@ public void edsResourceUpdated() { Locality.create("region2", "zone2", "subzone2"), LocalityLbEndpoints.create( ImmutableList.of( - LbEndpoint.create("172.44.2.2", 8000, 3, true)), 2, 0)); + LbEndpoint.create("172.44.2.2", 8000, 3, true, "endpoint-host-name")), 2, 0)); verifyResourceMetadataAcked(EDS, EDS_RESOURCE, updatedClusterLoadAssignment, VERSION_2, TIME_INCREMENT * 2); verifySubscribedResourcesMetadataSizes(0, 0, 0, 1); @@ -3139,9 +3141,9 @@ public void edsDuplicateLocalityInTheSamePriority() { Any updatedClusterLoadAssignment = Any.pack(mf.buildClusterLoadAssignment(EDS_RESOURCE, ImmutableList.of( mf.buildLocalityLbEndpoints("region2", "zone2", "subzone2", - mf.buildLbEndpoint("172.44.2.2", 8000, "unknown", 3), 2, 1), + mf.buildLbEndpoint("172.44.2.2", 8000, "unknown", 3, "endpoint-host-name"), 2, 1), mf.buildLocalityLbEndpoints("region2", "zone2", "subzone2", - mf.buildLbEndpoint("172.44.2.3", 8080, "healthy", 10), 2, 1) + mf.buildLbEndpoint("172.44.2.3", 8080, "healthy", 10, "endpoint-host-name"), 2, 1) ), ImmutableList.of())); call.sendResponse(EDS, updatedClusterLoadAssignment, "0", "0001"); @@ -3202,7 +3204,8 @@ public void edsResourceDeletedByCds() { mf.buildClusterLoadAssignment(resource, ImmutableList.of( mf.buildLocalityLbEndpoints("region2", "zone2", "subzone2", - mf.buildLbEndpoint("192.168.0.2", 9090, "healthy", 3), 1, 0)), + mf.buildLbEndpoint("192.168.0.2", 9090, "healthy", 3, + "endpoint-host-name"), 1, 0)), ImmutableList.of(mf.buildDropOverload("lb", 100))))); call.sendResponse(EDS, clusterLoadAssignments, VERSION_1, "0000"); verify(edsWatcher).onChanged(edsUpdateCaptor.capture()); @@ -3279,7 +3282,8 @@ public void multipleEdsWatchers() { mf.buildClusterLoadAssignment(edsResourceTwo, ImmutableList.of( mf.buildLocalityLbEndpoints("region2", "zone2", "subzone2", - mf.buildLbEndpoint("172.44.2.2", 8000, "healthy", 3), 2, 0)), + mf.buildLbEndpoint("172.44.2.2", 8000, "healthy", 3, "endpoint-host-name"), + 2, 0)), ImmutableList.of())); call.sendResponse(EDS, clusterLoadAssignmentTwo, VERSION_2, "0001"); @@ -3292,7 +3296,7 @@ public void multipleEdsWatchers() { Locality.create("region2", "zone2", "subzone2"), LocalityLbEndpoints.create( ImmutableList.of( - LbEndpoint.create("172.44.2.2", 8000, 3, true)), 2, 0)); + LbEndpoint.create("172.44.2.2", 8000, 3, true, "endpoint-host-name")), 2, 0)); verify(watcher2).onChanged(edsUpdateCaptor.capture()); edsUpdate = edsUpdateCaptor.getValue(); assertThat(edsUpdate.clusterName).isEqualTo(edsResourceTwo); @@ -3302,7 +3306,7 @@ public void multipleEdsWatchers() { Locality.create("region2", "zone2", "subzone2"), LocalityLbEndpoints.create( ImmutableList.of( - LbEndpoint.create("172.44.2.2", 8000, 3, true)), 2, 0)); + LbEndpoint.create("172.44.2.2", 8000, 3, true, "endpoint-host-name")), 2, 0)); verifyNoMoreInteractions(edsResourceWatcher); verifyResourceMetadataAcked( EDS, edsResourceTwo, clusterLoadAssignmentTwo, VERSION_2, TIME_INCREMENT * 2); @@ -3794,7 +3798,7 @@ private XdsClientImpl createXdsClient(String serverUri) { private BootstrapInfo buildBootStrap(String serverUri) { ServerInfo xdsServerInfo = ServerInfo.create(serverUri, CHANNEL_CREDENTIALS, - ignoreResourceDeletion()); + ignoreResourceDeletion(), true); return Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -4013,7 +4017,7 @@ protected Message buildLocalityLbEndpoints(String region, String zone, String su } protected abstract Message buildLbEndpoint(String address, int port, String healthStatus, - int lbWeight); + int lbWeight, String endpointHostname); protected abstract Message buildDropOverload(String category, int dropPerMillion); diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplV3Test.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplV3Test.java index 40a9bff514f..af34c7232d0 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplV3Test.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplV3Test.java @@ -705,7 +705,7 @@ protected Message buildLocalityLbEndpoints(String region, String zone, String su @Override protected Message buildLbEndpoint(String address, int port, String healthStatus, - int lbWeight) { + int lbWeight, String endpointHostname) { HealthStatus status; switch (healthStatus) { case "unknown": @@ -733,7 +733,8 @@ protected Message buildLbEndpoint(String address, int port, String healthStatus, .setEndpoint( Endpoint.newBuilder().setAddress( Address.newBuilder().setSocketAddress( - SocketAddress.newBuilder().setAddress(address).setPortValue(port)))) + SocketAddress.newBuilder().setAddress(address).setPortValue(port))) + .setHostname(endpointHostname)) .setHealthStatus(status) .setLoadBalancingWeight(UInt32Value.of(lbWeight)) .build(); diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 76b92cd8c03..f32c198f21f 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -318,13 +318,13 @@ public void resolving_targetAuthorityInAuthoritiesMap() { String serviceAuthority = "[::FFFF:129.144.52.38]:80"; bootstrapInfo = BootstrapInfo.builder() .servers(ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true))) + "td.googleapis.com", InsecureChannelCredentials.create(), true, true))) .node(Node.newBuilder().build()) .authorities( ImmutableMap.of(targetAuthority, AuthorityInfo.create( "xdstp://" + targetAuthority + "/envoy.config.listener.v3.Listener/%s?foo=1&bar=2", ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true))))) + "td.googleapis.com", InsecureChannelCredentials.create(), true, true))))) .build(); expectedLdsResourceName = "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/" + "%5B::FFFF:129.144.52.38%5D:80?bar=2&foo=1"; // query param canonified @@ -348,11 +348,11 @@ public void resolving_ldsResourceNotFound() { public void resolving_ldsResourceUpdateRdsName() { Route route1 = Route.forAction(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( - cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null), + cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()); Route route2 = Route.forAction(RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), RouteAction.forCluster( - cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(20L), null), + cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(20L), null, false), ImmutableMap.of()); bootstrapInfo = BootstrapInfo.builder() .servers(ImmutableList.of(ServerInfo.create( @@ -418,7 +418,7 @@ public void resolving_rdsResourceNotFound() { public void resolving_ldsResourceRevokedAndAddedBack() { Route route = Route.forAction(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( - cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null), + cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()); resolver.start(mockListener); @@ -457,7 +457,7 @@ public void resolving_ldsResourceRevokedAndAddedBack() { public void resolving_rdsResourceRevokedAndAddedBack() { Route route = Route.forAction(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( - cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null), + cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()); resolver.start(mockListener); @@ -534,7 +534,7 @@ public void resolving_encounterErrorLdsAndRdsWatchers() { public void resolving_matchingVirtualHostNotFound_matchingOverrideAuthority() { Route route = Route.forAction(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( - cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null), + cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()); VirtualHost virtualHost = VirtualHost.create("virtualhost", Collections.singletonList("random"), @@ -557,7 +557,7 @@ public void resolving_matchingVirtualHostNotFound_matchingOverrideAuthority() { public void resolving_matchingVirtualHostNotFound_notMatchingOverrideAuthority() { Route route = Route.forAction(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( - cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null), + cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()); VirtualHost virtualHost = VirtualHost.create("virtualhost", Collections.singletonList(AUTHORITY), @@ -604,11 +604,11 @@ public void resolving_matchingVirtualHostNotFoundInRdsResource() { private List buildUnmatchedVirtualHosts() { Route route1 = Route.forAction(RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), RouteAction.forCluster( - cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null), + cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()); Route route2 = Route.forAction(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( - cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null), + cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()); return Arrays.asList( VirtualHost.create("virtualhost-foo", Collections.singletonList("hello.googleapis.com"), @@ -625,7 +625,7 @@ public void resolved_noTimeout() { FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); Route route = Route.forAction(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( - cluster1, Collections.emptyList(), null, null), // per-route timeout unset + cluster1, Collections.emptyList(), null, null, false), // per-route timeout unset ImmutableMap.of()); VirtualHost virtualHost = VirtualHost.create("does not matter", Collections.singletonList(AUTHORITY), Collections.singletonList(route), @@ -643,7 +643,7 @@ public void resolved_fallbackToHttpMaxStreamDurationAsTimeout() { FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); Route route = Route.forAction(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( - cluster1, Collections.emptyList(), null, null), // per-route timeout unset + cluster1, Collections.emptyList(), null, null, false), // per-route timeout unset ImmutableMap.of()); VirtualHost virtualHost = VirtualHost.create("does not matter", Collections.singletonList(AUTHORITY), Collections.singletonList(route), @@ -675,7 +675,8 @@ public void retryPolicyInPerMethodConfigGeneratedByResolverIsValid() { cluster1, Collections.emptyList(), null, - retryPolicy), + retryPolicy, + false), ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); @@ -733,7 +734,7 @@ public void resolved_simpleCallFailedToRoute_routeWithNonForwardingAction() { Route.forAction( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), RouteAction.forCluster(cluster2, Collections.emptyList(), - TimeUnit.SECONDS.toNanos(15L), null), + TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); @@ -769,7 +770,8 @@ public void resolved_rpcHashingByHeader_withoutSubstitution() { Collections.singletonList( HashPolicy.forHeader(false, "custom-key", null, null)), null, - null), + null, + false), ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); InternalConfigSelector configSelector = @@ -801,9 +803,11 @@ public void resolved_rpcHashingByHeader_withSubstitution() { RouteAction.forCluster( cluster1, Collections.singletonList( - HashPolicy.forHeader(false, "custom-key", Pattern.compile("value"), "val")), + HashPolicy.forHeader(false, "custom-key", Pattern.compile("value"), + "val")), + null, null, - null), + false), ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); InternalConfigSelector configSelector = @@ -842,7 +846,8 @@ public void resolved_rpcHashingByChannelId() { cluster1, Collections.singletonList(HashPolicy.forChannelId(false)), null, - null), + null, + false), ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); InternalConfigSelector configSelector = @@ -878,7 +883,8 @@ public void resolved_rpcHashingByChannelId() { cluster1, Collections.singletonList(HashPolicy.forChannelId(false)), null, - null), + null, + false), ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); configSelector = resolutionResultCaptor.getValue().getAttributes().get( @@ -894,6 +900,68 @@ public void resolved_rpcHashingByChannelId() { assertThat(hash3).isNotEqualTo(hash1); } + @Test + public void resolved_routeActionHasAutoHostRewrite_emitsCallOptionForTheSame() { + resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, + syncContext, scheduler, xdsClientPoolFactory, mockRandom, + FilterRegistry.getDefaultRegistry(), null); + resolver.start(mockListener); + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + xdsClient.deliverLdsUpdate( + Collections.singletonList( + Route.forAction( + RouteMatch.withPathExactOnly( + "/" + TestMethodDescriptors.voidMethod().getFullMethodName()), + RouteAction.forCluster( + cluster1, + Collections.singletonList( + HashPolicy.forHeader(false, "custom-key", null, null)), + null, + null, + true), + ImmutableMap.of()))); + verify(mockListener).onResult(resolutionResultCaptor.capture()); + InternalConfigSelector configSelector = + resolutionResultCaptor.getValue().getAttributes().get(InternalConfigSelector.KEY); + + // First call, with header "custom-key": "custom-value". + startNewCall(TestMethodDescriptors.voidMethod(), configSelector, + ImmutableMap.of("custom-key", "custom-value"), CallOptions.DEFAULT); + + assertThat(testCall.callOptions.getOption(XdsNameResolver.AUTO_HOST_REWRITE_KEY)).isTrue(); + } + + @Test + public void resolved_routeActionNoAutoHostRewrite_doesntEmitCallOptionForTheSame() { + resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, + syncContext, scheduler, xdsClientPoolFactory, mockRandom, + FilterRegistry.getDefaultRegistry(), null); + resolver.start(mockListener); + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + xdsClient.deliverLdsUpdate( + Collections.singletonList( + Route.forAction( + RouteMatch.withPathExactOnly( + "/" + TestMethodDescriptors.voidMethod().getFullMethodName()), + RouteAction.forCluster( + cluster1, + Collections.singletonList( + HashPolicy.forHeader(false, "custom-key", null, null)), + null, + null, + false), + ImmutableMap.of()))); + verify(mockListener).onResult(resolutionResultCaptor.capture()); + InternalConfigSelector configSelector = + resolutionResultCaptor.getValue().getAttributes().get(InternalConfigSelector.KEY); + + // First call, with header "custom-key": "custom-value". + startNewCall(TestMethodDescriptors.voidMethod(), configSelector, + ImmutableMap.of("custom-key", "custom-value"), CallOptions.DEFAULT); + + assertThat(testCall.callOptions.getOption(XdsNameResolver.AUTO_HOST_REWRITE_KEY)).isNull(); + } + @SuppressWarnings("unchecked") @Test public void resolved_resourceUpdateAfterCallStarted() { @@ -909,13 +977,13 @@ public void resolved_resourceUpdateAfterCallStarted() { RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( "another-cluster", Collections.emptyList(), - TimeUnit.SECONDS.toNanos(20L), null), + TimeUnit.SECONDS.toNanos(20L), null, false), ImmutableMap.of()), Route.forAction( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), RouteAction.forCluster( cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), - null), + null, false), ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); @@ -949,13 +1017,13 @@ public void resolved_resourceUpdatedBeforeCallStarted() { RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( "another-cluster", Collections.emptyList(), - TimeUnit.SECONDS.toNanos(20L), null), + TimeUnit.SECONDS.toNanos(20L), null, false), ImmutableMap.of()), Route.forAction( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), RouteAction.forCluster( cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), - null), + null, false), ImmutableMap.of()))); // Two consecutive service config updates: one for removing clcuster1, // one for adding "another=cluster". @@ -985,13 +1053,13 @@ public void resolved_raceBetweenCallAndRepeatedResourceUpdate() { RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( "another-cluster", Collections.emptyList(), - TimeUnit.SECONDS.toNanos(20L), null), + TimeUnit.SECONDS.toNanos(20L), null, false), ImmutableMap.of()), Route.forAction( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), RouteAction.forCluster( cluster2, Collections.emptyList(), - TimeUnit.SECONDS.toNanos(15L), null), + TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); @@ -1006,13 +1074,13 @@ public void resolved_raceBetweenCallAndRepeatedResourceUpdate() { RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( "another-cluster", Collections.emptyList(), - TimeUnit.SECONDS.toNanos(15L), null), + TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()), Route.forAction( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), RouteAction.forCluster( cluster2, Collections.emptyList(), - TimeUnit.SECONDS.toNanos(15L), null), + TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()))); verifyNoMoreInteractions(mockListener); // no cluster added/deleted assertCallSelectClusterResult(call1, configSelector, "another-cluster", 15.0); @@ -1029,7 +1097,7 @@ public void resolved_raceBetweenClusterReleasedAndResourceUpdateAddBackAgain() { RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), RouteAction.forCluster( cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), - null), + null, false), ImmutableMap.of()))); xdsClient.deliverLdsUpdate( Arrays.asList( @@ -1037,13 +1105,13 @@ public void resolved_raceBetweenClusterReleasedAndResourceUpdateAddBackAgain() { RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), - null), + null, false), ImmutableMap.of()), Route.forAction( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), RouteAction.forCluster( cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), - null), + null, false), ImmutableMap.of()))); testCall.deliverErrorStatus(); verifyNoMoreInteractions(mockListener); @@ -1067,7 +1135,7 @@ public void resolved_simpleCallSucceeds_routeToWeightedCluster() { cluster2, 80, ImmutableMap.of())), Collections.emptyList(), TimeUnit.SECONDS.toNanos(20L), - null), + null, false), ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); @@ -1096,7 +1164,7 @@ public void resolved_simpleCallSucceeds_routeToRls() { ImmutableMap.of("lookupService", "rls-cbt.googleapis.com"))), Collections.emptyList(), TimeUnit.SECONDS.toNanos(20L), - null), + null, false), ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); @@ -1144,7 +1212,7 @@ public void resolved_simpleCallSucceeds_routeToRls() { Collections.emptyList(), // changed TimeUnit.SECONDS.toNanos(30L), - null), + null, false), ImmutableMap.of()))); verify(mockListener, times(2)).onResult(resolutionResultCaptor.capture()); ResolutionResult result2 = resolutionResultCaptor.getValue(); @@ -1250,13 +1318,13 @@ private InternalConfigSelector resolveToClusters() { RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), RouteAction.forCluster( cluster1, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), - null), + null, false), ImmutableMap.of()), Route.forAction( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), RouteAction.forCluster( cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), - null), + null, false), ImmutableMap.of()))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); @@ -1305,7 +1373,7 @@ public void generateServiceConfig_forClusterManagerLoadBalancingConfig() throws Route route1 = Route.forAction( RouteMatch.withPathExactOnly("HelloService/hi"), RouteAction.forCluster( - "cluster-foo", Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null), + "cluster-foo", Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()); Route route2 = Route.forAction( RouteMatch.withPathExactOnly("HelloService/hello"), @@ -1315,7 +1383,7 @@ public void generateServiceConfig_forClusterManagerLoadBalancingConfig() throws ClusterWeight.create("cluster-baz", 50, ImmutableMap.of())), ImmutableList.of(), TimeUnit.SECONDS.toNanos(15L), - null), + null, false), ImmutableMap.of()); Map rlsConfig = ImmutableMap.of("lookupService", "rls.bigtable.google.com"); Route route3 = Route.forAction( @@ -1324,7 +1392,7 @@ public void generateServiceConfig_forClusterManagerLoadBalancingConfig() throws NamedPluginConfig.create("plugin-foo", RlsPluginConfig.create(rlsConfig)), Collections.emptyList(), TimeUnit.SECONDS.toNanos(20L), - null), + null, false), ImmutableMap.of()); resolver.start(mockListener); @@ -1914,6 +1982,7 @@ private PickSubchannelArgs newPickSubchannelArgs( private final class FakeXdsClientPoolFactory implements XdsClientPoolFactory { Set targets = new HashSet<>(); + XdsClient xdsClient = new FakeXdsClient(); @Override public void setBootstrapOverride(Map bootstrap) {} @@ -1930,7 +1999,7 @@ public ObjectPool getOrCreate(String target) throws XdsInitialization return new ObjectPool() { @Override public XdsClient getObject() { - return new FakeXdsClient(); + return xdsClient; } @Override @@ -2058,7 +2127,8 @@ void deliverLdsUpdateWithFaultInjection( Collections.singletonList(clusterWeight), Collections.emptyList(), null, - null), + null, + false), overrideConfig); overrideConfig = virtualHostFaultConfig == null ? ImmutableMap.of() @@ -2125,7 +2195,8 @@ void deliverRdsUpdateWithFaultInjection( Collections.singletonList(clusterWeight), Collections.emptyList(), null, - null), + null, + false), overrideConfig); overrideConfig = virtualHostFaultConfig == null ? ImmutableMap.of() diff --git a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java index 55b8812cd17..66ac1475d8e 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java @@ -1035,7 +1035,8 @@ public void run() { "/FooService/barMethod", "foo.google.com", Route.RouteAction.forCluster( - "cluster", Collections.emptyList(), null, null)); + "cluster", Collections.emptyList(), null, null, + false)); ServerCall serverCall = mock(ServerCall.class); when(serverCall.getAttributes()).thenReturn( Attributes.newBuilder() From ef1fe87373b8436e518d7f74a89b2b3936484eb2 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Thu, 31 Oct 2024 11:51:40 +0530 Subject: [PATCH 072/591] okhttp: Use failing "source" for read bytes when sending GOAWAY due to insufficient thread pool size Create `ClientFrameHandler` with failing source to be used in case of failed 2nd thread scheduling. Fixes NPE from https://github.com/grpc/grpc-java/pull/11503. --- .../io/grpc/okhttp/OkHttpClientTransport.java | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java index 2f6b836dc3a..59f824b1a3c 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java @@ -515,21 +515,6 @@ public Runnable start(Listener listener) { serializingExecutor.execute(new Runnable() { @Override public void run() { - // This is a hack to make sure the connection preface and initial settings to be sent out - // without blocking the start. By doing this essentially prevents potential deadlock when - // network is not available during startup while another thread holding lock to send the - // initial preface. - try { - latch.await(); - barrier.await(1000, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (TimeoutException | BrokenBarrierException e) { - startGoAway(0, ErrorCode.INTERNAL_ERROR, Status.UNAVAILABLE - .withDescription("Timed out waiting for second handshake thread. " - + "The transport executor pool may have run out of threads")); - return; - } // Use closed source on failure so that the reader immediately shuts down. BufferedSource source = Okio.buffer(new Source() { @Override @@ -549,6 +534,22 @@ public void close() { Socket sock; SSLSession sslSession = null; try { + // This is a hack to make sure the connection preface and initial settings to be sent out + // without blocking the start. By doing this essentially prevents potential deadlock when + // network is not available during startup while another thread holding lock to send the + // initial preface. + try { + latch.await(); + barrier.await(1000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (TimeoutException | BrokenBarrierException e) { + startGoAway(0, ErrorCode.INTERNAL_ERROR, Status.UNAVAILABLE + .withDescription("Timed out waiting for second handshake thread. " + + "The transport executor pool may have run out of threads")); + return; + } + if (proxiedAddr == null) { sock = socketFactory.createSocket(address.getAddress(), address.getPort()); } else { @@ -1459,4 +1460,4 @@ public void alternateService(int streamId, String origin, ByteString protocol, S // TODO(madongfly): Deal with alternateService propagation } } -} +} \ No newline at end of file From 1993e68b038a3a9003374afc305c59b93532fa76 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 1 Nov 2024 07:50:08 -0700 Subject: [PATCH 073/591] Upgrade depedencies (#11655) --- MODULE.bazel | 10 ++-- api/src/main/java/io/grpc/ClientCall.java | 2 +- api/src/main/java/io/grpc/LoadBalancer.java | 11 +++- .../internal/AbstractServerStreamTest.java | 4 +- .../grpc/internal/AbstractTransportTest.java | 2 +- .../grpc/gcp/observability/LoggingTest.java | 2 +- gradle/libs.versions.toml | 58 +++++++++++-------- .../io/grpc/inprocess/InProcessTransport.java | 2 +- .../integration/AbstractInteropTest.java | 2 +- repositories.bzl | 10 ++-- .../java/io/grpc/util/AbstractTestHelper.java | 1 + .../grpc/xds/GrpcXdsClientImplDataTest.java | 2 +- 12 files changed, 61 insertions(+), 45 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index f58d2daa40a..724f537ee6f 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,16 +8,16 @@ module( # GRPC_DEPS_START IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.android:annotations:4.1.1.4", - "com.google.api.grpc:proto-google-common-protos:2.29.0", - "com.google.auth:google-auth-library-credentials:1.23.0", - "com.google.auth:google-auth-library-oauth2-http:1.23.0", + "com.google.api.grpc:proto-google-common-protos:2.48.0", + "com.google.auth:google-auth-library-credentials:1.24.1", + "com.google.auth:google-auth-library-oauth2-http:1.24.1", "com.google.auto.value:auto-value-annotations:1.11.0", "com.google.auto.value:auto-value:1.11.0", "com.google.code.findbugs:jsr305:3.0.2", "com.google.code.gson:gson:2.11.0", - "com.google.errorprone:error_prone_annotations:2.28.0", + "com.google.errorprone:error_prone_annotations:2.30.0", "com.google.guava:failureaccess:1.0.1", - "com.google.guava:guava:33.2.1-android", + "com.google.guava:guava:33.3.1-android", "com.google.re2j:re2j:1.7", "com.google.truth:truth:1.4.2", "com.squareup.okhttp:okhttp:2.7.5", diff --git a/api/src/main/java/io/grpc/ClientCall.java b/api/src/main/java/io/grpc/ClientCall.java index df9e15001e1..c915c8beaac 100644 --- a/api/src/main/java/io/grpc/ClientCall.java +++ b/api/src/main/java/io/grpc/ClientCall.java @@ -67,7 +67,7 @@ * manner, and notifies gRPC library to receive additional response after one is consumed by * a fictional processResponse(). * - *

+ * 
  *   call = channel.newCall(bidiStreamingMethod, callOptions);
  *   listener = new ClientCall.Listener<FooResponse>() {
  *     @Override
diff --git a/api/src/main/java/io/grpc/LoadBalancer.java b/api/src/main/java/io/grpc/LoadBalancer.java
index 97ead20ed36..d5f44dafa5e 100644
--- a/api/src/main/java/io/grpc/LoadBalancer.java
+++ b/api/src/main/java/io/grpc/LoadBalancer.java
@@ -1033,8 +1033,8 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) {
     }
 
     /**
-     * Out-of-band channel for LoadBalancer’s own RPC needs, e.g., talking to an external
-     * load-balancer service.
+     * Create an out-of-band channel for the LoadBalancer’s own RPC needs, e.g., talking to an
+     * external load-balancer service.
      *
      * 

The LoadBalancer is responsible for closing unused OOB channels, and closing all OOB * channels within {@link #shutdown}. @@ -1044,7 +1044,12 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { public abstract ManagedChannel createOobChannel(EquivalentAddressGroup eag, String authority); /** - * Accept a list of EAG for multiple authorities: https://github.com/grpc/grpc-java/issues/4618 + * Create an out-of-band channel for the LoadBalancer's own RPC needs, e.g., talking to an + * external load-balancer service. This version of the method allows multiple EAGs, so different + * addresses can have different authorities. + * + *

The LoadBalancer is responsible for closing unused OOB channels, and closing all OOB + * channels within {@link #shutdown}. * */ public ManagedChannel createOobChannel(List eag, String authority) { diff --git a/core/src/test/java/io/grpc/internal/AbstractServerStreamTest.java b/core/src/test/java/io/grpc/internal/AbstractServerStreamTest.java index 66fa92b1cf8..b41d45e972e 100644 --- a/core/src/test/java/io/grpc/internal/AbstractServerStreamTest.java +++ b/core/src/test/java/io/grpc/internal/AbstractServerStreamTest.java @@ -85,7 +85,7 @@ public void setUp() { } /** - * Test for issue https://github.com/grpc/grpc-java/issues/1795 + * Test for issue https://github.com/grpc/grpc-java/issues/1795 . */ @Test public void frameShouldBeIgnoredAfterDeframerClosed() { @@ -212,7 +212,7 @@ public void closed(Status status) { } /** - * Test for issue https://github.com/grpc/grpc-java/issues/615 + * Test for issue https://github.com/grpc/grpc-java/issues/615 . */ @Test public void completeWithoutClose() { diff --git a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java index 69d8e65955e..aea7ff49032 100644 --- a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java +++ b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java @@ -269,7 +269,7 @@ protected long fakeCurrentTimeNanos() { // (and maybe exceptions handled) /** - * Test for issue https://github.com/grpc/grpc-java/issues/1682 + * Test for issue https://github.com/grpc/grpc-java/issues/1682 . */ @Test public void frameAfterRstStreamShouldNotBreakClientChannel() throws Exception { diff --git a/gcp-observability/src/test/java/io/grpc/gcp/observability/LoggingTest.java b/gcp-observability/src/test/java/io/grpc/gcp/observability/LoggingTest.java index ee711cad097..92e67b01e01 100644 --- a/gcp-observability/src/test/java/io/grpc/gcp/observability/LoggingTest.java +++ b/gcp-observability/src/test/java/io/grpc/gcp/observability/LoggingTest.java @@ -73,7 +73,7 @@ public class LoggingTest { /** * Cloud logging test using global interceptors. * - *

Ignoring test, because it calls external Cloud Logging APIs. + *

Ignoring test, because it calls external Cloud Logging APIs. * To test cloud logging setup locally, * 1. Set up Cloud auth credentials * 2. Assign permissions to service account to write logs to project specified by diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8d7fb3766e0..b9f9c5350e1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,45 +10,54 @@ protobuf = "3.25.5" [libraries] android-annotations = "com.google.android:annotations:4.1.1.4" -androidx-annotation = "androidx.annotation:annotation:1.8.0" +androidx-annotation = "androidx.annotation:annotation:1.9.0" androidx-core = "androidx.core:core:1.13.1" -androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.8.3" -androidx-lifecycle-service = "androidx.lifecycle:lifecycle-service:2.8.3" +androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.8.6" +androidx-lifecycle-service = "androidx.lifecycle:lifecycle-service:2.8.6" androidx-test-core = "androidx.test:core:1.6.1" androidx-test-ext-junit = "androidx.test.ext:junit:1.2.1" androidx-test-rules = "androidx.test:rules:1.6.1" animalsniffer = "org.codehaus.mojo:animal-sniffer:1.24" animalsniffer-annotations = "org.codehaus.mojo:animal-sniffer-annotations:1.24" -assertj-core = "org.assertj:assertj-core:3.26.0" +assertj-core = "org.assertj:assertj-core:3.26.3" auto-value = "com.google.auto.value:auto-value:1.11.0" auto-value-annotations = "com.google.auto.value:auto-value-annotations:1.11.0" -checkstyle = "com.puppycrawl.tools:checkstyle:10.17.0" +checkstyle = "com.puppycrawl.tools:checkstyle:10.19.0" commons-math3 = "org.apache.commons:commons-math3:3.6.1" conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2" cronet-api = "org.chromium.net:cronet-api:119.6045.31" cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31" -errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.28.0" -errorprone-core = "com.google.errorprone:error_prone_core:2.28.0" -google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.41.0" -google-auth-credentials = "com.google.auth:google-auth-library-credentials:1.23.0" -google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.23.0" +# error-prone 2.31.0+ blocked on https://github.com/grpc/grpc-java/issues/10152 +# It breaks Bazel (ArrayIndexOutOfBoundsException in turbine) and Dexing ("D8: +# java.lang.NullPointerException"). We can trivially upgrade the Bazel CI to +# 6.3.0+ (https://github.com/bazelbuild/bazel/issues/18743). +errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.30.0" +# error-prone 2.32.0+ require Java 17+ +errorprone-core = "com.google.errorprone:error_prone_core:2.31.0" +google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.48.0" +# google-auth-library 1.25.0+ requires error_prone_annotations 2.31.0+, which +# breaks the Android build +google-auth-credentials = "com.google.auth:google-auth-library-credentials:1.24.1" +google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.24.1" # Release notes: https://cloud.google.com/logging/docs/release-notes -google-cloud-logging = "com.google.cloud:google-cloud-logging:3.19.0" +google-cloud-logging = "com.google.cloud:google-cloud-logging:3.20.6" gson = "com.google.code.gson:gson:2.11.0" -guava = "com.google.guava:guava:33.2.1-android" +guava = "com.google.guava:guava:33.3.1-android" guava-betaChecker = "com.google.guava:guava-beta-checker:1.0" -guava-testlib = "com.google.guava:guava-testlib:33.2.1-android" +guava-testlib = "com.google.guava:guava-testlib:33.3.1-android" # JRE version is needed for projects where its a transitive dependency, f.e. gcp-observability. # May be different from the -android version. -guava-jre = "com.google.guava:guava:33.2.1-jre" +guava-jre = "com.google.guava:guava:33.3.1-jre" hdrhistogram = "org.hdrhistogram:HdrHistogram:2.2.2" +# 6.0.0+ use java.lang.Deprecated forRemoval and since from Java 9 jakarta-servlet-api = "jakarta.servlet:jakarta.servlet-api:5.0.0" javax-annotation = "org.apache.tomcat:annotations-api:6.0.53" javax-servlet-api = "javax.servlet:javax.servlet-api:4.0.1" -jetty-client = "org.eclipse.jetty:jetty-client:10.0.20" -jetty-http2-server = "org.eclipse.jetty.http2:http2-server:11.0.22" +# 12.0.0+ require Java 17+ +jetty-client = "org.eclipse.jetty:jetty-client:11.0.24" +jetty-http2-server = "org.eclipse.jetty.http2:http2-server:11.0.24" jetty-http2-server10 = "org.eclipse.jetty.http2:http2-server:10.0.20" -jetty-servlet = "org.eclipse.jetty:jetty-servlet:11.0.22" +jetty-servlet = "org.eclipse.jetty:jetty-servlet:11.0.24" jetty-servlet10 = "org.eclipse.jetty:jetty-servlet:10.0.20" jsr305 = "com.google.code.findbugs:jsr305:3.0.2" junit = "junit:junit:4.13.2" @@ -76,11 +85,11 @@ opencensus-contrib-grpc-metrics = { module = "io.opencensus:opencensus-contrib-g opencensus-exporter-stats-stackdriver = { module = "io.opencensus:opencensus-exporter-stats-stackdriver", version.ref = "opencensus" } opencensus-exporter-trace-stackdriver = { module = "io.opencensus:opencensus-exporter-trace-stackdriver", version.ref = "opencensus" } opencensus-impl = { module = "io.opencensus:opencensus-impl", version.ref = "opencensus" } -opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.40.0" -opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.40.0-alpha" -opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.36.0-alpha" -opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.40.0" -opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.40.0" +opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.43.0" +opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.43.0-alpha" +opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.40.0-alpha" +opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.43.0" +opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.43.0" perfmark-api = "io.perfmark:perfmark-api:0.27.0" protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobuf" } @@ -90,11 +99,12 @@ re2j = "com.google.re2j:re2j:1.7" robolectric = "org.robolectric:robolectric:4.13" signature-android = "net.sf.androidscents.signature:android-api-level-19:4.4.2_r4" signature-java = "org.codehaus.mojo.signature:java18:1.0" -tomcat-embed-core = "org.apache.tomcat.embed:tomcat-embed-core:10.1.25" +# 11.0.0+ require Java 17+ +tomcat-embed-core = "org.apache.tomcat.embed:tomcat-embed-core:10.1.31" tomcat-embed-core9 = "org.apache.tomcat.embed:tomcat-embed-core:9.0.89" truth = "com.google.truth:truth:1.4.4" undertow-servlet22 = "io.undertow:undertow-servlet:2.2.32.Final" -undertow-servlet = "io.undertow:undertow-servlet:2.3.14.Final" +undertow-servlet = "io.undertow:undertow-servlet:2.3.18.Final" # Do not update: Pinned to the last version supporting Java 8. # See https://checkstyle.sourceforge.io/releasenotes.html#Release_10.1 diff --git a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java index 4212d96e9fb..eacf46ca4a2 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java @@ -606,7 +606,7 @@ public void close(Status status, Metadata trailers) { notifyClientClose(status, trailers); } - /** clientStream.serverClosed() must be called before this method */ + /** clientStream.serverClosed() must be called before this method. */ private void notifyClientClose(Status status, Metadata trailers) { Status clientStatus = cleanStatus(status, includeCauseWithStatus); synchronized (this) { diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java index a581c750028..53d24d74a36 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java @@ -133,7 +133,7 @@ /** * Abstract base class for all GRPC transport tests. * - *

New tests should avoid using Mockito to support running on AppEngine.

+ *

New tests should avoid using Mockito to support running on AppEngine. */ public abstract class AbstractInteropTest { private static Logger logger = Logger.getLogger(AbstractInteropTest.class.getName()); diff --git a/repositories.bzl b/repositories.bzl index f8cf77c8190..375c8872e66 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -12,16 +12,16 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # GRPC_DEPS_START IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.android:annotations:4.1.1.4", - "com.google.api.grpc:proto-google-common-protos:2.29.0", - "com.google.auth:google-auth-library-credentials:1.23.0", - "com.google.auth:google-auth-library-oauth2-http:1.23.0", + "com.google.api.grpc:proto-google-common-protos:2.48.0", + "com.google.auth:google-auth-library-credentials:1.24.1", + "com.google.auth:google-auth-library-oauth2-http:1.24.1", "com.google.auto.value:auto-value-annotations:1.11.0", "com.google.auto.value:auto-value:1.11.0", "com.google.code.findbugs:jsr305:3.0.2", "com.google.code.gson:gson:2.11.0", - "com.google.errorprone:error_prone_annotations:2.28.0", + "com.google.errorprone:error_prone_annotations:2.30.0", "com.google.guava:failureaccess:1.0.1", - "com.google.guava:guava:33.2.1-android", + "com.google.guava:guava:33.3.1-android", "com.google.re2j:re2j:1.7", "com.google.truth:truth:1.4.2", "com.squareup.okhttp:okhttp:2.7.5", diff --git a/util/src/testFixtures/java/io/grpc/util/AbstractTestHelper.java b/util/src/testFixtures/java/io/grpc/util/AbstractTestHelper.java index bdeff9d17c5..bbf2d5efe9f 100644 --- a/util/src/testFixtures/java/io/grpc/util/AbstractTestHelper.java +++ b/util/src/testFixtures/java/io/grpc/util/AbstractTestHelper.java @@ -55,6 +55,7 @@ * To use it replace
* \@mock Helper mockHelper
* with
+ * *

Helper mockHelper = mock(Helper.class, delegatesTo(new TestHelper()));

*
* TestHelper will need to define accessors for the maps that information is store within as diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 87e875e75f6..b0ef0131a6d 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -3173,7 +3173,7 @@ public void canonifyResourceName() { /** * Tests compliance with RFC 3986 section 3.3 - * https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + * https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 . */ @Test public void percentEncodePath() { From 88596868a457513844b1ca8ef95fa8eecd2c13b1 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 5 Nov 2024 10:56:33 +0530 Subject: [PATCH 074/591] xds: Envoy proto sync to 2024-10-23 (#11664) --- repositories.bzl | 6 +- xds/third_party/envoy/import.sh | 2 +- .../envoy/config/cluster/v3/cluster.proto | 36 ++++++- .../proto/envoy/config/core/v3/protocol.proto | 50 +++++++--- .../config/core/v3/socket_cmsg_headers.proto | 28 ++++++ .../envoy/config/core/v3/socket_option.proto | 29 +++++- .../config/endpoint/v3/load_report.proto | 27 +++++- .../config/listener/v3/quic_config.proto | 15 ++- .../config/route/v3/route_components.proto | 6 +- .../proto/envoy/config/trace/v3/datadog.proto | 16 ++++ .../envoy/config/trace/v3/dynamic_ot.proto | 4 +- .../v3/http_connection_manager.proto | 94 +++++++++++++++---- .../v3/client_side_weighted_round_robin.proto | 15 ++- .../transport_sockets/tls/v3/common.proto | 4 +- .../transport_sockets/tls/v3/tls.proto | 16 +++- 15 files changed, 287 insertions(+), 61 deletions(-) create mode 100644 xds/third_party/envoy/src/main/proto/envoy/config/core/v3/socket_cmsg_headers.proto diff --git a/repositories.bzl b/repositories.bzl index 375c8872e66..7d01675e9ad 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -141,10 +141,10 @@ def grpc_java_repositories(bzlmod = False): if not native.existing_rule("envoy_api"): http_archive( name = "envoy_api", - sha256 = "cb7cd388eaa297320d392c872ceb82571dee71f4b6f1c4546b0c0a399636f523", - strip_prefix = "data-plane-api-874e3aa8c3aa5086b6bffa2166e0e0077bb32f71", + sha256 = "f439add0cc01f718d53d6feb4d0972ac0d48b3e145c18b53439a3b5148a0cb6e", + strip_prefix = "data-plane-api-55f8b2351962d84c84a6534da67da1dd9f671c50", urls = [ - "https://github.com/envoyproxy/data-plane-api/archive/874e3aa8c3aa5086b6bffa2166e0e0077bb32f71.tar.gz", + "https://github.com/envoyproxy/data-plane-api/archive/55f8b2351962d84c84a6534da67da1dd9f671c50.tar.gz", ], ) diff --git a/xds/third_party/envoy/import.sh b/xds/third_party/envoy/import.sh index 41506c2ed32..8f0271e784e 100755 --- a/xds/third_party/envoy/import.sh +++ b/xds/third_party/envoy/import.sh @@ -17,7 +17,7 @@ set -e # import VERSION from the google internal copybara_version.txt for Envoy -VERSION=ab911ac2ff971f805ec822ad4d4ff6b42a61cc7c +VERSION=742a3b02e3b2a9dfb877a7e378607c6ed0c2aa53 DOWNLOAD_URL="https://github.com/envoyproxy/envoy/archive/${VERSION}.tar.gz" DOWNLOAD_BASE_DIR="envoy-${VERSION}" SOURCE_PROTO_BASE_DIR="${DOWNLOAD_BASE_DIR}/api" diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/cluster/v3/cluster.proto b/xds/third_party/envoy/src/main/proto/envoy/config/cluster/v3/cluster.proto index 0074e63dff6..0d2d6f1918e 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/cluster/v3/cluster.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/cluster/v3/cluster.proto @@ -45,7 +45,7 @@ message ClusterCollection { } // Configuration for a single upstream cluster. -// [#next-free-field: 57] +// [#next-free-field: 59] message Cluster { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.Cluster"; @@ -754,13 +754,13 @@ message Cluster { reserved "hosts", "tls_context", "extension_protocol_options"; - // Configuration to use different transport sockets for different endpoints. The entry of + // Configuration to use different transport sockets for different endpoints. The entry of // ``envoy.transport_socket_match`` in the :ref:`LbEndpoint.Metadata // ` is used to match against the // transport sockets as they appear in the list. If a match is not found, the search continues in // :ref:`LocalityLbEndpoints.Metadata - // `. The first :ref:`match - // ` is used. For example, with + // `. The first :ref:`match + // ` is used. For example, with // the following match // // .. code-block:: yaml @@ -956,6 +956,17 @@ message Cluster { google.protobuf.Duration dns_refresh_rate = 16 [(validate.rules).duration = {gt {nanos: 1000000}}]; + // DNS jitter can be optionally specified if the cluster type is either + // :ref:`STRICT_DNS`, + // or :ref:`LOGICAL_DNS`. + // DNS jitter causes the cluster to refresh DNS entries later by a random amount of time to avoid a + // stampede of DNS requests. This value sets the upper bound (exclusive) for the random amount. + // There will be no jitter if this value is omitted. For cluster types other than + // :ref:`STRICT_DNS` + // and :ref:`LOGICAL_DNS` + // this setting is ignored. + google.protobuf.Duration dns_jitter = 58; + // If the DNS failure refresh rate is specified and the cluster type is either // :ref:`STRICT_DNS`, // or :ref:`LOGICAL_DNS`, @@ -1151,6 +1162,23 @@ message Cluster { // from the LRS stream here.] core.v3.ConfigSource lrs_server = 42; + // A list of metric names from :ref:`ORCA load reports ` to propagate to LRS. + // + // If not specified, then ORCA load reports will not be propagated to LRS. + // + // For map fields in the ORCA proto, the string will be of the form ``.``. + // For example, the string ``named_metrics.foo`` will mean to look for the key ``foo`` in the ORCA + // :ref:`named_metrics ` field. + // + // The special map key ``*`` means to report all entries in the map (e.g., ``named_metrics.*`` means to + // report all entries in the ORCA named_metrics field). Note that this should be used only with trusted + // backends. + // + // The metric names in LRS will follow the same semantics as this field. In other words, if this field + // contains ``named_metrics.foo``, then the LRS load report will include the data with that same string + // as the key. + repeated string lrs_report_endpoint_metrics = 57; + // If track_timeout_budgets is true, the :ref:`timeout budget histograms // ` will be published for each // request. These show what percentage of a request's per try and global timeout was used. A value diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto index e2c5863d784..d8ce3cd817c 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto @@ -39,24 +39,21 @@ message QuicKeepAliveSettings { // // If zero, disable keepalive probing. // If absent, use the QUICHE default interval to probe. - google.protobuf.Duration max_interval = 1 [(validate.rules).duration = { - lte {} - gte {seconds: 1} - }]; + google.protobuf.Duration max_interval = 1; // The interval to send the first few keep-alive probing packets to prevent connection from hitting the idle timeout. Subsequent probes will be sent, each one with an interval exponentially longer than previous one, till it reaches :ref:`max_interval `. And the probes afterwards will always use :ref:`max_interval `. // // The value should be smaller than :ref:`connection idle_timeout ` to prevent idle timeout and smaller than max_interval to take effect. // - // If absent or zero, disable keepalive probing for a server connection. For a client connection, if :ref:`max_interval ` is also zero, do not keepalive, otherwise use max_interval or QUICHE default to probe all the time. + // If absent, disable keepalive probing for a server connection. For a client connection, if :ref:`max_interval ` is zero, do not keepalive, otherwise use max_interval or QUICHE default to probe all the time. google.protobuf.Duration initial_interval = 2 [(validate.rules).duration = { lte {} - gte {seconds: 1} + gte {nanos: 1000000} }]; } // QUIC protocol options which apply to both downstream and upstream connections. -// [#next-free-field: 9] +// [#next-free-field: 10] message QuicProtocolOptions { // Maximum number of streams that the client can negotiate per connection. 100 // if not specified. @@ -111,6 +108,10 @@ message QuicProtocolOptions { lte {seconds: 600} gte {seconds: 1} }]; + + // Maximum packet length for QUIC connections. It refers to the largest size of a QUIC packet that can be transmitted over the connection. + // If not specified, one of the `default values in QUICHE `_ is used. + google.protobuf.UInt64Value max_packet_length = 9; } message UpstreamHttpProtocolOptions { @@ -205,7 +206,7 @@ message AlternateProtocolsCacheOptions { repeated string canonical_suffixes = 5; } -// [#next-free-field: 7] +// [#next-free-field: 8] message HttpProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.HttpProtocolOptions"; @@ -249,18 +250,37 @@ message HttpProtocolOptions { google.protobuf.Duration idle_timeout = 1; // The maximum duration of a connection. The duration is defined as a period since a connection - // was established. If not set, there is no max duration. When max_connection_duration is reached - // and if there are no active streams, the connection will be closed. If the connection is a - // downstream connection and there are any active streams, the drain sequence will kick-in, - // and the connection will be force-closed after the drain period. See :ref:`drain_timeout + // was established. If not set, there is no max duration. When max_connection_duration is reached, + // the drain sequence will kick-in. The connection will be closed after the drain timeout period + // if there are no active streams. See :ref:`drain_timeout // `. google.protobuf.Duration max_connection_duration = 3; - // The maximum number of headers. If unconfigured, the default - // maximum number of request headers allowed is 100. Requests that exceed this limit will receive - // a 431 response for HTTP/1.x and cause a stream reset for HTTP/2. + // The maximum number of headers (request headers if configured on HttpConnectionManager, + // response headers when configured on a cluster). + // If unconfigured, the default maximum number of headers allowed is 100. + // The default value for requests can be overridden by setting runtime key ``envoy.reloadable_features.max_request_headers_count``. + // The default value for responses can be overridden by setting runtime key ``envoy.reloadable_features.max_response_headers_count``. + // Downstream requests that exceed this limit will receive a 431 response for HTTP/1.x and cause a stream + // reset for HTTP/2. + // Upstream responses that exceed this limit will result in a 503 response. google.protobuf.UInt32Value max_headers_count = 2 [(validate.rules).uint32 = {gte: 1}]; + // The maximum size of response headers. + // If unconfigured, the default is 60 KiB, except for HTTP/1 response headers which have a default + // of 80KiB. + // The default value can be overridden by setting runtime key ``envoy.reloadable_features.max_response_headers_size_kb``. + // Responses that exceed this limit will result in a 503 response. + // In Envoy, this setting is only valid when configured on an upstream cluster, not on the + // :ref:`HTTP Connection Manager + // `. + // + // Note: currently some protocol codecs impose limits on the maximum size of a single header: + // HTTP/2 (when using nghttp2) limits a single header to around 100kb. + // HTTP/3 limits a single header to around 1024kb. + google.protobuf.UInt32Value max_response_headers_kb = 7 + [(validate.rules).uint32 = {lte: 8192 gt: 0}]; + // Total duration to keep alive an HTTP request/response stream. If the time limit is reached the stream will be // reset independent of any other timeouts. If not specified, this value is not set. google.protobuf.Duration max_stream_duration = 4; diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/socket_cmsg_headers.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/socket_cmsg_headers.proto new file mode 100644 index 00000000000..4c8da0d1e4e --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/socket_cmsg_headers.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package envoy.config.core.v3; + +import "google/protobuf/wrappers.proto"; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.config.core.v3"; +option java_outer_classname = "SocketCmsgHeadersProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/config/core/v3;corev3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Socket CMSG headers] + +// Configuration for socket cmsg headers. +// See `:ref:CMSG `_ for further information. +message SocketCmsgHeaders { + // cmsg level. Default is unset. + google.protobuf.UInt32Value level = 1; + + // cmsg type. Default is unset. + google.protobuf.UInt32Value type = 2; + + // Expected size of cmsg value. Default is zero. + uint32 expected_size = 3; +} \ No newline at end of file diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/socket_option.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/socket_option.proto index 44f1ce3890a..ad73d72e490 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/socket_option.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/socket_option.proto @@ -36,7 +36,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // :ref:`admin's ` socket_options etc. // // It should be noted that the name or level may have different values on different platforms. -// [#next-free-field: 7] +// [#next-free-field: 8] message SocketOption { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.SocketOption"; @@ -51,6 +51,29 @@ message SocketOption { STATE_LISTENING = 2; } + // The `socket type `_ to apply the socket option to. + // Only one field should be set. If multiple fields are set, the precedence order will determine + // the selected one. If none of the fields is set, the socket option will be applied to all socket types. + // + // For example: + // If :ref:`stream ` is set, + // it takes precedence over :ref:`datagram `. + message SocketType { + // The stream socket type. + message Stream { + } + + // The datagram socket type. + message Datagram { + } + + // Apply the socket option to the stream socket type. + Stream stream = 1; + + // Apply the socket option to the datagram socket type. + Datagram datagram = 2; + } + // An optional name to give this socket option for debugging, etc. // Uniqueness is not required and no special meaning is assumed. string description = 1; @@ -74,6 +97,10 @@ message SocketOption { // The state in which the option will be applied. When used in BindConfig // STATE_PREBIND is currently the only valid value. SocketState state = 6 [(validate.rules).enum = {defined_only: true}]; + + // Apply the socket option to the specified `socket type `_. + // If not specified, the socket option will be applied to all socket types. + SocketType type = 7; } message SocketOptionsOverride { diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/load_report.proto b/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/load_report.proto index fbd1d36d5d0..32bbfe2d3f6 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/load_report.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/load_report.proto @@ -25,7 +25,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // These are stats Envoy reports to the management server at a frequency defined by // :ref:`LoadStatsResponse.load_reporting_interval`. // Stats per upstream region/zone and optionally per subzone. -// [#next-free-field: 12] +// [#next-free-field: 15] message UpstreamLocalityStats { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.endpoint.UpstreamLocalityStats"; @@ -75,7 +75,20 @@ message UpstreamLocalityStats { // [#not-implemented-hide:] uint64 total_fail_connections = 11 [(xds.annotations.v3.field_status).work_in_progress = true]; - // Stats for multi-dimensional load balancing. + // CPU utilization stats for multi-dimensional load balancing. + // This typically comes from endpoint metrics reported via ORCA. + UnnamedEndpointLoadMetricStats cpu_utilization = 12; + + // Memory utilization for multi-dimensional load balancing. + // This typically comes from endpoint metrics reported via ORCA. + UnnamedEndpointLoadMetricStats mem_utilization = 13; + + // Blended application-defined utilization for multi-dimensional load balancing. + // This typically comes from endpoint metrics reported via ORCA. + UnnamedEndpointLoadMetricStats application_utilization = 14; + + // Named stats for multi-dimensional load balancing. + // These typically come from endpoint metrics reported via ORCA. repeated EndpointLoadMetricStats load_metric_stats = 5; // Endpoint granularity stats information for this locality. This information @@ -145,6 +158,16 @@ message EndpointLoadMetricStats { double total_metric_value = 3; } +// Same as EndpointLoadMetricStats, except without the metric_name field. +message UnnamedEndpointLoadMetricStats { + // Number of calls that finished and included this metric. + uint64 num_requests_finished_with_metric = 1; + + // Sum of metric values across all calls that finished with this metric for + // load_reporting_interval. + double total_metric_value = 2; +} + // Per cluster load stats. Envoy reports these stats a management server in a // :ref:`LoadStatsRequest` // Next ID: 7 diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/quic_config.proto b/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/quic_config.proto index 3ddebe900ef..6c0a5bd201f 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/quic_config.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/quic_config.proto @@ -5,6 +5,7 @@ package envoy.config.listener.v3; import "envoy/config/core/v3/base.proto"; import "envoy/config/core/v3/extension.proto"; import "envoy/config/core/v3/protocol.proto"; +import "envoy/config/core/v3/socket_cmsg_headers.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/wrappers.proto"; @@ -24,7 +25,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: QUIC listener config] // Configuration specific to the UDP QUIC listener. -// [#next-free-field: 12] +// [#next-free-field: 14] message QuicProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.listener.QuicProtocolOptions"; @@ -86,4 +87,16 @@ message QuicProtocolOptions { // If not specified, no debug visitor will be attached to connections. // [#extension-category: envoy.quic.connection_debug_visitor] core.v3.TypedExtensionConfig connection_debug_visitor_config = 11; + + // Configure a type of UDP cmsg to pass to listener filters via QuicReceivedPacket. + // Both level and type must be specified for cmsg to be saved. + // Cmsg may be truncated or omitted if expected size is not set. + // If not specified, no cmsg will be saved to QuicReceivedPacket. + repeated core.v3.SocketCmsgHeaders save_cmsg_config = 12 + [(validate.rules).repeated = {max_items: 1}]; + + // If true, the listener will reject connection-establishing packets at the + // QUIC layer by replying with an empty version negotiation packet to the + // client. + bool reject_new_connections = 13; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/route/v3/route_components.proto b/xds/third_party/envoy/src/main/proto/envoy/config/route/v3/route_components.proto index 7e2ff33da5c..ce781d100c9 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/route/v3/route_components.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/route/v3/route_components.proto @@ -17,7 +17,6 @@ import "google/protobuf/any.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/wrappers.proto"; -import "xds/annotations/v3/status.proto"; import "xds/type/matcher/v3/matcher.proto"; import "envoy/annotations/deprecation.proto"; @@ -92,13 +91,12 @@ message VirtualHost { // The list of routes that will be matched, in order, for incoming requests. // The first route that matches will be used. // Only one of this and ``matcher`` can be specified. - repeated Route routes = 3; + repeated Route routes = 3 [(udpa.annotations.field_migrate).oneof_promotion = "route_selection"]; - // [#next-major-version: This should be included in a oneof with routes wrapped in a message.] // The match tree to use when resolving route actions for incoming requests. Only one of this and ``routes`` // can be specified. xds.type.matcher.v3.Matcher matcher = 21 - [(xds.annotations.v3.field_status).work_in_progress = true]; + [(udpa.annotations.field_migrate).oneof_promotion = "route_selection"]; // Specifies the type of TLS enforcement the virtual host expects. If this option is not // specified, there is no TLS requirement for the virtual host. diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/datadog.proto b/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/datadog.proto index bed6c8eec36..5359ec74267 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/datadog.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/datadog.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package envoy.config.trace.v3; +import "google/protobuf/duration.proto"; + import "udpa/annotations/migrate.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; @@ -16,6 +18,13 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Datadog tracer] +// Configuration for the Remote Configuration feature. +message DatadogRemoteConfig { + // Frequency at which new configuration updates are queried. + // If no value is provided, the default value is delegated to the Datadog tracing library. + google.protobuf.Duration polling_interval = 1; +} + // Configuration for the Datadog tracer. // [#extension: envoy.tracers.datadog] message DatadogConfig { @@ -31,4 +40,11 @@ message DatadogConfig { // Optional hostname to use when sending spans to the collector_cluster. Useful for collectors // that require a specific hostname. Defaults to :ref:`collector_cluster ` above. string collector_hostname = 3; + + // Enables and configures remote configuration. + // Remote Configuration allows to configure the tracer from Datadog's user interface. + // This feature can drastically increase the number of connections to the Datadog Agent. + // Each tracer regularly polls for configuration updates, and the number of tracers is the product + // of the number of listeners and worker threads. + DatadogRemoteConfig remote_config = 4; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/dynamic_ot.proto b/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/dynamic_ot.proto index d2664ef717e..40fe8526a5f 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/dynamic_ot.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/dynamic_ot.proto @@ -20,10 +20,10 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Dynamically loadable OpenTracing tracer] -// DynamicOtConfig is used to dynamically load a tracer from a shared library +// DynamicOtConfig was used to dynamically load a tracer from a shared library // that implements the `OpenTracing dynamic loading API // `_. -// [#extension: envoy.tracers.dynamic_ot] +// [#not-implemented-hide:] message DynamicOtConfig { option (udpa.annotations.versioning).previous_message_type = "envoy.config.trace.v2.DynamicOtConfig"; diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index 9e7274daa53..3b49f132956 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -37,7 +37,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 58] +// [#next-free-field: 59] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"; @@ -58,9 +58,8 @@ message HttpConnectionManager { // Prior knowledge is allowed). HTTP2 = 2; - // [#not-implemented-hide:] QUIC implementation is not production ready yet. Use this enum with - // caution to prevent accidental execution of QUIC code. I.e. `!= HTTP2` is no longer sufficient - // to distinguish HTTP1 and HTTP2 traffic. + // The connection manager will assume that the client is speaking HTTP/3. + // This needs to be consistent with listener and transport socket config. HTTP3 = 3; } @@ -447,6 +446,21 @@ message HttpConnectionManager { config.core.v3.HttpProtocolOptions common_http_protocol_options = 35 [(udpa.annotations.security).configure_for_untrusted_downstream = true]; + // If set to true, Envoy will not start a drain timer for downstream HTTP1 connections after + // :ref:`common_http_protocol_options.max_connection_duration + // ` passes. + // Instead, Envoy will wait for the next downstream request, add connection:close to the response + // headers, then close the connection after the stream ends. + // + // This behavior is compliant with `RFC 9112 section 9.6 `_ + // + // If set to false, ``max_connection_duration`` will cause Envoy to enter the normal drain + // sequence for HTTP1 with Envoy eventually closing the connection (once there are no active + // streams). + // + // Has no effect if ``max_connection_duration`` is unset. Defaults to false. + bool http1_safe_max_connection_duration = 58; + // Additional HTTP/1 settings that are passed to the HTTP/1 codec. // [#comment:TODO: The following fields are ignored when the // :ref:`header validation configuration ` @@ -459,7 +473,6 @@ message HttpConnectionManager { [(udpa.annotations.security).configure_for_untrusted_downstream = true]; // Additional HTTP/3 settings that are passed directly to the HTTP/3 codec. - // [#not-implemented-hide:] config.core.v3.Http3ProtocolOptions http3_protocol_options = 44; // An optional override that the connection manager will write to the server @@ -480,7 +493,12 @@ message HttpConnectionManager { // The maximum request headers size for incoming connections. // If unconfigured, the default max request headers allowed is 60 KiB. + // The default value can be overridden by setting runtime key ``envoy.reloadable_features.max_request_headers_size_kb``. // Requests that exceed this limit will receive a 431 response. + // + // Note: currently some protocol codecs impose limits on the maximum size of a single header: + // HTTP/2 (when using nghttp2) limits a single header to around 100kb. + // HTTP/3 limits a single header to around 1024kb. google.protobuf.UInt32Value max_request_headers_kb = 29 [(validate.rules).uint32 = {lte: 8192 gt: 0}]; @@ -547,9 +565,10 @@ message HttpConnectionManager { // race with the final GOAWAY frame. During this grace period, Envoy will // continue to accept new streams. After the grace period, a final GOAWAY // frame is sent and Envoy will start refusing new streams. Draining occurs - // both when a connection hits the idle timeout or during general server - // draining. The default grace period is 5000 milliseconds (5 seconds) if this - // option is not specified. + // either when a connection hits the idle timeout, when :ref:`max_connection_duration + // ` + // is reached, or during general server draining. The default grace period is + // 5000 milliseconds (5 seconds) if this option is not specified. google.protobuf.Duration drain_timeout = 12; // The delayed close timeout is for downstream connections managed by the HTTP connection manager. @@ -588,26 +607,33 @@ message HttpConnectionManager { // emitted by the connection manager. repeated config.accesslog.v3.AccessLog access_log = 13; + // The interval to flush the above access logs. + // // .. attention:: - // This field is deprecated in favor of - // :ref:`access_log_flush_interval - // `. - // Note that if both this field and :ref:`access_log_flush_interval - // ` - // are specified, the former (deprecated field) is ignored. + // + // This field is deprecated in favor of + // :ref:`access_log_flush_interval + // `. + // Note that if both this field and :ref:`access_log_flush_interval + // ` + // are specified, the former (deprecated field) is ignored. google.protobuf.Duration access_log_flush_interval = 54 [ deprecated = true, (validate.rules).duration = {gte {nanos: 1000000}}, (envoy.annotations.deprecated_at_minor_version) = "3.0" ]; + // If set to true, HCM will flush an access log once when a new HTTP request is received, after the request + // headers have been evaluated, and before iterating through the HTTP filter chain. + // // .. attention:: - // This field is deprecated in favor of - // :ref:`flush_access_log_on_new_request - // `. - // Note that if both this field and :ref:`flush_access_log_on_new_request - // ` - // are specified, the former (deprecated field) is ignored. + // + // This field is deprecated in favor of + // :ref:`flush_access_log_on_new_request + // `. + // Note that if both this field and :ref:`flush_access_log_on_new_request + // ` + // are specified, the former (deprecated field) is ignored. bool flush_access_log_on_new_request = 55 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; @@ -663,6 +689,34 @@ message HttpConnectionManager { // purposes. If unspecified, only RFC1918 IP addresses will be considered internal. // See the documentation for :ref:`config_http_conn_man_headers_x-envoy-internal` for more // information about internal/external addresses. + // + // .. warning:: + // In the next release, no IP addresses will be considered trusted. If you have tooling such as probes + // on your private network which need to be treated as trusted (e.g. changing arbitrary x-envoy headers) + // you will have to manually include those addresses or CIDR ranges like: + // + // .. validated-code-block:: yaml + // :type-name: envoy.extensions.filters.network.http_connection_manager.v3.InternalAddressConfig + // + // cidr_ranges: + // address_prefix: 10.0.0.0 + // prefix_len: 8 + // cidr_ranges: + // address_prefix: 192.168.0.0 + // prefix_len: 16 + // cidr_ranges: + // address_prefix: 172.16.0.0 + // prefix_len: 12 + // cidr_ranges: + // address_prefix: 127.0.0.1 + // prefix_len: 32 + // cidr_ranges: + // address_prefix: fd00:: + // prefix_len: 8 + // cidr_ranges: + // address_prefix: ::1 + // prefix_len: 128 + // InternalAddressConfig internal_address_config = 25; // If set, Envoy will not append the remote address to the diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3/client_side_weighted_round_robin.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3/client_side_weighted_round_robin.proto index c70360a0946..9520f6dbd43 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3/client_side_weighted_round_robin.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3/client_side_weighted_round_robin.proto @@ -15,7 +15,7 @@ option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/loa option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Client-Side Weighted Round Robin Load Balancing Policy] -// [#not-implemented-hide:] +// [#extension: envoy.load_balancing_policies.client_side_weighted_round_robin] // Configuration for the client_side_weighted_round_robin LB policy. // @@ -30,11 +30,12 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // regardless of result. Only failed queries count toward eps. A config // parameter error_utilization_penalty controls the penalty to adjust endpoint // weights using eps and qps. The weight of a given endpoint is computed as: -// qps / (utilization + eps/qps * error_utilization_penalty) +// ``qps / (utilization + eps/qps * error_utilization_penalty)``. // -// See the :ref:`load balancing architecture overview` for more information. +// See the :ref:`load balancing architecture +// overview` for more information. // -// [#next-free-field: 7] +// [#next-free-field: 8] message ClientSideWeightedRoundRobin { // Whether to enable out-of-band utilization reporting collection from // the endpoints. By default, per-request utilization reporting is used. @@ -68,4 +69,10 @@ message ClientSideWeightedRoundRobin { // calculated as eps/qps. Configuration is rejected if this value is negative. // Default is 1.0. google.protobuf.FloatValue error_utilization_penalty = 6 [(validate.rules).float = {gte: 0.0}]; + + // By default, endpoint weight is computed based on the :ref:`application_utilization ` field reported by the endpoint. + // If that field is not set, then utilization will instead be computed by taking the max of the values of the metrics specified here. + // For map fields in the ORCA proto, the string will be of the form ``.``. For example, the string ``named_metrics.foo`` will mean to look for the key ``foo`` in the ORCA :ref:`named_metrics ` field. + // If none of the specified metrics are present in the load report, then :ref:`cpu_utilization ` is used instead. + repeated string metric_names_for_computing_utilization = 7; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto index c1a3f5b33b3..46d86b65856 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto @@ -290,12 +290,12 @@ message TlsSessionTicketKeys { // respect to the TLS handshake. // [#not-implemented-hide:] message CertificateProviderPluginInstance { - // Provider instance name. If not present, defaults to "default". + // Provider instance name. // // Instance names should generally be defined not in terms of the underlying provider // implementation (e.g., "file_watcher") but rather in terms of the function of the // certificates (e.g., "foo_deployment_identity"). - string instance_name = 1; + string instance_name = 1 [(validate.rules).string = {min_len: 1}]; // Opaque name used to specify certificate instances or types. For example, "ROOTCA" to specify // a root-certificate (validation context) or "example.com" to specify a certificate for a diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto index 9d465c97321..c305ff74f42 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto @@ -63,7 +63,7 @@ message UpstreamTlsContext { google.protobuf.BoolValue enforce_rsa_key_usage = 5; } -// [#next-free-field: 11] +// [#next-free-field: 12] message DownstreamTlsContext { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.auth.DownstreamTlsContext"; @@ -140,6 +140,11 @@ message DownstreamTlsContext { // If the client provides SNI but no such cert matched, it will decide to full scan certificates or not based on this config. // Defaults to false. See more details in :ref:`Multiple TLS certificates `. google.protobuf.BoolValue full_scan_certs_on_sni_mismatch = 9; + + // By default, Envoy as a server uses its preferred cipher during the handshake. + // Setting this to true would allow the downstream client's preferred cipher to be used instead. + // Has no effect when using TLSv1_3. + bool prefer_client_ciphers = 11; } // TLS key log configuration. @@ -158,7 +163,7 @@ message TlsKeyLog { } // TLS context shared by both client and server TLS contexts. -// [#next-free-field: 16] +// [#next-free-field: 17] message CommonTlsContext { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.auth.CommonTlsContext"; @@ -269,6 +274,13 @@ message CommonTlsContext { // [#not-implemented-hide:] CertificateProviderPluginInstance tls_certificate_provider_instance = 14; + // Custom TLS certificate selector. + // + // Select TLS certificate based on TLS client hello. + // If empty, defaults to native TLS certificate selection behavior: + // DNS SANs or Subject Common Name in TLS certificates is extracted as server name pattern to match SNI. + config.core.v3.TypedExtensionConfig custom_tls_certificate_selector = 16; + // Certificate provider for fetching TLS certificates. // [#not-implemented-hide:] CertificateProvider tls_certificate_certificate_provider = 9 From 664f1fcf8ac98f5ff41cdf669b478a88c510ba38 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 5 Nov 2024 07:13:31 -0800 Subject: [PATCH 075/591] xds: Remove Bazel dependency on xds v2 feab4e54 removed xds v2 for the Gradle build. Testing with a deploy.jar, I see the same 4 MB size reduction (31 -> 27 MB) here. While an orca dependency is deleted in this commit, it is only a direct dependency. It remains in the :orca target, so doesn't contribute a size reduction. --- xds/BUILD.bazel | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index 05753a3a320..17700eb33bb 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -27,9 +27,7 @@ java_library( ]), visibility = ["//visibility:public"], deps = [ - ":envoy_service_discovery_v2_java_grpc", ":envoy_service_discovery_v3_java_grpc", - ":envoy_service_load_stats_v2_java_grpc", ":envoy_service_load_stats_v3_java_grpc", ":envoy_service_status_v3_java_grpc", ":orca", @@ -64,20 +62,11 @@ java_proto_library( name = "xds_protos_java", deps = [ "@com_github_cncf_xds//udpa/type/v1:pkg", - "@com_github_cncf_xds//xds/data/orca/v3:pkg", - "@com_github_cncf_xds//xds/service/orca/v3:pkg", "@com_github_cncf_xds//xds/type/v3:pkg", "@envoy_api//envoy/admin/v3:pkg", - "@envoy_api//envoy/api/v2:pkg", - "@envoy_api//envoy/api/v2/core:pkg", - "@envoy_api//envoy/api/v2/endpoint:pkg", - "@envoy_api//envoy/config/cluster/aggregate/v2alpha:pkg", "@envoy_api//envoy/config/cluster/v3:pkg", "@envoy_api//envoy/config/core/v3:pkg", "@envoy_api//envoy/config/endpoint/v3:pkg", - "@envoy_api//envoy/config/filter/http/fault/v2:pkg", - "@envoy_api//envoy/config/filter/http/router/v2:pkg", - "@envoy_api//envoy/config/filter/network/http_connection_manager/v2:pkg", "@envoy_api//envoy/config/listener/v3:pkg", "@envoy_api//envoy/config/rbac/v3:pkg", "@envoy_api//envoy/config/route/v3:pkg", @@ -94,9 +83,7 @@ java_proto_library( "@envoy_api//envoy/extensions/load_balancing_policies/round_robin/v3:pkg", "@envoy_api//envoy/extensions/load_balancing_policies/wrr_locality/v3:pkg", "@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg", - "@envoy_api//envoy/service/discovery/v2:pkg", "@envoy_api//envoy/service/discovery/v3:pkg", - "@envoy_api//envoy/service/load_stats/v2:pkg", "@envoy_api//envoy/service/load_stats/v3:pkg", "@envoy_api//envoy/service/status/v3:pkg", "@envoy_api//envoy/type/matcher/v3:pkg", @@ -104,24 +91,12 @@ java_proto_library( ], ) -java_grpc_library( - name = "envoy_service_discovery_v2_java_grpc", - srcs = ["@envoy_api//envoy/service/discovery/v2:pkg"], - deps = [":xds_protos_java"], -) - java_grpc_library( name = "envoy_service_discovery_v3_java_grpc", srcs = ["@envoy_api//envoy/service/discovery/v3:pkg"], deps = [":xds_protos_java"], ) -java_grpc_library( - name = "envoy_service_load_stats_v2_java_grpc", - srcs = ["@envoy_api//envoy/service/load_stats/v2:pkg"], - deps = [":xds_protos_java"], -) - java_grpc_library( name = "envoy_service_load_stats_v3_java_grpc", srcs = ["@envoy_api//envoy/service/load_stats/v3:pkg"], From dae078c0a636fd4b71ca7d20c333dd00b0b0714a Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 5 Nov 2024 23:52:20 +0530 Subject: [PATCH 076/591] api: When forwarding from Listener onAddresses to Listener2 continue to use onResult (#11666) When forwarding from Listener onAddresses to Listener2 continue to use onResult and not onResult2 because the latter requires to be called from within synchronization context and it breaks existing code that didn't need to do so when using the old Listener interface. --- api/src/main/java/io/grpc/NameResolver.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java index b9590ab5d5a..a1dea016377 100644 --- a/api/src/main/java/io/grpc/NameResolver.java +++ b/api/src/main/java/io/grpc/NameResolver.java @@ -219,13 +219,15 @@ public abstract static class Listener2 implements Listener { @Override @Deprecated @InlineMe( - replacement = "this.onResult2(ResolutionResult.newBuilder().setAddressesOrError(" + replacement = "this.onResult(ResolutionResult.newBuilder().setAddressesOrError(" + "StatusOr.fromValue(servers)).setAttributes(attributes).build())", imports = {"io.grpc.NameResolver.ResolutionResult", "io.grpc.StatusOr"}) public final void onAddresses( List servers, @ResolutionResultAttr Attributes attributes) { // TODO(jihuncho) need to promote Listener2 if we want to use ConfigOrError - onResult2( + // Calling onResult and not onResult2 because onResult2 can only be called from a + // synchronization context. + onResult( ResolutionResult.newBuilder().setAddressesOrError( StatusOr.fromValue(servers)).setAttributes(attributes).build()); } From a5db67d0cb4e0c4835a18860f7cbee1519ca2ca7 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Mon, 28 Oct 2024 11:39:32 -0500 Subject: [PATCH 077/591] Deframe failures should be logged on the server as warnings This brings grpc-servlet in line with the grpc-netty implementation found in NettyServerStream.TransportState. --- .../src/main/java/io/grpc/servlet/ServletServerStream.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java b/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java index b7ad6e0decc..5d11abf0f90 100644 --- a/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java +++ b/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java @@ -154,8 +154,8 @@ public void bytesRead(int numBytes) { @Override public void deframeFailed(Throwable cause) { - if (logger.isLoggable(FINE)) { - logger.log(FINE, String.format("[{%s}] Exception processing message", logId), cause); + if (logger.isLoggable(WARNING)) { + logger.log(WARNING, String.format("[{%s}] Exception processing message", logId), cause); } cancel(Status.fromThrowable(cause)); } From 76705c235c1396b0cbbf81a5726b93f199b1f619 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Wed, 6 Nov 2024 16:39:00 +0530 Subject: [PATCH 078/591] xds: Implement GcpAuthenticationFilter (#11638) --- xds/BUILD.bazel | 3 + .../io/grpc/xds/GcpAuthenticationFilter.java | 222 ++++++++++++++++++ .../grpc/xds/GcpAuthenticationFilterTest.java | 121 ++++++++++ xds/third_party/envoy/import.sh | 1 + .../filters/http/gcp_authn/v3/gcp_authn.proto | 63 +++++ 5 files changed, 410 insertions(+) create mode 100644 xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java create mode 100644 xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java create mode 100644 xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index 17700eb33bb..b235a79c526 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -35,6 +35,7 @@ java_library( "//:auto_value_annotations", "//alts", "//api", + "//auth", "//context", "//core:internal", "//netty", @@ -45,6 +46,7 @@ java_library( "@com_google_googleapis//google/rpc:rpc_java_proto", "@com_google_protobuf//:protobuf_java", "@com_google_protobuf//:protobuf_java_util", + "@maven//:com_google_auth_google_auth_library_oauth2_http", artifact("com.google.code.findbugs:jsr305"), artifact("com.google.code.gson:gson"), artifact("com.google.errorprone:error_prone_annotations"), @@ -73,6 +75,7 @@ java_proto_library( "@envoy_api//envoy/extensions/clusters/aggregate/v3:pkg", "@envoy_api//envoy/extensions/filters/common/fault/v3:pkg", "@envoy_api//envoy/extensions/filters/http/fault/v3:pkg", + "@envoy_api//envoy/extensions/filters/http/gcp_authn/v3:pkg", "@envoy_api//envoy/extensions/filters/http/rbac/v3:pkg", "@envoy_api//envoy/extensions/filters/http/router/v3:pkg", "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg", diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java new file mode 100644 index 00000000000..a14dbe801e7 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java @@ -0,0 +1,222 @@ +/* + * Copyright 2021 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 com.google.auth.oauth2.ComputeEngineCredentials; +import com.google.auth.oauth2.IdTokenCredentials; +import com.google.common.primitives.UnsignedLongs; +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig; +import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfig; +import io.grpc.CallCredentials; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.CompositeCallCredentials; +import io.grpc.LoadBalancer.PickSubchannelArgs; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.auth.MoreCallCredentials; +import io.grpc.xds.Filter.ClientInterceptorBuilder; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; +import javax.annotation.Nullable; + +/** + * A {@link Filter} that injects a {@link CallCredentials} to handle + * authentication for xDS credentials. + */ +final class GcpAuthenticationFilter implements Filter, ClientInterceptorBuilder { + + static final String TYPE_URL = + "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig"; + + @Override + public String[] typeUrls() { + return new String[] { TYPE_URL }; + } + + @Override + public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + GcpAuthnFilterConfig gcpAuthnProto; + if (!(rawProtoMessage instanceof Any)) { + return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); + } + Any anyMessage = (Any) rawProtoMessage; + + try { + gcpAuthnProto = anyMessage.unpack(GcpAuthnFilterConfig.class); + } catch (InvalidProtocolBufferException e) { + return ConfigOrError.fromError("Invalid proto: " + e); + } + + long cacheSize = 10; + // Validate cache_config + TokenCacheConfig cacheConfig = gcpAuthnProto.getCacheConfig(); + if (cacheConfig != null) { + cacheSize = cacheConfig.getCacheSize().getValue(); + if (cacheSize == 0) { + return ConfigOrError.fromError( + "cache_config.cache_size must be greater than zero"); + } + // LruCache's size is an int and briefly exceeds its maximum size before evicting entries + cacheSize = UnsignedLongs.min(cacheSize, Integer.MAX_VALUE - 1); + } + + GcpAuthenticationConfig config = new GcpAuthenticationConfig((int) cacheSize); + return ConfigOrError.fromConfig(config); + } + + @Override + public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { + return parseFilterConfig(rawProtoMessage); + } + + @Nullable + @Override + public ClientInterceptor buildClientInterceptor(FilterConfig config, + @Nullable FilterConfig overrideConfig, PickSubchannelArgs args, + ScheduledExecutorService scheduler) { + + ComputeEngineCredentials credentials = ComputeEngineCredentials.create(); + LruCache callCredentialsCache = + new LruCache<>(((GcpAuthenticationConfig) config).getCacheSize()); + return new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + + /*String clusterName = callOptions.getOption(InternalXdsAttributes.ATTR_CLUSTER_NAME); + if (clusterName == null) { + return next.newCall(method, callOptions); + }*/ + + // TODO: Fetch the CDS resource for the cluster. + // If the CDS resource is not available, fail the RPC with Status.UNAVAILABLE. + + // TODO: Extract the audience from the CDS resource metadata. + // If the audience is not found or is in the wrong format, fail the RPC. + String audience = "TEST_AUDIENCE"; + + try { + CallCredentials existingCallCredentials = callOptions.getCredentials(); + CallCredentials newCallCredentials = + getCallCredentials(callCredentialsCache, audience, credentials); + if (existingCallCredentials != null) { + callOptions = callOptions.withCallCredentials( + new CompositeCallCredentials(existingCallCredentials, newCallCredentials)); + } else { + callOptions = callOptions.withCallCredentials(newCallCredentials); + } + } + catch (Exception e) { + // If we fail to attach CallCredentials due to any reason, return a FailingClientCall + return new FailingClientCall<>(Status.UNAUTHENTICATED + .withDescription("Failed to attach CallCredentials.") + .withCause(e)); + } + return next.newCall(method, callOptions); + } + }; + } + + private CallCredentials getCallCredentials(LruCache cache, + String audience, ComputeEngineCredentials credentials) { + + synchronized (cache) { + return cache.getOrInsert(audience, key -> { + IdTokenCredentials creds = IdTokenCredentials.newBuilder() + .setIdTokenProvider(credentials) + .setTargetAudience(audience) + .build(); + return MoreCallCredentials.from(creds); + }); + } + } + + static final class GcpAuthenticationConfig implements FilterConfig { + + private final int cacheSize; + + public GcpAuthenticationConfig(int cacheSize) { + this.cacheSize = cacheSize; + } + + public int getCacheSize() { + return cacheSize; + } + + @Override + public String typeUrl() { + return GcpAuthenticationFilter.TYPE_URL; + } + } + + /** An implementation of {@link ClientCall} that fails when started. */ + private static final class FailingClientCall extends ClientCall { + + private final Status error; + + public FailingClientCall(Status error) { + this.error = error; + } + + @Override + public void start(ClientCall.Listener listener, Metadata headers) { + listener.onClose(error, new Metadata()); + } + + @Override + public void request(int numMessages) {} + + @Override + public void cancel(String message, Throwable cause) {} + + @Override + public void halfClose() {} + + @Override + public void sendMessage(ReqT message) {} + } + + private static final class LruCache { + + private final Map cache; + + LruCache(int maxSize) { + this.cache = new LinkedHashMap( + maxSize, + 0.75f, + true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > maxSize; + } + }; + } + + V getOrInsert(K key, Function create) { + return cache.computeIfAbsent(key, create); + } + } +} diff --git a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java new file mode 100644 index 00000000000..ddd244c8551 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; + +import com.google.protobuf.Any; +import com.google.protobuf.Empty; +import com.google.protobuf.Message; +import com.google.protobuf.UInt64Value; +import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig; +import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfig; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import io.grpc.testing.TestMethodDescriptors; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +@RunWith(JUnit4.class) +public class GcpAuthenticationFilterTest { + + @Test + public void testParseFilterConfig_withValidConfig() { + GcpAuthnFilterConfig config = GcpAuthnFilterConfig.newBuilder() + .setCacheConfig(TokenCacheConfig.newBuilder().setCacheSize(UInt64Value.of(20))) + .build(); + Any anyMessage = Any.pack(config); + + GcpAuthenticationFilter filter = new GcpAuthenticationFilter(); + ConfigOrError result = filter.parseFilterConfig(anyMessage); + + assertNotNull(result.config); + assertNull(result.errorDetail); + assertEquals(20L, + ((GcpAuthenticationFilter.GcpAuthenticationConfig) result.config).getCacheSize()); + } + + @Test + public void testParseFilterConfig_withZeroCacheSize() { + GcpAuthnFilterConfig config = GcpAuthnFilterConfig.newBuilder() + .setCacheConfig(TokenCacheConfig.newBuilder().setCacheSize(UInt64Value.of(0))) + .build(); + Any anyMessage = Any.pack(config); + + GcpAuthenticationFilter filter = new GcpAuthenticationFilter(); + ConfigOrError result = filter.parseFilterConfig(anyMessage); + + assertNull(result.config); + assertNotNull(result.errorDetail); + assertTrue(result.errorDetail.contains("cache_config.cache_size must be greater than zero")); + } + + @Test + public void testParseFilterConfig_withInvalidMessageType() { + GcpAuthenticationFilter filter = new GcpAuthenticationFilter(); + Message invalidMessage = Empty.getDefaultInstance(); + ConfigOrError result = filter.parseFilterConfig(invalidMessage); + + assertNull(result.config); + assertThat(result.errorDetail).contains("Invalid config type"); + } + + @Test + public void testClientInterceptor_createsAndReusesCachedCredentials() { + GcpAuthenticationFilter.GcpAuthenticationConfig config = + new GcpAuthenticationFilter.GcpAuthenticationConfig(10); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter(); + + // Create interceptor + ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null, null); + MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); + + // Mock channel and capture CallOptions + Channel mockChannel = Mockito.mock(Channel.class); + ArgumentCaptor callOptionsCaptor = ArgumentCaptor.forClass(CallOptions.class); + + // Execute interception twice to check caching + interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT, mockChannel); + interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT, mockChannel); + + // Capture and verify CallOptions for CallCredentials presence + Mockito.verify(mockChannel, Mockito.times(2)) + .newCall(eq(methodDescriptor), callOptionsCaptor.capture()); + + // Retrieve the CallOptions captured from both calls + CallOptions firstCapturedOptions = callOptionsCaptor.getAllValues().get(0); + CallOptions secondCapturedOptions = callOptionsCaptor.getAllValues().get(1); + + // Ensure that CallCredentials was added + assertNotNull(firstCapturedOptions.getCredentials()); + assertNotNull(secondCapturedOptions.getCredentials()); + + // Ensure that the CallCredentials from both calls are the same, indicating caching + assertSame(firstCapturedOptions.getCredentials(), secondCapturedOptions.getCredentials()); + } +} diff --git a/xds/third_party/envoy/import.sh b/xds/third_party/envoy/import.sh index 8f0271e784e..254abbe271f 100755 --- a/xds/third_party/envoy/import.sh +++ b/xds/third_party/envoy/import.sh @@ -75,6 +75,7 @@ envoy/extensions/clusters/aggregate/v3/cluster.proto envoy/extensions/filters/common/fault/v3/fault.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/extensions/filters/http/rate_limit_quota/v3/rate_limit_quota.proto +envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto envoy/extensions/filters/http/rbac/v3/rbac.proto envoy/extensions/filters/http/router/v3/router.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto new file mode 100644 index 00000000000..05757c23e59 --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto @@ -0,0 +1,63 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.gcp_authn.v3; + +import "envoy/config/core/v3/base.proto"; +import "envoy/config/core/v3/http_uri.proto"; + +import "google/protobuf/wrappers.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3"; +option java_outer_classname = "GcpAuthnProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3;gcp_authnv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: GCP authentication] +// GCP authentication :ref:`configuration overview `. +// [#extension: envoy.filters.http.gcp_authn] + +// Filter configuration. +message GcpAuthnFilterConfig { + // The HTTP URI to fetch tokens from GCE Metadata Server(https://cloud.google.com/compute/docs/metadata/overview). + // The URL format is "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=[AUDIENCE]" + config.core.v3.HttpUri http_uri = 1 [(validate.rules).message = {required: true}]; + + // Retry policy for fetching tokens. This field is optional. + config.core.v3.RetryPolicy retry_policy = 2; + + // Token cache configuration. This field is optional. + TokenCacheConfig cache_config = 3; + + // Request header location to extract the token. By default (i.e. if this field is not specified), the token + // is extracted to the Authorization HTTP header, in the format "Authorization: Bearer ". + TokenHeader token_header = 4; +} + +// Audience is the URL of the receiving service that performs token authentication. +// It will be provided to the filter through cluster's typed_filter_metadata. +message Audience { + string url = 1 [(validate.rules).string = {min_len: 1}]; +} + +// Token Cache configuration. +message TokenCacheConfig { + // The number of cache entries. The maximum number of entries is INT64_MAX as it is constrained by underlying cache implementation. + // Default value 0 (i.e., proto3 defaults) disables the cache by default. Other default values will enable the cache. + google.protobuf.UInt64Value cache_size = 1 [(validate.rules).uint64 = {lte: 9223372036854775807}]; +} + +message TokenHeader { + // The HTTP header's name. + string name = 1 + [(validate.rules).string = {min_len: 1 well_known_regex: HTTP_HEADER_NAME strict: false}]; + + // The header's prefix. The format is "value_prefix" + // For example, for "Authorization: Bearer ", value_prefix="Bearer " with a space at the + // end. + string value_prefix = 2 + [(validate.rules).string = {well_known_regex: HTTP_HEADER_VALUE strict: false}]; +} From d6c80294a736a377a8b39e31c253f918eec5aa1d Mon Sep 17 00:00:00 2001 From: erm-g <110920239+erm-g@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:03:15 -0500 Subject: [PATCH 079/591] xds: Spiffe Trust Bundle Support (#11627) Adds verification of SPIFFE based identities using SPIFFE trust bundles. For in-progress gRFC A87. --- testing/src/main/resources/certs/README | 29 +++ .../main/resources/certs/client_spiffe.pem | 25 +++ .../main/resources/certs/server1_spiffe.pem | 26 +++ .../main/resources/certs/spiffe-openssl.cnf | 18 +- .../main/resources/certs/spiffebundle.json | 101 ++++++++++ .../main/resources/certs/spiffebundle1.json | 59 ++++++ .../CertProviderClientSslContextProvider.java | 12 +- .../CertProviderServerSslContextProvider.java | 18 +- .../CertProviderSslContextProvider.java | 13 +- .../certprovider/CertificateProvider.java | 21 ++- .../FileWatcherCertificateProvider.java | 78 +++++--- ...ileWatcherCertificateProviderProvider.java | 24 ++- .../trust/XdsTrustManagerFactory.java | 42 ++++- .../security/trust/XdsX509TrustManager.java | 54 +++++- .../grpc/xds/CommonBootstrapperTestUtils.java | 11 +- .../grpc/xds/XdsSecurityClientServerTest.java | 176 ++++++++++++++---- .../security/CommonTlsContextTestsUtil.java | 4 + .../SecurityProtocolNegotiatorsTest.java | 8 +- .../security/TlsContextManagerTest.java | 8 +- ...atcherCertificateProviderProviderTest.java | 100 +++++++++- .../FileWatcherCertificateProviderTest.java | 119 +++++++++--- .../trust/XdsTrustManagerFactoryTest.java | 42 +++++ .../trust/XdsX509TrustManagerTest.java | 85 +++++++++ 23 files changed, 953 insertions(+), 120 deletions(-) create mode 100644 testing/src/main/resources/certs/client_spiffe.pem create mode 100644 testing/src/main/resources/certs/server1_spiffe.pem create mode 100644 testing/src/main/resources/certs/spiffebundle.json create mode 100644 testing/src/main/resources/certs/spiffebundle1.json diff --git a/testing/src/main/resources/certs/README b/testing/src/main/resources/certs/README index 1fa6b733950..13e375784c5 100644 --- a/testing/src/main/resources/certs/README +++ b/testing/src/main/resources/certs/README @@ -67,6 +67,35 @@ ecdsa.key is used to test keys with algorithm other than RSA: $ openssl ecparam -name secp256k1 -genkey -noout -out ecdsa.pem $ openssl pkcs8 -topk8 -in ecdsa.pem -out ecdsa.key -nocrypt +SPIFFE test credentials: +======================= + +The SPIFFE related extensions are listed in spiffe-openssl.cnf config. Both +client_spiffe.pem and server1_spiffe.pem are generated in the same way with +original client.pem and server1.pem but with using that config. Here are the +exact commands (we pass "-subj" as argument in this case): +---------------------- +$ openssl req -new -key client.key -out spiffe-cert.csr \ + -subj /C=US/ST=CA/L=SVL/O=gRPC/CN=testclient/ \ + -config spiffe-openssl.cnf -reqexts spiffe_client_e2e +$ openssl x509 -req -CA ca.pem -CAkey ca.key -CAcreateserial \ + -in spiffe-cert.csr -out client_spiffe.pem -extensions spiffe_client_e2e \ + -extfile spiffe-openssl.cnf -days 3650 -sha256 +$ openssl req -new -key server1.key -out spiffe-cert.csr \ + -subj /C=US/ST=CA/L=SVL/O=gRPC/CN=*.test.google.com/ \ + -config spiffe-openssl.cnf -reqexts spiffe_server_e2e +$ openssl x509 -req -CA ca.pem -CAkey ca.key -CAcreateserial \ + -in spiffe-cert.csr -out server1_spiffe.pem -extensions spiffe_server_e2e \ + -extfile spiffe-openssl.cnf -days 3650 -sha256 + +Additionally, SPIFFE trust bundle map files spiffebundle.json and \ +spiffebundle1.json are manually created for end to end testing. The \ +spiffebundle.json contains "example.com" trust domain (only this entry is used \ +in e2e tests) matching URI SAN of server1_spiffe.pem, and the CA certificate \ +there is ca.pem. The spiffebundle.json file contains "foo.bar.com" trust \ +domain (only this entry is used in e2e tests) matching URI SAN of \ +client_spiffe.pem, and the CA certificate there is also ca.pem. + Clean up: --------- $ rm *.rsa diff --git a/testing/src/main/resources/certs/client_spiffe.pem b/testing/src/main/resources/certs/client_spiffe.pem new file mode 100644 index 00000000000..c70981a4030 --- /dev/null +++ b/testing/src/main/resources/certs/client_spiffe.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIUVXGlXjNENtOZbI12epjgIhMaShUwDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTI0 +MTAyNDE2NDAzN1oXDTM0MTAyMjE2NDAzN1owaDELMAkGA1UEBhMCQVUxEzARBgNV +BAgMClNvbWUtU3RhdGUxDDAKBgNVBAcMA1NWTDEhMB8GA1UECgwYSW50ZXJuZXQg +V2lkZ2l0cyBQdHkgTHRkMRMwEQYDVQQDDAp0ZXN0Y2xpZW50MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsqmEafg11ae9jRW0B/IXYU2S8nGVzpSYZjLK +yZq459qe6SP/Jk2f9BQvkhlgRmVfhC4h65gl+c32iC6/SLsOxoa91c6Hn4vK+tqy +7qVTzYv6naso1pNnRAhwvWd/gINysyk8nq11oynL8ilZjNGcRNEV4Q1v0aEG6mbF +NhioNQdq4VFPCjdIFZip9KyRzsc0VUmHmC2KeWJ+yq7TyXCsqPWlbhK+3RgDc6ch +epYP52AVnPvUhsJKC3RbyrwAWCTMq2zYR1EH79H82mdD/OnX0xDaw8cwC68xp6nM +dyk68CY5Gf2kq9bcg9P7V77pERYj8VgSYYx0O9BqkxUGNfUW4QIDAQABo4HlMIHi +MEQGA1UdEQQ9MDuGOXNwaWZmZTovL2Zvby5iYXIuY29tLzllZWJjY2QyLTEyYmYt +NDBhNi1iMjYyLTY1ZmUwNDg3ZDQ1MzAdBgNVHQ4EFgQU28U8sUTGNEDyeCrvJDJd +AALabSMwewYDVR0jBHQwcqFapFgwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNv +bWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0G +A1UEAwwGdGVzdGNhghRas/RW8dzL4s/pS5g22Iv2AGEPmjANBgkqhkiG9w0BAQsF +AAOCAQEAE3LLE8GR283q/aE646SgAfltqpESP38NmYdJMdZgWRxbOqdWabYDfibt +9r8j+IRvVuuTWuH2eNS5wXJtS1BZ+z24wTLa+a2KjOV12gChP+3N7jhqId4eolSL +1fjscPY6luZP4Pm3D73lBvIoBvXpDGyrxleiUCEEkKXmTOA8doFvbrcbwm+yUJOP +VKUKvAzTNztb0BGDzKKU4E2yK5PSyv2n5m2NpzxYYfHoGeVcxvj7nCnSfoX/EWHb +d8ztJYDg9X0iNcfQXt7PZ+j6VcxfDpGCDxe2rFQoYvlWjhr3xOi/1e5A1zx1Ly07 +m9MB4hntu4e2656ZDWbgOHLpO0q1iQ== +-----END CERTIFICATE----- diff --git a/testing/src/main/resources/certs/server1_spiffe.pem b/testing/src/main/resources/certs/server1_spiffe.pem new file mode 100644 index 00000000000..76cb41d6922 --- /dev/null +++ b/testing/src/main/resources/certs/server1_spiffe.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEZDCCA0ygAwIBAgIUVXGlXjNENtOZbI12epjgIhMaShMwDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTI0 +MTAyMTAyMTQxNVoXDTM0MTAxOTAyMTQxNVowZTELMAkGA1UEBhMCVVMxETAPBgNV +BAgMCElsbGlub2lzMRAwDgYDVQQHDAdDaGljYWdvMRUwEwYDVQQKDAxFeGFtcGxl +LCBDby4xGjAYBgNVBAMMESoudGVzdC5nb29nbGUuY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA5xOONxJJ8b8Qauvob5/7dPYZfIcd+uhAWL2ZlTPz +Qvu4oF0QI4iYgP5iGgry9zEtCM+YQS8UhiAlPlqa6ANxgiBSEyMHH/xE8lo/+caY +GeACqy640Jpl/JocFGo3xd1L8DCawjlaj6eu7T7T/tpAV2qq13b5710eNRbCAfFe +8yALiGQemx0IYhlZXNbIGWLBNhBhvVjJh7UvOqpADk4xtl8o5j0xgMIRg6WJGK6c +6ffSIg4eP1XmovNYZ9LLEJG68tF0Q/yIN43B4dt1oq4jzSdCbG4F1EiykT2TmwPV +YDi8tml6DfOCDGnit8svnMEmBv/fcPd31GSbXjF8M+KGGQIDAQABo4IBGTCCARUw +dwYDVR0RBHAwboIQKi50ZXN0Lmdvb2dsZS5mcoIYd2F0ZXJ6b29pLnRlc3QuZ29v +Z2xlLmJlghIqLnRlc3QueW91dHViZS5jb22HBMCoAQOGJnNwaWZmZTovL2V4YW1w +bGUuY29tL3dvcmtsb2FkLzllZWJjY2QyMB0GA1UdDgQWBBRvRpAYHQYP6dFPf5V7 +/MyCftnNjTB7BgNVHSMEdDByoVqkWDBWMQswCQYDVQQGEwJBVTETMBEGA1UECAwK +U29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQ8w +DQYDVQQDDAZ0ZXN0Y2GCFFqz9Fbx3Mviz+lLmDbYi/YAYQ+aMA0GCSqGSIb3DQEB +CwUAA4IBAQBJ1bnbBHa1n15vvhpGIzokuiJ+9q/zim63UuVDnkhrQM2N+RQbStGT +Tis2tNse1bh460dJFm6ArgHWogzx6fQZzgaDeCOAXvrAe4jM9IHr9K7lkq/33CZS +BDV+jCmm2sRsqSMkKUcX6JhyqWGFHuTDAKJzsEV2MlcswleKlGHDkeelAaxlLzpz +RHOSQd0N9xAs18lzx95SQEx90PtrBOmvIDDiI5o5z9Oz12Iy1toiksFl4jmknkDD +5VF3AyCRgN8NPW0uNC8D2vo4L+tgj9U6NPlmMOrjRsEH257LJ1wopAGr+yezkIId +QQodGSVm5cOuw/K7Ma4nBDjVJkjcdY3t +-----END CERTIFICATE----- diff --git a/testing/src/main/resources/certs/spiffe-openssl.cnf b/testing/src/main/resources/certs/spiffe-openssl.cnf index 40c473da7e5..f03af40a782 100644 --- a/testing/src/main/resources/certs/spiffe-openssl.cnf +++ b/testing/src/main/resources/certs/spiffe-openssl.cnf @@ -4,9 +4,25 @@ subjectAltName = @alt_names [spiffe_client_multi] subjectAltName = @alt_names_multi +[spiffe_server_e2e] +subjectAltName = @alt_names_server_e2e + +[spiffe_client_e2e] +subjectAltName = @alt_names_client_e2e + [alt_names] URI = spiffe://foo.bar.com/client/workload/1 [alt_names_multi] URI.1 = spiffe://foo.bar.com/client/workload/1 -URI.2 = spiffe://foo.bar.com/client/workload/2 \ No newline at end of file +URI.2 = spiffe://foo.bar.com/client/workload/2 + +[alt_names_server_e2e] +DNS.1 = *.test.google.fr +DNS.2 = waterzooi.test.google.be +DNS.3 = *.test.youtube.com +IP.1 = "192.168.1.3" +URI = spiffe://example.com/workload/9eebccd2 + +[alt_names_client_e2e] +URI = spiffe://foo.bar.com/9eebccd2-12bf-40a6-b262-65fe0487d453 \ No newline at end of file diff --git a/testing/src/main/resources/certs/spiffebundle.json b/testing/src/main/resources/certs/spiffebundle.json new file mode 100644 index 00000000000..5bc8fcfb432 --- /dev/null +++ b/testing/src/main/resources/certs/spiffebundle.json @@ -0,0 +1,101 @@ +{ + "trust_domains": { + "example.com": { + "spiffe_sequence": 12035488, + "keys": [ + { + "kty": "RSA", + "use": "x509-svid", + "x5c": ["MIIDWjCCAkKgAwIBAgIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQEL + BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM + GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw + MDMxNzE4NTk1MVoXDTMwMDMxNTE4NTk1MVowVjELMAkGA1UEBhMCQVUxEzARBgNV + BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 + ZDEPMA0GA1UEAwwGdGVzdGNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC + AQEAsGL0oXflF0LzoM+Bh+qUU9yhqzw2w8OOX5mu/iNCyUOBrqaHi7mGHx73GD01 + diNzCzvlcQqdNIH6NQSL7DTpBjca66jYT9u73vZe2MDrr1nVbuLvfu9850cdxiUO + Inv5xf8+sTHG0C+a+VAvMhsLiRjsq+lXKRJyk5zkbbsETybqpxoJ+K7CoSy3yc/k + QIY3TipwEtwkKP4hzyo6KiGd/DPexie4nBUInN3bS1BUeNZ5zeaIC2eg3bkeeW7c + qT55b+Yen6CxY0TEkzBK6AKt/WUialKMgT0wbTxRZO7kUCH3Sq6e/wXeFdJ+HvdV + LPlAg5TnMaNpRdQih/8nRFpsdwIDAQABoyAwHjAMBgNVHRMEBTADAQH/MA4GA1Ud + DwEB/wQEAwICBDANBgkqhkiG9w0BAQsFAAOCAQEAkTrKZjBrJXHps/HrjNCFPb5a + THuGPCSsepe1wkKdSp1h4HGRpLoCgcLysCJ5hZhRpHkRihhef+rFHEe60UePQO3S + CVTtdJB4CYWpcNyXOdqefrbJW5QNljxgi6Fhvs7JJkBqdXIkWXtFk2eRgOIP2Eo9 + /OHQHlYnwZFrk6sp4wPyR+A95S0toZBcyDVz7u+hOW0pGK3wviOe9lvRgj/H3Pwt + bewb0l+MhRig0/DVHamyVxrDRbqInU1/GTNCwcZkXKYFWSf92U+kIcTth24Q1gcw + eZiLl5FfrWokUNytFElXob0V0a5/kbhiLc3yWmvWqHTpqCALbVyF+rKJo2f5Kw=="], + "n": "", + "e": "AQAB" + } + ] + }, + "test.example.com": { + "keys": [ + { + "kty": "RSA", + "use": "x509-svid", + "x5c": ["MIIFsjCCA5qgAwIBAgIURygVMMzdr+Q7rsUaz189JozyHMwwDQYJKoZIhvcNAQEL + BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL + BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMTEyMjMxODQy + NTJaFw0zMTEyMjExODQyNTJaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM + MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu + dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ4AqpGetyVSqGUuBJ + LVFla+7bEfca7UYzfVSSZLZ/X+JDmWIVN8UIPuFib5jhMEc3XaUnFXUmM7zEtz/Z + G5hapwLwOb2C3ZxOP6PQjYCJxbkLie+b43UQrFu1xxd3vMhVJgcj/AIxEpmszuqO + a6kUrkYifjJADQ+64kZgl66bsTdXMCzpxyFl9xUfff59L8OX+HUfAcoZz3emjg3Z + JPYURQEmjdZTOau1EjFilwHgd989Jt7NKgx30NXoHmw7nusVBIY94fL2VKN3f1XV + m0dHu5NI279Q6zr0ZBU7k5T3IeHnzsUesQS4NGlklDWoVTKk73Uv9Pna8yQsSW75 + 7PEbHOGp9Knu4bnoGPOlsG81yIPipO6hTgGFK24pF97M9kpGbWqYX4+2vLlrCAfc + msHqaUPmQlYeRVTT6vw7ctYo2kyUYGtnODXk76LqewRBVvkzx75QUhfjAyb740Yc + DmIenc56Tq6gebJHjhEmVSehR6xIpXP7SVeurTyhPsEQnpJHtgs4dcwWOZp7BvPN + zHXmJqfr7vsshie3vS5kQ0u1e1yqAqXgyDjqKXOkx+dpgUTehSJHhPNHvTc5LXRs + vvXKYz6FrwR/DZ8t7BNEvPeLjFgxpH7QVJFLCvCbXs5K6yYbsnLfxFIBPRnrbJkI + sK+sQwnRdnsiUdPsTkG5B2lQfQIDAQABo4GHMIGEMB0GA1UdDgQWBBQ2lBp0PiRH + HvQ5IRURm8aHsj4RETAfBgNVHSMEGDAWgBQ2lBp0PiRHHvQ5IRURm8aHsj4RETAP + BgNVHRMBAf8EBTADAQH/MDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29t + L2NsaWVudC93b3JrbG9hZC8xMA0GCSqGSIb3DQEBCwUAA4ICAQA1mSkgRclAl+E/ + aS9zJ7t8+Y4n3T24nOKKveSIjxXm/zjhWqVsLYBI6kglWtih2+PELvU8JdPqNZK3 + 4Kl0Q6FWpVSGDdWN1i6NyORt2ocggL3ke3iXxRk3UpUKJmqwz81VhA2KUHnMlyE0 + IufFfZNwNWWHBv13uJfRbjeQpKPhU+yf4DeXrsWcvrZlGvAET+mcplafUzCp7Iv+ + PcISJtUerbxbVtuHVeZCLlgDXWkLAWJN8rf0dIG4x060LJ+j6j9uRVhb9sZn1HJV + +j4XdIYm1VKilluhOtNwP2d3Ox/JuTBxf7hFHXZPfMagQE5k5PzmxRaCAEMJ1l2D + vUbZw+shJfSNoWcBo2qadnUaWT3BmmJRBDh7ZReib/RQ1Rd4ygOyzP3E0vkV4/gq + yjLdApXh5PZP8KLQZ+1JN/sdWt7VfIt9wYOpkIqujdll51ESHzwQeAK9WVCB4UvV + z6zdhItB9CRbXPreWC+wCB1xDovIzFKOVsLs5+Gqs1m7VinG2LxbDqaKyo/FB0Hx + x0acBNzezLWoDwXYQrN0T0S4pnqhKD1CYPpdArBkNezUYAjS725FkApuK+mnBX3U + 0msBffEaUEOkcyar1EW2m/33vpetD/k3eQQkmvQf4Hbiu9AF+9cNDm/hMuXEw5EX + GA91fn0891b5eEW8BJHXX0jri0aN8g=="], + "n": "", + "e": "AQAB" + }, + { + "kty": "RSA", + "use": "x509-svid", + "x5c": ["MIIELTCCAxWgAwIBAgIUVXGlXjNENtOZbI12epjgIhMaShEwDQYJKoZIhvcNAQEL + BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM + GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTI0 + MDkxNzE2MTk0NFoXDTM0MDkxNTE2MTk0NFowTjELMAkGA1UEBhMCVVMxCzAJBgNV + BAgMAkNBMQwwCgYDVQQHDANTVkwxDTALBgNVBAoMBGdSUEMxFTATBgNVBAMMDHRl + c3QtY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOcTjjcS + SfG/EGrr6G+f+3T2GXyHHfroQFi9mZUz80L7uKBdECOImID+YhoK8vcxLQjPmEEv + FIYgJT5amugDcYIgUhMjBx/8RPJaP/nGmBngAqsuuNCaZfyaHBRqN8XdS/AwmsI5 + Wo+nru0+0/7aQFdqqtd2+e9dHjUWwgHxXvMgC4hkHpsdCGIZWVzWyBliwTYQYb1Y + yYe1LzqqQA5OMbZfKOY9MYDCEYOliRiunOn30iIOHj9V5qLzWGfSyxCRuvLRdEP8 + iDeNweHbdaKuI80nQmxuBdRIspE9k5sD1WA4vLZpeg3zggxp4rfLL5zBJgb/33D3 + d9Rkm14xfDPihhkCAwEAAaOB+jCB9zBZBgNVHREEUjBQhiZzcGlmZmU6Ly9mb28u + YmFyLmNvbS9jbGllbnQvd29ya2xvYWQvMYYmc3BpZmZlOi8vZm9vLmJhci5jb20v + Y2xpZW50L3dvcmtsb2FkLzIwHQYDVR0OBBYEFG9GkBgdBg/p0U9/lXv8zIJ+2c2N + MHsGA1UdIwR0MHKhWqRYMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0 + YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMM + BnRlc3RjYYIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQELBQADggEB + AJ4Cbxv+02SpUgkEu4hP/1+8DtSBXUxNxI0VG4e3Ap2+Rhjm3YiFeS/UeaZhNrrw + UEjkSTPFODyXR7wI7UO9OO1StyD6CMkp3SEvevU5JsZtGL6mTiTLTi3Qkywa91Bt + GlyZdVMghA1bBJLBMwiD5VT5noqoJBD7hDy6v9yNmt1Sw2iYBJPqI3Gnf5bMjR3s + UICaxmFyqaMCZsPkfJh0DmZpInGJys3m4QqGz6ZE2DWgcSr1r/ML7/5bSPjjr8j4 + WFFSqFR3dMu8CbGnfZTCTXa4GTX/rARXbAO67Z/oJbJBK7VKayskL+PzKuohb9ox + jGL772hQMbwtFCOFXu5VP0s="] + } + ] + } + } +} \ No newline at end of file diff --git a/testing/src/main/resources/certs/spiffebundle1.json b/testing/src/main/resources/certs/spiffebundle1.json new file mode 100644 index 00000000000..f79af09a3e7 --- /dev/null +++ b/testing/src/main/resources/certs/spiffebundle1.json @@ -0,0 +1,59 @@ +{ + "trust_domains": { + "example.com": { + "spiffe_sequence": 12035488, + "keys": [ + { + "kty": "RSA", + "use": "x509-svid", + "x5c": ["MIIDWjCCAkKgAwIBAgIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQEL + BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM + GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw + MDMxNzE4NTk1MVoXDTMwMDMxNTE4NTk1MVowVjELMAkGA1UEBhMCQVUxEzARBgNV + BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 + ZDEPMA0GA1UEAwwGdGVzdGNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC + AQEAsGL0oXflF0LzoM+Bh+qUU9yhqzw2w8OOX5mu/iNCyUOBrqaHi7mGHx73GD01 + diNzCzvlcQqdNIH6NQSL7DTpBjca66jYT9u73vZe2MDrr1nVbuLvfu9850cdxiUO + Inv5xf8+sTHG0C+a+VAvMhsLiRjsq+lXKRJyk5zkbbsETybqpxoJ+K7CoSy3yc/k + QIY3TipwEtwkKP4hzyo6KiGd/DPexie4nBUInN3bS1BUeNZ5zeaIC2eg3bkeeW7c + qT55b+Yen6CxY0TEkzBK6AKt/WUialKMgT0wbTxRZO7kUCH3Sq6e/wXeFdJ+HvdV + LPlAg5TnMaNpRdQih/8nRFpsdwIDAQABoyAwHjAMBgNVHRMEBTADAQH/MA4GA1Ud + DwEB/wQEAwICBDANBgkqhkiG9w0BAQsFAAOCAQEAkTrKZjBrJXHps/HrjNCFPb5a + THuGPCSsepe1wkKdSp1h4HGRpLoCgcLysCJ5hZhRpHkRihhef+rFHEe60UePQO3S + CVTtdJB4CYWpcNyXOdqefrbJW5QNljxgi6Fhvs7JJkBqdXIkWXtFk2eRgOIP2Eo9 + /OHQHlYnwZFrk6sp4wPyR+A95S0toZBcyDVz7u+hOW0pGK3wviOe9lvRgj/H3Pwt + bewb0l+MhRig0/DVHamyVxrDRbqInU1/GTNCwcZkXKYFWSf92U+kIcTth24Q1gcw + eZiLl5FfrWokUNytFElXob0V0a5/kbhiLc3yWmvWqHTpqCALbVyF+rKJo2f5Kw=="], + "n": "", + "e": "AQAB" + } + ] + }, + "foo.bar.com": { + "keys": [ + { + "kty": "RSA", + "use": "x509-svid", + "x5c": ["MIIDWjCCAkKgAwIBAgIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQEL + BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM + GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw + MDMxNzE4NTk1MVoXDTMwMDMxNTE4NTk1MVowVjELMAkGA1UEBhMCQVUxEzARBgNV + BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 + ZDEPMA0GA1UEAwwGdGVzdGNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC + AQEAsGL0oXflF0LzoM+Bh+qUU9yhqzw2w8OOX5mu/iNCyUOBrqaHi7mGHx73GD01 + diNzCzvlcQqdNIH6NQSL7DTpBjca66jYT9u73vZe2MDrr1nVbuLvfu9850cdxiUO + Inv5xf8+sTHG0C+a+VAvMhsLiRjsq+lXKRJyk5zkbbsETybqpxoJ+K7CoSy3yc/k + QIY3TipwEtwkKP4hzyo6KiGd/DPexie4nBUInN3bS1BUeNZ5zeaIC2eg3bkeeW7c + qT55b+Yen6CxY0TEkzBK6AKt/WUialKMgT0wbTxRZO7kUCH3Sq6e/wXeFdJ+HvdV + LPlAg5TnMaNpRdQih/8nRFpsdwIDAQABoyAwHjAMBgNVHRMEBTADAQH/MA4GA1Ud + DwEB/wQEAwICBDANBgkqhkiG9w0BAQsFAAOCAQEAkTrKZjBrJXHps/HrjNCFPb5a + THuGPCSsepe1wkKdSp1h4HGRpLoCgcLysCJ5hZhRpHkRihhef+rFHEe60UePQO3S + CVTtdJB4CYWpcNyXOdqefrbJW5QNljxgi6Fhvs7JJkBqdXIkWXtFk2eRgOIP2Eo9 + /OHQHlYnwZFrk6sp4wPyR+A95S0toZBcyDVz7u+hOW0pGK3wviOe9lvRgj/H3Pwt + bewb0l+MhRig0/DVHamyVxrDRbqInU1/GTNCwcZkXKYFWSf92U+kIcTth24Q1gcw + eZiLl5FfrWokUNytFElXob0V0a5/kbhiLc3yWmvWqHTpqCALbVyF+rKJo2f5Kw=="] + } + ] + } + } +} \ No newline at end of file diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java index 8aa74b2b50f..d7c2267c48f 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java @@ -58,15 +58,21 @@ protected final SslContextBuilder getSslContextBuilder( // Null rootCertInstance implies hasSystemRootCerts because of the check in // CertProviderClientSslContextProviderFactory. if (rootCertInstance != null) { - sslContextBuilder.trustManager( + if (savedSpiffeTrustMap != null) { + sslContextBuilder = sslContextBuilder.trustManager( new XdsTrustManagerFactory( - savedTrustedRoots.toArray(new X509Certificate[0]), + savedSpiffeTrustMap, certificateValidationContextdationContext)); + } else { + sslContextBuilder = sslContextBuilder.trustManager( + new XdsTrustManagerFactory( + savedTrustedRoots.toArray(new X509Certificate[0]), + certificateValidationContextdationContext)); + } } if (isMtls()) { sslContextBuilder.keyManager(savedKey, savedCertChain); } return sslContextBuilder; } - } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProvider.java index e43452a53e1..ef65bbfb6f9 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProvider.java @@ -59,13 +59,17 @@ protected final SslContextBuilder getSslContextBuilder( CertificateValidationContext certificateValidationContextdationContext) throws CertStoreException, CertificateException, IOException { SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(savedKey, savedCertChain); - setClientAuthValues( - sslContextBuilder, - isMtls() - ? new XdsTrustManagerFactory( - savedTrustedRoots.toArray(new X509Certificate[0]), - certificateValidationContextdationContext) - : null); + XdsTrustManagerFactory trustManagerFactory = null; + if (isMtls() && savedSpiffeTrustMap != null) { + trustManagerFactory = new XdsTrustManagerFactory( + savedSpiffeTrustMap, + certificateValidationContextdationContext); + } else if (isMtls()) { + trustManagerFactory = new XdsTrustManagerFactory( + savedTrustedRoots.toArray(new X509Certificate[0]), + certificateValidationContextdationContext); + } + setClientAuthValues(sslContextBuilder, trustManagerFactory); sslContextBuilder = GrpcSslContexts.configure(sslContextBuilder); return sslContextBuilder; } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java index b68f705fedb..f9cd14f2efd 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java @@ -41,6 +41,7 @@ abstract class CertProviderSslContextProvider extends DynamicSslContextProvider @Nullable protected PrivateKey savedKey; @Nullable protected List savedCertChain; @Nullable protected List savedTrustedRoots; + @Nullable protected Map> savedSpiffeTrustMap; private final boolean isUsingSystemRootCerts; protected CertProviderSslContextProvider( @@ -152,14 +153,21 @@ public final void updateTrustedRoots(List trustedRoots) { updateSslContextWhenReady(); } + @Override + public final void updateSpiffeTrustMap(Map> spiffeTrustMap) { + savedSpiffeTrustMap = spiffeTrustMap; + updateSslContextWhenReady(); + } + private void updateSslContextWhenReady() { if (isMtls()) { - if (savedKey != null && (savedTrustedRoots != null || isUsingSystemRootCerts)) { + if (savedKey != null + && (savedTrustedRoots != null || isUsingSystemRootCerts || savedSpiffeTrustMap != null)) { updateSslContext(); clearKeysAndCerts(); } } else if (isClientSideTls()) { - if (savedTrustedRoots != null) { + if (savedTrustedRoots != null || savedSpiffeTrustMap != null) { updateSslContext(); clearKeysAndCerts(); } @@ -174,6 +182,7 @@ private void updateSslContextWhenReady() { private void clearKeysAndCerts() { savedKey = null; savedTrustedRoots = null; + savedSpiffeTrustMap = null; savedCertChain = null; } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertificateProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertificateProvider.java index a0d5d0fc69f..009bb7bf566 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertificateProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertificateProvider.java @@ -26,6 +26,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -45,6 +46,8 @@ public interface Watcher { void updateTrustedRoots(List trustedRoots); + void updateSpiffeTrustMap(Map> spiffeTrustMap); + void onError(Status errorStatus); } @@ -53,6 +56,7 @@ public static final class DistributorWatcher implements Watcher { private PrivateKey privateKey; private List certChain; private List trustedRoots; + private Map> spiffeTrustMap; @VisibleForTesting final Set downstreamWatchers = new HashSet<>(); @@ -65,6 +69,9 @@ synchronized void addWatcher(Watcher watcher) { if (trustedRoots != null) { sendLastTrustedRootsUpdate(watcher); } + if (spiffeTrustMap != null) { + sendLastSpiffeTrustMapUpdate(watcher); + } } synchronized void removeWatcher(Watcher watcher) { @@ -83,6 +90,10 @@ private void sendLastTrustedRootsUpdate(Watcher watcher) { watcher.updateTrustedRoots(trustedRoots); } + private void sendLastSpiffeTrustMapUpdate(Watcher watcher) { + watcher.updateSpiffeTrustMap(spiffeTrustMap); + } + @Override public synchronized void updateCertificate(PrivateKey key, List certChain) { checkNotNull(key, "key"); @@ -103,6 +114,14 @@ public synchronized void updateTrustedRoots(List trustedRoots) } } + @Override + public void updateSpiffeTrustMap(Map> spiffeTrustMap) { + this.spiffeTrustMap = spiffeTrustMap; + for (Watcher watcher : downstreamWatchers) { + sendLastSpiffeTrustMapUpdate(watcher); + } + } + @Override public synchronized void onError(Status errorStatus) { for (Watcher watcher : downstreamWatchers) { @@ -147,7 +166,7 @@ protected CertificateProvider(DistributorWatcher watcher, boolean notifyCertUpda @Override public abstract void close(); - /** Starts the cert refresh and watcher update cycle. */ + /** Starts the async cert refresh and watcher update cycle. */ public abstract void start(); private final DistributorWatcher watcher; diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProvider.java index dd945ce850e..304124cc7f2 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProvider.java @@ -16,10 +16,12 @@ package io.grpc.xds.internal.security.certprovider; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; import io.grpc.Status; +import io.grpc.internal.SpiffeUtil; import io.grpc.internal.TimeProvider; import io.grpc.xds.internal.security.trust.CertificateUtils; import java.io.ByteArrayInputStream; @@ -30,6 +32,7 @@ import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.Arrays; +import java.util.HashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -47,11 +50,13 @@ final class FileWatcherCertificateProvider extends CertificateProvider implement private final Path certFile; private final Path keyFile; private final Path trustFile; + private final Path spiffeTrustMapFile; private final long refreshIntervalInSeconds; @VisibleForTesting ScheduledFuture scheduledFuture; private FileTime lastModifiedTimeCert; private FileTime lastModifiedTimeKey; private FileTime lastModifiedTimeRoot; + private FileTime lastModifiedTimespiffeTrustMap; private boolean shutdown; FileWatcherCertificateProvider( @@ -60,6 +65,7 @@ final class FileWatcherCertificateProvider extends CertificateProvider implement String certFile, String keyFile, String trustFile, + String spiffeTrustMapFile, long refreshIntervalInSeconds, ScheduledExecutorService scheduledExecutorService, TimeProvider timeProvider) { @@ -69,7 +75,15 @@ final class FileWatcherCertificateProvider extends CertificateProvider implement this.timeProvider = checkNotNull(timeProvider, "timeProvider"); this.certFile = Paths.get(checkNotNull(certFile, "certFile")); this.keyFile = Paths.get(checkNotNull(keyFile, "keyFile")); - this.trustFile = Paths.get(checkNotNull(trustFile, "trustFile")); + checkArgument((trustFile != null || spiffeTrustMapFile != null), + "either trustFile or spiffeTrustMapFile must be present"); + if (spiffeTrustMapFile != null) { + this.spiffeTrustMapFile = Paths.get(spiffeTrustMapFile); + this.trustFile = null; + } else { + this.spiffeTrustMapFile = null; + this.trustFile = Paths.get(trustFile); + } this.refreshIntervalInSeconds = refreshIntervalInSeconds; } @@ -107,39 +121,48 @@ void checkAndReloadCertificates() { byte[] keyFileContents = Files.readAllBytes(keyFile); FileTime currentCertTime2 = Files.getLastModifiedTime(certFile); FileTime currentKeyTime2 = Files.getLastModifiedTime(keyFile); - if (!currentCertTime2.equals(currentCertTime)) { - return; - } - if (!currentKeyTime2.equals(currentKeyTime)) { - return; - } - try (ByteArrayInputStream certStream = new ByteArrayInputStream(certFileContents); - ByteArrayInputStream keyStream = new ByteArrayInputStream(keyFileContents)) { - PrivateKey privateKey = CertificateUtils.getPrivateKey(keyStream); - X509Certificate[] certs = CertificateUtils.toX509Certificates(certStream); - getWatcher().updateCertificate(privateKey, Arrays.asList(certs)); + if (currentCertTime2.equals(currentCertTime) && currentKeyTime2.equals(currentKeyTime)) { + try (ByteArrayInputStream certStream = new ByteArrayInputStream(certFileContents); + ByteArrayInputStream keyStream = new ByteArrayInputStream(keyFileContents)) { + PrivateKey privateKey = CertificateUtils.getPrivateKey(keyStream); + X509Certificate[] certs = CertificateUtils.toX509Certificates(certStream); + getWatcher().updateCertificate(privateKey, Arrays.asList(certs)); + } + lastModifiedTimeCert = currentCertTime; + lastModifiedTimeKey = currentKeyTime; } - lastModifiedTimeCert = currentCertTime; - lastModifiedTimeKey = currentKeyTime; } } catch (Throwable t) { generateErrorIfCurrentCertExpired(t); } try { - FileTime currentRootTime = Files.getLastModifiedTime(trustFile); - if (currentRootTime.equals(lastModifiedTimeRoot)) { - return; - } - byte[] rootFileContents = Files.readAllBytes(trustFile); - FileTime currentRootTime2 = Files.getLastModifiedTime(trustFile); - if (!currentRootTime2.equals(currentRootTime)) { - return; + if (spiffeTrustMapFile != null) { + FileTime currentSpiffeTime = Files.getLastModifiedTime(spiffeTrustMapFile); + if (!currentSpiffeTime.equals(lastModifiedTimespiffeTrustMap)) { + SpiffeUtil.SpiffeBundle trustBundle = SpiffeUtil + .loadTrustBundleFromFile(spiffeTrustMapFile.toString()); + getWatcher().updateSpiffeTrustMap(new HashMap<>(trustBundle.getBundleMap())); + lastModifiedTimespiffeTrustMap = currentSpiffeTime; + } } - try (ByteArrayInputStream rootStream = new ByteArrayInputStream(rootFileContents)) { - X509Certificate[] caCerts = CertificateUtils.toX509Certificates(rootStream); - getWatcher().updateTrustedRoots(Arrays.asList(caCerts)); + } catch (Throwable t) { + getWatcher().onError(Status.fromThrowable(t)); + } + try { + if (trustFile != null) { + FileTime currentRootTime = Files.getLastModifiedTime(trustFile); + if (!currentRootTime.equals(lastModifiedTimeRoot)) { + byte[] rootFileContents = Files.readAllBytes(trustFile); + FileTime currentRootTime2 = Files.getLastModifiedTime(trustFile); + if (currentRootTime2.equals(currentRootTime)) { + try (ByteArrayInputStream rootStream = new ByteArrayInputStream(rootFileContents)) { + X509Certificate[] caCerts = CertificateUtils.toX509Certificates(rootStream); + getWatcher().updateTrustedRoots(Arrays.asList(caCerts)); + } + lastModifiedTimeRoot = currentRootTime; + } + } } - lastModifiedTimeRoot = currentRootTime; } catch (Throwable t) { getWatcher().onError(Status.fromThrowable(t)); } @@ -195,6 +218,7 @@ FileWatcherCertificateProvider create( String certFile, String keyFile, String trustFile, + String spiffeTrustMapFile, long refreshIntervalInSeconds, ScheduledExecutorService scheduledExecutorService, TimeProvider timeProvider) { @@ -204,6 +228,7 @@ FileWatcherCertificateProvider create( certFile, keyFile, trustFile, + spiffeTrustMapFile, refreshIntervalInSeconds, scheduledExecutorService, timeProvider); @@ -220,6 +245,7 @@ abstract FileWatcherCertificateProvider create( String certFile, String keyFile, String trustFile, + String spiffeTrustMapFile, long refreshIntervalInSeconds, ScheduledExecutorService scheduledExecutorService, TimeProvider timeProvider); diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderProvider.java index c4b140442cb..53864ae33f1 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderProvider.java @@ -23,6 +23,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.protobuf.Duration; import com.google.protobuf.util.Durations; +import io.grpc.internal.GrpcUtil; import io.grpc.internal.JsonUtil; import io.grpc.internal.TimeProvider; import java.text.ParseException; @@ -33,11 +34,15 @@ /** * Provider of {@link FileWatcherCertificateProvider}s. */ -final class FileWatcherCertificateProviderProvider implements CertificateProviderProvider { +public final class FileWatcherCertificateProviderProvider implements CertificateProviderProvider { + @VisibleForTesting + public static boolean enableSpiffe = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_SPIFFE_TRUST_BUNDLE_MAP", + false); private static final String CERT_FILE_KEY = "certificate_file"; private static final String KEY_FILE_KEY = "private_key_file"; private static final String ROOT_FILE_KEY = "ca_certificate_file"; + private static final String SPIFFE_TRUST_MAP_FILE_KEY = "spiffe_trust_bundle_map_file"; private static final String REFRESH_INTERVAL_KEY = "refresh_interval"; @VisibleForTesting static final long REFRESH_INTERVAL_DEFAULT = 600L; @@ -82,6 +87,7 @@ public CertificateProvider createCertificateProvider( configObj.certFile, configObj.keyFile, configObj.rootFile, + configObj.spiffeTrustMapFile, configObj.refrehInterval, scheduledExecutorServiceFactory.create(), timeProvider); @@ -98,7 +104,20 @@ private static Config validateAndTranslateConfig(Object config) { Config configObj = new Config(); configObj.certFile = checkForNullAndGet(map, CERT_FILE_KEY); configObj.keyFile = checkForNullAndGet(map, KEY_FILE_KEY); - configObj.rootFile = checkForNullAndGet(map, ROOT_FILE_KEY); + if (enableSpiffe) { + if (!map.containsKey(ROOT_FILE_KEY) && !map.containsKey(SPIFFE_TRUST_MAP_FILE_KEY)) { + throw new NullPointerException( + String.format("either '%s' or '%s' is required in the config", + ROOT_FILE_KEY, SPIFFE_TRUST_MAP_FILE_KEY)); + } + if (map.containsKey(SPIFFE_TRUST_MAP_FILE_KEY)) { + configObj.spiffeTrustMapFile = JsonUtil.getString(map, SPIFFE_TRUST_MAP_FILE_KEY); + } else { + configObj.rootFile = JsonUtil.getString(map, ROOT_FILE_KEY); + } + } else { + configObj.rootFile = checkForNullAndGet(map, ROOT_FILE_KEY); + } String refreshIntervalString = JsonUtil.getString(map, REFRESH_INTERVAL_KEY); if (refreshIntervalString != null) { try { @@ -139,6 +158,7 @@ static class Config { String certFile; String keyFile; String rootFile; + String spiffeTrustMapFile; Long refrehInterval; } } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactory.java b/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactory.java index c9d83902ec2..8cb44117065 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactory.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactory.java @@ -17,6 +17,7 @@ package io.grpc.xds.internal.security.trust; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.common.annotations.VisibleForTesting; @@ -33,6 +34,9 @@ import java.security.cert.CertStoreException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.ManagerFactoryParameters; @@ -63,6 +67,11 @@ public XdsTrustManagerFactory( this(certs, staticCertificateValidationContext, true); } + public XdsTrustManagerFactory(Map> spiffeTrustMap, + CertificateValidationContext staticCertificateValidationContext) throws CertStoreException { + this(spiffeTrustMap, staticCertificateValidationContext, true); + } + private XdsTrustManagerFactory( X509Certificate[] certs, CertificateValidationContext certificateValidationContext, @@ -76,6 +85,19 @@ private XdsTrustManagerFactory( xdsX509TrustManager = createX509TrustManager(certs, certificateValidationContext); } + private XdsTrustManagerFactory( + Map> spiffeTrustMap, + CertificateValidationContext certificateValidationContext, + boolean validationContextIsStatic) + throws CertStoreException { + if (validationContextIsStatic) { + checkArgument( + certificateValidationContext == null || !certificateValidationContext.hasTrustedCa(), + "only static certificateValidationContext expected"); + xdsX509TrustManager = createX509TrustManager(spiffeTrustMap, certificateValidationContext); + } + } + private static X509Certificate[] getTrustedCaFromCertContext( CertificateValidationContext certificateValidationContext) throws CertificateException, IOException { @@ -100,6 +122,24 @@ private static X509Certificate[] getTrustedCaFromCertContext( @VisibleForTesting static XdsX509TrustManager createX509TrustManager( X509Certificate[] certs, CertificateValidationContext certContext) throws CertStoreException { + return new XdsX509TrustManager(certContext, createTrustManager(certs)); + } + + @VisibleForTesting + static XdsX509TrustManager createX509TrustManager( + Map> spiffeTrustMapFile, + CertificateValidationContext certContext) throws CertStoreException { + checkNotNull(spiffeTrustMapFile, "spiffeTrustMapFile"); + Map delegates = new HashMap<>(); + for (Map.Entry> entry:spiffeTrustMapFile.entrySet()) { + delegates.put(entry.getKey(), createTrustManager( + entry.getValue().toArray(new X509Certificate[0]))); + } + return new XdsX509TrustManager(certContext, delegates); + } + + private static X509ExtendedTrustManager createTrustManager(X509Certificate[] certs) + throws CertStoreException { TrustManagerFactory tmf = null; try { tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); @@ -131,7 +171,7 @@ static XdsX509TrustManager createX509TrustManager( if (myDelegate == null) { throw new CertStoreException("Native X509 TrustManager not found."); } - return new XdsX509TrustManager(certContext, myDelegate); + return myDelegate; } @Override diff --git a/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java b/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java index 6181d70fa51..dcd3f2006a1 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java @@ -19,18 +19,25 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; import com.google.re2j.Pattern; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; +import io.grpc.internal.SpiffeUtil; import java.net.Socket; import java.security.cert.CertificateException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Set; import javax.annotation.Nullable; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLParameters; @@ -51,6 +58,7 @@ final class XdsX509TrustManager extends X509ExtendedTrustManager implements X509 private static final int ALT_IPA_NAME = 7; private final X509ExtendedTrustManager delegate; + private final Map spiffeTrustMapDelegates; private final CertificateValidationContext certContext; XdsX509TrustManager(@Nullable CertificateValidationContext certContext, @@ -58,6 +66,15 @@ final class XdsX509TrustManager extends X509ExtendedTrustManager implements X509 checkNotNull(delegate, "delegate"); this.certContext = certContext; this.delegate = delegate; + this.spiffeTrustMapDelegates = null; + } + + XdsX509TrustManager(@Nullable CertificateValidationContext certContext, + Map spiffeTrustMapDelegates) { + checkNotNull(spiffeTrustMapDelegates, "spiffeTrustMapDelegates"); + this.spiffeTrustMapDelegates = ImmutableMap.copyOf(spiffeTrustMapDelegates); + this.certContext = certContext; + this.delegate = null; } private static boolean verifyDnsNameInPattern( @@ -204,21 +221,21 @@ void verifySubjectAltNameInChain(X509Certificate[] peerCertChain) throws Certifi @Override public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { - delegate.checkClientTrusted(chain, authType, socket); + chooseDelegate(chain).checkClientTrusted(chain, authType, socket); verifySubjectAltNameInChain(chain); } @Override public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { - delegate.checkClientTrusted(chain, authType, sslEngine); + chooseDelegate(chain).checkClientTrusted(chain, authType, sslEngine); verifySubjectAltNameInChain(chain); } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - delegate.checkClientTrusted(chain, authType); + chooseDelegate(chain).checkClientTrusted(chain, authType); verifySubjectAltNameInChain(chain); } @@ -233,7 +250,7 @@ public void checkServerTrusted(X509Certificate[] chain, String authType, Socket sslSocket.setSSLParameters(sslParams); } } - delegate.checkServerTrusted(chain, authType, socket); + chooseDelegate(chain).checkServerTrusted(chain, authType, socket); verifySubjectAltNameInChain(chain); } @@ -245,19 +262,44 @@ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngi sslParams.setEndpointIdentificationAlgorithm(""); sslEngine.setSSLParameters(sslParams); } - delegate.checkServerTrusted(chain, authType, sslEngine); + chooseDelegate(chain).checkServerTrusted(chain, authType, sslEngine); verifySubjectAltNameInChain(chain); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - delegate.checkServerTrusted(chain, authType); + chooseDelegate(chain).checkServerTrusted(chain, authType); verifySubjectAltNameInChain(chain); } + private X509ExtendedTrustManager chooseDelegate(X509Certificate[] chain) + throws CertificateException { + if (spiffeTrustMapDelegates != null) { + Optional spiffeId = SpiffeUtil.extractSpiffeId(chain); + if (!spiffeId.isPresent()) { + throw new CertificateException("Failed to extract SPIFFE ID from peer leaf certificate"); + } + String trustDomain = spiffeId.get().getTrustDomain(); + if (!spiffeTrustMapDelegates.containsKey(trustDomain)) { + throw new CertificateException(String.format("Spiffe Trust Map doesn't contain trust" + + " domain '%s' from peer leaf certificate", trustDomain)); + } + return spiffeTrustMapDelegates.get(trustDomain); + } else { + return delegate; + } + } + @Override public X509Certificate[] getAcceptedIssuers() { + if (spiffeTrustMapDelegates != null) { + Set result = new HashSet<>(); + for (X509ExtendedTrustManager tm: spiffeTrustMapDelegates.values()) { + result.addAll(Arrays.asList(tm.getAcceptedIssuers())); + } + return result.toArray(new X509Certificate[0]); + } return delegate.getAcceptedIssuers(); } } diff --git a/xds/src/test/java/io/grpc/xds/CommonBootstrapperTestUtils.java b/xds/src/test/java/io/grpc/xds/CommonBootstrapperTestUtils.java index 0b2f3c7136b..6a8eced298c 100644 --- a/xds/src/test/java/io/grpc/xds/CommonBootstrapperTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/CommonBootstrapperTestUtils.java @@ -88,7 +88,7 @@ public static Bootstrapper.BootstrapInfo buildBootstrapInfo( String certInstanceName1, @Nullable String privateKey1, @Nullable String cert1, @Nullable String trustCa1, String certInstanceName2, String privateKey2, String cert2, - String trustCa2) { + String trustCa2, @Nullable String spiffeTrustMap) { // get temp file for each file try { if (privateKey1 != null) { @@ -109,6 +109,9 @@ public static Bootstrapper.BootstrapInfo buildBootstrapInfo( if (trustCa2 != null) { trustCa2 = CommonTlsContextTestsUtil.getTempFileNameForResourcesFile(trustCa2); } + if (spiffeTrustMap != null) { + spiffeTrustMap = CommonTlsContextTestsUtil.getTempFileNameForResourcesFile(spiffeTrustMap); + } } catch (IOException ioe) { throw new RuntimeException(ioe); } @@ -116,6 +119,9 @@ public static Bootstrapper.BootstrapInfo buildBootstrapInfo( config.put("certificate_file", cert1); config.put("private_key_file", privateKey1); config.put("ca_certificate_file", trustCa1); + if (spiffeTrustMap != null) { + config.put("spiffe_trust_bundle_map_file", spiffeTrustMap); + } Bootstrapper.CertificateProviderInfo certificateProviderInfo = Bootstrapper.CertificateProviderInfo.create("file_watcher", config); HashMap certProviders = @@ -126,6 +132,9 @@ public static Bootstrapper.BootstrapInfo buildBootstrapInfo( config.put("certificate_file", cert2); config.put("private_key_file", privateKey2); config.put("ca_certificate_file", trustCa2); + if (spiffeTrustMap != null) { + config.put("spiffe_trust_bundle_map_file", spiffeTrustMap); + } certificateProviderInfo = Bootstrapper.CertificateProviderInfo.create("file_watcher", config); certProviders.put(certInstanceName2, certificateProviderInfo); diff --git a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java index f56a7c367bb..8e1220fe9d0 100644 --- a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java @@ -24,8 +24,12 @@ import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CA_PEM_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CLIENT_KEY_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CLIENT_PEM_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CLIENT_SPIFFE_PEM_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SERVER_1_KEY_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SERVER_1_PEM_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SERVER_1_SPIFFE_PEM_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SPIFFE_TRUST_MAP_1_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SPIFFE_TRUST_MAP_FILE; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; @@ -67,6 +71,7 @@ import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import io.grpc.xds.internal.security.SslContextProviderSupplier; import io.grpc.xds.internal.security.TlsContextManagerImpl; +import io.grpc.xds.internal.security.certprovider.FileWatcherCertificateProviderProvider; import io.netty.handler.ssl.NotSslRecordException; import java.io.File; import java.io.FileOutputStream; @@ -85,6 +90,7 @@ import java.security.cert.CertificateFactory; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.Executors; @@ -92,18 +98,25 @@ import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import org.junit.After; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; /** * Unit tests for {@link XdsChannelCredentials} and {@link XdsServerBuilder} for plaintext/TLS/mTLS * modes. */ -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class XdsSecurityClientServerTest { + @Parameter + public Boolean enableSpiffe; + private Boolean originalEnableSpiffe; + @Rule public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule(); private int port; private FakeNameResolverFactory fakeNameResolverFactory; @@ -115,11 +128,27 @@ public class XdsSecurityClientServerTest { private FakeXdsClientPoolFactory fakePoolFactory = new FakeXdsClientPoolFactory(xdsClient); private static final String OVERRIDE_AUTHORITY = "foo.test.google.fr"; + @Parameters(name = "enableSpiffe={0}") + public static Collection data() { + return ImmutableList.of(true, false); + } + + @Before + public void setUp() throws IOException { + saveEnvironment(); + FileWatcherCertificateProviderProvider.enableSpiffe = enableSpiffe; + } + + private void saveEnvironment() { + originalEnableSpiffe = FileWatcherCertificateProviderProvider.enableSpiffe; + } + @After public void tearDown() throws IOException { if (fakeNameResolverFactory != null) { NameResolverRegistry.getDefaultRegistry().deregister(fakeNameResolverFactory); } + FileWatcherCertificateProviderProvider.enableSpiffe = originalEnableSpiffe; } @Test @@ -146,13 +175,13 @@ public void nullFallbackCredentials_expectException() throws Exception { @Test public void tlsClientServer_noClientAuthentication() throws Exception { DownstreamTlsContext downstreamTlsContext = - setBootstrapInfoAndBuildDownstreamTlsContext(null, null, null, null, false, false); + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, false, false); buildServerWithTlsContext(downstreamTlsContext); // for TLS, client only needs trustCa UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContext( - CLIENT_KEY_FILE, - CLIENT_PEM_FILE, false); + CLIENT_KEY_FILE, CLIENT_PEM_FILE, null, false); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); @@ -169,7 +198,8 @@ public void tlsClientServer_useSystemRootCerts_useCombinedValidationContext() th try { setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); DownstreamTlsContext downstreamTlsContext = - setBootstrapInfoAndBuildDownstreamTlsContext(null, null, null, null, false, false); + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, false, false); buildServerWithTlsContext(downstreamTlsContext); UpstreamTlsContext upstreamTlsContext = @@ -195,7 +225,8 @@ public void tlsClientServer_useSystemRootCerts_validationContext() throws Except try { setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); DownstreamTlsContext downstreamTlsContext = - setBootstrapInfoAndBuildDownstreamTlsContext(null, null, null, null, false, false); + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, false, false); buildServerWithTlsContext(downstreamTlsContext); UpstreamTlsContext upstreamTlsContext = @@ -221,7 +252,8 @@ public void tlsClientServer_useSystemRootCerts_requireClientAuth() throws Except try { setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); DownstreamTlsContext downstreamTlsContext = - setBootstrapInfoAndBuildDownstreamTlsContext(null, null, null, null, false, false); + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, false, false); buildServerWithTlsContext(downstreamTlsContext); UpstreamTlsContext upstreamTlsContext = @@ -252,17 +284,59 @@ private Path getCacertFilePathForTestCa() return trustStoreFile.toPath(); } + @Test + public void tlsClientServer_Spiffe_noClientAuthentication() throws Exception { + DownstreamTlsContext downstreamTlsContext = + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_SPIFFE_PEM_FILE, null, null, null, + null, null, false, false); + buildServerWithTlsContext(downstreamTlsContext); + + // for TLS, client only needs trustCa, so BAD certs don't matter + UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContext( + BAD_CLIENT_KEY_FILE, BAD_CLIENT_PEM_FILE, SPIFFE_TRUST_MAP_FILE, false); + + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = + getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); + assertThat(unaryRpc(/* requestMessage= */ "buddy", blockingStub)).isEqualTo("Hello buddy"); + } + + @Test + public void tlsClientServer_Spiffe_noClientAuthentication_wrongServerCert() throws Exception { + if (!enableSpiffe) { + return; + } + DownstreamTlsContext downstreamTlsContext = + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, false, false); + buildServerWithTlsContext(downstreamTlsContext); + + // for TLS, client only needs trustCa, so BAD certs don't matter + UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContext( + BAD_CLIENT_KEY_FILE, BAD_CLIENT_PEM_FILE, SPIFFE_TRUST_MAP_FILE, false); + + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = + getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); + try { + unaryRpc("buddy", blockingStub); + fail("exception expected"); + } catch (StatusRuntimeException sre) { + assertThat(sre.getStatus().getCode()).isEqualTo(Status.UNAVAILABLE.getCode()); + assertThat(sre.getCause().getCause().getMessage()) + .contains("Failed to extract SPIFFE ID from peer leaf certificate"); + } + } + @Test public void requireClientAuth_noClientCert_expectException() throws Exception { DownstreamTlsContext downstreamTlsContext = - setBootstrapInfoAndBuildDownstreamTlsContext(null, null, null, null, true, true); + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, true, true); buildServerWithTlsContext(downstreamTlsContext); // for TLS, client only uses trustCa UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContext( - CLIENT_KEY_FILE, - CLIENT_PEM_FILE, false); + CLIENT_KEY_FILE, CLIENT_PEM_FILE, null, false); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); @@ -284,12 +358,12 @@ public void requireClientAuth_noClientCert_expectException() @Test public void noClientAuth_sendBadClientCert_passes() throws Exception { DownstreamTlsContext downstreamTlsContext = - setBootstrapInfoAndBuildDownstreamTlsContext(null, null, null, null, false, false); + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, false, false); buildServerWithTlsContext(downstreamTlsContext); UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContext( - BAD_CLIENT_KEY_FILE, - BAD_CLIENT_PEM_FILE, true); + BAD_CLIENT_KEY_FILE, BAD_CLIENT_PEM_FILE, null, true); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); @@ -299,8 +373,7 @@ public void noClientAuth_sendBadClientCert_passes() throws Exception { @Test public void mtls_badClientCert_expectException() throws Exception { UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContext( - BAD_CLIENT_KEY_FILE, - BAD_CLIENT_PEM_FILE, true); + BAD_CLIENT_KEY_FILE, BAD_CLIENT_PEM_FILE, null, true); try { performMtlsTestAndGetListenerWatcher(upstreamTlsContext, null, null, null, null); fail("exception expected"); @@ -316,20 +389,58 @@ public void mtls_badClientCert_expectException() throws Exception { } } - /** mTLS - client auth enabled - using {@link XdsChannelCredentials} API. */ + /** mTLS with Spiffe Trust Bundle - client auth enabled - using {@link XdsChannelCredentials} + * API. */ + @Test + public void mtlsClientServer_Spiffe_withClientAuthentication_withXdsChannelCreds() + throws Exception { + DownstreamTlsContext downstreamTlsContext = + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_SPIFFE_PEM_FILE, null, null, null, + null, SPIFFE_TRUST_MAP_1_FILE, true, true); + buildServerWithTlsContext(downstreamTlsContext); + + UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContext( + CLIENT_KEY_FILE, CLIENT_SPIFFE_PEM_FILE, SPIFFE_TRUST_MAP_1_FILE, true); + + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = + getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); + assertThat(unaryRpc(/* requestMessage= */ "buddy", blockingStub)).isEqualTo("Hello buddy"); + } + + @Test + public void mtlsClientServer_Spiffe_badClientCert_expectException() + throws Exception { + DownstreamTlsContext downstreamTlsContext = + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_SPIFFE_PEM_FILE, null, null, null, + null, SPIFFE_TRUST_MAP_1_FILE, true, true); + buildServerWithTlsContext(downstreamTlsContext); + + UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContext( + CLIENT_KEY_FILE, BAD_CLIENT_PEM_FILE, SPIFFE_TRUST_MAP_1_FILE, true); + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = + getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); + try { + assertThat(unaryRpc(/* requestMessage= */ "buddy", blockingStub)).isEqualTo("Hello buddy"); + fail("exception expected"); + } catch (StatusRuntimeException sre) { + assertThat(sre.getStatus().getCode()).isEqualTo(Status.UNAVAILABLE.getCode()); + assertThat(sre.getMessage()).contains("ssl exception"); + } + } + @Test public void mtlsClientServer_withClientAuthentication_withXdsChannelCreds() throws Exception { UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContext( - CLIENT_KEY_FILE, - CLIENT_PEM_FILE, true); + CLIENT_KEY_FILE, CLIENT_PEM_FILE, null, true); performMtlsTestAndGetListenerWatcher(upstreamTlsContext, null, null, null, null); } @Test public void tlsServer_plaintextClient_expectException() throws Exception { DownstreamTlsContext downstreamTlsContext = - setBootstrapInfoAndBuildDownstreamTlsContext(null, null, null, null, false, false); + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, false, false); buildServerWithTlsContext(downstreamTlsContext); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = @@ -349,8 +460,7 @@ public void plaintextServer_tlsClient_expectException() throws Exception { // for TLS, client only needs trustCa UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContext( - CLIENT_KEY_FILE, - CLIENT_PEM_FILE, false); + CLIENT_KEY_FILE, CLIENT_PEM_FILE, null, false); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); @@ -368,8 +478,7 @@ public void plaintextServer_tlsClient_expectException() throws Exception { public void mtlsClientServer_changeServerContext_expectException() throws Exception { UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContext( - CLIENT_KEY_FILE, - CLIENT_PEM_FILE, true); + CLIENT_KEY_FILE, CLIENT_PEM_FILE, null, true); performMtlsTestAndGetListenerWatcher(upstreamTlsContext, "cert-instance-name2", BAD_SERVER_KEY_FILE, BAD_SERVER_PEM_FILE, CA_PEM_FILE); @@ -396,8 +505,8 @@ private void performMtlsTestAndGetListenerWatcher( String privateKey2, String cert2, String trustCa2) throws Exception { DownstreamTlsContext downstreamTlsContext = - setBootstrapInfoAndBuildDownstreamTlsContext(certInstanceName2, privateKey2, cert2, - trustCa2, true, true); + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, certInstanceName2, + privateKey2, cert2, trustCa2, null, true, false); buildServerWithFallbackServerCredentials( InsecureServerCredentials.create(), downstreamTlsContext); @@ -408,22 +517,21 @@ private void performMtlsTestAndGetListenerWatcher( } private DownstreamTlsContext setBootstrapInfoAndBuildDownstreamTlsContext( - String certInstanceName2, - String privateKey2, - String cert2, String trustCa2, boolean hasRootCert, boolean requireClientCertificate) { + String cert1, String certInstanceName2, String privateKey2, + String cert2, String trustCa2, String spiffeFile, + boolean hasRootCert, boolean requireClientCertificate) { bootstrapInfoForServer = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-server", SERVER_1_KEY_FILE, - SERVER_1_PEM_FILE, CA_PEM_FILE, certInstanceName2, privateKey2, cert2, trustCa2); + cert1, CA_PEM_FILE, certInstanceName2, privateKey2, cert2, trustCa2, spiffeFile); return CommonTlsContextTestsUtil.buildDownstreamTlsContext( "google_cloud_private_spiffe-server", hasRootCert, requireClientCertificate); } private UpstreamTlsContext setBootstrapInfoAndBuildUpstreamTlsContext(String clientKeyFile, - String clientPemFile, - boolean hasIdentityCert) { + String clientPemFile, String spiffeFile, boolean hasIdentityCert) { bootstrapInfoForClient = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-client", clientKeyFile, clientPemFile, - CA_PEM_FILE, null, null, null, null); + CA_PEM_FILE, null, null, null, null, spiffeFile); return CommonTlsContextTestsUtil .buildUpstreamTlsContext("google_cloud_private_spiffe-client", hasIdentityCert); } @@ -434,7 +542,7 @@ private UpstreamTlsContext setBootstrapInfoAndBuildUpstreamTlsContextForUsingSys boolean useCombinedValidationContext) { bootstrapInfoForClient = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-client", clientKeyFile, clientPemFile, - CA_PEM_FILE, null, null, null, null); + CA_PEM_FILE, null, null, null, null, null); if (useCombinedValidationContext) { return CommonTlsContextTestsUtil.buildUpstreamTlsContextForCertProviderInstance( "google_cloud_private_spiffe-client", "ROOT", null, diff --git a/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java b/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java index db82a8682e0..a6cf2c52a15 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java @@ -48,10 +48,14 @@ public class CommonTlsContextTestsUtil { public static final String SERVER_0_PEM_FILE = "server0.pem"; public static final String SERVER_0_KEY_FILE = "server0.key"; public static final String SERVER_1_PEM_FILE = "server1.pem"; + public static final String SERVER_1_SPIFFE_PEM_FILE = "server1_spiffe.pem"; public static final String SERVER_1_KEY_FILE = "server1.key"; public static final String CLIENT_PEM_FILE = "client.pem"; + public static final String CLIENT_SPIFFE_PEM_FILE = "client_spiffe.pem"; public static final String CLIENT_KEY_FILE = "client.key"; public static final String CA_PEM_FILE = "ca.pem"; + public static final String SPIFFE_TRUST_MAP_FILE = "spiffebundle.json"; + public static final String SPIFFE_TRUST_MAP_1_FILE = "spiffebundle1.json"; /** Bad/untrusted server certs. */ public static final String BAD_SERVER_PEM_FILE = "badserver.pem"; public static final String BAD_SERVER_KEY_FILE = "badserver.key"; diff --git a/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java b/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java index da7f8113dfa..fdfcd369937 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java @@ -149,7 +149,7 @@ public void clientSecurityHandler_addLast() CommonCertProviderTestUtils.register(executor); Bootstrapper.BootstrapInfo bootstrapInfoForClient = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-client", CLIENT_KEY_FILE, CLIENT_PEM_FILE, - CA_PEM_FILE, null, null, null, null); + CA_PEM_FILE, null, null, null, null, null); UpstreamTlsContext upstreamTlsContext = CommonTlsContextTestsUtil .buildUpstreamTlsContext("google_cloud_private_spiffe-client", true); @@ -216,7 +216,7 @@ public SocketAddress remoteAddress() { pipeline = channel.pipeline(); Bootstrapper.BootstrapInfo bootstrapInfoForServer = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-server", SERVER_1_KEY_FILE, - SERVER_1_PEM_FILE, CA_PEM_FILE, null, null, null, null); + SERVER_1_PEM_FILE, CA_PEM_FILE, null, null, null, null, null); DownstreamTlsContext downstreamTlsContext = CommonTlsContextTestsUtil.buildDownstreamTlsContext( "google_cloud_private_spiffe-server", true, true); @@ -361,7 +361,7 @@ public void clientSecurityProtocolNegotiatorNewHandler_fireProtocolNegotiationEv CommonCertProviderTestUtils.register(executor); Bootstrapper.BootstrapInfo bootstrapInfoForClient = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-client", CLIENT_KEY_FILE, CLIENT_PEM_FILE, - CA_PEM_FILE, null, null, null, null); + CA_PEM_FILE, null, null, null, null, null); UpstreamTlsContext upstreamTlsContext = CommonTlsContextTestsUtil .buildUpstreamTlsContext("google_cloud_private_spiffe-client", true); @@ -412,7 +412,7 @@ public void clientSecurityProtocolNegotiatorNewHandler_handleHandlerRemoved() { CommonCertProviderTestUtils.register(executor); Bootstrapper.BootstrapInfo bootstrapInfoForClient = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-client", CLIENT_KEY_FILE, CLIENT_PEM_FILE, - CA_PEM_FILE, null, null, null, null); + CA_PEM_FILE, null, null, null, null, null); UpstreamTlsContext upstreamTlsContext = CommonTlsContextTestsUtil .buildUpstreamTlsContext("google_cloud_private_spiffe-client", true); diff --git a/xds/src/test/java/io/grpc/xds/internal/security/TlsContextManagerTest.java b/xds/src/test/java/io/grpc/xds/internal/security/TlsContextManagerTest.java index 4d04eeb41e0..29d131cb8d7 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/TlsContextManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/TlsContextManagerTest.java @@ -57,7 +57,7 @@ public class TlsContextManagerTest { public void createServerSslContextProvider() { Bootstrapper.BootstrapInfo bootstrapInfoForServer = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-server", SERVER_1_KEY_FILE, - SERVER_1_PEM_FILE, CA_PEM_FILE, null, null, null, null); + SERVER_1_PEM_FILE, CA_PEM_FILE, null, null, null, null, null); DownstreamTlsContext downstreamTlsContext = CommonTlsContextTestsUtil.buildDownstreamTlsContext( "google_cloud_private_spiffe-server", false, false); @@ -76,7 +76,7 @@ public void createServerSslContextProvider() { public void createClientSslContextProvider() { Bootstrapper.BootstrapInfo bootstrapInfoForClient = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-client", CLIENT_KEY_FILE, CLIENT_PEM_FILE, - CA_PEM_FILE, null, null, null, null); + CA_PEM_FILE, null, null, null, null, null); UpstreamTlsContext upstreamTlsContext = CommonTlsContextTestsUtil .buildUpstreamTlsContext("google_cloud_private_spiffe-client", false); @@ -96,7 +96,7 @@ public void createServerSslContextProvider_differentInstance() { Bootstrapper.BootstrapInfo bootstrapInfoForServer = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-server", SERVER_1_KEY_FILE, SERVER_1_PEM_FILE, CA_PEM_FILE, "cert-instance2", SERVER_0_KEY_FILE, SERVER_0_PEM_FILE, - CA_PEM_FILE); + CA_PEM_FILE, null); DownstreamTlsContext downstreamTlsContext = CommonTlsContextTestsUtil.buildDownstreamTlsContext( "google_cloud_private_spiffe-server", false, false); @@ -120,7 +120,7 @@ public void createServerSslContextProvider_differentInstance() { public void createClientSslContextProvider_differentInstance() { Bootstrapper.BootstrapInfo bootstrapInfoForClient = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-client", CLIENT_KEY_FILE, CLIENT_PEM_FILE, - CA_PEM_FILE, "cert-instance-2", CLIENT_KEY_FILE, CLIENT_PEM_FILE, CA_PEM_FILE); + CA_PEM_FILE, "cert-instance-2", CLIENT_KEY_FILE, CLIENT_PEM_FILE, CA_PEM_FILE, null); UpstreamTlsContext upstreamTlsContext = CommonTlsContextTestsUtil .buildUpstreamTlsContext("google_cloud_private_spiffe-client", false); diff --git a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderProviderTest.java index a0bdd618004..304a2dd5441 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderProviderTest.java @@ -24,22 +24,28 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.common.collect.ImmutableList; import io.grpc.internal.JsonParser; import io.grpc.internal.TimeProvider; import java.io.IOException; +import java.util.Collection; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; +import org.junit.After; +import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; /** Unit tests for {@link FileWatcherCertificateProviderProvider}. */ -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class FileWatcherCertificateProviderProviderTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @@ -48,13 +54,28 @@ public class FileWatcherCertificateProviderProviderTest { scheduledExecutorServiceFactory; @Mock private TimeProvider timeProvider; + @Parameter + public boolean enableSpiffe; + private boolean originalEnableSpiffe; private FileWatcherCertificateProviderProvider provider; + @Parameters(name = "enableSpiffe={0}") + public static Collection data() { + return ImmutableList.of(true, false); + } + @Before public void setUp() throws IOException { provider = new FileWatcherCertificateProviderProvider( fileWatcherCertificateProviderFactory, scheduledExecutorServiceFactory, timeProvider); + originalEnableSpiffe = FileWatcherCertificateProviderProvider.enableSpiffe; + FileWatcherCertificateProviderProvider.enableSpiffe = enableSpiffe; + } + + @After + public void restoreEnvironment() { + FileWatcherCertificateProviderProvider.enableSpiffe = originalEnableSpiffe; } @Test @@ -85,6 +106,30 @@ public void createProvider_minimalConfig() throws IOException { eq("/var/run/gke-spiffe/certs/certificates.pem"), eq("/var/run/gke-spiffe/certs/private_key.pem"), eq("/var/run/gke-spiffe/certs/ca_certificates.pem"), + eq(null), + eq(600L), + eq(mockService), + eq(timeProvider)); + } + + @Test + public void createProvider_minimalSpiffeConfig() throws IOException { + Assume.assumeTrue(enableSpiffe); + CertificateProvider.DistributorWatcher distWatcher = + new CertificateProvider.DistributorWatcher(); + @SuppressWarnings("unchecked") + Map map = (Map) JsonParser.parse(MINIMAL_FILE_WATCHER_WITH_SPIFFE_CONFIG); + ScheduledExecutorService mockService = mock(ScheduledExecutorService.class); + when(scheduledExecutorServiceFactory.create()).thenReturn(mockService); + provider.createCertificateProvider(map, distWatcher, true); + verify(fileWatcherCertificateProviderFactory, times(1)) + .create( + eq(distWatcher), + eq(true), + eq("/var/run/gke-spiffe/certs/certificates.pem"), + eq("/var/run/gke-spiffe/certs/private_key.pem"), + eq(null), + eq("/var/run/gke-spiffe/certs/spiffe_bundle.json"), eq(600L), eq(mockService), eq(timeProvider)); @@ -106,6 +151,30 @@ public void createProvider_fullConfig() throws IOException { eq("/var/run/gke-spiffe/certs/certificates2.pem"), eq("/var/run/gke-spiffe/certs/private_key3.pem"), eq("/var/run/gke-spiffe/certs/ca_certificates4.pem"), + eq(null), + eq(7890L), + eq(mockService), + eq(timeProvider)); + } + + @Test + public void createProvider_spiffeConfig() throws IOException { + Assume.assumeTrue(enableSpiffe); + CertificateProvider.DistributorWatcher distWatcher = + new CertificateProvider.DistributorWatcher(); + @SuppressWarnings("unchecked") + Map map = (Map) JsonParser.parse(FULL_FILE_WATCHER_WITH_SPIFFE_CONFIG); + ScheduledExecutorService mockService = mock(ScheduledExecutorService.class); + when(scheduledExecutorServiceFactory.create()).thenReturn(mockService); + provider.createCertificateProvider(map, distWatcher, true); + verify(fileWatcherCertificateProviderFactory, times(1)) + .create( + eq(distWatcher), + eq(true), + eq("/var/run/gke-spiffe/certs/certificates2.pem"), + eq("/var/run/gke-spiffe/certs/private_key3.pem"), + eq(null), + eq("/var/run/gke-spiffe/certs/spiffe_bundle.json"), eq(7890L), eq(mockService), eq(timeProvider)); @@ -157,15 +226,18 @@ public void createProvider_missingKey_expectException() throws IOException { @Test public void createProvider_missingRoot_expectException() throws IOException { + String expectedMessage = enableSpiffe ? "either 'ca_certificate_file' or " + + "'spiffe_trust_bundle_map_file' is required in the config" + : "'ca_certificate_file' is required in the config"; CertificateProvider.DistributorWatcher distWatcher = new CertificateProvider.DistributorWatcher(); @SuppressWarnings("unchecked") - Map map = (Map) JsonParser.parse(MISSING_ROOT_CONFIG); + Map map = (Map) JsonParser.parse(MISSING_ROOT_AND_SPIFFE_CONFIG); try { provider.createCertificateProvider(map, distWatcher, true); fail("exception expected"); } catch (NullPointerException npe) { - assertThat(npe).hasMessageThat().isEqualTo("'ca_certificate_file' is required in the config"); + assertThat(npe).hasMessageThat().isEqualTo(expectedMessage); } } @@ -176,6 +248,14 @@ public void createProvider_missingRoot_expectException() throws IOException { + " \"ca_certificate_file\": \"/var/run/gke-spiffe/certs/ca_certificates.pem\"" + " }"; + private static final String MINIMAL_FILE_WATCHER_WITH_SPIFFE_CONFIG = + "{\n" + + " \"certificate_file\": \"/var/run/gke-spiffe/certs/certificates.pem\"," + + " \"private_key_file\": \"/var/run/gke-spiffe/certs/private_key.pem\"," + + " \"spiffe_trust_bundle_map_file\":" + + " \"/var/run/gke-spiffe/certs/spiffe_bundle.json\"" + + " }"; + private static final String FULL_FILE_WATCHER_CONFIG = "{\n" + " \"certificate_file\": \"/var/run/gke-spiffe/certs/certificates2.pem\"," @@ -184,6 +264,16 @@ public void createProvider_missingRoot_expectException() throws IOException { + " \"refresh_interval\": \"7890s\"" + " }"; + private static final String FULL_FILE_WATCHER_WITH_SPIFFE_CONFIG = + "{\n" + + " \"certificate_file\": \"/var/run/gke-spiffe/certs/certificates2.pem\"," + + " \"private_key_file\": \"/var/run/gke-spiffe/certs/private_key3.pem\"," + + " \"ca_certificate_file\": \"/var/run/gke-spiffe/certs/ca_certificates4.pem\"," + + " \"spiffe_trust_bundle_map_file\":" + + " \"/var/run/gke-spiffe/certs/spiffe_bundle.json\"," + + " \"refresh_interval\": \"7890s\"" + + " }"; + private static final String MISSING_CERT_CONFIG = "{\n" + " \"private_key_file\": \"/var/run/gke-spiffe/certs/private_key.pem\"," @@ -196,7 +286,7 @@ public void createProvider_missingRoot_expectException() throws IOException { + " \"ca_certificate_file\": \"/var/run/gke-spiffe/certs/ca_certificates.pem\"" + " }"; - private static final String MISSING_ROOT_CONFIG = + private static final String MISSING_ROOT_AND_SPIFFE_CONFIG = "{\n" + " \"certificate_file\": \"/var/run/gke-spiffe/certs/certificates.pem\"," + " \"private_key_file\": \"/var/run/gke-spiffe/certs/private_key.pem\"" diff --git a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderTest.java index 210ec056732..620ee0a7ff7 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderTest.java @@ -23,6 +23,7 @@ import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SERVER_0_KEY_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SERVER_0_PEM_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SERVER_1_PEM_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SPIFFE_TRUST_MAP_1_FILE; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -47,6 +48,7 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.Delayed; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -73,6 +75,7 @@ public class FileWatcherCertificateProviderTest { private static final String CERT_FILE = "cert.pem"; private static final String KEY_FILE = "key.pem"; private static final String ROOT_FILE = "root.pem"; + private static final String SPIFFE_TRUST_MAP_FILE = "spiffebundle.json"; @Mock private CertificateProvider.Watcher mockWatcher; @Mock private ScheduledExecutorService timeService; @@ -84,28 +87,33 @@ public class FileWatcherCertificateProviderTest { private String certFile; private String keyFile; private String rootFile; + private String spiffeTrustMapFile; private FileWatcherCertificateProvider provider; + private DistributorWatcher watcher; @Before public void setUp() throws IOException { - DistributorWatcher watcher = new DistributorWatcher(); + watcher = new DistributorWatcher(); watcher.addWatcher(mockWatcher); certFile = new File(tempFolder.getRoot(), CERT_FILE).getAbsolutePath(); keyFile = new File(tempFolder.getRoot(), KEY_FILE).getAbsolutePath(); rootFile = new File(tempFolder.getRoot(), ROOT_FILE).getAbsolutePath(); + spiffeTrustMapFile = new File(tempFolder.getRoot(), SPIFFE_TRUST_MAP_FILE).getAbsolutePath(); provider = - new FileWatcherCertificateProvider( - watcher, true, certFile, keyFile, rootFile, 600L, timeService, timeProvider); + new FileWatcherCertificateProvider(watcher, true, certFile, keyFile, rootFile, null, 600L, + timeService, timeProvider); } private void populateTarget( String certFileSource, String keyFileSource, String rootFileSource, + String spiffeTrustMapFileSource, boolean deleteCurCert, boolean deleteCurKey, + boolean deleteCurSpiffeTrustMap, boolean deleteCurRoot) throws IOException { if (deleteCurCert) { @@ -135,6 +143,17 @@ private void populateTarget( Files.setLastModifiedTime( Paths.get(rootFile), FileTime.fromMillis(timeProvider.currentTimeMillis())); } + if (deleteCurSpiffeTrustMap) { + Files.delete(Paths.get(spiffeTrustMapFile)); + } + if (spiffeTrustMapFileSource != null) { + spiffeTrustMapFileSource = CommonTlsContextTestsUtil + .getTempFileNameForResourcesFile(spiffeTrustMapFileSource); + Files.copy(Paths.get(spiffeTrustMapFileSource), + Paths.get(spiffeTrustMapFile), REPLACE_EXISTING); + Files.setLastModifiedTime( + Paths.get(spiffeTrustMapFile), FileTime.fromMillis(timeProvider.currentTimeMillis())); + } } @Test @@ -144,9 +163,9 @@ public void getCertificateAndCheckUpdates() throws IOException, CertificateExcep doReturn(scheduledFuture) .when(timeService) .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); - populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE, false, false, false); + populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE, null, false, false, false, false); provider.checkAndReloadCertificates(); - verifyWatcherUpdates(CLIENT_PEM_FILE, CA_PEM_FILE); + verifyWatcherUpdates(CLIENT_PEM_FILE, CA_PEM_FILE, null); verifyTimeServiceAndScheduledFuture(); reset(mockWatcher, timeService); @@ -165,7 +184,7 @@ public void allUpdateSecondTime() throws IOException, CertificateException, Inte doReturn(scheduledFuture) .when(timeService) .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); - populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE, false, false, false); + populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE, null, false, false, false, false); provider.checkAndReloadCertificates(); reset(mockWatcher, timeService); @@ -173,9 +192,10 @@ public void allUpdateSecondTime() throws IOException, CertificateException, Inte .when(timeService) .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); timeProvider.forwardTime(1, TimeUnit.SECONDS); - populateTarget(SERVER_0_PEM_FILE, SERVER_0_KEY_FILE, SERVER_1_PEM_FILE, false, false, false); + populateTarget(SERVER_0_PEM_FILE, SERVER_0_KEY_FILE, SERVER_1_PEM_FILE, null, false, false, + false, false); provider.checkAndReloadCertificates(); - verifyWatcherUpdates(SERVER_0_PEM_FILE, SERVER_1_PEM_FILE); + verifyWatcherUpdates(SERVER_0_PEM_FILE, SERVER_1_PEM_FILE, null); verifyTimeServiceAndScheduledFuture(); } @@ -186,12 +206,13 @@ public void closeDoesNotScheduleNext() throws IOException, CertificateException doReturn(scheduledFuture) .when(timeService) .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); - populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE, false, false, false); + populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE, null, false, false, false, false); provider.close(); provider.checkAndReloadCertificates(); verify(mockWatcher, never()) .updateCertificate(any(PrivateKey.class), ArgumentMatchers.anyList()); verify(mockWatcher, never()).updateTrustedRoots(ArgumentMatchers.anyList()); + verify(mockWatcher, never()).updateSpiffeTrustMap(ArgumentMatchers.anyMap()); verify(timeService, never()).schedule(any(Runnable.class), any(Long.TYPE), any(TimeUnit.class)); verify(timeService, times(1)).shutdownNow(); } @@ -204,7 +225,7 @@ public void rootFileUpdateOnly() throws IOException, CertificateException, Inter doReturn(scheduledFuture) .when(timeService) .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); - populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE, false, false, false); + populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE, null, false, false, false, false); provider.checkAndReloadCertificates(); reset(mockWatcher, timeService); @@ -212,9 +233,9 @@ public void rootFileUpdateOnly() throws IOException, CertificateException, Inter .when(timeService) .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); timeProvider.forwardTime(1, TimeUnit.SECONDS); - populateTarget(null, null, SERVER_1_PEM_FILE, false, false, false); + populateTarget(null, null, SERVER_1_PEM_FILE, null, false, false, false, false); provider.checkAndReloadCertificates(); - verifyWatcherUpdates(null, SERVER_1_PEM_FILE); + verifyWatcherUpdates(null, SERVER_1_PEM_FILE, null); verifyTimeServiceAndScheduledFuture(); } @@ -226,7 +247,7 @@ public void certAndKeyFileUpdateOnly() doReturn(scheduledFuture) .when(timeService) .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); - populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE, false, false, false); + populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE, null, false, false, false, false); provider.checkAndReloadCertificates(); reset(mockWatcher, timeService); @@ -234,9 +255,44 @@ public void certAndKeyFileUpdateOnly() .when(timeService) .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); timeProvider.forwardTime(1, TimeUnit.SECONDS); - populateTarget(SERVER_0_PEM_FILE, SERVER_0_KEY_FILE, null, false, false, false); + populateTarget(SERVER_0_PEM_FILE, SERVER_0_KEY_FILE, null, null, false, false, false, false); provider.checkAndReloadCertificates(); - verifyWatcherUpdates(SERVER_0_PEM_FILE, null); + verifyWatcherUpdates(SERVER_0_PEM_FILE, null, null); + verifyTimeServiceAndScheduledFuture(); + } + + @Test + public void spiffeTrustMapFileUpdateOnly() throws Exception { + provider = new FileWatcherCertificateProvider(watcher, true, certFile, keyFile, null, + spiffeTrustMapFile, 600L, timeService, timeProvider); + TestScheduledFuture scheduledFuture = + new TestScheduledFuture<>(); + doReturn(scheduledFuture) + .when(timeService) + .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); + populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, null, null, false, false, false, false); + provider.checkAndReloadCertificates(); + verify(mockWatcher, never()).updateSpiffeTrustMap(ArgumentMatchers.anyMap()); + + reset(timeService); + doReturn(scheduledFuture) + .when(timeService) + .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); + timeProvider.forwardTime(1, TimeUnit.SECONDS); + populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, null, SPIFFE_TRUST_MAP_FILE, false, + false, false, false); + provider.checkAndReloadCertificates(); + verify(mockWatcher, times(1)).updateSpiffeTrustMap(ArgumentMatchers.anyMap()); + + reset(timeService); + doReturn(scheduledFuture) + .when(timeService) + .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); + timeProvider.forwardTime(1, TimeUnit.SECONDS); + populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, null, SPIFFE_TRUST_MAP_1_FILE, false, + false, false, false); + provider.checkAndReloadCertificates(); + verify(mockWatcher, times(2)).updateSpiffeTrustMap(ArgumentMatchers.anyMap()); verifyTimeServiceAndScheduledFuture(); } @@ -247,7 +303,7 @@ public void getCertificate_initialMissingCertFile() throws IOException { doReturn(scheduledFuture) .when(timeService) .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); - populateTarget(null, CLIENT_KEY_FILE, CA_PEM_FILE, false, false, false); + populateTarget(null, CLIENT_KEY_FILE, CA_PEM_FILE, null, false, false, false, false); provider.checkAndReloadCertificates(); verifyWatcherErrorUpdates(Status.Code.UNKNOWN, NoSuchFileException.class, 0, 1, "cert.pem"); } @@ -255,13 +311,14 @@ public void getCertificate_initialMissingCertFile() throws IOException { @Test public void getCertificate_missingCertFile() throws IOException, InterruptedException { commonErrorTest( - null, CLIENT_KEY_FILE, CA_PEM_FILE, NoSuchFileException.class, 0, 1, 0, 0, "cert.pem"); + null, CLIENT_KEY_FILE, CA_PEM_FILE, null, NoSuchFileException.class, 0, 1, 0, 0, + "cert.pem"); } @Test public void getCertificate_missingKeyFile() throws IOException, InterruptedException { commonErrorTest( - CLIENT_PEM_FILE, null, CA_PEM_FILE, NoSuchFileException.class, 0, 1, 0, 0, "key.pem"); + CLIENT_PEM_FILE, null, CA_PEM_FILE, null, NoSuchFileException.class, 0, 1, 0, 0, "key.pem"); } @Test @@ -270,6 +327,7 @@ public void getCertificate_badKeyFile() throws IOException, InterruptedException CLIENT_PEM_FILE, SERVER_0_PEM_FILE, CA_PEM_FILE, + null, java.security.spec.InvalidKeySpecException.class, 0, 1, @@ -285,12 +343,13 @@ public void getCertificate_missingRootFile() throws IOException, InterruptedExce doReturn(scheduledFuture) .when(timeService) .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); - populateTarget(SERVER_0_PEM_FILE, SERVER_0_KEY_FILE, SERVER_1_PEM_FILE, false, false, false); + populateTarget(SERVER_0_PEM_FILE, SERVER_0_KEY_FILE, SERVER_1_PEM_FILE, null, false, false, + false, false); provider.checkAndReloadCertificates(); reset(mockWatcher); timeProvider.forwardTime(1, TimeUnit.SECONDS); - populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, null, false, false, true); + populateTarget(CLIENT_PEM_FILE, CLIENT_KEY_FILE, null, null, false, false, false, true); timeProvider.forwardTime( CERT0_EXPIRY_TIME_MILLIS - 610_000L - timeProvider.currentTimeMillis(), TimeUnit.MILLISECONDS); @@ -302,6 +361,7 @@ private void commonErrorTest( String certFile, String keyFile, String rootFile, + String spiffeFile, Class throwableType, int firstUpdateCertCount, int firstUpdateRootCount, @@ -314,13 +374,15 @@ private void commonErrorTest( doReturn(scheduledFuture) .when(timeService) .schedule(any(Runnable.class), any(Long.TYPE), eq(TimeUnit.SECONDS)); - populateTarget(SERVER_0_PEM_FILE, SERVER_0_KEY_FILE, SERVER_1_PEM_FILE, false, false, false); + populateTarget(SERVER_0_PEM_FILE, SERVER_0_KEY_FILE, SERVER_1_PEM_FILE, + SPIFFE_TRUST_MAP_1_FILE, false, false, false, false); provider.checkAndReloadCertificates(); reset(mockWatcher); timeProvider.forwardTime(1, TimeUnit.SECONDS); populateTarget( - certFile, keyFile, rootFile, certFile == null, keyFile == null, rootFile == null); + certFile, keyFile, rootFile, spiffeFile, certFile == null, keyFile == null, + rootFile == null, spiffeFile == null); timeProvider.forwardTime( CERT0_EXPIRY_TIME_MILLIS - 610_000L - timeProvider.currentTimeMillis(), TimeUnit.MILLISECONDS); @@ -372,7 +434,7 @@ private void verifyTimeServiceAndScheduledFuture() { assertThat(provider.scheduledFuture.isCancelled()).isFalse(); } - private void verifyWatcherUpdates(String certPemFile, String rootPemFile) + private void verifyWatcherUpdates(String certPemFile, String rootPemFile, String spiffeFile) throws IOException, CertificateException { if (certPemFile != null) { @SuppressWarnings("unchecked") @@ -399,6 +461,17 @@ private void verifyWatcherUpdates(String certPemFile, String rootPemFile) } else { verify(mockWatcher, never()).updateTrustedRoots(ArgumentMatchers.anyList()); } + if (spiffeFile != null) { + @SuppressWarnings("unchecked") + ArgumentCaptor>> spiffeCaptor = + ArgumentCaptor.forClass(Map.class); + verify(mockWatcher, times(1)).updateSpiffeTrustMap(spiffeCaptor.capture()); + Map> trustMap = spiffeCaptor.getValue(); + assertThat(trustMap).hasSize(2); + verify(mockWatcher, never()).onError(any(Status.class)); + } else { + verify(mockWatcher, never()).updateSpiffeTrustMap(ArgumentMatchers.anyMap()); + } } static class TestScheduledFuture implements ScheduledFuture { diff --git a/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactoryTest.java index 77749814cf2..db83961cfc3 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactoryTest.java @@ -23,6 +23,8 @@ import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CLIENT_PEM_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SERVER_1_PEM_FILE; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.protobuf.ByteString; import io.envoyproxy.envoy.config.core.v3.DataSource; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; @@ -105,6 +107,46 @@ public void constructor_fromRootCert() .isEqualTo(CertificateUtils.toX509Certificates(TlsTesting.loadCert(CA_PEM_FILE))[0]); } + @Test + public void constructor_fromSpiffeTrustMap() + throws CertificateException, IOException, CertStoreException { + X509Certificate x509Cert = TestUtils.loadX509Cert(CA_PEM_FILE); + CertificateValidationContext staticValidationContext = buildStaticValidationContext("san1", + "san2"); + // Single domain and single cert + XdsTrustManagerFactory factory = new XdsTrustManagerFactory(ImmutableMap + .of("example.com", ImmutableList.of(x509Cert)), staticValidationContext); + assertThat(factory).isNotNull(); + TrustManager[] tms = factory.getTrustManagers(); + assertThat(tms).isNotNull(); + assertThat(tms).hasLength(1); + TrustManager myTm = tms[0]; + assertThat(myTm).isInstanceOf(XdsX509TrustManager.class); + XdsX509TrustManager xdsX509TrustManager = (XdsX509TrustManager) myTm; + assertThat(xdsX509TrustManager.getAcceptedIssuers()).isNotNull(); + assertThat(xdsX509TrustManager.getAcceptedIssuers()).hasLength(1); + assertThat(xdsX509TrustManager.getAcceptedIssuers()[0].getIssuerX500Principal().getName()) + .isEqualTo("CN=testca,O=Internet Widgits Pty Ltd,ST=Some-State,C=AU"); + // Multiple domains and multiple certs for one of it + X509Certificate anotherCert = TestUtils.loadX509Cert(CLIENT_PEM_FILE); + factory = new XdsTrustManagerFactory(ImmutableMap + .of("example.com", ImmutableList.of(x509Cert), + "google.com", ImmutableList.of(x509Cert, anotherCert)), staticValidationContext); + assertThat(factory).isNotNull(); + tms = factory.getTrustManagers(); + assertThat(tms).isNotNull(); + assertThat(tms).hasLength(1); + myTm = tms[0]; + assertThat(myTm).isInstanceOf(XdsX509TrustManager.class); + xdsX509TrustManager = (XdsX509TrustManager) myTm; + assertThat(xdsX509TrustManager.getAcceptedIssuers()).isNotNull(); + assertThat(xdsX509TrustManager.getAcceptedIssuers()).hasLength(2); + assertThat(xdsX509TrustManager.getAcceptedIssuers()[0].getIssuerX500Principal().getName()) + .isEqualTo("CN=testca,O=Internet Widgits Pty Ltd,ST=Some-State,C=AU"); + assertThat(xdsX509TrustManager.getAcceptedIssuers()[1].getIssuerX500Principal().getName()) + .isEqualTo("CN=testca,O=Internet Widgits Pty Ltd,ST=Some-State,C=AU"); + } + @Test public void constructorRootCert_checkServerTrusted() throws CertificateException, IOException, CertStoreException { diff --git a/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsX509TrustManagerTest.java b/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsX509TrustManagerTest.java index 9ceb6f706fe..6fa3d2e7d24 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsX509TrustManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsX509TrustManagerTest.java @@ -20,7 +20,9 @@ import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.BAD_SERVER_PEM_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CA_PEM_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CLIENT_PEM_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CLIENT_SPIFFE_PEM_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SERVER_1_PEM_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SERVER_1_SPIFFE_PEM_FILE; import static org.junit.Assert.fail; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.doReturn; @@ -30,6 +32,7 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; @@ -38,6 +41,7 @@ import java.security.cert.CertStoreException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.Collections; import java.util.List; import javax.net.ssl.SSLEngine; @@ -537,6 +541,71 @@ public void checkServerTrustedSslEngine() assertThat(sslEngine.getSSLParameters().getEndpointIdentificationAlgorithm()).isEmpty(); } + @Test + public void checkServerTrustedSslEngineSpiffeTrustMap() + throws CertificateException, IOException, CertStoreException { + TestSslEngine sslEngine = buildTrustManagerAndGetSslEngine(); + X509Certificate[] serverCerts = + CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_SPIFFE_PEM_FILE)); + List caCerts = Arrays.asList(CertificateUtils + .toX509Certificates(TlsTesting.loadCert(CA_PEM_FILE))); + trustManager = XdsTrustManagerFactory.createX509TrustManager( + ImmutableMap.of("example.com", caCerts), null); + trustManager.checkServerTrusted(serverCerts, "ECDHE_ECDSA", sslEngine); + verify(sslEngine, times(1)).getHandshakeSession(); + assertThat(sslEngine.getSSLParameters().getEndpointIdentificationAlgorithm()).isEmpty(); + } + + @Test + public void checkServerTrustedSslEngineSpiffeTrustMap_missing_spiffe_id() + throws CertificateException, IOException, CertStoreException { + TestSslEngine sslEngine = buildTrustManagerAndGetSslEngine(); + X509Certificate[] serverCerts = + CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); + List caCerts = Arrays.asList(CertificateUtils + .toX509Certificates(TlsTesting.loadCert(CA_PEM_FILE))); + trustManager = XdsTrustManagerFactory.createX509TrustManager( + ImmutableMap.of("example.com", caCerts), null); + try { + trustManager.checkServerTrusted(serverCerts, "ECDHE_ECDSA", sslEngine); + fail("exception expected"); + } catch (CertificateException expected) { + assertThat(expected).hasMessageThat() + .isEqualTo("Failed to extract SPIFFE ID from peer leaf certificate"); + } + } + + @Test + public void checkServerTrustedSpiffeSslEngineTrustMap_missing_trust_domain() + throws CertificateException, IOException, CertStoreException { + TestSslEngine sslEngine = buildTrustManagerAndGetSslEngine(); + X509Certificate[] serverCerts = + CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_SPIFFE_PEM_FILE)); + List caCerts = Arrays.asList(CertificateUtils + .toX509Certificates(TlsTesting.loadCert(CA_PEM_FILE))); + trustManager = XdsTrustManagerFactory.createX509TrustManager( + ImmutableMap.of("unknown.com", caCerts), null); + try { + trustManager.checkServerTrusted(serverCerts, "ECDHE_ECDSA", sslEngine); + fail("exception expected"); + } catch (CertificateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Spiffe Trust Map doesn't contain trust" + + " domain 'example.com' from peer leaf certificate"); + } + } + + @Test + public void checkClientTrustedSpiffeTrustMap() + throws CertificateException, IOException, CertStoreException { + X509Certificate[] clientCerts = + CertificateUtils.toX509Certificates(TlsTesting.loadCert(CLIENT_SPIFFE_PEM_FILE)); + List caCerts = Arrays.asList(CertificateUtils + .toX509Certificates(TlsTesting.loadCert(CA_PEM_FILE))); + trustManager = XdsTrustManagerFactory.createX509TrustManager( + ImmutableMap.of("foo.bar.com", caCerts), null); + trustManager.checkClientTrusted(clientCerts, "RSA"); + } + @Test public void checkServerTrustedSslEngine_untrustedServer_expectException() throws CertificateException, IOException, CertStoreException { @@ -565,6 +634,22 @@ public void checkServerTrustedSslSocket() assertThat(sslSocket.getSSLParameters().getEndpointIdentificationAlgorithm()).isEmpty(); } + @Test + public void checkServerTrustedSslSocketSpiffeTrustMap() + throws CertificateException, IOException, CertStoreException { + TestSslSocket sslSocket = buildTrustManagerAndGetSslSocket(); + X509Certificate[] serverCerts = + CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_SPIFFE_PEM_FILE)); + List caCerts = Arrays.asList(CertificateUtils + .toX509Certificates(TlsTesting.loadCert(CA_PEM_FILE))); + trustManager = XdsTrustManagerFactory.createX509TrustManager( + ImmutableMap.of("example.com", caCerts), null); + trustManager.checkServerTrusted(serverCerts, "ECDHE_ECDSA", sslSocket); + verify(sslSocket, times(1)).isConnected(); + verify(sslSocket, times(1)).getHandshakeSession(); + assertThat(sslSocket.getSSLParameters().getEndpointIdentificationAlgorithm()).isEmpty(); + } + @Test public void checkServerTrustedSslSocket_untrustedServer_expectException() throws CertificateException, IOException, CertStoreException { From 5081e60626d952c4ed60ecf57a85668b7b59064e Mon Sep 17 00:00:00 2001 From: Kannan J Date: Fri, 8 Nov 2024 13:17:24 +0530 Subject: [PATCH 080/591] xds: Replace null check with has value check because proto fields can never be null. (#11675) --- xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java index a14dbe801e7..edd8a09e4ca 100644 --- a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java +++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java @@ -72,8 +72,8 @@ public ConfigOrError parseFilterConfig(Message rawProtoM long cacheSize = 10; // Validate cache_config - TokenCacheConfig cacheConfig = gcpAuthnProto.getCacheConfig(); - if (cacheConfig != null) { + if (gcpAuthnProto.hasCacheConfig()) { + TokenCacheConfig cacheConfig = gcpAuthnProto.getCacheConfig(); cacheSize = cacheConfig.getCacheSize().getValue(); if (cacheSize == 0) { return ConfigOrError.fromError( From 546efd79f1cf48aac7e5b6c9426d5fbfd7958fec Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:25:49 -0800 Subject: [PATCH 081/591] s2a: fix flake in FakeS2AServerTest (#11673) While here: * add an awaitTermination to after calling shutdown on server * don't use port picker Fixes #11648 --- .../handshaker/FakeS2AServerTest.java | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java index e61f8eea1a1..e1ee878b9cc 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java @@ -20,13 +20,13 @@ import static java.util.concurrent.TimeUnit.SECONDS; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.SettableFuture; import com.google.protobuf.ByteString; import io.grpc.Grpc; import io.grpc.InsecureChannelCredentials; import io.grpc.ManagedChannel; import io.grpc.Server; import io.grpc.ServerBuilder; -import io.grpc.benchmarks.Utils; import io.grpc.s2a.internal.handshaker.ValidatePeerCertificateChainReq.VerificationMode; import io.grpc.stub.StreamObserver; import java.io.IOException; @@ -49,51 +49,52 @@ public final class FakeS2AServerTest { private static final ImmutableList FAKE_CERT_DER_CHAIN = ImmutableList.of(ByteString.copyFrom("fake-der-chain".getBytes(StandardCharsets.US_ASCII))); - private int port; private String serverAddress; - private SessionResp response = null; private Server fakeS2AServer; @Before public void setUp() throws Exception { - port = Utils.pickUnusedPort(); - fakeS2AServer = ServerBuilder.forPort(port).addService(new FakeS2AServer()).build(); + fakeS2AServer = ServerBuilder.forPort(0).addService(new FakeS2AServer()).build(); fakeS2AServer.start(); - serverAddress = String.format("localhost:%d", port); + serverAddress = String.format("localhost:%d", fakeS2AServer.getPort()); } @After - public void tearDown() { + public void tearDown() throws Exception { fakeS2AServer.shutdown(); + fakeS2AServer.awaitTermination(10, SECONDS); } @Test public void callS2AServerOnce_getTlsConfiguration_returnsValidResult() - throws InterruptedException, IOException { + throws InterruptedException, IOException, java.util.concurrent.ExecutionException { ExecutorService executor = Executors.newSingleThreadExecutor(); logger.info("Client connecting to: " + serverAddress); ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, InsecureChannelCredentials.create()) .executor(executor) .build(); - + SettableFuture respFuture = SettableFuture.create(); try { S2AServiceGrpc.S2AServiceStub asyncStub = S2AServiceGrpc.newStub(channel); StreamObserver requestObserver = asyncStub.setUpSession( new StreamObserver() { + SessionResp recvResp; @Override public void onNext(SessionResp resp) { - response = resp; + recvResp = resp; } @Override public void onError(Throwable t) { - throw new RuntimeException(t); + respFuture.setException(t); } @Override - public void onCompleted() {} + public void onCompleted() { + respFuture.set(recvResp); + } }); try { requestObserver.onNext( @@ -138,36 +139,39 @@ public void onCompleted() {} .addCiphersuites( Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256))) .build(); - assertThat(response).ignoringRepeatedFieldOrder().isEqualTo(expected); + assertThat(respFuture.get()).ignoringRepeatedFieldOrder().isEqualTo(expected); } @Test public void callS2AServerOnce_validatePeerCertifiate_returnsValidResult() - throws InterruptedException { + throws InterruptedException, java.util.concurrent.ExecutionException { ExecutorService executor = Executors.newSingleThreadExecutor(); logger.info("Client connecting to: " + serverAddress); ManagedChannel channel = Grpc.newChannelBuilder(serverAddress, InsecureChannelCredentials.create()) .executor(executor) .build(); - + SettableFuture respFuture = SettableFuture.create(); try { S2AServiceGrpc.S2AServiceStub asyncStub = S2AServiceGrpc.newStub(channel); StreamObserver requestObserver = asyncStub.setUpSession( new StreamObserver() { + private SessionResp recvResp; @Override public void onNext(SessionResp resp) { - response = resp; + recvResp = resp; } @Override public void onError(Throwable t) { - throw new RuntimeException(t); + respFuture.setException(t); } @Override - public void onCompleted() {} + public void onCompleted() { + respFuture.set(recvResp); + } }); try { requestObserver.onNext( @@ -200,7 +204,7 @@ public void onCompleted() {} ValidatePeerCertificateChainResp.newBuilder() .setValidationResult(ValidatePeerCertificateChainResp.ValidationResult.SUCCESS)) .build(); - assertThat(response).ignoringRepeatedFieldOrder().isEqualTo(expected); + assertThat(respFuture.get()).ignoringRepeatedFieldOrder().isEqualTo(expected); } @Test From 8237ae270adf6c9e0a56156e40cf3ae1357c6f2c Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sun, 10 Nov 2024 13:01:12 -0800 Subject: [PATCH 082/591] util: Remove EAG conveniences from MultiChildLb This is a step toward removing ResolvedAddresses from ChildLbState, which isn't actually used by MultiChildLb. Most usages of the EAG usages can be served more directly without peering into MultiChildLb's internals or even accessing ChildLbStates, which make the tests less sensitive to implementation changes. Some changes do leverage the new behavior of MultiChildLb where it preserves the order of the entries. This does fix an important bug in shutdown tests. The tests looped over the ChildLbStates after shutdown, but shutdown deleted all the children so it looped over an entry collection. Fixing that exposed that deliverSubchannelState() didn't function after shutdown, as the listener was removed from the map when the subchannel was shut down. Moving the listener onto the TestSubchannel allowed having access to the listener even after shutdown. A few places in LeastRequestLb lines were just deleted, but that's because an existing assertion already provided the same check but without digging into MultiChildLb. --- .../io/grpc/util/MultiChildLoadBalancer.java | 23 --- .../grpc/util/MultiChildLoadBalancerTest.java | 32 ++-- .../grpc/util/RoundRobinLoadBalancerTest.java | 53 ++----- .../java/io/grpc/util/AbstractTestHelper.java | 42 ++--- .../io/grpc/xds/LeastRequestLoadBalancer.java | 9 -- .../xds/LeastRequestLoadBalancerTest.java | 66 +++----- .../io/grpc/xds/RingHashLoadBalancerTest.java | 7 +- .../WeightedRoundRobinLoadBalancerTest.java | 145 ++++++++---------- 8 files changed, 134 insertions(+), 243 deletions(-) diff --git a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java index 3e56f41d038..36a480f63c2 100644 --- a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java @@ -24,7 +24,6 @@ import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Objects; import com.google.common.collect.Maps; import io.grpc.Attributes; import io.grpc.ConnectivityState; @@ -237,21 +236,6 @@ public final Collection getChildLbStates() { return childLbStates; } - @VisibleForTesting - public final ChildLbState getChildLbState(Object key) { - for (ChildLbState state : childLbStates) { - if (Objects.equal(state.getKey(), key)) { - return state; - } - } - return null; - } - - @VisibleForTesting - public final ChildLbState getChildLbStateEag(EquivalentAddressGroup eag) { - return getChildLbState(new Endpoint(eag)); - } - /** * Filters out non-ready child load balancers (subchannels). */ @@ -337,13 +321,6 @@ protected final void setCurrentPicker(SubchannelPicker newPicker) { currentPicker = newPicker; } - public final EquivalentAddressGroup getEag() { - if (resolvedAddresses == null || resolvedAddresses.getAddresses().isEmpty()) { - return null; - } - return resolvedAddresses.getAddresses().get(0); - } - protected final void setResolvedAddresses(ResolvedAddresses newAddresses) { checkNotNull(newAddresses, "Missing address list for child"); resolvedAddresses = newAddresses; diff --git a/util/src/test/java/io/grpc/util/MultiChildLoadBalancerTest.java b/util/src/test/java/io/grpc/util/MultiChildLoadBalancerTest.java index 6bfd6d7a659..c128364dfd3 100644 --- a/util/src/test/java/io/grpc/util/MultiChildLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/MultiChildLoadBalancerTest.java @@ -52,7 +52,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -153,8 +152,7 @@ public void pickAfterResolvedUpdatedHosts() { LoadBalancer.Subchannel removedSubchannel = getSubchannel(removedEag); LoadBalancer.Subchannel oldSubchannel = getSubchannel(oldEag1); LoadBalancer.SubchannelStateListener removedListener = - testHelperInst.getSubchannelStateListeners() - .get(testHelperInst.getRealForMockSubChannel(removedSubchannel)); + testHelperInst.getSubchannelStateListener(removedSubchannel); inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); @@ -168,8 +166,6 @@ public void pickAfterResolvedUpdatedHosts() { verify(removedSubchannel, times(1)).requestConnection(); verify(oldSubchannel, times(1)).requestConnection(); - assertThat(getChildEags(loadBalancer)).containsExactly(removedEag, oldEag1); - // This time with Attributes List latestServers = Lists.newArrayList(oldEag2, newEag); @@ -186,10 +182,10 @@ public void pickAfterResolvedUpdatedHosts() { removedListener.onSubchannelState(ConnectivityStateInfo.forNonError(SHUTDOWN)); deliverSubchannelState(newSubchannel, ConnectivityStateInfo.forNonError(READY)); - assertThat(getChildEags(loadBalancer)).containsExactly(oldEag2, newEag); - verify(mockHelper, times(3)).createSubchannel(any(LoadBalancer.CreateSubchannelArgs.class)); inOrder.verify(mockHelper, times(2)).updateBalancingState(eq(READY), pickerCaptor.capture()); + picker = pickerCaptor.getValue(); + assertThat(getList(picker)).containsExactly(oldSubchannel, newSubchannel); AbstractTestHelper.verifyNoMoreMeaningfulInteractions(mockHelper); } @@ -328,12 +324,6 @@ private LoadBalancer.Subchannel getSubchannel(EquivalentAddressGroup eag) { return null; } - private static List getChildEags(MultiChildLoadBalancer loadBalancer) { - return loadBalancer.getChildLbStates().stream() - .map(ChildLbState::getEag) - .collect(Collectors.toList()); - } - private void deliverSubchannelState(LoadBalancer.Subchannel subchannel, ConnectivityStateInfo newState) { testHelperInst.deliverSubchannelState(subchannel, newState); @@ -348,13 +338,16 @@ protected TestLb(Helper mockHelper) { protected void updateOverallBalancingState() { ConnectivityState overallState = null; final Map childPickers = new HashMap<>(); + final Map childConnStates = new HashMap<>(); for (ChildLbState childLbState : getChildLbStates()) { childPickers.put(childLbState.getKey(), childLbState.getCurrentPicker()); + childConnStates.put(childLbState.getKey(), childLbState.getCurrentState()); overallState = aggregateState(overallState, childLbState.getCurrentState()); } if (overallState != null) { - getHelper().updateBalancingState(overallState, new TestSubchannelPicker(childPickers)); + getHelper().updateBalancingState( + overallState, new TestSubchannelPicker(childPickers, childConnStates)); currentConnectivityState = overallState; } @@ -364,18 +357,17 @@ private class TestSubchannelPicker extends SubchannelPicker { Map childPickerMap; Map childStates = new HashMap<>(); - TestSubchannelPicker(Map childPickers) { - childPickerMap = childPickers; - for (Object key : childPickerMap.keySet()) { - childStates.put(key, getChildLbState(key).getCurrentState()); - } + TestSubchannelPicker( + Map childPickers, Map childStates) { + this.childPickerMap = childPickers; + this.childStates = childStates; } List getReadySubchannels() { List readySubchannels = new ArrayList<>(); for ( Map.Entry cur : childStates.entrySet()) { if (cur.getValue() == READY) { - Subchannel s = subchannels.get(Arrays.asList(getChildLbState(cur.getKey()).getEag())); + Subchannel s = childPickerMap.get(cur.getKey()).pickSubchannel(null).getSubchannel(); readySubchannels.add(s); } } diff --git a/util/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java b/util/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java index 743bbbef796..0201fb578e9 100644 --- a/util/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java @@ -61,7 +61,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -207,10 +206,6 @@ public void pickAfterResolvedUpdatedHosts() throws Exception { assertThat(childLbState.getResolvedAddresses().getAttributes().get(IS_PETIOLE_POLICY)) .isTrue(); } - assertThat(loadBalancer.getChildLbStateEag(removedEag).getCurrentPicker().pickSubchannel(null) - .getSubchannel()).isEqualTo(removedSubchannel); - assertThat(loadBalancer.getChildLbStateEag(oldEag1).getCurrentPicker().pickSubchannel(null) - .getSubchannel()).isEqualTo(oldSubchannel); // This time with Attributes List latestServers = Lists.newArrayList(oldEag2, newEag); @@ -225,12 +220,6 @@ public void pickAfterResolvedUpdatedHosts() throws Exception { deliverSubchannelState(newSubchannel, ConnectivityStateInfo.forNonError(READY)); - assertThat(loadBalancer.getChildLbStates().size()).isEqualTo(2); - assertThat(loadBalancer.getChildLbStateEag(newEag).getCurrentPicker() - .pickSubchannel(null).getSubchannel()).isEqualTo(newSubchannel); - assertThat(loadBalancer.getChildLbStateEag(oldEag2).getCurrentPicker() - .pickSubchannel(null).getSubchannel()).isEqualTo(oldSubchannel); - verify(mockHelper, times(6)).createSubchannel(any(CreateSubchannelArgs.class)); inOrder.verify(mockHelper, times(2)).updateBalancingState(eq(READY), pickerCaptor.capture()); @@ -243,35 +232,35 @@ public void pickAfterResolvedUpdatedHosts() throws Exception { @Test public void pickAfterStateChange() throws Exception { InOrder inOrder = inOrder(mockHelper); - Status addressesAcceptanceStatus = acceptAddresses(servers, Attributes.EMPTY); + Status addressesAcceptanceStatus = + acceptAddresses(Arrays.asList(servers.get(0)), Attributes.EMPTY); assertThat(addressesAcceptanceStatus.isOk()).isTrue(); + inOrder.verify(mockHelper).createSubchannel(any(CreateSubchannelArgs.class)); // TODO figure out if this method testing the right things - ChildLbState childLbState = loadBalancer.getChildLbStates().iterator().next(); - Subchannel subchannel = subchannels.get(Arrays.asList(childLbState.getEag())); + assertThat(subchannels).hasSize(1); + Subchannel subchannel = subchannels.values().iterator().next(); inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), eq(EMPTY_PICKER)); - assertThat(childLbState.getCurrentState()).isEqualTo(CONNECTING); deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY)); inOrder.verify(mockHelper).updateBalancingState(eq(READY), pickerCaptor.capture()); assertThat(pickerCaptor.getValue()).isInstanceOf(ReadyPicker.class); - assertThat(childLbState.getCurrentState()).isEqualTo(READY); Status error = Status.UNKNOWN.withDescription("¯\\_(ツ)_//¯"); deliverSubchannelState(subchannel, ConnectivityStateInfo.forTransientFailure(error)); - assertThat(childLbState.getCurrentState()).isEqualTo(TRANSIENT_FAILURE); - AbstractTestHelper.refreshInvokedAndUpdateBS(inOrder, CONNECTING, mockHelper, pickerCaptor); - assertThat(pickerCaptor.getValue()).isEqualTo(EMPTY_PICKER); + AbstractTestHelper.refreshInvokedAndUpdateBS( + inOrder, TRANSIENT_FAILURE, mockHelper, pickerCaptor); + assertThat(pickerCaptor.getValue().pickSubchannel(mockArgs).getStatus()).isEqualTo(error); deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(IDLE)); inOrder.verify(mockHelper).refreshNameResolution(); - assertThat(childLbState.getCurrentState()).isEqualTo(TRANSIENT_FAILURE); + inOrder.verify(mockHelper, never()) + .updateBalancingState(eq(TRANSIENT_FAILURE), any(SubchannelPicker.class)); verify(subchannel, atLeastOnce()).requestConnection(); - verify(mockHelper, times(3)).createSubchannel(any(CreateSubchannelArgs.class)); AbstractTestHelper.verifyNoMoreMeaningfulInteractions(mockHelper); } @@ -282,10 +271,10 @@ public void ignoreShutdownSubchannelStateChange() { assertThat(addressesAcceptanceStatus.isOk()).isTrue(); inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), eq(EMPTY_PICKER)); + List savedSubchannels = new ArrayList<>(subchannels.values()); loadBalancer.shutdown(); - for (ChildLbState child : loadBalancer.getChildLbStates()) { - Subchannel sc = child.getCurrentPicker().pickSubchannel(null).getSubchannel(); - verify(child).shutdown(); + for (Subchannel sc : savedSubchannels) { + verify(sc).shutdown(); // When the subchannel is being shut down, a SHUTDOWN connectivity state is delivered // back to the subchannel state listener. deliverSubchannelState(sc, ConnectivityStateInfo.forNonError(SHUTDOWN)); @@ -300,34 +289,27 @@ public void stayTransientFailureUntilReady() { Status addressesAcceptanceStatus = acceptAddresses(servers, Attributes.EMPTY); assertThat(addressesAcceptanceStatus.isOk()).isTrue(); + inOrder.verify(mockHelper, times(3)).createSubchannel(any(CreateSubchannelArgs.class)); inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), eq(EMPTY_PICKER)); - Map childToSubChannelMap = new HashMap<>(); // Simulate state transitions for each subchannel individually. - for ( ChildLbState child : loadBalancer.getChildLbStates()) { - Subchannel sc = subchannels.get(Arrays.asList(child.getEag())); - childToSubChannelMap.put(child, sc); + for (Subchannel sc : subchannels.values()) { Status error = Status.UNKNOWN.withDescription("connection broken"); deliverSubchannelState( sc, ConnectivityStateInfo.forTransientFailure(error)); - assertEquals(TRANSIENT_FAILURE, child.getCurrentState()); deliverSubchannelState( sc, ConnectivityStateInfo.forNonError(CONNECTING)); - assertEquals(TRANSIENT_FAILURE, child.getCurrentState()); } inOrder.verify(mockHelper).updateBalancingState(eq(TRANSIENT_FAILURE), isA(ReadyPicker.class)); inOrder.verify(mockHelper, atLeast(0)).refreshNameResolution(); inOrder.verifyNoMoreInteractions(); - ChildLbState child = loadBalancer.getChildLbStates().iterator().next(); - Subchannel subchannel = childToSubChannelMap.get(child); + Subchannel subchannel = subchannels.values().iterator().next(); deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY)); - assertThat(child.getCurrentState()).isEqualTo(READY); inOrder.verify(mockHelper).updateBalancingState(eq(READY), isA(ReadyPicker.class)); - verify(mockHelper, times(3)).createSubchannel(any(CreateSubchannelArgs.class)); inOrder.verify(mockHelper, atLeast(0)).refreshNameResolution(); inOrder.verifyNoMoreInteractions(); } @@ -342,8 +324,7 @@ public void refreshNameResolutionWhenSubchannelConnectionBroken() { inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), eq(EMPTY_PICKER)); // Simulate state transitions for each subchannel individually. - for (ChildLbState child : loadBalancer.getChildLbStates()) { - Subchannel sc = subchannels.get(Arrays.asList(child.getEag())); + for (Subchannel sc : subchannels.values()) { verify(sc).requestConnection(); deliverSubchannelState(sc, ConnectivityStateInfo.forNonError(CONNECTING)); Status error = Status.UNKNOWN.withDescription("connection broken"); diff --git a/util/src/testFixtures/java/io/grpc/util/AbstractTestHelper.java b/util/src/testFixtures/java/io/grpc/util/AbstractTestHelper.java index bbf2d5efe9f..837dc68c057 100644 --- a/util/src/testFixtures/java/io/grpc/util/AbstractTestHelper.java +++ b/util/src/testFixtures/java/io/grpc/util/AbstractTestHelper.java @@ -16,6 +16,7 @@ package io.grpc.util; +import static com.google.common.base.Preconditions.checkNotNull; import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; @@ -23,7 +24,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import com.google.common.collect.Maps; import io.grpc.Attributes; import io.grpc.Channel; import io.grpc.ChannelLogger; @@ -63,10 +63,8 @@ */ public abstract class AbstractTestHelper extends ForwardingLoadBalancerHelper { - private final Map mockToRealSubChannelMap = new HashMap<>(); + private final Map mockToRealSubChannelMap = new HashMap<>(); protected final Map realToMockSubChannelMap = new HashMap<>(); - private final Map subchannelStateListeners = - Maps.newLinkedHashMap(); private final FakeClock fakeClock; private final SynchronizationContext syncContext; @@ -87,22 +85,14 @@ public AbstractTestHelper(FakeClock fakeClock, SynchronizationContext syncContex this.syncContext = syncContext; } - public Map getMockToRealSubChannelMap() { - return mockToRealSubChannelMap; - } - - public Subchannel getRealForMockSubChannel(Subchannel mock) { - Subchannel realSc = getMockToRealSubChannelMap().get(mock); + private TestSubchannel getRealForMockSubChannel(Subchannel mock) { + TestSubchannel realSc = mockToRealSubChannelMap.get(mock); if (realSc == null) { - realSc = mock; + realSc = (TestSubchannel) mock; } return realSc; } - public Map getSubchannelStateListeners() { - return subchannelStateListeners; - } - public static final FakeClock.TaskFilter NOT_START_NEXT_CONNECTION = new FakeClock.TaskFilter() { @Override @@ -116,15 +106,15 @@ public static int getNumFilteredPendingTasks(FakeClock fakeClock) { } public void deliverSubchannelState(Subchannel subchannel, ConnectivityStateInfo newState) { - Subchannel realSc = getMockToRealSubChannelMap().get(subchannel); - if (realSc == null) { - realSc = subchannel; - } - SubchannelStateListener listener = getSubchannelStateListeners().get(realSc); + getSubchannelStateListener(subchannel).onSubchannelState(newState); + } + + public SubchannelStateListener getSubchannelStateListener(Subchannel subchannel) { + SubchannelStateListener listener = getRealForMockSubChannel(subchannel).listener; if (listener == null) { - throw new IllegalArgumentException("subchannel does not have a matching listener"); + throw new IllegalArgumentException("subchannel has not been started"); } - listener.onSubchannelState(newState); + return listener; } @Override @@ -144,7 +134,7 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { TestSubchannel delegate = createRealSubchannel(args); subchannel = mock(Subchannel.class, delegatesTo(delegate)); getSubchannelMap().put(args.getAddresses(), subchannel); - getMockToRealSubChannelMap().put(subchannel, delegate); + mockToRealSubChannelMap.put(subchannel, delegate); realToMockSubChannelMap.put(delegate, subchannel); } @@ -161,7 +151,7 @@ public void refreshNameResolution() { } public void setChannel(Subchannel subchannel, Channel channel) { - ((TestSubchannel)subchannel).channel = channel; + getRealForMockSubChannel(subchannel).channel = channel; } @Override @@ -208,6 +198,7 @@ public static void verifyNoMoreMeaningfulInteractions(Helper helper, InOrder inO protected class TestSubchannel extends ForwardingSubchannel { CreateSubchannelArgs args; + SubchannelStateListener listener; Channel channel; public TestSubchannel(CreateSubchannelArgs args) { @@ -250,12 +241,11 @@ public void updateAddresses(List addrs) { @Override public void start(SubchannelStateListener listener) { - getSubchannelStateListeners().put(this, listener); + this.listener = checkNotNull(listener, "listener"); } @Override public void shutdown() { - getSubchannelStateListeners().remove(this); for (EquivalentAddressGroup eag : getAllAddresses()) { getSubchannelMap().remove(Collections.singletonList(eag)); } diff --git a/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.java b/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.java index 6c13530ff49..dda2ad177e6 100644 --- a/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.java @@ -32,7 +32,6 @@ import io.grpc.ClientStreamTracer; import io.grpc.ClientStreamTracer.StreamInfo; import io.grpc.ConnectivityState; -import io.grpc.EquivalentAddressGroup; import io.grpc.LoadBalancer; import io.grpc.LoadBalancerProvider; import io.grpc.Metadata; @@ -155,7 +154,6 @@ private static AtomicInteger getInFlights(ChildLbState childLbState) { static final class ReadyPicker extends SubchannelPicker { private final List childPickers; // non-empty private final List childInFlights; // 1:1 with childPickers - private final List childEags; // 1:1 with childPickers private final int choiceCount; private final ThreadSafeRandom random; private final int hashCode; @@ -164,11 +162,9 @@ static final class ReadyPicker extends SubchannelPicker { checkArgument(!childLbStates.isEmpty(), "empty list"); this.childPickers = new ArrayList<>(childLbStates.size()); this.childInFlights = new ArrayList<>(childLbStates.size()); - this.childEags = new ArrayList<>(childLbStates.size()); for (ChildLbState state : childLbStates) { childPickers.add(state.getCurrentPicker()); childInFlights.add(getInFlights(state)); - childEags.add(state.getEag()); } this.choiceCount = choiceCount; this.random = checkNotNull(random, "random"); @@ -224,11 +220,6 @@ List getChildPickers() { return childPickers; } - @VisibleForTesting - List getChildEags() { - return childEags; - } - @Override public int hashCode() { return hashCode; diff --git a/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java index 64e18465597..6fb6507fa4e 100644 --- a/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java @@ -186,8 +186,7 @@ public void pickAfterResolvedUpdatedHosts() throws Exception { Subchannel removedSubchannel = getSubchannel(removedEag); Subchannel oldSubchannel = getSubchannel(oldEag1); SubchannelStateListener removedListener = - testHelperInstance.getSubchannelStateListeners() - .get(testHelperInstance.getRealForMockSubChannel(removedSubchannel)); + testHelperInstance.getSubchannelStateListener(removedSubchannel); inOrder.verify(helper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); @@ -201,8 +200,6 @@ public void pickAfterResolvedUpdatedHosts() throws Exception { verify(removedSubchannel, times(1)).requestConnection(); verify(oldSubchannel, times(1)).requestConnection(); - assertThat(getChildEags(loadBalancer)).containsExactly(removedEag, oldEag1); - // This time with Attributes List latestServers = Lists.newArrayList(oldEag2, newEag); @@ -219,8 +216,6 @@ public void pickAfterResolvedUpdatedHosts() throws Exception { removedListener.onSubchannelState(ConnectivityStateInfo.forNonError(SHUTDOWN)); deliverSubchannelState(newSubchannel, ConnectivityStateInfo.forNonError(READY)); - assertThat(getChildEags(loadBalancer)).containsExactly(oldEag2, newEag); - verify(helper, times(3)).createSubchannel(any(CreateSubchannelArgs.class)); inOrder.verify(helper, times(2)).updateBalancingState(eq(READY), pickerCaptor.capture()); @@ -233,28 +228,6 @@ private Subchannel getSubchannel(EquivalentAddressGroup removedEag) { return subchannels.get(Collections.singletonList(removedEag)); } - private Subchannel getSubchannel(ChildLbState childLbState) { - return subchannels.get(Collections.singletonList(childLbState.getEag())); - } - - private static List getChildEags(LeastRequestLoadBalancer loadBalancer) { - return loadBalancer.getChildLbStates().stream() - .map(ChildLbState::getEag) - // .map(EquivalentAddressGroup::getAddresses) - .collect(Collectors.toList()); - } - - private List getSubchannels(LeastRequestLoadBalancer lb) { - return lb.getChildLbStates().stream() - .map(this::getSubchannel) - .collect(Collectors.toList()); - } - - private LeastRequestLbState getChildLbState(PickResult pickResult) { - EquivalentAddressGroup eag = pickResult.getSubchannel().getAddresses(); - return (LeastRequestLbState) loadBalancer.getChildLbStateEag(eag); - } - @Test public void pickAfterStateChange() throws Exception { InOrder inOrder = inOrder(helper); @@ -263,7 +236,7 @@ public void pickAfterStateChange() throws Exception { .build()); assertThat(addressesAcceptanceStatus.isOk()).isTrue(); ChildLbState childLbState = loadBalancer.getChildLbStates().iterator().next(); - Subchannel subchannel = getSubchannel(childLbState); + Subchannel subchannel = getSubchannel(servers.get(0)); inOrder.verify(helper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); assertThat(childLbState.getCurrentState()).isEqualTo(CONNECTING); @@ -331,8 +304,9 @@ public void ignoreShutdownSubchannelStateChange() { assertThat(addressesAcceptanceStatus.isOk()).isTrue(); inOrder.verify(helper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); + List savedSubchannels = new ArrayList<>(subchannels.values()); loadBalancer.shutdown(); - for (Subchannel sc : getSubchannels(loadBalancer)) { + for (Subchannel sc : savedSubchannels) { verify(sc).shutdown(); // When the subchannel is being shut down, a SHUTDOWN connectivity state is delivered // back to the subchannel state listener. @@ -353,8 +327,10 @@ public void stayTransientFailureUntilReady() { inOrder.verify(helper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); // Simulate state transitions for each subchannel individually. - for (ChildLbState childLbState : loadBalancer.getChildLbStates()) { - Subchannel sc = getSubchannel(childLbState); + List children = new ArrayList<>(loadBalancer.getChildLbStates()); + for (int i = 0; i < children.size(); i++) { + ChildLbState childLbState = children.get(i); + Subchannel sc = getSubchannel(servers.get(i)); Status error = Status.UNKNOWN.withDescription("connection broken"); deliverSubchannelState(sc, ConnectivityStateInfo.forTransientFailure(error)); deliverSubchannelState(sc, ConnectivityStateInfo.forNonError(CONNECTING)); @@ -369,7 +345,7 @@ public void stayTransientFailureUntilReady() { inOrder.verifyNoMoreInteractions(); ChildLbState childLbState = loadBalancer.getChildLbStates().iterator().next(); - Subchannel subchannel = getSubchannel(childLbState); + Subchannel subchannel = getSubchannel(servers.get(0)); deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY)); assertThat(childLbState.getCurrentState()).isEqualTo(READY); inOrder.verify(helper).updateBalancingState(eq(READY), isA(ReadyPicker.class)); @@ -411,7 +387,7 @@ public void refreshNameResolutionWhenSubchannelConnectionBroken() { inOrder.verify(helper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); // Simulate state transitions for each subchannel individually. - for (Subchannel sc : getSubchannels(loadBalancer)) { + for (Subchannel sc : subchannels.values()) { verify(sc).requestConnection(); deliverSubchannelState(sc, ConnectivityStateInfo.forNonError(CONNECTING)); Status error = Status.UNKNOWN.withDescription("connection broken"); @@ -449,8 +425,8 @@ public void pickerLeastRequest() throws Exception { ((LeastRequestLbState) childLbStates.get(i)).getActiveRequests()); } - for (ChildLbState cs : childLbStates) { - deliverSubchannelState(getSubchannel(cs), ConnectivityStateInfo.forNonError(READY)); + for (Subchannel sc : subchannels.values()) { + deliverSubchannelState(sc, ConnectivityStateInfo.forNonError(READY)); } // Capture the active ReadyPicker once all subchannels are READY @@ -460,37 +436,37 @@ public void pickerLeastRequest() throws Exception { ReadyPicker picker = (ReadyPicker) pickerCaptor.getValue(); - assertThat(picker.getChildEags()) - .containsExactlyElementsIn(childLbStates.stream().map(ChildLbState::getEag).toArray()); + assertThat(picker.getChildPickers()).containsExactlyElementsIn( + childLbStates.stream().map(ChildLbState::getCurrentPicker).toArray()); // Make random return 0, then 2 for the sample indexes. when(mockRandom.nextInt(childLbStates.size())).thenReturn(0, 2); PickResult pickResult1 = picker.pickSubchannel(mockArgs); verify(mockRandom, times(choiceCount)).nextInt(childLbStates.size()); - assertEquals(childLbStates.get(0), getChildLbState(pickResult1)); + assertThat(pickResult1.getSubchannel()).isEqualTo(getSubchannel(servers.get(0))); // This simulates sending the actual RPC on the picked channel ClientStreamTracer streamTracer1 = pickResult1.getStreamTracerFactory() .newClientStreamTracer(StreamInfo.newBuilder().build(), new Metadata()); streamTracer1.streamCreated(Attributes.EMPTY, new Metadata()); - assertEquals(1, getChildLbState(pickResult1).getActiveRequests()); + assertEquals(1, ((LeastRequestLbState) childLbStates.get(0)).getActiveRequests()); // For the second pick it should pick the one with lower inFlight. when(mockRandom.nextInt(childLbStates.size())).thenReturn(0, 2); PickResult pickResult2 = picker.pickSubchannel(mockArgs); // Since this is the second pick we expect the total random samples to be choiceCount * 2 verify(mockRandom, times(choiceCount * 2)).nextInt(childLbStates.size()); - assertEquals(childLbStates.get(2), getChildLbState(pickResult2)); + assertThat(pickResult2.getSubchannel()).isEqualTo(getSubchannel(servers.get(2))); // For the third pick we unavoidably pick subchannel with index 1. when(mockRandom.nextInt(childLbStates.size())).thenReturn(1, 1); PickResult pickResult3 = picker.pickSubchannel(mockArgs); verify(mockRandom, times(choiceCount * 3)).nextInt(childLbStates.size()); - assertEquals(childLbStates.get(1), getChildLbState(pickResult3)); + assertThat(pickResult3.getSubchannel()).isEqualTo(getSubchannel(servers.get(1))); // Finally ensure a finished RPC decreases inFlight streamTracer1.streamClosed(Status.OK); - assertEquals(0, getChildLbState(pickResult1).getActiveRequests()); + assertEquals(0, ((LeastRequestLbState) childLbStates.get(0)).getActiveRequests()); } @Test @@ -648,8 +624,8 @@ public void emptyAddresses() { private List getList(SubchannelPicker picker) { if (picker instanceof ReadyPicker) { - return ((ReadyPicker) picker).getChildEags().stream() - .map(this::getSubchannel) + return ((ReadyPicker) picker).getChildPickers().stream() + .map((p) -> p.pickSubchannel(mockArgs).getSubchannel()) .collect(Collectors.toList()); } else { return Collections.emptyList(); diff --git a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java index dd7de3691a7..4afe440c90e 100644 --- a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java @@ -177,13 +177,12 @@ public void subchannelNotAutoReconnectAfterReenteringIdle() { assertThat(addressesAcceptanceStatus.isOk()).isTrue(); verify(helper).updateBalancingState(eq(IDLE), pickerCaptor.capture()); - ChildLbState childLbState = loadBalancer.getChildLbStates().iterator().next(); - assertThat(subchannels.get(Collections.singletonList(childLbState.getEag()))).isNull(); + assertThat(subchannels).isEmpty(); // Picking subchannel triggers connection. PickSubchannelArgs args = getDefaultPickSubchannelArgs(hashFunc.hashVoid()); pickerCaptor.getValue().pickSubchannel(args); - Subchannel subchannel = subchannels.get(Collections.singletonList(childLbState.getEag())); + Subchannel subchannel = subchannels.get(Collections.singletonList(servers.get(0))); InOrder inOrder = Mockito.inOrder(helper, subchannel); int expectedTimes = PickFirstLoadBalancerProvider.isEnabledHappyEyeballs() || !PickFirstLoadBalancerProvider.isEnabledNewPickFirst() ? 2 : 1; @@ -422,7 +421,7 @@ public void skipFailingHosts_pickNextNonFailingHost() { assertThat(addressesAcceptanceStatus.isOk()).isTrue(); // Create subchannel for the first address - loadBalancer.getChildLbStateEag(servers.get(0)).getCurrentPicker() + loadBalancer.getChildLbStates().iterator().next().getCurrentPicker() .pickSubchannel(getDefaultPickSubchannelArgs(hashFunc.hashVoid())); verifyConnection(1); diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 05ad1f56ece..9ab783223d9 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -40,6 +40,7 @@ import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; +import io.grpc.ClientStreamTracer; import io.grpc.ConnectivityState; import io.grpc.ConnectivityStateInfo; import io.grpc.DoubleHistogramMetricInstrument; @@ -68,6 +69,7 @@ import io.grpc.internal.PickFirstLoadBalancerProvider; import io.grpc.internal.TestUtils; import io.grpc.internal.testing.StreamRecorder; +import io.grpc.protobuf.ProtoUtils; import io.grpc.services.InternalCallMetricRecorder; import io.grpc.services.MetricReport; import io.grpc.stub.ClientCalls; @@ -130,9 +132,6 @@ public class WeightedRoundRobinLoadBalancerTest { private final List servers = Lists.newArrayList(); private final Map, Subchannel> subchannels = Maps.newLinkedHashMap(); - private final Map mockToRealSubChannelMap = new HashMap<>(); - private final Map subchannelStateListeners = - Maps.newLinkedHashMap(); private final Queue> oobCalls = new ConcurrentLinkedQueue<>(); @@ -188,7 +187,7 @@ public ClientCall answer( return clientCall; } }); - testHelperInstance.setChannel(mockToRealSubChannelMap.get(sc), channel); + testHelperInstance.setChannel(sc, channel); subchannels.put(Arrays.asList(eag), sc); } wrr = new WeightedRoundRobinLoadBalancer(helper, fakeClock.getDeadlineTicker(), @@ -257,7 +256,7 @@ weightedChild2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).on int expectedTasks = isEnabledHappyEyeballs() ? 2 : 1; assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(expectedTasks); - assertThat(getAddressesFromPick(weightedPicker)).isEqualTo(weightedChild1.getEag()); + assertThat(getAddressesFromPick(weightedPicker)).isEqualTo(servers.get(0)); assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); weightedConfig = WeightedRoundRobinLoadBalancerConfig.newBuilder() .setWeightUpdatePeriodNanos(500_000_000L) //.5s @@ -312,8 +311,7 @@ weightedChild2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).on int expectedTasks = isEnabledHappyEyeballs() ? 2 : 1; assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(expectedTasks); PickResult pickResult = weightedPicker.pickSubchannel(mockArgs); - assertThat(getAddresses(pickResult)) - .isEqualTo(weightedChild1.getEag()); + assertThat(getAddresses(pickResult)).isEqualTo(servers.get(0)); assertThat(pickResult.getStreamTracerFactory()).isNotNull(); // verify per-request listener assertThat(oobCalls.isEmpty()).isTrue(); @@ -327,8 +325,7 @@ weightedChild2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).on eq(ConnectivityState.READY), pickerCaptor2.capture()); weightedPicker = (WeightedRoundRobinPicker) pickerCaptor2.getAllValues().get(2); pickResult = weightedPicker.pickSubchannel(mockArgs); - assertThat(getAddresses(pickResult)) - .isEqualTo(weightedChild1.getEag()); + assertThat(getAddresses(pickResult)).isEqualTo(servers.get(0)); assertThat(pickResult.getStreamTracerFactory()).isNull(); OrcaLoadReportRequest golden = OrcaLoadReportRequest.newBuilder().setReportInterval( Duration.newBuilder().setSeconds(20).setNanos(30000000).build()).build(); @@ -375,16 +372,16 @@ private void pickByWeight(MetricReport r1, MetricReport r2, MetricReport r3, pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } assertThat(pickCount.size()).isEqualTo(3); - assertThat(Math.abs(pickCount.get(weightedChild1.getEag()) / 10000.0 - subchannel1PickRatio)) + assertThat(Math.abs(pickCount.get(servers.get(0)) / 10000.0 - subchannel1PickRatio)) .isAtMost(0.0002); - assertThat(Math.abs(pickCount.get(weightedChild2.getEag()) / 10000.0 - subchannel2PickRatio )) + assertThat(Math.abs(pickCount.get(servers.get(1)) / 10000.0 - subchannel2PickRatio )) .isAtMost(0.0002); - assertThat(Math.abs(pickCount.get(weightedChild3.getEag()) / 10000.0 - subchannel3PickRatio )) + assertThat(Math.abs(pickCount.get(servers.get(2)) / 10000.0 - subchannel3PickRatio )) .isAtMost(0.0002); } private SubchannelStateListener getSubchannelStateListener(Subchannel mockSubChannel) { - return subchannelStateListeners.get(mockToRealSubChannelMap.get(mockSubChannel)); + return testHelperInstance.getSubchannelStateListener(mockSubChannel); } private static ChildLbState getChild(WeightedRoundRobinPicker picker, int index) { @@ -578,8 +575,8 @@ weightedChild2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).on } assertThat(pickCount.size()).isEqualTo(2); // within blackout period, fallback to simple round robin - assertThat(Math.abs(pickCount.get(weightedChild1.getEag()) / 10000.0 - 0.5)).isLessThan(0.002); - assertThat(Math.abs(pickCount.get(weightedChild2.getEag()) / 10000.0 - 0.5)).isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(0)) / 10000.0 - 0.5)).isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(1)) / 10000.0 - 0.5)).isLessThan(0.002); assertThat(fakeClock.forwardTime(5, TimeUnit.SECONDS)).isEqualTo(1); pickCount = new HashMap<>(); @@ -589,10 +586,8 @@ weightedChild2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).on } assertThat(pickCount.size()).isEqualTo(2); // after blackout period - assertThat(Math.abs(pickCount.get(weightedChild1.getEag()) / 10000.0 - 2.0 / 3)) - .isLessThan(0.002); - assertThat(Math.abs(pickCount.get(weightedChild2.getEag()) / 10000.0 - 1.0 / 3)) - .isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(0)) / 10000.0 - 2.0 / 3)).isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(1)) / 10000.0 - 1.0 / 3)).isLessThan(0.002); } private boolean isEnabledHappyEyeballs() { @@ -636,8 +631,7 @@ weightedChild2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).on 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); int expectedTasks = isEnabledHappyEyeballs() ? 2 : 1; assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(expectedTasks); - assertThat(getAddressesFromPick(weightedPicker)) - .isEqualTo(weightedChild1.getEag()); + assertThat(getAddressesFromPick(weightedPicker)).isEqualTo(servers.get(0)); assertThat(getNumFilteredPendingTasks()).isEqualTo(1); weightedConfig = WeightedRoundRobinLoadBalancerConfig.newBuilder() .setWeightUpdatePeriodNanos(500_000_000L) //.5s @@ -655,10 +649,8 @@ weightedChild2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).on //timer fires, new weight updated expectedTasks = isEnabledHappyEyeballs() ? 2 : 1; assertThat(fakeClock.forwardTime(500, TimeUnit.MILLISECONDS)).isEqualTo(expectedTasks); - assertThat(getAddressesFromPick(weightedPicker)) - .isEqualTo(weightedChild2.getEag()); - assertThat(getAddressesFromPick(weightedPicker)) - .isEqualTo(weightedChild1.getEag()); + assertThat(getAddressesFromPick(weightedPicker)).isEqualTo(servers.get(1)); + assertThat(getAddressesFromPick(weightedPicker)).isEqualTo(servers.get(0)); } @Test @@ -697,10 +689,8 @@ weightedChild2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).on pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } assertThat(pickCount.size()).isEqualTo(2); - assertThat(Math.abs(pickCount.get(weightedChild1.getEag()) / 1000.0 - 2.0 / 3)) - .isLessThan(0.002); - assertThat(Math.abs(pickCount.get(weightedChild2.getEag()) / 1000.0 - 1.0 / 3)) - .isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(0)) / 1000.0 - 2.0 / 3)).isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(1)) / 1000.0 - 1.0 / 3)).isLessThan(0.002); // weight expired, fallback to simple round robin assertThat(fakeClock.forwardTime(300, TimeUnit.SECONDS)).isEqualTo(1); @@ -710,10 +700,8 @@ weightedChild2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).on pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } assertThat(pickCount.size()).isEqualTo(2); - assertThat(Math.abs(pickCount.get(weightedChild1.getEag()) / 1000.0 - 0.5)) - .isLessThan(0.002); - assertThat(Math.abs(pickCount.get(weightedChild2.getEag()) / 1000.0 - 0.5)) - .isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(0)) / 1000.0 - 0.5)).isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(1)) / 1000.0 - 0.5)).isLessThan(0.002); } @Test @@ -738,26 +726,17 @@ public void rrFallback() { (WeightedRoundRobinPicker) pickerCaptor.getAllValues().get(1); int expectedTasks = isEnabledHappyEyeballs() ? 2 : 1; assertThat(fakeClock.forwardTime(10, TimeUnit.SECONDS)).isEqualTo(expectedTasks); - WeightedChildLbState weightedChild1 = (WeightedChildLbState) getChild(weightedPicker, 0); - WeightedChildLbState weightedChild2 = (WeightedChildLbState) getChild(weightedPicker, 1); - Map qpsByChannel = ImmutableMap.of(weightedChild1.getEag(), 2, - weightedChild2.getEag(), 1); + Map qpsByChannel = ImmutableMap.of(servers.get(0), 2, + servers.get(1), 1); Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { PickResult pickResult = weightedPicker.pickSubchannel(mockArgs); EquivalentAddressGroup addresses = getAddresses(pickResult); pickCount.merge(addresses, 1, Integer::sum); - assertThat(pickResult.getStreamTracerFactory()).isNotNull(); - WeightedChildLbState childLbState = (WeightedChildLbState) wrr.getChildLbStateEag(addresses); - childLbState.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( - InternalCallMetricRecorder.createMetricReport( - 0.1, 0, 0.1, qpsByChannel.get(addresses), 0, - new HashMap<>(), new HashMap<>(), new HashMap<>())); + reportLoadOnRpc(pickResult, 0.1, 0, 0.1, qpsByChannel.get(addresses), 0); } - assertThat(Math.abs(pickCount.get(weightedChild1.getEag()) / 1000.0 - 1.0 / 2)) - .isAtMost(0.1); - assertThat(Math.abs(pickCount.get(weightedChild2.getEag()) / 1000.0 - 1.0 / 2)) - .isAtMost(0.1); + assertThat(Math.abs(pickCount.get(servers.get(0)) / 1000.0 - 1.0 / 2)).isAtMost(0.1); + assertThat(Math.abs(pickCount.get(servers.get(1)) / 1000.0 - 1.0 / 2)).isAtMost(0.1); // Identical to above except forwards time after each pick pickCount.clear(); @@ -765,19 +744,12 @@ childLbState.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLo PickResult pickResult = weightedPicker.pickSubchannel(mockArgs); EquivalentAddressGroup addresses = getAddresses(pickResult); pickCount.merge(addresses, 1, Integer::sum); - assertThat(pickResult.getStreamTracerFactory()).isNotNull(); - WeightedChildLbState childLbState = (WeightedChildLbState) wrr.getChildLbStateEag(addresses); - childLbState.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( - InternalCallMetricRecorder.createMetricReport( - 0.1, 0, 0.1, qpsByChannel.get(addresses), 0, - new HashMap<>(), new HashMap<>(), new HashMap<>())); + reportLoadOnRpc(pickResult, 0.1, 0, 0.1, qpsByChannel.get(addresses), 0); fakeClock.forwardTime(50, TimeUnit.MILLISECONDS); } assertThat(pickCount.size()).isEqualTo(2); - assertThat(Math.abs(pickCount.get(weightedChild1.getEag()) / 1000.0 - 2.0 / 3)) - .isAtMost(0.1); - assertThat(Math.abs(pickCount.get(weightedChild2.getEag()) / 1000.0 - 1.0 / 3)) - .isAtMost(0.1); + assertThat(Math.abs(pickCount.get(servers.get(0)) / 1000.0 - 2.0 / 3)).isAtMost(0.1); + assertThat(Math.abs(pickCount.get(servers.get(1)) / 1000.0 - 1.0 / 3)).isAtMost(0.1); } private static EquivalentAddressGroup getAddresses(PickResult pickResult) { @@ -809,7 +781,6 @@ public void unknownWeightIsAvgWeight() { (WeightedRoundRobinPicker) pickerCaptor.getAllValues().get(2); WeightedChildLbState weightedChild1 = (WeightedChildLbState) getChild(weightedPicker, 0); WeightedChildLbState weightedChild2 = (WeightedChildLbState) getChild(weightedPicker, 1); - WeightedChildLbState weightedChild3 = (WeightedChildLbState) getChild(weightedPicker, 2); weightedChild1.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); @@ -823,13 +794,10 @@ weightedChild2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).on pickCount.merge(result.getAddresses(), 1, Integer::sum); } assertThat(pickCount.size()).isEqualTo(3); - assertThat(Math.abs(pickCount.get(weightedChild1.getEag()) / 1000.0 - 4.0 / 9)) - .isLessThan(0.002); - assertThat(Math.abs(pickCount.get(weightedChild2.getEag()) / 1000.0 - 2.0 / 9)) - .isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(0)) / 1000.0 - 4.0 / 9)).isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(1)) / 1000.0 - 2.0 / 9)).isLessThan(0.002); // subchannel3's weight is average of subchannel1 and subchannel2 - assertThat(Math.abs(pickCount.get(weightedChild3.getEag()) / 1000.0 - 3.0 / 9)) - .isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(2)) / 1000.0 - 3.0 / 9)).isLessThan(0.002); } @Test @@ -862,8 +830,8 @@ weightedChild2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).on 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); CyclicBarrier barrier = new CyclicBarrier(2); Map pickCount = new ConcurrentHashMap<>(); - pickCount.put(weightedChild1.getEag(), new AtomicInteger(0)); - pickCount.put(weightedChild2.getEag(), new AtomicInteger(0)); + pickCount.put(servers.get(0), new AtomicInteger(0)); + pickCount.put(servers.get(1), new AtomicInteger(0)); new Thread(new Runnable() { @Override public void run() { @@ -890,10 +858,8 @@ public void run() { barrier.await(); assertThat(pickCount.size()).isEqualTo(2); // after blackout period - assertThat(Math.abs(pickCount.get(weightedChild1.getEag()).get() / 2000.0 - 2.0 / 3)) - .isLessThan(0.002); - assertThat(Math.abs(pickCount.get(weightedChild2.getEag()).get() / 2000.0 - 1.0 / 3)) - .isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(0)).get() / 2000.0 - 2.0 / 3)).isLessThan(0.002); + assertThat(Math.abs(pickCount.get(servers.get(1)).get() / 2000.0 - 1.0 / 3)).isLessThan(0.002); } @Test(expected = NullPointerException.class) @@ -1334,6 +1300,35 @@ private int getNumFilteredPendingTasks() { return AbstractTestHelper.getNumFilteredPendingTasks(fakeClock); } + private static final Metadata.Key ORCA_LOAD_METRICS_KEY = + Metadata.Key.of( + "endpoint-load-metrics-bin", + ProtoUtils.metadataMarshaller(OrcaLoadReport.getDefaultInstance())); + private static final ClientStreamTracer.StreamInfo STREAM_INFO = + ClientStreamTracer.StreamInfo.newBuilder().build(); + + private static void reportLoadOnRpc( + PickResult pickResult, + double cpuUtilization, + double applicationUtilization, + double memoryUtilization, + double qps, + double eps) { + ClientStreamTracer childTracer = pickResult.getStreamTracerFactory() + .newClientStreamTracer(STREAM_INFO, new Metadata()); + Metadata trailer = new Metadata(); + trailer.put( + ORCA_LOAD_METRICS_KEY, + OrcaLoadReport.newBuilder() + .setCpuUtilization(cpuUtilization) + .setApplicationUtilization(applicationUtilization) + .setMemUtilization(memoryUtilization) + .setRpsFractional(qps) + .setEps(eps) + .build()); + childTracer.inboundTrailers(trailer); + } + private static final class VerifyingScheduler { private final StaticStrideScheduler delegate; private final int max; @@ -1389,16 +1384,6 @@ public Map, Subchannel> getSubchannelMap() { return subchannels; } - @Override - public Map getMockToRealSubChannelMap() { - return mockToRealSubChannelMap; - } - - @Override - public Map getSubchannelStateListeners() { - return subchannelStateListeners; - } - @Override public MetricRecorder getMetricRecorder() { return mockMetricRecorder; From 921f88ae30e5a4b1d890f3ea7ff709f475a86298 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 12 Nov 2024 12:27:40 +0530 Subject: [PATCH 083/591] services: Deprecate V1alpha (#11681) --- documentation/server-reflection-tutorial.md | 8 ++++---- .../grpc/examples/debug/HelloWorldDebuggableClient.java | 2 +- .../io/grpc/examples/debug/HostnameDebuggableServer.java | 2 +- .../java/io/grpc/examples/hostname/HostnameServer.java | 4 ++-- examples/example-reflection/README.md | 2 +- .../io/grpc/examples/reflection/ReflectionServer.java | 4 ++-- .../grpc/examples/helloworldxds/XdsHelloWorldServer.java | 4 ++-- .../java/io/grpc/testing/integration/XdsTestClient.java | 5 ++++- .../java/io/grpc/testing/integration/XdsTestServer.java | 7 +++++-- .../io/grpc/protobuf/services/ProtoReflectionService.java | 4 +++- .../protobuf/services/ProtoReflectionServiceTest.java | 4 ++-- 11 files changed, 27 insertions(+), 19 deletions(-) diff --git a/documentation/server-reflection-tutorial.md b/documentation/server-reflection-tutorial.md index 5fad5a22333..c4d2e8c4aea 100644 --- a/documentation/server-reflection-tutorial.md +++ b/documentation/server-reflection-tutorial.md @@ -10,9 +10,9 @@ proto-based services. ## Enable Server Reflection gRPC-Java Server Reflection is implemented by -`io.grpc.protobuf.services.ProtoReflectionService` in the `grpc-services` +`io.grpc.protobuf.services.ProtoReflectionServiceV1` in the `grpc-services` package. To enable server reflection, you need to add the -`ProtoReflectionService` to your gRPC server. +`ProtoReflectionServiceV1` to your gRPC server. For example, to enable server reflection in `examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java`, we @@ -35,7 +35,7 @@ need to make the following changes: import io.grpc.Server; import io.grpc.ServerBuilder; -+import io.grpc.protobuf.services.ProtoReflectionService; ++import io.grpc.protobuf.services.ProtoReflectionServiceV1; import io.grpc.stub.StreamObserver; import java.io.IOException; import java.util.logging.Logger; @@ -43,7 +43,7 @@ need to make the following changes: int port = 50051; server = ServerBuilder.forPort(port) .addService(new GreeterImpl()) -+ .addService(ProtoReflectionService.newInstance()) ++ .addService(ProtoReflectionServiceV1.newInstance()) .build() .start(); logger.info("Server started, listening on " + port); diff --git a/examples/example-debug/src/main/java/io/grpc/examples/debug/HelloWorldDebuggableClient.java b/examples/example-debug/src/main/java/io/grpc/examples/debug/HelloWorldDebuggableClient.java index 61391b60415..ef1340cf259 100644 --- a/examples/example-debug/src/main/java/io/grpc/examples/debug/HelloWorldDebuggableClient.java +++ b/examples/example-debug/src/main/java/io/grpc/examples/debug/HelloWorldDebuggableClient.java @@ -27,7 +27,7 @@ import io.grpc.examples.helloworld.GreeterGrpc; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloRequest; -import io.grpc.protobuf.services.ProtoReflectionService; +import io.grpc.protobuf.services.ProtoReflectionServiceV1; import io.grpc.services.AdminInterface; import java.util.concurrent.TimeUnit; import java.util.logging.Level; diff --git a/examples/example-debug/src/main/java/io/grpc/examples/debug/HostnameDebuggableServer.java b/examples/example-debug/src/main/java/io/grpc/examples/debug/HostnameDebuggableServer.java index 89ffc39b599..5525ba91d9c 100644 --- a/examples/example-debug/src/main/java/io/grpc/examples/debug/HostnameDebuggableServer.java +++ b/examples/example-debug/src/main/java/io/grpc/examples/debug/HostnameDebuggableServer.java @@ -21,7 +21,7 @@ import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.health.v1.HealthCheckResponse.ServingStatus; -import io.grpc.protobuf.services.ProtoReflectionService; +import io.grpc.protobuf.services.ProtoReflectionServiceV1; import io.grpc.services.AdminInterface; import io.grpc.services.HealthStatusManager; import java.io.IOException; diff --git a/examples/example-hostname/src/main/java/io/grpc/examples/hostname/HostnameServer.java b/examples/example-hostname/src/main/java/io/grpc/examples/hostname/HostnameServer.java index 3c63296d7fa..ca38c7cdc7b 100644 --- a/examples/example-hostname/src/main/java/io/grpc/examples/hostname/HostnameServer.java +++ b/examples/example-hostname/src/main/java/io/grpc/examples/hostname/HostnameServer.java @@ -21,7 +21,7 @@ import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.health.v1.HealthCheckResponse.ServingStatus; -import io.grpc.protobuf.services.ProtoReflectionService; +import io.grpc.protobuf.services.ProtoReflectionServiceV1; import io.grpc.services.HealthStatusManager; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -53,7 +53,7 @@ public static void main(String[] args) throws IOException, InterruptedException HealthStatusManager health = new HealthStatusManager(); final Server server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()) .addService(new HostnameGreeter(hostname)) - .addService(ProtoReflectionService.newInstance()) + .addService(ProtoReflectionServiceV1.newInstance()) .addService(health.getHealthService()) .build() .start(); diff --git a/examples/example-reflection/README.md b/examples/example-reflection/README.md index 801a27343db..4bc30e84b3b 100644 --- a/examples/example-reflection/README.md +++ b/examples/example-reflection/README.md @@ -1,7 +1,7 @@ gRPC Reflection Example ================ -The reflection example has a Hello World server with `ProtoReflectionService` registered. +The reflection example has a Hello World server with `ProtoReflectionServiceV1` registered. ### Build the example diff --git a/examples/example-reflection/src/main/java/io/grpc/examples/reflection/ReflectionServer.java b/examples/example-reflection/src/main/java/io/grpc/examples/reflection/ReflectionServer.java index ad702247ba7..8406317aad6 100644 --- a/examples/example-reflection/src/main/java/io/grpc/examples/reflection/ReflectionServer.java +++ b/examples/example-reflection/src/main/java/io/grpc/examples/reflection/ReflectionServer.java @@ -7,7 +7,7 @@ import io.grpc.examples.helloworld.GreeterGrpc; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloRequest; -import io.grpc.protobuf.services.ProtoReflectionService; +import io.grpc.protobuf.services.ProtoReflectionServiceV1; import io.grpc.stub.StreamObserver; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -26,7 +26,7 @@ private void start() throws IOException { int port = 50051; server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()) .addService(new GreeterImpl()) - .addService(ProtoReflectionService.newInstance()) // add reflection service + .addService(ProtoReflectionServiceV1.newInstance()) // add reflection service .build() .start(); logger.info("Server started, listening on " + port); diff --git a/examples/example-xds/src/main/java/io/grpc/examples/helloworldxds/XdsHelloWorldServer.java b/examples/example-xds/src/main/java/io/grpc/examples/helloworldxds/XdsHelloWorldServer.java index 93317dda23e..c7c67f8d681 100644 --- a/examples/example-xds/src/main/java/io/grpc/examples/helloworldxds/XdsHelloWorldServer.java +++ b/examples/example-xds/src/main/java/io/grpc/examples/helloworldxds/XdsHelloWorldServer.java @@ -20,7 +20,7 @@ import io.grpc.Server; import io.grpc.ServerCredentials; import io.grpc.health.v1.HealthCheckResponse.ServingStatus; -import io.grpc.protobuf.services.ProtoReflectionService; +import io.grpc.protobuf.services.ProtoReflectionServiceV1; import io.grpc.services.HealthStatusManager; import io.grpc.xds.XdsServerBuilder; import io.grpc.xds.XdsServerCredentials; @@ -66,7 +66,7 @@ public static void main(String[] args) throws IOException, InterruptedException final HealthStatusManager health = new HealthStatusManager(); final Server server = XdsServerBuilder.forPort(port, credentials) .addService(new HostnameGreeter(hostname)) - .addService(ProtoReflectionService.newInstance()) // convenient for command line tools + .addService(ProtoReflectionServiceV1.newInstance()) // convenient for command line tools .addService(health.getHealthService()) // allow management servers to monitor health .build() .start(); diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestClient.java index c697bd9f305..23bc12a6b65 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestClient.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestClient.java @@ -28,6 +28,7 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import com.google.protobuf.ByteString; +import io.grpc.BindableService; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; @@ -273,11 +274,13 @@ private void run() { .build(); csmObservability.registerGlobal(); } + @SuppressWarnings("deprecation") + BindableService oldReflectionService = ProtoReflectionService.newInstance(); statsServer = Grpc.newServerBuilderForPort(statsPort, InsecureServerCredentials.create()) .addService(new XdsStatsImpl()) .addService(new ConfigureUpdateServiceImpl()) - .addService(ProtoReflectionService.newInstance()) + .addService(oldReflectionService) .addService(ProtoReflectionServiceV1.newInstance()) .addServices(AdminInterface.getStandardServices()) .build(); diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java b/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java index 2f1625d1581..1bc4ff88981 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java @@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.protobuf.ByteString; +import io.grpc.BindableService; import io.grpc.ForwardingServerCall.SimpleForwardingServerCall; import io.grpc.Grpc; import io.grpc.InsecureServerCredentials; @@ -212,6 +213,8 @@ void start() throws Exception { throw new RuntimeException(e); } health = new HealthStatusManager(); + @SuppressWarnings("deprecation") + BindableService oldReflectionService = ProtoReflectionService.newInstance(); if (secureMode) { if (addressType != Util.AddressType.IPV4_IPV6) { throw new IllegalArgumentException("Secure mode only supports IPV4_IPV6 address type"); @@ -220,7 +223,7 @@ void start() throws Exception { Grpc.newServerBuilderForPort(maintenancePort, InsecureServerCredentials.create()) .addService(new XdsUpdateHealthServiceImpl(health)) .addService(health.getHealthService()) - .addService(ProtoReflectionService.newInstance()) + .addService(oldReflectionService) .addService(ProtoReflectionServiceV1.newInstance()) .addServices(AdminInterface.getStandardServices()) .build(); @@ -272,7 +275,7 @@ void start() throws Exception { new TestServiceImpl(serverId, host), new TestInfoInterceptor(host))) .addService(new XdsUpdateHealthServiceImpl(health)) .addService(health.getHealthService()) - .addService(ProtoReflectionService.newInstance()) + .addService(oldReflectionService) .addService(ProtoReflectionServiceV1.newInstance()) .addServices(AdminInterface.getStandardServices()) .build(); diff --git a/services/src/main/java/io/grpc/protobuf/services/ProtoReflectionService.java b/services/src/main/java/io/grpc/protobuf/services/ProtoReflectionService.java index 45947ed44ee..07008b682c3 100644 --- a/services/src/main/java/io/grpc/protobuf/services/ProtoReflectionService.java +++ b/services/src/main/java/io/grpc/protobuf/services/ProtoReflectionService.java @@ -28,11 +28,11 @@ /** * Provides a reflection service for Protobuf services (including the reflection service itself). - * Uses the deprecated v1alpha proto. New users should use ProtoReflectionServiceV1 instead. * *

Separately tracks mutable and immutable services. Throws an exception if either group of * services contains multiple Protobuf files with declarations of the same service, method, type, or * extension. + * Uses the deprecated v1alpha proto. New users should use {@link ProtoReflectionServiceV1} instead. */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2222") public final class ProtoReflectionService implements BindableService { @@ -40,11 +40,13 @@ public final class ProtoReflectionService implements BindableService { private ProtoReflectionService() { } + @Deprecated public static BindableService newInstance() { return new ProtoReflectionService(); } @Override + @SuppressWarnings("deprecation") public ServerServiceDefinition bindService() { ServerServiceDefinition serverServiceDefinitionV1 = ProtoReflectionServiceV1.newInstance() .bindService(); diff --git a/services/src/test/java/io/grpc/protobuf/services/ProtoReflectionServiceTest.java b/services/src/test/java/io/grpc/protobuf/services/ProtoReflectionServiceTest.java index c9dd1014141..115dd11b0f1 100644 --- a/services/src/test/java/io/grpc/protobuf/services/ProtoReflectionServiceTest.java +++ b/services/src/test/java/io/grpc/protobuf/services/ProtoReflectionServiceTest.java @@ -71,7 +71,8 @@ public class ProtoReflectionServiceTest { private static final String TEST_HOST = "localhost"; private MutableHandlerRegistry handlerRegistry = new MutableHandlerRegistry(); - private BindableService reflectionService; + @SuppressWarnings("deprecation") + private BindableService reflectionService = ProtoReflectionService.newInstance(); private ServerServiceDefinition dynamicService = new DynamicServiceGrpc.DynamicServiceImplBase() {}.bindService(); private ServerServiceDefinition anotherDynamicService = @@ -80,7 +81,6 @@ public class ProtoReflectionServiceTest { @Before public void setUp() throws Exception { - reflectionService = ProtoReflectionService.newInstance(); Server server = InProcessServerBuilder.forName("proto-reflection-test") .directExecutor() From b1703345f74fd211f9c5199825f2b8973885ea0a Mon Sep 17 00:00:00 2001 From: John Cormie Date: Wed, 13 Nov 2024 16:50:14 -0800 Subject: [PATCH 084/591] Make channelz work with proto lite (#11685) Allows android apps to expose internal grpc state for debugging. --- .../protobuf/services/ChannelzProtoUtil.java | 72 ++++++-- .../services/ChannelzProtoUtilTest.java | 164 ++++++++++-------- 2 files changed, 149 insertions(+), 87 deletions(-) diff --git a/services/src/main/java/io/grpc/protobuf/services/ChannelzProtoUtil.java b/services/src/main/java/io/grpc/protobuf/services/ChannelzProtoUtil.java index cf003b2f881..74448a8c5bf 100644 --- a/services/src/main/java/io/grpc/protobuf/services/ChannelzProtoUtil.java +++ b/services/src/main/java/io/grpc/protobuf/services/ChannelzProtoUtil.java @@ -21,6 +21,7 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; import com.google.protobuf.Int64Value; +import com.google.protobuf.MessageLite; import com.google.protobuf.util.Durations; import com.google.protobuf.util.Timestamps; import io.grpc.ConnectivityState; @@ -79,6 +80,8 @@ /** * A static utility class for turning internal data structures into protos. + * + *

Works with both regular and lite protos. */ final class ChannelzProtoUtil { private static final Logger logger = Logger.getLogger(ChannelzProtoUtil.class.getName()); @@ -254,22 +257,20 @@ static SocketOption toSocketOptionLinger(int lingerSeconds) { } else { lingerOpt = SocketOptionLinger.getDefaultInstance(); } - return SocketOption - .newBuilder() + return SocketOption.newBuilder() .setName(SO_LINGER) - .setAdditional(Any.pack(lingerOpt)) + .setAdditional(packToAny("SocketOptionLinger", lingerOpt)) .build(); } static SocketOption toSocketOptionTimeout(String name, int timeoutMillis) { Preconditions.checkNotNull(name); - return SocketOption - .newBuilder() + return SocketOption.newBuilder() .setName(name) .setAdditional( - Any.pack( - SocketOptionTimeout - .newBuilder() + packToAny( + "SocketOptionTimeout", + SocketOptionTimeout.newBuilder() .setDuration(Durations.fromMillis(timeoutMillis)) .build())) .build(); @@ -307,10 +308,9 @@ static SocketOption toSocketOptionTcpInfo(InternalChannelz.TcpInfo i) { .setTcpiAdvmss(i.advmss) .setTcpiReordering(i.reordering) .build(); - return SocketOption - .newBuilder() + return SocketOption.newBuilder() .setName(TCP_INFO) - .setAdditional(Any.pack(tcpInfo)) + .setAdditional(packToAny("SocketOptionTcpInfo", tcpInfo)) .build(); } @@ -380,10 +380,11 @@ private static ChannelTrace toChannelTrace(InternalChannelz.ChannelTrace channel private static List toChannelTraceEvents(List events) { List channelTraceEvents = new ArrayList<>(); for (Event event : events) { - ChannelTraceEvent.Builder builder = ChannelTraceEvent.newBuilder() - .setDescription(event.description) - .setSeverity(Severity.valueOf(event.severity.name())) - .setTimestamp(Timestamps.fromNanos(event.timestampNanos)); + ChannelTraceEvent.Builder builder = + ChannelTraceEvent.newBuilder() + .setDescription(event.description) + .setSeverity(toSeverity(event.severity)) + .setTimestamp(Timestamps.fromNanos(event.timestampNanos)); if (event.channelRef != null) { builder.setChannelRef(toChannelRef(event.channelRef)); } @@ -395,14 +396,39 @@ private static List toChannelTraceEvents(List events) return Collections.unmodifiableList(channelTraceEvents); } + static Severity toSeverity(Event.Severity severity) { + if (severity == null) { + return Severity.CT_UNKNOWN; + } + switch (severity) { + case CT_INFO: + return Severity.CT_INFO; + case CT_ERROR: + return Severity.CT_ERROR; + case CT_WARNING: + return Severity.CT_WARNING; + default: + return Severity.CT_UNKNOWN; + } + } + static State toState(ConnectivityState state) { if (state == null) { return State.UNKNOWN; } - try { - return Enum.valueOf(State.class, state.name()); - } catch (IllegalArgumentException e) { - return State.UNKNOWN; + switch (state) { + case IDLE: + return State.IDLE; + case READY: + return State.READY; + case CONNECTING: + return State.CONNECTING; + case SHUTDOWN: + return State.SHUTDOWN; + case TRANSIENT_FAILURE: + return State.TRANSIENT_FAILURE; + default: + return State.UNKNOWN; } } @@ -468,4 +494,12 @@ private static T getFuture(ListenableFuture future) { throw Status.INTERNAL.withCause(e).asRuntimeException(); } } + + // A version of Any.pack() that works with protolite. + private static Any packToAny(String typeName, MessageLite value) { + return Any.newBuilder() + .setTypeUrl("type.googleapis.com/grpc.channelz.v1." + typeName) + .setValue(value.toByteString()) + .build(); + } } diff --git a/services/src/test/java/io/grpc/protobuf/services/ChannelzProtoUtilTest.java b/services/src/test/java/io/grpc/protobuf/services/ChannelzProtoUtilTest.java index 0d2e6063d5e..598a8625e58 100644 --- a/services/src/test/java/io/grpc/protobuf/services/ChannelzProtoUtilTest.java +++ b/services/src/test/java/io/grpc/protobuf/services/ChannelzProtoUtilTest.java @@ -27,7 +27,7 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; import com.google.protobuf.Int64Value; -import com.google.protobuf.Message; +import com.google.protobuf.MessageLite; import com.google.protobuf.util.Durations; import com.google.protobuf.util.Timestamps; import io.grpc.ConnectivityState; @@ -154,33 +154,44 @@ public final class ChannelzProtoUtilTest { .setData(serverData) .build(); - private final SocketOption sockOptLingerDisabled = SocketOption - .newBuilder() - .setName("SO_LINGER") - .setAdditional( - Any.pack(SocketOptionLinger.getDefaultInstance())) - .build(); - - private final SocketOption sockOptlinger10s = SocketOption - .newBuilder() - .setName("SO_LINGER") - .setAdditional( - Any.pack(SocketOptionLinger - .newBuilder() - .setActive(true) - .setDuration(Durations.fromSeconds(10)) - .build())) - .build(); - - private final SocketOption sockOptTimeout200ms = SocketOption - .newBuilder() - .setName("SO_TIMEOUT") - .setAdditional( - Any.pack(SocketOptionTimeout - .newBuilder() - .setDuration(Durations.fromMillis(200)) - .build()) - ).build(); + private final SocketOption sockOptLingerDisabled = + SocketOption.newBuilder() + .setName("SO_LINGER") + .setAdditional( + Any.newBuilder() + .setTypeUrl("type.googleapis.com/grpc.channelz.v1.SocketOptionLinger") + .setValue(SocketOptionLinger.getDefaultInstance().toByteString()) + .build()) + .build(); + + private final SocketOption sockOptlinger10s = + SocketOption.newBuilder() + .setName("SO_LINGER") + .setAdditional( + Any.newBuilder() + .setTypeUrl("type.googleapis.com/grpc.channelz.v1.SocketOptionLinger") + .setValue( + SocketOptionLinger.newBuilder() + .setActive(true) + .setDuration(Durations.fromSeconds(10)) + .build() + .toByteString()) + .build()) + .build(); + + private final SocketOption sockOptTimeout200ms = + SocketOption.newBuilder() + .setName("SO_TIMEOUT") + .setAdditional( + Any.newBuilder() + .setTypeUrl("type.googleapis.com/grpc.channelz.v1.SocketOptionTimeout") + .setValue( + SocketOptionTimeout.newBuilder() + .setDuration(Durations.fromMillis(200)) + .build() + .toByteString()) + .build()) + .build(); private final SocketOption sockOptAdditional = SocketOption .newBuilder() @@ -221,43 +232,46 @@ public final class ChannelzProtoUtilTest { .setReordering(728) .build(); - private final SocketOption socketOptionTcpInfo = SocketOption - .newBuilder() - .setName("TCP_INFO") - .setAdditional( - Any.pack( - SocketOptionTcpInfo.newBuilder() - .setTcpiState(70) - .setTcpiCaState(71) - .setTcpiRetransmits(72) - .setTcpiProbes(73) - .setTcpiBackoff(74) - .setTcpiOptions(75) - .setTcpiSndWscale(76) - .setTcpiRcvWscale(77) - .setTcpiRto(78) - .setTcpiAto(79) - .setTcpiSndMss(710) - .setTcpiRcvMss(711) - .setTcpiUnacked(712) - .setTcpiSacked(713) - .setTcpiLost(714) - .setTcpiRetrans(715) - .setTcpiFackets(716) - .setTcpiLastDataSent(717) - .setTcpiLastAckSent(718) - .setTcpiLastDataRecv(719) - .setTcpiLastAckRecv(720) - .setTcpiPmtu(721) - .setTcpiRcvSsthresh(722) - .setTcpiRtt(723) - .setTcpiRttvar(724) - .setTcpiSndSsthresh(725) - .setTcpiSndCwnd(726) - .setTcpiAdvmss(727) - .setTcpiReordering(728) - .build())) - .build(); + private final SocketOption socketOptionTcpInfo = + SocketOption.newBuilder() + .setName("TCP_INFO") + .setAdditional( + Any.newBuilder() + .setTypeUrl("type.googleapis.com/grpc.channelz.v1.SocketOptionTcpInfo") + .setValue( + SocketOptionTcpInfo.newBuilder() + .setTcpiState(70) + .setTcpiCaState(71) + .setTcpiRetransmits(72) + .setTcpiProbes(73) + .setTcpiBackoff(74) + .setTcpiOptions(75) + .setTcpiSndWscale(76) + .setTcpiRcvWscale(77) + .setTcpiRto(78) + .setTcpiAto(79) + .setTcpiSndMss(710) + .setTcpiRcvMss(711) + .setTcpiUnacked(712) + .setTcpiSacked(713) + .setTcpiLost(714) + .setTcpiRetrans(715) + .setTcpiFackets(716) + .setTcpiLastDataSent(717) + .setTcpiLastAckSent(718) + .setTcpiLastDataRecv(719) + .setTcpiLastAckRecv(720) + .setTcpiPmtu(721) + .setTcpiRcvSsthresh(722) + .setTcpiRtt(723) + .setTcpiRttvar(724) + .setTcpiSndSsthresh(725) + .setTcpiSndCwnd(726) + .setTcpiAdvmss(727) + .setTcpiReordering(728) + .build() + .toByteString())) + .build(); private final TestListenSocket listenSocket = new TestListenSocket(); private final SocketRef listenSocketRef = SocketRef @@ -336,6 +350,16 @@ public void toServerRef() { assertEquals(serverRef, ChannelzProtoUtil.toServerRef(server)); } + @Test + public void toSeverity() { + for (Severity severity : Severity.values()) { + assertEquals( + severity.name(), + ChannelzProtoUtil.toSeverity(severity).name()); // OK because test isn't proguarded. + } + assertEquals(ChannelTraceEvent.Severity.CT_UNKNOWN, ChannelzProtoUtil.toSeverity(null)); + } + @Test public void toSocketRef() { assertEquals(socketRef, ChannelzProtoUtil.toSocketRef(socket)); @@ -346,7 +370,7 @@ public void toState() { for (ConnectivityState connectivityState : ConnectivityState.values()) { assertEquals( connectivityState.name(), - ChannelzProtoUtil.toState(connectivityState).getValueDescriptor().getName()); + ChannelzProtoUtil.toState(connectivityState).name()); // OK because test isn't proguarded. } assertEquals(State.UNKNOWN, ChannelzProtoUtil.toState(null)); } @@ -475,8 +499,12 @@ public void socketSecurityTls() throws Exception { @Test public void socketSecurityOther() throws Exception { // what is packed here is not important, just pick some proto message - Message contents = GetChannelRequest.newBuilder().setChannelId(1).build(); - Any packed = Any.pack(contents); + MessageLite contents = GetChannelRequest.newBuilder().setChannelId(1).build(); + Any packed = + Any.newBuilder() + .setTypeUrl("type.googleapis.com/grpc.channelz.v1.GetChannelRequest") + .setValue(contents.toByteString()) + .build(); socket.security = new InternalChannelz.Security( new InternalChannelz.OtherSecurity("other_security", packed)); From 4e8f7df589cbe5a192e705ab372f2b27957bf662 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 14 Nov 2024 12:56:24 -0800 Subject: [PATCH 085/591] util: Remove resolvedAddresses from MultiChildLb.ChildLbState It isn't actually used by MultiChildLb, and using the health API gives us more confidence that health is properly plumbed. --- .../PickFirstLoadBalancerProvider.java | 2 +- ...PickFirstLoadBalancerProviderAccessor.java | 28 +++++++ .../io/grpc/util/MultiChildLoadBalancer.java | 13 ---- .../grpc/util/RoundRobinLoadBalancerTest.java | 67 +++++++++++++++-- .../io/grpc/xds/RingHashLoadBalancerTest.java | 75 ++++++++++++++++++- 5 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 core/src/testFixtures/java/io/grpc/internal/PickFirstLoadBalancerProviderAccessor.java diff --git a/core/src/main/java/io/grpc/internal/PickFirstLoadBalancerProvider.java b/core/src/main/java/io/grpc/internal/PickFirstLoadBalancerProvider.java index 92178ccae24..83b3fb7d8e6 100644 --- a/core/src/main/java/io/grpc/internal/PickFirstLoadBalancerProvider.java +++ b/core/src/main/java/io/grpc/internal/PickFirstLoadBalancerProvider.java @@ -36,7 +36,7 @@ public final class PickFirstLoadBalancerProvider extends LoadBalancerProvider { public static final String GRPC_PF_USE_HAPPY_EYEBALLS = "GRPC_PF_USE_HAPPY_EYEBALLS"; private static final String SHUFFLE_ADDRESS_LIST_KEY = "shuffleAddressList"; - private static boolean enableNewPickFirst = + static boolean enableNewPickFirst = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_ENABLE_NEW_PICK_FIRST", false); public static boolean isEnabledHappyEyeballs() { diff --git a/core/src/testFixtures/java/io/grpc/internal/PickFirstLoadBalancerProviderAccessor.java b/core/src/testFixtures/java/io/grpc/internal/PickFirstLoadBalancerProviderAccessor.java new file mode 100644 index 00000000000..a6e94df03c2 --- /dev/null +++ b/core/src/testFixtures/java/io/grpc/internal/PickFirstLoadBalancerProviderAccessor.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +/** + * Accessor for PickFirstLoadBalancerProvider, allowing access only during tests. + */ +public final class PickFirstLoadBalancerProviderAccessor { + private PickFirstLoadBalancerProviderAccessor() {} + + public static void setEnableNewPickFirst(boolean enableNewPickFirst) { + PickFirstLoadBalancerProvider.enableNewPickFirst = enableNewPickFirst; + } +} diff --git a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java index 36a480f63c2..b51d2772d3e 100644 --- a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java @@ -191,7 +191,6 @@ private List updateChildrenWithResolvedAddresses( } newChildLbStates.add(childLbState); if (entry.getValue() != null) { - childLbState.setResolvedAddresses(entry.getValue()); // update child childLbState.lb.handleResolvedAddresses(entry.getValue()); // update child LB } } @@ -262,8 +261,6 @@ protected final List getReadyChildren() { */ public class ChildLbState { private final Object key; - private ResolvedAddresses resolvedAddresses; - private final LoadBalancer lb; private ConnectivityState currentState; private SubchannelPicker currentPicker = new FixedResultPicker(PickResult.withNoResult()); @@ -321,16 +318,6 @@ protected final void setCurrentPicker(SubchannelPicker newPicker) { currentPicker = newPicker; } - protected final void setResolvedAddresses(ResolvedAddresses newAddresses) { - checkNotNull(newAddresses, "Missing address list for child"); - resolvedAddresses = newAddresses; - } - - @VisibleForTesting - public final ResolvedAddresses getResolvedAddresses() { - return resolvedAddresses; - } - /** * ChildLbStateHelper is the glue between ChildLbState and the helpers associated with the * petiole policy above and the PickFirstLoadBalancer's helper below. diff --git a/util/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java b/util/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java index 0201fb578e9..18854ca1bb6 100644 --- a/util/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java @@ -22,7 +22,6 @@ import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.SHUTDOWN; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import static io.grpc.util.MultiChildLoadBalancer.IS_PETIOLE_POLICY; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; @@ -54,13 +53,15 @@ import io.grpc.LoadBalancer.Subchannel; import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.Status; +import io.grpc.internal.PickFirstLoadBalancerProvider; +import io.grpc.internal.PickFirstLoadBalancerProviderAccessor; import io.grpc.internal.TestUtils; -import io.grpc.util.MultiChildLoadBalancer.ChildLbState; import io.grpc.util.RoundRobinLoadBalancer.ReadyPicker; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -104,6 +105,7 @@ public class RoundRobinLoadBalancerTest { private ArgumentCaptor createArgsCaptor; private TestHelper testHelperInst = new TestHelper(); private Helper mockHelper = mock(Helper.class, delegatesTo(testHelperInst)); + private boolean defaultNewPickFirst = PickFirstLoadBalancerProvider.isEnabledNewPickFirst(); @Mock // This LoadBalancer doesn't use any of the arg fields, as verified in tearDown(). private PickSubchannelArgs mockArgs; @@ -126,6 +128,7 @@ private Status acceptAddresses(List eagList, Attributes @After public void tearDown() throws Exception { + PickFirstLoadBalancerProviderAccessor.setEnableNewPickFirst(defaultNewPickFirst); verifyNoMoreInteractions(mockArgs); } @@ -201,12 +204,6 @@ public void pickAfterResolvedUpdatedHosts() throws Exception { verify(removedSubchannel, times(1)).requestConnection(); verify(oldSubchannel, times(1)).requestConnection(); - assertThat(loadBalancer.getChildLbStates().size()).isEqualTo(2); - for (ChildLbState childLbState : loadBalancer.getChildLbStates()) { - assertThat(childLbState.getResolvedAddresses().getAttributes().get(IS_PETIOLE_POLICY)) - .isTrue(); - } - // This time with Attributes List latestServers = Lists.newArrayList(oldEag2, newEag); @@ -464,6 +461,60 @@ public void subchannelStateIsolation() throws Exception { assertThat(pickers.hasNext()).isFalse(); } + @Test + public void subchannelHealthObserved() throws Exception { + // Only the new PF policy observes the new separate listener for health + PickFirstLoadBalancerProviderAccessor.setEnableNewPickFirst(true); + // PickFirst does most of this work. If the test fails, check IS_PETIOLE_POLICY + Map healthListeners = new HashMap<>(); + loadBalancer = new RoundRobinLoadBalancer(new ForwardingLoadBalancerHelper() { + @Override + public Subchannel createSubchannel(CreateSubchannelArgs args) { + Subchannel subchannel = super.createSubchannel(args.toBuilder() + .setAttributes(args.getAttributes().toBuilder() + .set(LoadBalancer.HAS_HEALTH_PRODUCER_LISTENER_KEY, true) + .build()) + .build()); + healthListeners.put( + subchannel, args.getOption(LoadBalancer.HEALTH_CONSUMER_LISTENER_ARG_KEY)); + return subchannel; + } + + @Override + protected Helper delegate() { + return mockHelper; + } + }); + + InOrder inOrder = inOrder(mockHelper); + Status addressesAcceptanceStatus = acceptAddresses(servers, Attributes.EMPTY); + assertThat(addressesAcceptanceStatus.isOk()).isTrue(); + Subchannel subchannel0 = subchannels.get(Arrays.asList(servers.get(0))); + Subchannel subchannel1 = subchannels.get(Arrays.asList(servers.get(1))); + Subchannel subchannel2 = subchannels.get(Arrays.asList(servers.get(2))); + + // Subchannels go READY, but the LB waits for health + for (Subchannel subchannel : subchannels.values()) { + deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY)); + } + inOrder.verify(mockHelper, times(0)) + .updateBalancingState(eq(READY), any(SubchannelPicker.class)); + + // Health results lets subchannels go READY + healthListeners.get(subchannel0).onSubchannelState( + ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE.withDescription("oh no"))); + healthListeners.get(subchannel1).onSubchannelState(ConnectivityStateInfo.forNonError(READY)); + healthListeners.get(subchannel2).onSubchannelState(ConnectivityStateInfo.forNonError(READY)); + inOrder.verify(mockHelper, times(2)).updateBalancingState(eq(READY), pickerCaptor.capture()); + SubchannelPicker picker = pickerCaptor.getValue(); + List picks = Arrays.asList( + picker.pickSubchannel(mockArgs).getSubchannel(), + picker.pickSubchannel(mockArgs).getSubchannel(), + picker.pickSubchannel(mockArgs).getSubchannel(), + picker.pickSubchannel(mockArgs).getSubchannel()); + assertThat(picks).containsExactly(subchannel1, subchannel2, subchannel1, subchannel2); + } + @Test public void readyPicker_emptyList() { // ready picker list must be non-empty diff --git a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java index 4afe440c90e..50c2ef1d549 100644 --- a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java @@ -23,7 +23,6 @@ import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.SHUTDOWN; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import static io.grpc.util.MultiChildLoadBalancer.IS_PETIOLE_POLICY; import static io.grpc.xds.RingHashLoadBalancerTest.InitializationFlags.DO_NOT_RESET_HELPER; import static io.grpc.xds.RingHashLoadBalancerTest.InitializationFlags.DO_NOT_VERIFY; import static io.grpc.xds.RingHashLoadBalancerTest.InitializationFlags.RESET_SUBCHANNEL_MOCKS; @@ -48,6 +47,7 @@ import io.grpc.ConnectivityState; import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.CreateSubchannelArgs; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancer.PickDetailsConsumer; @@ -62,9 +62,11 @@ import io.grpc.SynchronizationContext; import io.grpc.internal.FakeClock; import io.grpc.internal.PickFirstLoadBalancerProvider; +import io.grpc.internal.PickFirstLoadBalancerProviderAccessor; import io.grpc.internal.PickSubchannelArgsImpl; import io.grpc.testing.TestMethodDescriptors; import io.grpc.util.AbstractTestHelper; +import io.grpc.util.ForwardingLoadBalancerHelper; import io.grpc.util.MultiChildLoadBalancer.ChildLbState; import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; import java.lang.Thread.UncaughtExceptionHandler; @@ -74,8 +76,11 @@ import java.util.Collections; import java.util.Deque; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Random; +import java.util.Set; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -115,6 +120,7 @@ public void uncaughtException(Thread t, Throwable e) { @Captor private ArgumentCaptor pickerCaptor; private RingHashLoadBalancer loadBalancer; + private boolean defaultNewPickFirst = PickFirstLoadBalancerProvider.isEnabledNewPickFirst(); @Before public void setUp() { @@ -126,6 +132,7 @@ public void setUp() { @After public void tearDown() { + PickFirstLoadBalancerProviderAccessor.setEnableNewPickFirst(defaultNewPickFirst); loadBalancer.shutdown(); for (Subchannel subchannel : subchannels.values()) { verify(subchannel).shutdown(); @@ -906,6 +913,70 @@ public void duplicateAddresses() { assertThat(description).contains("Address: FakeSocketAddress-server2, count: 3"); } + @Test + public void subchannelHealthObserved() throws Exception { + // Only the new PF policy observes the new separate listener for health + PickFirstLoadBalancerProviderAccessor.setEnableNewPickFirst(true); + // PickFirst does most of this work. If the test fails, check IS_PETIOLE_POLICY + Map healthListeners = new HashMap<>(); + loadBalancer = new RingHashLoadBalancer(new ForwardingLoadBalancerHelper() { + @Override + public Subchannel createSubchannel(CreateSubchannelArgs args) { + Subchannel subchannel = super.createSubchannel(args.toBuilder() + .setAttributes(args.getAttributes().toBuilder() + .set(LoadBalancer.HAS_HEALTH_PRODUCER_LISTENER_KEY, true) + .build()) + .build()); + healthListeners.put( + subchannel, args.getOption(LoadBalancer.HEALTH_CONSUMER_LISTENER_ARG_KEY)); + return subchannel; + } + + @Override + protected Helper delegate() { + return helper; + } + }); + + InOrder inOrder = Mockito.inOrder(helper); + List servers = createWeightedServerAddrs(1, 1); + initializeLbSubchannels(new RingHashConfig(10, 100), servers); + Subchannel subchannel0 = subchannels.get(Collections.singletonList(servers.get(0))); + Subchannel subchannel1 = subchannels.get(Collections.singletonList(servers.get(1))); + + // Subchannels go READY, but the LB waits for health + for (Subchannel subchannel : subchannels.values()) { + deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY)); + } + inOrder.verify(helper, times(0)).updateBalancingState(eq(READY), any(SubchannelPicker.class)); + + // Health results lets subchannels go READY + healthListeners.get(subchannel0).onSubchannelState(ConnectivityStateInfo.forNonError(READY)); + healthListeners.get(subchannel1).onSubchannelState(ConnectivityStateInfo.forNonError(READY)); + inOrder.verify(helper, times(2)).updateBalancingState(eq(READY), pickerCaptor.capture()); + SubchannelPicker picker = pickerCaptor.getValue(); + Random random = new Random(1); + Set picks = new HashSet<>(); + for (int i = 0; i < 10; i++) { + picks.add( + picker.pickSubchannel(getDefaultPickSubchannelArgs(random.nextLong())).getSubchannel()); + } + assertThat(picks).containsExactly(subchannel0, subchannel1); + + // Unhealthy subchannel skipped + healthListeners.get(subchannel0).onSubchannelState( + ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE.withDescription("oh no"))); + inOrder.verify(helper).updateBalancingState(eq(READY), pickerCaptor.capture()); + picker = pickerCaptor.getValue(); + random.setSeed(1); + picks.clear(); + for (int i = 0; i < 10; i++) { + picks.add( + picker.pickSubchannel(getDefaultPickSubchannelArgs(random.nextLong())).getSubchannel()); + } + assertThat(picks).containsExactly(subchannel1); + } + private List initializeLbSubchannels(RingHashConfig config, List servers, InitializationFlags... initFlags) { @@ -950,8 +1021,6 @@ private List initializeLbSubchannels(RingHashConfig config, for (ChildLbState childLbState : loadBalancer.getChildLbStates()) { childLbState.getCurrentPicker() .pickSubchannel(getDefaultPickSubchannelArgs(hashFunc.hashVoid())); - assertThat(childLbState.getResolvedAddresses().getAttributes().get(IS_PETIOLE_POLICY)) - .isTrue(); } if (doVerifies) { From 1f159d7899915245e6d3bd48b58b71b428a9e37b Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 14 Nov 2024 09:47:26 -0800 Subject: [PATCH 086/591] xds: Fix XdsSecurityClientServerTest TrustManagerStore race When spiffe support was added it caused tlsClientServer_useSystemRootCerts_validationContext to become flaky. This is because test execution order was important for whether the race would occur. Fixes #11678 --- .../grpc/xds/XdsSecurityClientServerTest.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java index 8e1220fe9d0..590b6c79a10 100644 --- a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java @@ -97,6 +97,7 @@ import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManagerFactory; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -687,16 +688,32 @@ public void run() { return settableFuture; } - private void setTrustStoreSystemProperties(String trustStoreFilePath) { + private void setTrustStoreSystemProperties(String trustStoreFilePath) throws Exception { System.setProperty("javax.net.ssl.trustStore", trustStoreFilePath); System.setProperty("javax.net.ssl.trustStorePassword", "changeit"); System.setProperty("javax.net.ssl.trustStoreType", "JKS"); + createDefaultTrustManager(); } - private void clearTrustStoreSystemProperties() { + private void clearTrustStoreSystemProperties() throws Exception { System.clearProperty("javax.net.ssl.trustStore"); System.clearProperty("javax.net.ssl.trustStorePassword"); System.clearProperty("javax.net.ssl.trustStoreType"); + createDefaultTrustManager(); + } + + /** + * Workaround the JDK's TrustManagerStore race. TrustManagerStore has a cache for the default + * certs based on the system properties. But updating the cache is not thread-safe and can cause a + * half-updated cache to appear fully-updated. When both the client and server initialize their + * trust store simultaneously, one can see a half-updated value. Creating the trust manager here + * fixes the cache while no other threads are running and thus the client and server threads won't + * race to update it. See https://github.com/grpc/grpc-java/issues/11678. + */ + private void createDefaultTrustManager() throws Exception { + TrustManagerFactory factory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + factory.init((KeyStore) null); } private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase { From 5431bf7e773940c35e97bf627f1a753df0e36be6 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 14 Nov 2024 14:18:40 -0800 Subject: [PATCH 087/591] services: Don't track code coverage of reflection.v1 gencode Generated code for v1alpha was ignored, but not v1. Ignoring v1 reduces lines being checked from 16,145 to 6,303, significantly improving the overall code coverage and removing noise. This was noticed because there was a very clear drop at 0aa976c4 visible in the coveralls.io coverage graph, the point when v1 was introduced. --- services/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/services/build.gradle b/services/build.gradle index fade7aef3fb..6daa7b0511d 100644 --- a/services/build.gradle +++ b/services/build.gradle @@ -58,6 +58,7 @@ tasks.named("jacocoTestReport").configure { '**/io/grpc/binarylog/v1/**', '**/io/grpc/channelz/v1/**', '**/io/grpc/health/v1/**', + '**/io/grpc/reflection/v1/**', '**/io/grpc/reflection/v1alpha/**', ]) } From 4ae04b7d940afc9a96473aedb06198a53c32f866 Mon Sep 17 00:00:00 2001 From: vinodhabib <47808007+vinodhabib@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:12:51 +0530 Subject: [PATCH 088/591] core: increased test tolerance to 1 second Fixes #11680 --- .../test/java/io/grpc/internal/InstantTimeProviderTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java b/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java index ef97d374b10..46d1891cbb9 100644 --- a/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java +++ b/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java @@ -44,7 +44,7 @@ public void testInstantCurrentTimeNanos() throws Exception { + instantNow.getNano(); // Validate the time returned is close to the expected value within a tolerance - // (i,e 10 millisecond tolerance in nanoseconds). - assertThat(actualTimeNanos).isWithin(10_000_000L).of(expectedTimeNanos); + // (i,e 1000 millisecond (1 second) tolerance in nanoseconds). + assertThat(actualTimeNanos).isWithin(1000_000_000L).of(expectedTimeNanos); } } From 6a92a2a22e0ee192c0c5450c000f0cca2975882e Mon Sep 17 00:00:00 2001 From: zbilun Date: Mon, 18 Nov 2024 11:57:02 -0800 Subject: [PATCH 089/591] interop-testing: Add concurrency condition to the soak test using existing blocking api The goal of this PR is to increase the test coverage of the C2P E2E load test by improving the rpc_soak and channel_soak tests to support concurrency. **rpc_soak:** The client performs many large_unary RPCs in sequence over the same channel. The test can run in either a concurrent or non-concurrent mode, depending on the number of threads specified (soak_num_threads): - Non-Concurrent Mode: When soak_num_threads = 1, all RPCs are performed sequentially on a single thread. - Concurrent Mode: When soak_num_threads > 1, the client uses multiple threads to distribute the workload. Each thread performs a portion of the total soak_iterations, executing its own set of RPCs concurrently. **channel_soak:** Similar to rpc_soak, but this time each RPC is performed on a new channel. The channel is created just before each RPC and is destroyed just after. Note on Concurrent Execution and Channel Creation: In a concurrent execution setting (i.e., when soak_num_threads > 1), each thread performs a portion of the total soak_iterations and creates and destroys its own channel for each RPC iteration. - createNewChannel Function: In channel_soak, the createNewChannel function is used by each thread to create a new channel before every RPC. This function ensures that each RPC has a separate channel, preventing race conditions by isolating channels between threads. It shuts down the previous channel (if any) and creates a new one for each iteration, ensuring accurate latency measurement per RPC. - Thread-specific logs will include the thread_id, helping to track performance across threads, especially when each thread is managing its own channel lifecycle. --- .../integration/AbstractInteropTest.java | 197 +++++++++++++----- .../integration/TestServiceClient.java | 17 +- .../integration/XdsFederationTestClient.java | 67 +++--- 3 files changed, 197 insertions(+), 84 deletions(-) diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java index 53d24d74a36..11fe9832fd9 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java @@ -28,6 +28,7 @@ import static org.junit.Assert.fail; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; @@ -1698,9 +1699,28 @@ public Status getStatus() { private Status status = Status.OK; } + + private static class ThreadResults { + private int threadFailures = 0; + private int iterationsDone = 0; + private Histogram latencies = new Histogram(4); + + public int getThreadFailures() { + return threadFailures; + } + + public int getIterationsDone() { + return iterationsDone; + } + + public Histogram getLatencies() { + return latencies; + } + } + private SoakIterationResult performOneSoakIteration( TestServiceGrpc.TestServiceBlockingStub soakStub, int soakRequestSize, int soakResponseSize) - throws Exception { + throws InterruptedException { long startNs = System.nanoTime(); Status status = Status.OK; try { @@ -1724,71 +1744,67 @@ private SoakIterationResult performOneSoakIteration( } /** - * Runs large unary RPCs in a loop with configurable failure thresholds - * and channel creation behavior. + * Runs large unary RPCs in a loop with configurable failure thresholds + * and channel creation behavior. */ public void performSoakTest( String serverUri, - boolean resetChannelPerIteration, int soakIterations, int maxFailures, int maxAcceptablePerIterationLatencyMs, int minTimeMsBetweenRpcs, int overallTimeoutSeconds, int soakRequestSize, - int soakResponseSize) - throws Exception { - int iterationsDone = 0; - int totalFailures = 0; - Histogram latencies = new Histogram(4 /* number of significant value digits */); + int soakResponseSize, + int numThreads, + Function createNewChannel) + throws InterruptedException { + if (soakIterations % numThreads != 0) { + throw new IllegalArgumentException("soakIterations must be evenly divisible by numThreads."); + } + ManagedChannel sharedChannel = createChannel(); long startNs = System.nanoTime(); - ManagedChannel soakChannel = createChannel(); - TestServiceGrpc.TestServiceBlockingStub soakStub = TestServiceGrpc - .newBlockingStub(soakChannel) - .withInterceptors(recordClientCallInterceptor(clientCallCapture)); - for (int i = 0; i < soakIterations; i++) { - if (System.nanoTime() - startNs >= TimeUnit.SECONDS.toNanos(overallTimeoutSeconds)) { - break; - } - long earliestNextStartNs = System.nanoTime() - + TimeUnit.MILLISECONDS.toNanos(minTimeMsBetweenRpcs); - if (resetChannelPerIteration) { - soakChannel.shutdownNow(); - soakChannel.awaitTermination(10, TimeUnit.SECONDS); - soakChannel = createChannel(); - soakStub = TestServiceGrpc - .newBlockingStub(soakChannel) - .withInterceptors(recordClientCallInterceptor(clientCallCapture)); - } - SoakIterationResult result = - performOneSoakIteration(soakStub, soakRequestSize, soakResponseSize); - SocketAddress peer = clientCallCapture - .get().getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); - StringBuilder logStr = new StringBuilder( - String.format( - Locale.US, - "soak iteration: %d elapsed_ms: %d peer: %s server_uri: %s", - i, result.getLatencyMs(), peer != null ? peer.toString() : "null", serverUri)); - if (!result.getStatus().equals(Status.OK)) { - totalFailures++; - logStr.append(String.format(" failed: %s", result.getStatus())); - } else if (result.getLatencyMs() > maxAcceptablePerIterationLatencyMs) { - totalFailures++; - logStr.append( - " exceeds max acceptable latency: " + maxAcceptablePerIterationLatencyMs); - } else { - logStr.append(" succeeded"); - } - System.err.println(logStr.toString()); - iterationsDone++; - latencies.recordValue(result.getLatencyMs()); - long remainingNs = earliestNextStartNs - System.nanoTime(); - if (remainingNs > 0) { - TimeUnit.NANOSECONDS.sleep(remainingNs); - } + Thread[] threads = new Thread[numThreads]; + int soakIterationsPerThread = soakIterations / numThreads; + List threadResultsList = new ArrayList<>(numThreads); + for (int i = 0; i < numThreads; i++) { + threadResultsList.add(new ThreadResults()); + } + for (int threadInd = 0; threadInd < numThreads; threadInd++) { + final int currentThreadInd = threadInd; + threads[threadInd] = new Thread(() -> { + try { + executeSoakTestInThread( + soakIterationsPerThread, + startNs, + minTimeMsBetweenRpcs, + soakRequestSize, + soakResponseSize, + maxAcceptablePerIterationLatencyMs, + overallTimeoutSeconds, + serverUri, + threadResultsList.get(currentThreadInd), + sharedChannel, + createNewChannel); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread interrupted: " + e.getMessage(), e); + } + }); + threads[threadInd].start(); + } + for (Thread thread : threads) { + thread.join(); + } + + int totalFailures = 0; + int iterationsDone = 0; + Histogram latencies = new Histogram(4); + for (ThreadResults threadResult :threadResultsList) { + totalFailures += threadResult.getThreadFailures(); + iterationsDone += threadResult.getIterationsDone(); + latencies.add(threadResult.getLatencies()); } - soakChannel.shutdownNow(); - soakChannel.awaitTermination(10, TimeUnit.SECONDS); System.err.println( String.format( Locale.US, @@ -1820,6 +1836,77 @@ public void performSoakTest( + "threshold: %d.", serverUri, totalFailures, maxFailures); assertTrue(tooManyFailuresErrorMessage, totalFailures <= maxFailures); + shutdownChannel(sharedChannel); + } + + private void shutdownChannel(ManagedChannel channel) throws InterruptedException { + if (channel != null) { + channel.shutdownNow(); + channel.awaitTermination(10, TimeUnit.SECONDS); + } + } + + protected ManagedChannel createNewChannel(ManagedChannel currentChannel) { + try { + shutdownChannel(currentChannel); + return createChannel(); + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted while creating a new channel", e); + } + } + + private void executeSoakTestInThread( + int soakIterationsPerThread, + long startNs, + int minTimeMsBetweenRpcs, + int soakRequestSize, + int soakResponseSize, + int maxAcceptablePerIterationLatencyMs, + int overallTimeoutSeconds, + String serverUri, + ThreadResults threadResults, + ManagedChannel sharedChannel, + Function maybeCreateChannel) throws InterruptedException { + ManagedChannel currentChannel = sharedChannel; + for (int i = 0; i < soakIterationsPerThread; i++) { + if (System.nanoTime() - startNs >= TimeUnit.SECONDS.toNanos(overallTimeoutSeconds)) { + break; + } + long earliestNextStartNs = System.nanoTime() + + TimeUnit.MILLISECONDS.toNanos(minTimeMsBetweenRpcs); + + currentChannel = maybeCreateChannel.apply(currentChannel); + TestServiceGrpc.TestServiceBlockingStub currentStub = TestServiceGrpc + .newBlockingStub(currentChannel) + .withInterceptors(recordClientCallInterceptor(clientCallCapture)); + SoakIterationResult result = performOneSoakIteration(currentStub, + soakRequestSize, soakResponseSize); + SocketAddress peer = clientCallCapture + .get().getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + StringBuilder logStr = new StringBuilder( + String.format( + Locale.US, + "thread id: %d soak iteration: %d elapsed_ms: %d peer: %s server_uri: %s", + Thread.currentThread().getId(), + i, result.getLatencyMs(), peer != null ? peer.toString() : "null", serverUri)); + if (!result.getStatus().equals(Status.OK)) { + threadResults.threadFailures++; + logStr.append(String.format(" failed: %s", result.getStatus())); + } else if (result.getLatencyMs() > maxAcceptablePerIterationLatencyMs) { + threadResults.threadFailures++; + logStr.append( + " exceeds max acceptable latency: " + maxAcceptablePerIterationLatencyMs); + } else { + logStr.append(" succeeded"); + } + System.err.println(logStr.toString()); + threadResults.iterationsDone++; + threadResults.getLatencies().recordValue(result.getLatencyMs()); + long remainingNs = earliestNextStartNs - System.nanoTime(); + if (remainingNs > 0) { + TimeUnit.NANOSECONDS.sleep(remainingNs); + } + } } private static void assertSuccess(StreamRecorder recorder) { diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java index e6829be11cb..8ade38cb024 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java @@ -134,6 +134,7 @@ public static void main(String[] args) throws Exception { soakIterations * soakPerIterationMaxAcceptableLatencyMs / 1000; private int soakRequestSize = 271828; private int soakResponseSize = 314159; + private int numThreads = 1; private String additionalMetadata = ""; private static LoadBalancerProvider customBackendMetricsLoadBalancerProvider; @@ -214,6 +215,8 @@ void parseArgs(String[] args) throws Exception { soakRequestSize = Integer.parseInt(value); } else if ("soak_response_size".equals(key)) { soakResponseSize = Integer.parseInt(value); + } else if ("soak_num_threads".equals(key)) { + numThreads = Integer.parseInt(value); } else if ("additional_metadata".equals(key)) { additionalMetadata = value; } else { @@ -290,6 +293,9 @@ void parseArgs(String[] args) throws Exception { + "\n --soak_response_size " + "\n The response size in a soak RPC. Default " + c.soakResponseSize + + "\n --soak_num_threads The number of threads for concurrent execution of the " + + "\n soak tests (rpc_soak or channel_soak). Default " + + c.numThreads + "\n --additional_metadata " + "\n Additional metadata to send in each request, as a " + "\n semicolon-separated list of key:value pairs. Default " @@ -519,30 +525,31 @@ private void runTest(TestCases testCase) throws Exception { case RPC_SOAK: { tester.performSoakTest( serverHost, - false /* resetChannelPerIteration */, soakIterations, soakMaxFailures, soakPerIterationMaxAcceptableLatencyMs, soakMinTimeMsBetweenRpcs, soakOverallTimeoutSeconds, soakRequestSize, - soakResponseSize); + soakResponseSize, + numThreads, + (currentChannel) -> currentChannel); break; } case CHANNEL_SOAK: { tester.performSoakTest( serverHost, - true /* resetChannelPerIteration */, soakIterations, soakMaxFailures, soakPerIterationMaxAcceptableLatencyMs, soakMinTimeMsBetweenRpcs, soakOverallTimeoutSeconds, soakRequestSize, - soakResponseSize); + soakResponseSize, + numThreads, + (currentChannel) -> tester.createNewChannel(currentChannel)); break; - } case ORCA_PER_RPC: { diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/XdsFederationTestClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/XdsFederationTestClient.java index f55ccbdefa7..08d845422a5 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/XdsFederationTestClient.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/XdsFederationTestClient.java @@ -245,29 +245,41 @@ public boolean runSucceeded() { /** * Run the intended soak test. */ - public void run() { - boolean resetChannelPerIteration; - switch (testCase) { - case "rpc_soak": - resetChannelPerIteration = false; - break; - case "channel_soak": - resetChannelPerIteration = true; - break; - default: - throw new RuntimeException("invalid testcase: " + testCase); - } + public void run() throws InterruptedException { try { - performSoakTest( - serverUri, - resetChannelPerIteration, - soakIterations, - soakMaxFailures, - soakPerIterationMaxAcceptableLatencyMs, - soakMinTimeMsBetweenRpcs, - soakOverallTimeoutSeconds, - soakRequestSize, - soakResponseSize); + switch (testCase) { + case "rpc_soak": { + performSoakTest( + serverUri, + soakIterations, + soakMaxFailures, + soakPerIterationMaxAcceptableLatencyMs, + soakMinTimeMsBetweenRpcs, + soakOverallTimeoutSeconds, + soakRequestSize, + soakResponseSize, + 1, + (currentChannel) -> currentChannel); + } + break; + case "channel_soak": { + performSoakTest( + serverUri, + soakIterations, + soakMaxFailures, + soakPerIterationMaxAcceptableLatencyMs, + soakMinTimeMsBetweenRpcs, + soakOverallTimeoutSeconds, + soakRequestSize, + soakResponseSize, + 1, + (currentChannel) -> createNewChannel(currentChannel)); + } + break; + default: + throw new RuntimeException("invalid testcase: " + testCase); + } + logger.info("Test case: " + testCase + " done for server: " + serverUri); runSucceeded = true; } catch (Exception e) { @@ -295,11 +307,18 @@ protected ManagedChannelBuilder createChannelBuilder() { } } - private void run() throws Exception { + private void run() throws InterruptedException { logger.info("Begin test case: " + testCase); ArrayList threads = new ArrayList<>(); for (InnerClient c : clients) { - Thread t = new Thread(c::run); + Thread t = new Thread(() -> { + try { + c.run(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); // Properly re-interrupt the thread + throw new RuntimeException("Thread was interrupted during execution", e); + } + }); t.start(); threads.add(t); } From e58c998a42d07a8745cfd5ed19f4e211a4400938 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Mon, 18 Nov 2024 17:31:01 -0800 Subject: [PATCH 090/591] AndroidComponentAddress includes a target UserHandle (#11670) The target UserHandle is best modeled as part of the SocketAddress not the Channel since it's part of the server's location. This change allows a NameResolver to select different target users over time within a single Channel. --- .../grpc/binder/AndroidComponentAddress.java | 90 +++++++++++++++++-- .../io/grpc/binder/BinderChannelBuilder.java | 20 +++-- .../BinderClientTransportFactory.java | 10 +-- .../grpc/binder/internal/BinderTransport.java | 4 +- .../binder/AndroidComponentAddressTest.java | 77 +++++++++++++++- 5 files changed, 181 insertions(+), 20 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java b/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java index 6c1026e2127..c4c17bb2cef 100644 --- a/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java +++ b/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java @@ -18,10 +18,14 @@ import static android.content.Intent.URI_ANDROID_APP_SCHEME; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.os.UserHandle; +import com.google.common.base.Objects; +import io.grpc.ExperimentalApi; import java.net.SocketAddress; import javax.annotation.Nullable; @@ -41,18 +45,25 @@ * fields, namely, an action of {@link ApiConstants#ACTION_BIND}, an empty category set and null * type and data URI. * - *

The semantics of {@link #equals(Object)} are the same as {@link Intent#filterEquals(Intent)}. + *

Optionally contains a {@link UserHandle} that must be considered wherever the {@link Intent} + * is evaluated. + * + *

{@link #equals(Object)} uses {@link Intent#filterEquals(Intent)} semantics to compare Intents. */ public final class AndroidComponentAddress extends SocketAddress { private static final long serialVersionUID = 0L; private final Intent bindIntent; // "Explicit", having either a component or package restriction. - protected AndroidComponentAddress(Intent bindIntent) { + @Nullable + private final UserHandle targetUser; // null means the same user that hosts this process. + + protected AndroidComponentAddress(Intent bindIntent, @Nullable UserHandle targetUser) { checkArgument( bindIntent.getComponent() != null || bindIntent.getPackage() != null, "'bindIntent' must be explicit. Specify either a package or ComponentName."); this.bindIntent = bindIntent; + this.targetUser = targetUser; } /** @@ -99,7 +110,7 @@ public static AndroidComponentAddress forRemoteComponent( * @throws IllegalArgumentException if 'intent' isn't "explicit" */ public static AndroidComponentAddress forBindIntent(Intent intent) { - return new AndroidComponentAddress(intent.cloneFilter()); + return new AndroidComponentAddress(intent.cloneFilter(), null); } /** @@ -108,7 +119,7 @@ public static AndroidComponentAddress forBindIntent(Intent intent) { */ public static AndroidComponentAddress forComponent(ComponentName component) { return new AndroidComponentAddress( - new Intent(ApiConstants.ACTION_BIND).setComponent(component)); + new Intent(ApiConstants.ACTION_BIND).setComponent(component), null); } /** @@ -141,6 +152,9 @@ public ComponentName getComponent() { /** * Returns this address as an explicit {@link Intent} suitable for passing to {@link * Context#bindService}. + * + *

NB: The returned Intent does not specify a target Android user. If {@link #getTargetUser()} + * is non-null, {@link Context#bindServiceAsUser} should be called instead. */ public Intent asBindIntent() { return bindIntent.cloneFilter(); // Intent is mutable so return a copy. @@ -177,13 +191,77 @@ public int hashCode() { public boolean equals(Object obj) { if (obj instanceof AndroidComponentAddress) { AndroidComponentAddress that = (AndroidComponentAddress) obj; - return bindIntent.filterEquals(that.bindIntent); + return bindIntent.filterEquals(that.bindIntent) + && Objects.equal(this.targetUser, that.targetUser); } return false; } @Override public String toString() { - return "AndroidComponentAddress[" + bindIntent + "]"; + StringBuilder builder = new StringBuilder("AndroidComponentAddress["); + if (targetUser != null) { + builder.append(targetUser); + builder.append("@"); + } + builder.append(bindIntent); + builder.append("]"); + return builder.toString(); + } + + /** + * Identifies the Android user in which the bind Intent will be evaluated. + * + *

Returns the {@link UserHandle}, or null which means that the Android user hosting the + * current process will be used. + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173") + @Nullable + public UserHandle getTargetUser() { + return targetUser; + } + + public static Builder newBuilder() { + return new Builder(); + } + + /** Fluently builds instances of {@link AndroidComponentAddress}. */ + public static class Builder { + Intent bindIntent; + UserHandle targetUser; + + /** + * Sets the binding {@link Intent} to one having the "filter matching" fields of 'intent'. + * + *

'intent' must be "explicit", i.e. having either a target component ({@link + * Intent#getComponent()}) or package restriction ({@link Intent#getPackage()}). + */ + public Builder setBindIntent(Intent intent) { + this.bindIntent = intent.cloneFilter(); + return this; + } + + /** + * Sets the binding {@link Intent} to one with the specified 'component' and default values for + * all other fields, for convenience. + */ + public Builder setBindIntentFromComponent(ComponentName component) { + this.bindIntent = new Intent(ApiConstants.ACTION_BIND).setComponent(component); + return this; + } + + /** See {@link AndroidComponentAddress#getTargetUser()}. */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173") + public Builder setTargetUser(@Nullable UserHandle targetUser) { + this.targetUser = targetUser; + return this; + } + + public AndroidComponentAddress build() { + // We clone any incoming mutable intent in the setter, not here. AndroidComponentAddress + // itself is immutable so multiple instances built from here can safely share 'bindIntent'. + checkState(bindIntent != null, "Required property 'bindIntent' unset"); + return new AndroidComponentAddress(bindIntent, targetUser); + } } } diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java index d054c8d8ba6..0b8f0bb4b3f 100644 --- a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java +++ b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java @@ -236,20 +236,28 @@ public BinderChannelBuilder securityPolicy(SecurityPolicy securityPolicy) { } /** - * Provides the target {@UserHandle} of the remote Android service. + * Specifies the {@link UserHandle} to be searched for the remote Android Service by default. * - *

When targetUserHandle is set, Context.bindServiceAsUser will used and additional Android - * permissions will be required. If your usage does not require cross-user communications, please - * do not set this field. It is the caller's responsibility to make sure that it holds the - * corresponding permissions. + *

Used only as a fallback if the direct or resolved {@link AndroidComponentAddress} doesn't + * specify a {@link UserHandle}. If neither the Channel nor the {@link AndroidComponentAddress} + * specifies a target user, the {@link UserHandle} of the current process will be used. * + *

Targeting a Service in a different Android user is uncommon and requires special permissions + * normally reserved for system apps. See {@link android.content.Context#bindServiceAsUser} for + * details. + * + * @deprecated This method's name is misleading because it implies an impersonated client identity + * when it's actually specifying part of the server's location. It's also no longer necessary + * since the target user is part of {@link AndroidComponentAddress}. Prefer to specify target + * user in the address instead, either directly or via a {@link io.grpc.NameResolverProvider}. * @param targetUserHandle the target user to bind into. * @return this */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173") @RequiresApi(30) + @Deprecated public BinderChannelBuilder bindAsUser(UserHandle targetUserHandle) { - transportFactoryBuilder.setTargetUserHandle(targetUserHandle); + transportFactoryBuilder.setDefaultTargetUserHandle(targetUserHandle); return this; } diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java index 1e2b80b2fdb..3852b21d5c3 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java @@ -50,7 +50,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor final ObjectPool scheduledExecutorPool; final ObjectPool offloadExecutorPool; final SecurityPolicy securityPolicy; - @Nullable final UserHandle targetUserHandle; + @Nullable final UserHandle defaultTargetUserHandle; final BindServiceFlags bindServiceFlags; final InboundParcelablePolicy inboundParcelablePolicy; final OneWayBinderProxy.Decorator binderDecorator; @@ -70,7 +70,7 @@ private BinderClientTransportFactory(Builder builder) { scheduledExecutorPool = checkNotNull(builder.scheduledExecutorPool); offloadExecutorPool = checkNotNull(builder.offloadExecutorPool); securityPolicy = checkNotNull(builder.securityPolicy); - targetUserHandle = builder.targetUserHandle; + defaultTargetUserHandle = builder.defaultTargetUserHandle; bindServiceFlags = checkNotNull(builder.bindServiceFlags); inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy); binderDecorator = checkNotNull(builder.binderDecorator); @@ -123,7 +123,7 @@ public static final class Builder implements ClientTransportFactoryBuilder { ObjectPool scheduledExecutorPool = SharedResourcePool.forResource(GrpcUtil.TIMER_SERVICE); SecurityPolicy securityPolicy = SecurityPolicies.internalOnly(); - @Nullable UserHandle targetUserHandle; + @Nullable UserHandle defaultTargetUserHandle; BindServiceFlags bindServiceFlags = BindServiceFlags.DEFAULTS; InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT; OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR; @@ -165,8 +165,8 @@ public Builder setSecurityPolicy(SecurityPolicy securityPolicy) { return this; } - public Builder setTargetUserHandle(@Nullable UserHandle targetUserHandle) { - this.targetUserHandle = targetUserHandle; + public Builder setDefaultTargetUserHandle(@Nullable UserHandle defaultTargetUserHandle) { + this.defaultTargetUserHandle = defaultTargetUserHandle; return this; } diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 8c428fac98c..254ad5bb407 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -611,7 +611,9 @@ public BinderClientTransport( factory.sourceContext, factory.channelCredentials, targetAddress.asBindIntent(), - factory.targetUserHandle, + targetAddress.getTargetUser() != null + ? targetAddress.getTargetUser() + : factory.defaultTargetUserHandle, factory.bindServiceFlags.toInteger(), this); } diff --git a/binder/src/test/java/io/grpc/binder/AndroidComponentAddressTest.java b/binder/src/test/java/io/grpc/binder/AndroidComponentAddressTest.java index 6d7e53e5a19..d7d77d7feb1 100644 --- a/binder/src/test/java/io/grpc/binder/AndroidComponentAddressTest.java +++ b/binder/src/test/java/io/grpc/binder/AndroidComponentAddressTest.java @@ -18,11 +18,14 @@ import static android.content.Intent.URI_ANDROID_APP_SCHEME; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.Parcel; +import android.os.UserHandle; import androidx.test.core.app.ApplicationProvider; import com.google.common.testing.EqualsTester; import java.net.URISyntaxException; @@ -83,6 +86,32 @@ public void testAsBindIntent() { assertThat(addr.asBindIntent().filterEquals(bindIntent)).isTrue(); } + @Test + public void testPostCreateIntentMutation() { + Intent bindIntent = new Intent().setAction("foo-action").setComponent(hostComponent); + AndroidComponentAddress addr = AndroidComponentAddress.forBindIntent(bindIntent); + bindIntent.setAction("bar-action"); + assertThat(addr.asBindIntent().getAction()).isEqualTo("foo-action"); + } + + @Test + public void testPostBuildIntentMutation() { + Intent bindIntent = new Intent().setAction("foo-action").setComponent(hostComponent); + AndroidComponentAddress addr = + AndroidComponentAddress.newBuilder().setBindIntent(bindIntent).build(); + bindIntent.setAction("bar-action"); + assertThat(addr.asBindIntent().getAction()).isEqualTo("foo-action"); + } + + @Test + public void testBuilderMissingRequired() { + IllegalStateException ise = + assertThrows( + IllegalStateException.class, + () -> AndroidComponentAddress.newBuilder().setTargetUser(newUserHandle(123)).build()); + assertThat(ise.getMessage()).contains("bindIntent"); + } + @Test @Config(sdk = 30) public void testAsAndroidAppUriSdk30() throws URISyntaxException { @@ -117,13 +146,21 @@ public void testEquality() { AndroidComponentAddress.forContext(appContext), AndroidComponentAddress.forLocalComponent(appContext, appContext.getClass()), AndroidComponentAddress.forRemoteComponent( - appContext.getPackageName(), appContext.getClass().getName())) + appContext.getPackageName(), appContext.getClass().getName()), + AndroidComponentAddress.newBuilder() + .setBindIntentFromComponent(hostComponent) + .setTargetUser(null) + .build()) .addEqualityGroup( AndroidComponentAddress.forRemoteComponent("appy.mcappface", ".McActivity")) .addEqualityGroup(AndroidComponentAddress.forLocalComponent(appContext, getClass())) .addEqualityGroup( AndroidComponentAddress.forBindIntent( - new Intent().setAction("custom-action").setComponent(hostComponent))) + new Intent().setAction("custom-action").setComponent(hostComponent)), + AndroidComponentAddress.newBuilder() + .setBindIntent(new Intent().setAction("custom-action").setComponent(hostComponent)) + .setTargetUser(null) + .build()) .addEqualityGroup( AndroidComponentAddress.forBindIntent( new Intent() @@ -133,6 +170,31 @@ public void testEquality() { .testEquals(); } + @Test + public void testUnequalTargetUsers() { + new EqualsTester() + .addEqualityGroup( + AndroidComponentAddress.newBuilder() + .setBindIntentFromComponent(hostComponent) + .setTargetUser(newUserHandle(10)) + .build(), + AndroidComponentAddress.newBuilder() + .setBindIntentFromComponent(hostComponent) + .setTargetUser(newUserHandle(10)) + .build()) + .addEqualityGroup( + AndroidComponentAddress.newBuilder() + .setBindIntentFromComponent(hostComponent) + .setTargetUser(newUserHandle(11)) + .build()) + .addEqualityGroup( + AndroidComponentAddress.newBuilder() + .setBindIntentFromComponent(hostComponent) + .setTargetUser(null) + .build()) + .testEquals(); + } + @Test @Config(sdk = 30) public void testPackageFilterEquality30AndUp() { @@ -163,4 +225,15 @@ public void testPackageFilterEqualityPre30() { .setComponent(new ComponentName("pkg", "cls")))) .testEquals(); } + + private static UserHandle newUserHandle(int userId) { + Parcel parcel = Parcel.obtain(); + try { + parcel.writeInt(userId); + parcel.setDataPosition(0); + return new UserHandle(parcel); + } finally { + parcel.recycle(); + } + } } From 32f4cf432a5258af64822e3dadde2778f389c260 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 18 Nov 2024 07:21:06 -0800 Subject: [PATCH 091/591] gae-interop-testing: Upgrade to Java 17 Java 11 is out-of-support on GAE. Unfortunately the docs use the term "deprecated" as "deleted," not "discouraged." So they talk about it being deprecated _after_ it is no longer supported. https://cloud.google.com/appengine/docs/standard/lifecycle/support-schedule#java https://cloud.google.com/appengine/docs/flexible/lifecycle/support-schedule#java --- .../gae-jdk8/src/main/webapp/WEB-INF/appengine-web.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gae-interop-testing/gae-jdk8/src/main/webapp/WEB-INF/appengine-web.xml b/gae-interop-testing/gae-jdk8/src/main/webapp/WEB-INF/appengine-web.xml index 2fcbe5d8221..715906ada47 100644 --- a/gae-interop-testing/gae-jdk8/src/main/webapp/WEB-INF/appengine-web.xml +++ b/gae-interop-testing/gae-jdk8/src/main/webapp/WEB-INF/appengine-web.xml @@ -14,6 +14,6 @@ java-gae-interop-test - java11 + java17 From 92de2f34dcbd7efa6f5b74175112d2933253b8b1 Mon Sep 17 00:00:00 2001 From: vinodhabib <47808007+vinodhabib@users.noreply.github.com> Date: Sat, 23 Nov 2024 00:50:25 +0530 Subject: [PATCH 092/591] testing: enabled smallLatency test (#11671) --- .../src/test/java/io/grpc/testing/integration/ProxyTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/ProxyTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/ProxyTest.java index f550d657a12..725e98d0fe3 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/ProxyTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/ProxyTest.java @@ -62,7 +62,6 @@ public void shutdownTest() throws IOException { } @Test - @org.junit.Ignore // flaky. latency commonly too high public void smallLatency() throws Exception { server = new Server(); int serverPort = server.init(); From 20d09cee57d3b1445c9903f2eb578d99bcaba81c Mon Sep 17 00:00:00 2001 From: Vindhya Ningegowda Date: Mon, 25 Nov 2024 16:47:32 -0800 Subject: [PATCH 093/591] xds: Add counter and gauge metrics (#11661) Adds the following xDS client metrics defined in [A78](https://github.com/grpc/proposal/blob/master/A78-grpc-metrics-wrr-pf-xds.md#xdsclient). Counters - grpc.xds_client.server_failure - grpc.xds_client.resource_updates_valid - grpc.xds_client.resource_updates_invalid Gauges - grpc.xds_client.connected - grpc.xds_client.resources --- api/src/main/java/io/grpc/NameResolver.java | 27 +- .../test/java/io/grpc/NameResolverTest.java | 4 + .../io/grpc/internal/ManagedChannelImpl.java | 5 +- .../grpc/internal/ManagedChannelImplTest.java | 25 + .../InternalSharedXdsClientPoolProvider.java | 8 +- .../grpc/xds/SharedXdsClientPoolProvider.java | 28 +- .../grpc/xds/XdsClientMetricReporterImpl.java | 215 +++++++ .../io/grpc/xds/XdsClientPoolFactory.java | 4 +- .../java/io/grpc/xds/XdsNameResolver.java | 14 +- .../io/grpc/xds/XdsNameResolverProvider.java | 3 +- .../java/io/grpc/xds/XdsServerWrapper.java | 5 +- .../grpc/xds/client/ControlPlaneClient.java | 17 + .../java/io/grpc/xds/client/XdsClient.java | 47 +- .../io/grpc/xds/client/XdsClientImpl.java | 68 ++- .../xds/client/XdsClientMetricReporter.java | 48 ++ .../java/io/grpc/xds/CsdsServiceTest.java | 4 +- .../grpc/xds/GrpcXdsClientImplTestBase.java | 573 ++++++++++++++---- .../xds/SharedXdsClientPoolProviderTest.java | 17 +- .../io/grpc/xds/XdsClientFederationTest.java | 4 +- .../xds/XdsClientMetricReporterImplTest.java | 394 ++++++++++++ .../grpc/xds/XdsNameResolverProviderTest.java | 2 + .../java/io/grpc/xds/XdsNameResolverTest.java | 49 +- .../java/io/grpc/xds/XdsServerTestHelper.java | 4 +- 23 files changed, 1381 insertions(+), 184 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java create mode 100644 xds/src/main/java/io/grpc/xds/client/XdsClientMetricReporter.java create mode 100644 xds/src/test/java/io/grpc/xds/XdsClientMetricReporterImplTest.java diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java index a1dea016377..b35601289a3 100644 --- a/api/src/main/java/io/grpc/NameResolver.java +++ b/api/src/main/java/io/grpc/NameResolver.java @@ -290,6 +290,7 @@ public static final class Args { @Nullable private final ChannelLogger channelLogger; @Nullable private final Executor executor; @Nullable private final String overrideAuthority; + @Nullable private final MetricRecorder metricRecorder; private Args( Integer defaultPort, @@ -299,7 +300,8 @@ private Args( @Nullable ScheduledExecutorService scheduledExecutorService, @Nullable ChannelLogger channelLogger, @Nullable Executor executor, - @Nullable String overrideAuthority) { + @Nullable String overrideAuthority, + @Nullable MetricRecorder metricRecorder) { this.defaultPort = checkNotNull(defaultPort, "defaultPort not set"); this.proxyDetector = checkNotNull(proxyDetector, "proxyDetector not set"); this.syncContext = checkNotNull(syncContext, "syncContext not set"); @@ -308,6 +310,7 @@ private Args( this.channelLogger = channelLogger; this.executor = executor; this.overrideAuthority = overrideAuthority; + this.metricRecorder = metricRecorder; } /** @@ -405,6 +408,14 @@ public String getOverrideAuthority() { return overrideAuthority; } + /** + * Returns the {@link MetricRecorder} that the channel uses to record metrics. + */ + @Nullable + public MetricRecorder getMetricRecorder() { + return metricRecorder; + } + @Override public String toString() { @@ -417,6 +428,7 @@ public String toString() { .add("channelLogger", channelLogger) .add("executor", executor) .add("overrideAuthority", overrideAuthority) + .add("metricRecorder", metricRecorder) .toString(); } @@ -435,6 +447,7 @@ public Builder toBuilder() { builder.setChannelLogger(channelLogger); builder.setOffloadExecutor(executor); builder.setOverrideAuthority(overrideAuthority); + builder.setMetricRecorder(metricRecorder); return builder; } @@ -461,6 +474,7 @@ public static final class Builder { private ChannelLogger channelLogger; private Executor executor; private String overrideAuthority; + private MetricRecorder metricRecorder; Builder() { } @@ -547,6 +561,14 @@ public Builder setOverrideAuthority(String authority) { return this; } + /** + * See {@link Args#getMetricRecorder()}. This is an optional field. + */ + public Builder setMetricRecorder(MetricRecorder metricRecorder) { + this.metricRecorder = metricRecorder; + return this; + } + /** * Builds an {@link Args}. * @@ -556,7 +578,8 @@ public Args build() { return new Args( defaultPort, proxyDetector, syncContext, serviceConfigParser, - scheduledExecutorService, channelLogger, executor, overrideAuthority); + scheduledExecutorService, channelLogger, executor, overrideAuthority, + metricRecorder); } } } diff --git a/api/src/test/java/io/grpc/NameResolverTest.java b/api/src/test/java/io/grpc/NameResolverTest.java index 1bc32ee7b1d..1a7c59f8df5 100644 --- a/api/src/test/java/io/grpc/NameResolverTest.java +++ b/api/src/test/java/io/grpc/NameResolverTest.java @@ -64,6 +64,7 @@ public class NameResolverTest { private final ChannelLogger channelLogger = mock(ChannelLogger.class); private final Executor executor = Executors.newSingleThreadExecutor(); private final String overrideAuthority = "grpc.io"; + private final MetricRecorder metricRecorder = new MetricRecorder() {}; @Mock NameResolver.Listener mockListener; @Test @@ -77,6 +78,7 @@ public void args() { assertThat(args.getChannelLogger()).isSameInstanceAs(channelLogger); assertThat(args.getOffloadExecutor()).isSameInstanceAs(executor); assertThat(args.getOverrideAuthority()).isSameInstanceAs(overrideAuthority); + assertThat(args.getMetricRecorder()).isSameInstanceAs(metricRecorder); NameResolver.Args args2 = args.toBuilder().build(); assertThat(args2.getDefaultPort()).isEqualTo(defaultPort); @@ -87,6 +89,7 @@ public void args() { assertThat(args2.getChannelLogger()).isSameInstanceAs(channelLogger); assertThat(args2.getOffloadExecutor()).isSameInstanceAs(executor); assertThat(args2.getOverrideAuthority()).isSameInstanceAs(overrideAuthority); + assertThat(args.getMetricRecorder()).isSameInstanceAs(metricRecorder); assertThat(args2).isNotSameInstanceAs(args); assertThat(args2).isNotEqualTo(args); @@ -102,6 +105,7 @@ private NameResolver.Args createArgs() { .setChannelLogger(channelLogger) .setOffloadExecutor(executor) .setOverrideAuthority(overrideAuthority) + .setMetricRecorder(metricRecorder) .build(); } diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index b89a126517f..88fbf5e2c62 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -589,6 +589,8 @@ ClientStream newSubstream( builder.maxHedgedAttempts, loadBalancerFactory); this.authorityOverride = builder.authorityOverride; + this.metricRecorder = new MetricRecorderImpl(builder.metricSinks, + MetricInstrumentRegistry.getDefaultRegistry()); this.nameResolverArgs = NameResolver.Args.newBuilder() .setDefaultPort(builder.getDefaultPort()) @@ -599,6 +601,7 @@ ClientStream newSubstream( .setChannelLogger(channelLogger) .setOffloadExecutor(this.offloadExecutorHolder) .setOverrideAuthority(this.authorityOverride) + .setMetricRecorder(this.metricRecorder) .build(); this.nameResolver = getNameResolver( targetUri, authorityOverride, nameResolverProvider, nameResolverArgs); @@ -671,8 +674,6 @@ public CallTracer create() { } serviceConfigUpdated = true; } - this.metricRecorder = new MetricRecorderImpl(builder.metricSinks, - MetricInstrumentRegistry.getDefaultRegistry()); } @VisibleForTesting diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 16700096827..535086716bd 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -687,6 +687,30 @@ public void metricRecorder_recordsToMetricSink() { eq(optionalLabelValues)); } + @Test + public void metricRecorder_fromNameResolverArgs_recordsToMetricSink() { + MetricSink mockSink1 = mock(MetricSink.class); + MetricSink mockSink2 = mock(MetricSink.class); + channelBuilder.addMetricSink(mockSink1); + channelBuilder.addMetricSink(mockSink2); + createChannel(); + + LongCounterMetricInstrument counter = metricInstrumentRegistry.registerLongCounter( + "test_counter", "Time taken by metric recorder", "s", + ImmutableList.of("grpc.method"), Collections.emptyList(), false); + List requiredLabelValues = ImmutableList.of("testMethod"); + List optionalLabelValues = Collections.emptyList(); + + NameResolver.Args args = helper.getNameResolverArgs(); + assertThat(args.getMetricRecorder()).isNotNull(); + args.getMetricRecorder() + .addLongCounter(counter, 10, requiredLabelValues, optionalLabelValues); + verify(mockSink1).addLongCounter(eq(counter), eq(10L), eq(requiredLabelValues), + eq(optionalLabelValues)); + verify(mockSink2).addLongCounter(eq(counter), eq(10L), eq(requiredLabelValues), + eq(optionalLabelValues)); + } + @Test public void shutdownWithNoTransportsEverCreated() { channelBuilder.nameResolverFactory( @@ -2240,6 +2264,7 @@ public void lbHelper_getNameResolverArgs() { assertThat(args.getSynchronizationContext()) .isSameInstanceAs(helper.getSynchronizationContext()); assertThat(args.getServiceConfigParser()).isNotNull(); + assertThat(args.getMetricRecorder()).isNotNull(); } @Test diff --git a/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java index 0073cce1a88..85b59fabfa0 100644 --- a/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java +++ b/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java @@ -17,6 +17,7 @@ package io.grpc.xds; import io.grpc.Internal; +import io.grpc.MetricRecorder; import io.grpc.internal.ObjectPool; import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsInitializationException; @@ -36,6 +37,11 @@ public static void setDefaultProviderBootstrapOverride(Map bootstrap) public static ObjectPool getOrCreate(String target) throws XdsInitializationException { - return SharedXdsClientPoolProvider.getDefaultProvider().getOrCreate(target); + return getOrCreate(target, new MetricRecorder() {}); + } + + public static ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) + throws XdsInitializationException { + return SharedXdsClientPoolProvider.getDefaultProvider().getOrCreate(target, metricRecorder); } } diff --git a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java index c9195896d82..779349744ff 100644 --- a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java +++ b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java @@ -21,6 +21,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import io.grpc.MetricRecorder; import io.grpc.internal.ExponentialBackoffPolicy; import io.grpc.internal.GrpcUtil; import io.grpc.internal.ObjectPool; @@ -51,6 +52,8 @@ final class SharedXdsClientPoolProvider implements XdsClientPoolFactory { private static final boolean LOG_XDS_NODE_ID = Boolean.parseBoolean( System.getenv("GRPC_LOG_XDS_NODE_ID")); private static final Logger log = Logger.getLogger(XdsClientImpl.class.getName()); + private static final ExponentialBackoffPolicy.Provider BACKOFF_POLICY_PROVIDER = + new ExponentialBackoffPolicy.Provider(); private final Bootstrapper bootstrapper; private final Object lock = new Object(); @@ -82,7 +85,8 @@ public ObjectPool get(String target) { } @Override - public ObjectPool getOrCreate(String target) throws XdsInitializationException { + public ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) + throws XdsInitializationException { ObjectPool ref = targetToXdsClientMap.get(target); if (ref == null) { synchronized (lock) { @@ -98,7 +102,7 @@ public ObjectPool getOrCreate(String target) throws XdsInitialization if (bootstrapInfo.servers().isEmpty()) { throw new XdsInitializationException("No xDS server provided"); } - ref = new RefCountedXdsClientObjectPool(bootstrapInfo, target); + ref = new RefCountedXdsClientObjectPool(bootstrapInfo, target, metricRecorder); targetToXdsClientMap.put(target, ref); } } @@ -111,19 +115,17 @@ public ImmutableList getTargets() { return ImmutableList.copyOf(targetToXdsClientMap.keySet()); } - private static class SharedXdsClientPoolProviderHolder { private static final SharedXdsClientPoolProvider instance = new SharedXdsClientPoolProvider(); } @ThreadSafe @VisibleForTesting - static class RefCountedXdsClientObjectPool implements ObjectPool { + class RefCountedXdsClientObjectPool implements ObjectPool { - private static final ExponentialBackoffPolicy.Provider BACKOFF_POLICY_PROVIDER = - new ExponentialBackoffPolicy.Provider(); private final BootstrapInfo bootstrapInfo; private final String target; // The target associated with the xDS client. + private final MetricRecorder metricRecorder; private final Object lock = new Object(); @GuardedBy("lock") private ScheduledExecutorService scheduler; @@ -131,11 +133,15 @@ static class RefCountedXdsClientObjectPool implements ObjectPool { private XdsClient xdsClient; @GuardedBy("lock") private int refCount; + @GuardedBy("lock") + private XdsClientMetricReporterImpl metricReporter; @VisibleForTesting - RefCountedXdsClientObjectPool(BootstrapInfo bootstrapInfo, String target) { + RefCountedXdsClientObjectPool(BootstrapInfo bootstrapInfo, String target, + MetricRecorder metricRecorder) { this.bootstrapInfo = checkNotNull(bootstrapInfo); this.target = target; + this.metricRecorder = metricRecorder; } @Override @@ -146,6 +152,7 @@ public XdsClient getObject() { log.log(Level.INFO, "xDS node ID: {0}", bootstrapInfo.node().getId()); } scheduler = SharedResourceHolder.get(GrpcUtil.TIMER_SERVICE); + metricReporter = new XdsClientMetricReporterImpl(metricRecorder, target); xdsClient = new XdsClientImpl( DEFAULT_XDS_TRANSPORT_FACTORY, bootstrapInfo, @@ -154,7 +161,9 @@ public XdsClient getObject() { GrpcUtil.STOPWATCH_SUPPLIER, TimeProvider.SYSTEM_TIME_PROVIDER, MessagePrinter.INSTANCE, - new TlsContextManagerImpl(bootstrapInfo)); + new TlsContextManagerImpl(bootstrapInfo), + metricReporter); + metricReporter.setXdsClient(xdsClient); } refCount++; return xdsClient; @@ -168,6 +177,9 @@ public XdsClient returnObject(Object object) { if (refCount == 0) { xdsClient.shutdown(); xdsClient = null; + metricReporter.close(); + metricReporter = null; + targetToXdsClientMap.remove(target); scheduler = SharedResourceHolder.release(GrpcUtil.TIMER_SERVICE, scheduler); } return null; diff --git a/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java b/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java new file mode 100644 index 00000000000..fa88237a7ea --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java @@ -0,0 +1,215 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ListenableFuture; +import io.grpc.LongCounterMetricInstrument; +import io.grpc.LongGaugeMetricInstrument; +import io.grpc.MetricInstrumentRegistry; +import io.grpc.MetricRecorder; +import io.grpc.MetricRecorder.BatchCallback; +import io.grpc.MetricRecorder.BatchRecorder; +import io.grpc.MetricRecorder.Registration; +import io.grpc.xds.client.XdsClient; +import io.grpc.xds.client.XdsClient.ResourceMetadata; +import io.grpc.xds.client.XdsClient.ResourceMetadata.ResourceMetadataStatus; +import io.grpc.xds.client.XdsClient.ServerConnectionCallback; +import io.grpc.xds.client.XdsClientMetricReporter; +import io.grpc.xds.client.XdsResourceType; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * XdsClientMetricReporter implementation. + */ +final class XdsClientMetricReporterImpl implements XdsClientMetricReporter { + + private static final Logger logger = Logger.getLogger( + XdsClientMetricReporterImpl.class.getName()); + private static final LongCounterMetricInstrument SERVER_FAILURE_COUNTER; + private static final LongCounterMetricInstrument RESOURCE_UPDATES_VALID_COUNTER; + private static final LongCounterMetricInstrument RESOURCE_UPDATES_INVALID_COUNTER; + private static final LongGaugeMetricInstrument CONNECTED_GAUGE; + private static final LongGaugeMetricInstrument RESOURCES_GAUGE; + + private final MetricRecorder metricRecorder; + private final String target; + @Nullable + private Registration gaugeRegistration = null; + + static { + MetricInstrumentRegistry metricInstrumentRegistry + = MetricInstrumentRegistry.getDefaultRegistry(); + SERVER_FAILURE_COUNTER = metricInstrumentRegistry.registerLongCounter( + "grpc.xds_client.server_failure", + "EXPERIMENTAL. A counter of xDS servers going from healthy to unhealthy. A server goes" + + " unhealthy when we have a connectivity failure or when the ADS stream fails without" + + " seeing a response message, as per gRFC A57.", "{failure}", + Arrays.asList("grpc.target", "grpc.xds.server"), Collections.emptyList(), false); + RESOURCE_UPDATES_VALID_COUNTER = metricInstrumentRegistry.registerLongCounter( + "grpc.xds_client.resource_updates_valid", + "EXPERIMENTAL. A counter of resources received that were considered valid. The counter will" + + " be incremented even for resources that have not changed.", "{resource}", + Arrays.asList("grpc.target", "grpc.xds.server", "grpc.xds.resource_type"), + Collections.emptyList(), false); + RESOURCE_UPDATES_INVALID_COUNTER = metricInstrumentRegistry.registerLongCounter( + "grpc.xds_client.resource_updates_invalid", + "EXPERIMENTAL. A counter of resources received that were considered invalid.", "{resource}", + Arrays.asList("grpc.target", "grpc.xds.server", "grpc.xds.resource_type"), + Collections.emptyList(), false); + CONNECTED_GAUGE = metricInstrumentRegistry.registerLongGauge("grpc.xds_client.connected", + "EXPERIMENTAL. Whether or not the xDS client currently has a working ADS stream to the xDS" + + " server. For a given server, this will be set to 1 when the stream is initially" + + " created. It will be set to 0 when we have a connectivity failure or when the ADS" + + " stream fails without seeing a response message, as per gRFC A57. Once set to 0, it" + + " will be reset to 1 when we receive the first response on an ADS stream.", "{bool}", + Arrays.asList("grpc.target", "grpc.xds.server"), Collections.emptyList(), false); + RESOURCES_GAUGE = metricInstrumentRegistry.registerLongGauge("grpc.xds_client.resources", + "EXPERIMENTAL. Number of xDS resources.", "{resource}", + Arrays.asList("grpc.target", "grpc.xds.authority", "grpc.xds.cache_state", + "grpc.xds.resource_type"), Collections.emptyList(), false); + } + + XdsClientMetricReporterImpl(MetricRecorder metricRecorder, String target) { + this.metricRecorder = metricRecorder; + this.target = target; + } + + @Override + public void reportResourceUpdates(long validResourceCount, long invalidResourceCount, + String xdsServer, String resourceType) { + metricRecorder.addLongCounter(RESOURCE_UPDATES_VALID_COUNTER, validResourceCount, + Arrays.asList(target, xdsServer, resourceType), Collections.emptyList()); + metricRecorder.addLongCounter(RESOURCE_UPDATES_INVALID_COUNTER, invalidResourceCount, + Arrays.asList(target, xdsServer, resourceType), Collections.emptyList()); + } + + @Override + public void reportServerFailure(long serverFailure, String xdsServer) { + metricRecorder.addLongCounter(SERVER_FAILURE_COUNTER, serverFailure, + Arrays.asList(target, xdsServer), Collections.emptyList()); + } + + void setXdsClient(XdsClient xdsClient) { + assert gaugeRegistration == null; + // register gauge here + this.gaugeRegistration = metricRecorder.registerBatchCallback(new BatchCallback() { + @Override + public void accept(BatchRecorder recorder) { + reportCallbackMetrics(recorder, xdsClient); + } + }, CONNECTED_GAUGE, RESOURCES_GAUGE); + } + + void close() { + if (gaugeRegistration != null) { + gaugeRegistration.close(); + gaugeRegistration = null; + } + } + + void reportCallbackMetrics(BatchRecorder recorder, XdsClient xdsClient) { + MetricReporterCallback callback = new MetricReporterCallback(recorder, target); + try { + Future reportServerConnectionsCompleted = xdsClient.reportServerConnections(callback); + + ListenableFuture, Map>> + getResourceMetadataCompleted = xdsClient.getSubscribedResourcesMetadataSnapshot(); + + Map, Map> metadataByType = + getResourceMetadataCompleted.get(10, TimeUnit.SECONDS); + + computeAndReportResourceCounts(metadataByType, callback); + + // Normally this shouldn't take long, but adding a timeout to avoid indefinite blocking + Void unused = reportServerConnectionsCompleted.get(5, TimeUnit.SECONDS); + } catch (ExecutionException | TimeoutException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); // re-set the current thread's interruption state + } + logger.log(Level.WARNING, "Failed to report gauge metrics", e); + } + } + + private void computeAndReportResourceCounts( + Map, Map> metadataByType, + MetricReporterCallback callback) { + for (Map.Entry, Map> metadataByTypeEntry : + metadataByType.entrySet()) { + XdsResourceType type = metadataByTypeEntry.getKey(); + + Map resourceCountsByState = new HashMap<>(); + for (ResourceMetadata metadata : metadataByTypeEntry.getValue().values()) { + String cacheState = cacheStateFromResourceStatus(metadata.getStatus(), metadata.isCached()); + resourceCountsByState.compute(cacheState, (k, v) -> (v == null) ? 1 : v + 1); + } + + resourceCountsByState.forEach((cacheState, count) -> + callback.reportResourceCountGauge(count, cacheState, type.typeUrl())); + } + } + + private static String cacheStateFromResourceStatus(ResourceMetadataStatus metadataStatus, + boolean isResourceCached) { + switch (metadataStatus) { + case REQUESTED: + return "requested"; + case DOES_NOT_EXIST: + return "does_not_exist"; + case ACKED: + return "acked"; + case NACKED: + return isResourceCached ? "nacked_but_cached" : "nacked"; + default: + return "unknown"; + } + } + + @VisibleForTesting + static final class MetricReporterCallback implements ServerConnectionCallback { + private final BatchRecorder recorder; + private final String target; + + MetricReporterCallback(BatchRecorder recorder, String target) { + this.recorder = recorder; + this.target = target; + } + + // TODO(dnvindhya): include the "authority" label once xds.authority is available. + void reportResourceCountGauge(long resourceCount, String cacheState, + String resourceType) { + recorder.recordLongGauge(RESOURCES_GAUGE, resourceCount, + Arrays.asList(target, cacheState, resourceType), Collections.emptyList()); + } + + @Override + public void reportServerConnectionGauge(boolean isConnected, String xdsServer) { + recorder.recordLongGauge(CONNECTED_GAUGE, isConnected ? 1 : 0, + Arrays.asList(target, xdsServer), Collections.emptyList()); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/XdsClientPoolFactory.java b/xds/src/main/java/io/grpc/xds/XdsClientPoolFactory.java index 313eb675116..f10d6504d79 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClientPoolFactory.java +++ b/xds/src/main/java/io/grpc/xds/XdsClientPoolFactory.java @@ -16,6 +16,7 @@ package io.grpc.xds; +import io.grpc.MetricRecorder; import io.grpc.internal.ObjectPool; import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsInitializationException; @@ -29,7 +30,8 @@ interface XdsClientPoolFactory { @Nullable ObjectPool get(String target); - ObjectPool getOrCreate(String target) throws XdsInitializationException; + ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) + throws XdsInitializationException; List getTargets(); } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 0beb6dc2483..c51709c174c 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -41,6 +41,7 @@ import io.grpc.LoadBalancer.PickSubchannelArgs; import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import io.grpc.MetricRecorder; import io.grpc.NameResolver; import io.grpc.Status; import io.grpc.Status.Code; @@ -125,6 +126,7 @@ final class XdsNameResolver extends NameResolver { private final ConcurrentMap clusterRefs = new ConcurrentHashMap<>(); private final ConfigSelector configSelector = new ConfigSelector(); private final long randomChannelId; + private final MetricRecorder metricRecorder; private volatile RoutingConfig routingConfig = RoutingConfig.empty; private Listener2 listener; @@ -140,10 +142,12 @@ final class XdsNameResolver extends NameResolver { URI targetUri, String name, @Nullable String overrideAuthority, ServiceConfigParser serviceConfigParser, SynchronizationContext syncContext, ScheduledExecutorService scheduler, - @Nullable Map bootstrapOverride) { + @Nullable Map bootstrapOverride, + MetricRecorder metricRecorder) { this(targetUri, targetUri.getAuthority(), name, overrideAuthority, serviceConfigParser, syncContext, scheduler, SharedXdsClientPoolProvider.getDefaultProvider(), - ThreadSafeRandomImpl.instance, FilterRegistry.getDefaultRegistry(), bootstrapOverride); + ThreadSafeRandomImpl.instance, FilterRegistry.getDefaultRegistry(), bootstrapOverride, + metricRecorder); } @VisibleForTesting @@ -152,7 +156,8 @@ final class XdsNameResolver extends NameResolver { @Nullable String overrideAuthority, ServiceConfigParser serviceConfigParser, SynchronizationContext syncContext, ScheduledExecutorService scheduler, XdsClientPoolFactory xdsClientPoolFactory, ThreadSafeRandom random, - FilterRegistry filterRegistry, @Nullable Map bootstrapOverride) { + FilterRegistry filterRegistry, @Nullable Map bootstrapOverride, + MetricRecorder metricRecorder) { this.targetAuthority = targetAuthority; target = targetUri.toString(); @@ -170,6 +175,7 @@ final class XdsNameResolver extends NameResolver { this.xdsClientPoolFactory.setBootstrapOverride(bootstrapOverride); this.random = checkNotNull(random, "random"); this.filterRegistry = checkNotNull(filterRegistry, "filterRegistry"); + this.metricRecorder = metricRecorder; randomChannelId = random.nextLong(); logId = InternalLogId.allocate("xds-resolver", name); logger = XdsLogger.withLogId(logId); @@ -185,7 +191,7 @@ public String getServiceAuthority() { public void start(Listener2 listener) { this.listener = checkNotNull(listener, "listener"); try { - xdsClientPool = xdsClientPoolFactory.getOrCreate(target); + xdsClientPool = xdsClientPoolFactory.getOrCreate(target, metricRecorder); } catch (Exception e) { listener.onError( Status.UNAVAILABLE.withDescription("Failed to initialize xDS").withCause(e)); diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java b/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java index 8d0e59eaa91..74518331269 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java @@ -81,7 +81,8 @@ public XdsNameResolver newNameResolver(URI targetUri, Args args) { targetUri, name, args.getOverrideAuthority(), args.getServiceConfigParser(), args.getSynchronizationContext(), args.getScheduledExecutorService(), - bootstrapOverride); + bootstrapOverride, + args.getMetricRecorder()); } return null; } diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index bd622a71124..8c03e64f185 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -29,6 +29,7 @@ import io.grpc.InternalServerInterceptors; import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import io.grpc.MetricRecorder; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.ServerCall; @@ -171,7 +172,9 @@ public void run() { private void internalStart() { try { - xdsClientPool = xdsClientPoolFactory.getOrCreate(""); + // TODO(dnvindhya): Add "#server" as "grpc.target" attribute value for + // xDS enabled servers. + xdsClientPool = xdsClientPoolFactory.getOrCreate("", new MetricRecorder() {}); } catch (Exception e) { StatusException statusException = Status.UNAVAILABLE.withDescription( "Failed to initialize xDS").withCause(e).asException(); diff --git a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java index 5ac979277c4..62076fb8bf1 100644 --- a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java +++ b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java @@ -78,6 +78,7 @@ final class ControlPlaneClient { private final Map, String> versions = new HashMap<>(); private boolean shutdown; + private boolean streamClosedNoResponse; @Nullable private AdsStream adsStream; @Nullable @@ -224,6 +225,19 @@ void readyHandler() { xdsClient.startSubscriberTimersIfNeeded(serverInfo); } + /** + * Indicates whether there is an active ADS stream. + * + *

Return {@code true} when the {@code AdsStream} is created. + * {@code false} when the ADS stream fails without a response. Resets to true + * upon receiving the first response on a new ADS stream. + */ + // Must be synchronized + boolean hasWorkingAdsStream() { + return !streamClosedNoResponse; + } + + /** * Establishes the RPC connection by creating a new RPC stream on the given channel for * xDS protocol communication. @@ -332,6 +346,8 @@ public void onRecvMessage(DiscoveryResponse response) { syncContext.execute(new Runnable() { @Override public void run() { + // Reset flag as message has been received on a stream + streamClosedNoResponse = false; XdsResourceType type = fromTypeUrl(response.getTypeUrl()); if (logger.isLoggable(XdsLogLevel.DEBUG)) { logger.log( @@ -408,6 +424,7 @@ private void handleRpcStreamClosed(Status status) { "ADS stream closed by server after a response was received"); } } else { + streamClosedNoResponse = true; // If the ADS stream is closed without ever having received a response from the server, then // the XdsClient should consider that a connectivity error (see gRFC A57). if (status.isOk()) { diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClient.java b/xds/src/main/java/io/grpc/xds/client/XdsClient.java index fc7e1777384..06f15005c22 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClient.java @@ -36,6 +36,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Executor; +import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; @@ -154,44 +155,46 @@ public static final class ResourceMetadata { private final String version; private final ResourceMetadataStatus status; private final long updateTimeNanos; + private final boolean cached; @Nullable private final Any rawResource; @Nullable private final UpdateFailureState errorState; private ResourceMetadata( - ResourceMetadataStatus status, String version, long updateTimeNanos, + ResourceMetadataStatus status, String version, long updateTimeNanos, boolean cached, @Nullable Any rawResource, @Nullable UpdateFailureState errorState) { this.status = checkNotNull(status, "status"); this.version = checkNotNull(version, "version"); this.updateTimeNanos = updateTimeNanos; + this.cached = cached; this.rawResource = rawResource; this.errorState = errorState; } - static ResourceMetadata newResourceMetadataUnknown() { - return new ResourceMetadata(ResourceMetadataStatus.UNKNOWN, "", 0, null, null); + public static ResourceMetadata newResourceMetadataUnknown() { + return new ResourceMetadata(ResourceMetadataStatus.UNKNOWN, "", 0, false,null, null); } - static ResourceMetadata newResourceMetadataRequested() { - return new ResourceMetadata(ResourceMetadataStatus.REQUESTED, "", 0, null, null); + public static ResourceMetadata newResourceMetadataRequested() { + return new ResourceMetadata(ResourceMetadataStatus.REQUESTED, "", 0, false, null, null); } - static ResourceMetadata newResourceMetadataDoesNotExist() { - return new ResourceMetadata(ResourceMetadataStatus.DOES_NOT_EXIST, "", 0, null, null); + public static ResourceMetadata newResourceMetadataDoesNotExist() { + return new ResourceMetadata(ResourceMetadataStatus.DOES_NOT_EXIST, "", 0, false, null, null); } public static ResourceMetadata newResourceMetadataAcked( Any rawResource, String version, long updateTimeNanos) { checkNotNull(rawResource, "rawResource"); return new ResourceMetadata( - ResourceMetadataStatus.ACKED, version, updateTimeNanos, rawResource, null); + ResourceMetadataStatus.ACKED, version, updateTimeNanos, true, rawResource, null); } - static ResourceMetadata newResourceMetadataNacked( + public static ResourceMetadata newResourceMetadataNacked( ResourceMetadata metadata, String failedVersion, long failedUpdateTime, - String failedDetails) { + String failedDetails, boolean cached) { checkNotNull(metadata, "metadata"); return new ResourceMetadata(ResourceMetadataStatus.NACKED, - metadata.getVersion(), metadata.getUpdateTimeNanos(), metadata.getRawResource(), + metadata.getVersion(), metadata.getUpdateTimeNanos(), cached, metadata.getRawResource(), new UpdateFailureState(failedVersion, failedUpdateTime, failedDetails)); } @@ -210,6 +213,11 @@ public long getUpdateTimeNanos() { return updateTimeNanos; } + /** Returns whether the resource was cached. */ + public boolean isCached() { + return cached; + } + /** The last successfully updated xDS resource as it was returned by the server. */ @Nullable public Any getRawResource() { @@ -378,6 +386,23 @@ public Map getServerLrsClientMap() { throw new UnsupportedOperationException(); } + /** Callback used to report a gauge metric value for server connections. */ + public interface ServerConnectionCallback { + void reportServerConnectionGauge(boolean isConnected, String xdsServer); + } + + /** + * Reports whether xDS client has a "working" ADS stream to xDS server. The definition of a + * working stream is defined in gRFC A78. + * + * @see + * A78-grpc-metrics-wrr-pf-xds.md + */ + public Future reportServerConnections(ServerConnectionCallback callback) { + throw new UnsupportedOperationException(); + } + static final class ProcessingTracker { private final AtomicInteger pendingTask = new AtomicInteger(1); private final Executor executor; diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index e2380b9ed73..529ac2747df 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -41,7 +41,6 @@ import io.grpc.xds.client.Bootstrapper.AuthorityInfo; import io.grpc.xds.client.Bootstrapper.ServerInfo; import io.grpc.xds.client.XdsClient.ResourceStore; -import io.grpc.xds.client.XdsClient.XdsResponseHandler; import io.grpc.xds.client.XdsLogger.XdsLogLevel; import java.net.URI; import java.util.Collection; @@ -52,6 +51,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -60,7 +60,7 @@ * XdsClient implementation. */ @Internal -public final class XdsClientImpl extends XdsClient implements XdsResponseHandler, ResourceStore { +public final class XdsClientImpl extends XdsClient implements ResourceStore { // Longest time to wait, since the subscription to some resource, for concluding its absence. @VisibleForTesting @@ -100,6 +100,7 @@ public void uncaughtException(Thread t, Throwable e) { private final XdsLogger logger; private volatile boolean isShutdown; private final MessagePrettyPrinter messagePrinter; + private final XdsClientMetricReporter metricReporter; public XdsClientImpl( XdsTransportFactory xdsTransportFactory, @@ -109,7 +110,8 @@ public XdsClientImpl( Supplier stopwatchSupplier, TimeProvider timeProvider, MessagePrettyPrinter messagePrinter, - Object securityConfig) { + Object securityConfig, + XdsClientMetricReporter metricReporter) { this.xdsTransportFactory = xdsTransportFactory; this.bootstrapInfo = bootstrapInfo; this.timeService = timeService; @@ -118,13 +120,13 @@ public XdsClientImpl( this.timeProvider = timeProvider; this.messagePrinter = messagePrinter; this.securityConfig = securityConfig; + this.metricReporter = metricReporter; logId = InternalLogId.allocate("xds-client", null); logger = XdsLogger.withLogId(logId); logger.log(XdsLogLevel.INFO, "Created"); } - @Override - public void handleResourceResponse( + private void handleResourceResponse( XdsResourceType xdsResourceType, ServerInfo serverInfo, String versionInfo, List resources, String nonce, ProcessingTracker processingTracker) { checkNotNull(xdsResourceType, "xdsResourceType"); @@ -138,11 +140,11 @@ public void handleResourceResponse( handleResourceUpdate(args, resources, xdsResourceType, processingTracker); } - @Override - public void handleStreamClosed(Status error) { + private void handleStreamClosed(Status error, ServerInfo serverInfo) { syncContext.throwIfNotInThisSynchronizationContext(); cleanUpResourceTimers(); if (!error.isOk()) { + metricReporter.reportServerFailure(1L, serverInfo.target()); for (Map> subscriberMap : resourceSubscribers.values()) { for (ResourceSubscriber subscriber : subscriberMap.values()) { @@ -154,8 +156,7 @@ public void handleStreamClosed(Status error) { } } - @Override - public void handleStreamRestarted(ServerInfo serverInfo) { + private void handleStreamRestarted(ServerInfo serverInfo) { syncContext.throwIfNotInThisSynchronizationContext(); for (Map> subscriberMap : resourceSubscribers.values()) { @@ -394,7 +395,27 @@ public ControlPlaneClient getOrCreateControlPlaneClient(ServerInfo serverInfo) { xdsTransport, serverInfo, bootstrapInfo.node(), - this, + new XdsResponseHandler() { + + @Override + public void handleResourceResponse( + XdsResourceType resourceType, ServerInfo serverInfo, String versionInfo, + List resources, String nonce, ProcessingTracker processingTracker) { + XdsClientImpl.this.handleResourceResponse(resourceType, serverInfo, versionInfo, + resources, nonce, + processingTracker); + } + + @Override + public void handleStreamClosed(Status error) { + XdsClientImpl.this.handleStreamClosed(error, serverInfo); + } + + @Override + public void handleStreamRestarted(ServerInfo serverInfo) { + XdsClientImpl.this.handleStreamRestarted(serverInfo); + } + }, this, timeService, syncContext, @@ -448,6 +469,10 @@ private void handleResourceUpdate( xdsResourceType.typeName(), args.versionInfo, args.nonce, result.unpackedResources); Map> parsedResources = result.parsedResources; Set invalidResources = result.invalidResources; + metricReporter.reportResourceUpdates(Long.valueOf(parsedResources.size()), + Long.valueOf(invalidResources.size()), + args.getServerInfo().target(), xdsResourceType.typeUrl()); + List errors = result.errors; String errorDetail = null; if (errors.isEmpty()) { @@ -504,9 +529,19 @@ private void handleResourceUpdate( } } - /** - * Tracks a single subscribed resource. - */ + @Override + public Future reportServerConnections(ServerConnectionCallback callback) { + SettableFuture future = SettableFuture.create(); + syncContext.execute(() -> { + serverCpClientMap.forEach((serverInfo, controlPlaneClient) -> + callback.reportServerConnectionGauge( + controlPlaneClient.hasWorkingAdsStream(), serverInfo.target())); + future.set(null); + }); + return future; + } + + /** Tracks a single subscribed resource. */ private final class ResourceSubscriber { @Nullable private final ServerInfo serverInfo; @Nullable private final ControlPlaneClient controlPlaneClient; @@ -644,10 +679,10 @@ void onData(ParsedResource parsedResource, String version, long updateTime, respTimer.cancel(); respTimer = null; } - this.metadata = ResourceMetadata - .newResourceMetadataAcked(parsedResource.getRawResource(), version, updateTime); ResourceUpdate oldData = this.data; this.data = parsedResource.getResourceUpdate(); + this.metadata = ResourceMetadata + .newResourceMetadataAcked(parsedResource.getRawResource(), version, updateTime); absent = false; if (resourceDeletionIgnored) { logger.log(XdsLogLevel.FORCE_INFO, "xds server {0}: server returned new version " @@ -741,7 +776,8 @@ void onError(Status error, @Nullable ProcessingTracker tracker) { void onRejected(String rejectedVersion, long rejectedTime, String rejectedDetails) { metadata = ResourceMetadata - .newResourceMetadataNacked(metadata, rejectedVersion, rejectedTime, rejectedDetails); + .newResourceMetadataNacked(metadata, rejectedVersion, rejectedTime, rejectedDetails, + data != null); } private void notifyWatcher(ResourceWatcher watcher, T update) { diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientMetricReporter.java b/xds/src/main/java/io/grpc/xds/client/XdsClientMetricReporter.java new file mode 100644 index 00000000000..a044d501759 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientMetricReporter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.client; + +import io.grpc.Internal; + +/** + * Interface for reporting metrics from the xDS client. + */ +@Internal +public interface XdsClientMetricReporter { + + /** + * Reports number of valid and invalid resources. + * + * @param validResourceCount Number of resources that were valid. + * @param invalidResourceCount Number of resources that were invalid. + * @param xdsServer Target URI of the xDS server with which the XdsClient is communicating. + * @param resourceType Type of XDS resource (e.g., "envoy.config.listener.v3.Listener"). + */ + default void reportResourceUpdates(long validResourceCount, long invalidResourceCount, + String xdsServer, String resourceType) { + } + + /** + * Reports number of xDS servers going from healthy to unhealthy. + * + * @param serverFailure Number of xDS server failures. + * @param xdsServer Target URI of the xDS server with which the XdsClient is communicating. + */ + default void reportServerFailure(long serverFailure, String xdsServer) { + } + +} diff --git a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java index 8166027033f..7c6821dc560 100644 --- a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java +++ b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java @@ -39,6 +39,7 @@ import io.envoyproxy.envoy.type.matcher.v3.NodeMatcher; import io.grpc.Deadline; import io.grpc.InsecureChannelCredentials; +import io.grpc.MetricRecorder; import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.StatusRuntimeException; @@ -553,8 +554,9 @@ public void setBootstrapOverride(Map bootstrap) { throw new UnsupportedOperationException("Should not be called"); } + @Override - public ObjectPool getOrCreate(String target) { + public ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) { throw new UnsupportedOperationException("Should not be called"); } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 8ecb40383b1..198faea7fdc 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -95,7 +95,9 @@ import io.grpc.xds.client.XdsClient.ResourceMetadata.UpdateFailureState; import io.grpc.xds.client.XdsClient.ResourceUpdate; import io.grpc.xds.client.XdsClient.ResourceWatcher; +import io.grpc.xds.client.XdsClient.ServerConnectionCallback; import io.grpc.xds.client.XdsClientImpl; +import io.grpc.xds.client.XdsClientMetricReporter; import io.grpc.xds.client.XdsResourceType; import io.grpc.xds.client.XdsResourceType.ResourceInvalidException; import io.grpc.xds.client.XdsTransportFactory; @@ -112,6 +114,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Executor; +import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -142,6 +145,7 @@ // The base class was used to test both xds v2 and v3. V2 is dropped now so the base class is not // necessary. Still keep it for future version usage. Remove if too much trouble to maintain. public abstract class GrpcXdsClientImplTestBase { + private static final String SERVER_URI = "trafficdirector.googleapis.com"; private static final String SERVER_URI_CUSTOME_AUTHORITY = "trafficdirector2.googleapis.com"; private static final String SERVER_URI_EMPTY_AUTHORITY = "trafficdirector3.googleapis.com"; @@ -287,6 +291,10 @@ public long currentTimeNanos() { private ResourceWatcher cdsResourceWatcher; @Mock private ResourceWatcher edsResourceWatcher; + @Mock + private XdsClientMetricReporter xdsClientMetricReporter; + @Mock + private ServerConnectionCallback serverConnectionCallback; private ManagedChannel channel; private ManagedChannel channelForCustomAuthority; @@ -369,7 +377,8 @@ public XdsTransport create(ServerInfo serverInfo) { fakeClock.getStopwatchSupplier(), timeProvider, MessagePrinter.INSTANCE, - new TlsContextManagerImpl(bootstrapInfo)); + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); assertThat(resourceDiscoveryCalls).isEmpty(); assertThat(loadReportCalls).isEmpty(); @@ -476,10 +485,11 @@ private void verifyResourceMetadataAcked( private void verifyResourceMetadataNacked( XdsResourceType type, String resourceName, Any rawResource, String versionInfo, long updateTime, String failedVersion, long failedUpdateTimeNanos, - List failedDetails) { + List failedDetails, boolean cached) { ResourceMetadata resourceMetadata = verifyResourceMetadata(type, resourceName, rawResource, ResourceMetadataStatus.NACKED, versionInfo, updateTime, true); + assertThat(resourceMetadata.isCached()).isEqualTo(cached); UpdateFailureState errorState = resourceMetadata.getErrorState(); assertThat(errorState).isNotNull(); @@ -602,6 +612,48 @@ private void validateGoldenClusterLoadAssignment(EdsUpdate edsUpdate) { LocalityLbEndpoints.create(ImmutableList.of(), 2, 1)); } + /** + * Verifies that the {@link XdsClientMetricReporter#reportResourceUpdates} method has been called + * the expected number of times with the expected values for valid resource count, invalid + * resource count, and corresponding metric labels. + */ + private void verifyResourceValidInvalidCount(int times, long validResourceCount, + long invalidResourceCount, String xdsServerTargetLabel, + String resourceType) { + verify(xdsClientMetricReporter, times(times)).reportResourceUpdates( + eq(validResourceCount), + eq(invalidResourceCount), + eq(xdsServerTargetLabel), + eq(resourceType)); + } + + private void verifyServerFailureCount(int times, long serverFailureCount, String xdsServer) { + verify(xdsClientMetricReporter, times(times)).reportServerFailure( + eq(serverFailureCount), + eq(xdsServer)); + } + + /** + * Invokes the callback, which will be called by {@link XdsClientMetricReporter} to record + * whether XdsClient has a working ADS stream. + */ + private void callback_ReportServerConnection() { + try { + Future unused = xdsClient.reportServerConnections(serverConnectionCallback); + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new AssertionError(e); + } + } + + private void verifyServerConnection(int times, boolean isConnected, String xdsServer) { + verify(serverConnectionCallback, times(times)).reportServerConnectionGauge( + eq(isConnected), + eq(xdsServer)); + } + @Test public void ldsResourceNotFound() { DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE, @@ -621,6 +673,7 @@ public void ldsResourceNotFound() { verify(ldsResourceWatcher).onResourceDoesNotExist(LDS_RESOURCE); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataDoesNotExist(LDS, LDS_RESOURCE); + // Check metric data. verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); } @@ -628,7 +681,7 @@ public void ldsResourceNotFound() { public void ldsResourceUpdated_withXdstpResourceName_withUnknownAuthority() { String ldsResourceName = "xdstp://unknown.example.com/envoy.config.listener.v3.Listener/listener1"; - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),ldsResourceName, + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), ldsResourceName, ldsResourceWatcher); verify(ldsResourceWatcher).onError(errorCaptor.capture()); Status error = errorCaptor.getValue(); @@ -636,7 +689,7 @@ public void ldsResourceUpdated_withXdstpResourceName_withUnknownAuthority() { assertThat(error.getDescription()).isEqualTo( "Wrong configuration: xds server does not exist for resource " + ldsResourceName); assertThat(resourceDiscoveryCalls.poll()).isNull(); - xdsClient.cancelXdsResourceWatch(XdsListenerResource.getInstance(),ldsResourceName, + xdsClient.cancelXdsResourceWatch(XdsListenerResource.getInstance(), ldsResourceName, ldsResourceWatcher); assertThat(resourceDiscoveryCalls.poll()).isNull(); } @@ -682,15 +735,16 @@ public void ldsResponseErrorHandling_someResourcesFailedUnpack() { /** * Tests a subscribed LDS resource transitioned to and from the invalid state. * - * @see - * A40-csds-support.md + * @see + * A40-csds-support.md */ @Test public void ldsResponseErrorHandling_subscribedResourceInvalid() { List subscribedResourceNames = ImmutableList.of("A", "B", "C"); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),"A", ldsResourceWatcher); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),"B", ldsResourceWatcher); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),"C", ldsResourceWatcher); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), "A", ldsResourceWatcher); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), "B", ldsResourceWatcher); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), "C", ldsResourceWatcher); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); assertThat(call).isNotNull(); verifyResourceMetadataRequested(LDS, "A"); @@ -708,6 +762,8 @@ public void ldsResponseErrorHandling_subscribedResourceInvalid() { verifyResourceMetadataAcked(LDS, "A", resourcesV1.get("A"), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(LDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(LDS, "C", resourcesV1.get("C"), VERSION_1, TIME_INCREMENT); + // Check metric data. + verifyResourceValidInvalidCount(1, 3, 0, xdsServerInfo.target(), LDS.typeUrl()); call.verifyRequest(LDS, subscribedResourceNames, VERSION_1, "0000", NODE); // LDS -> {A, B}, version 2 @@ -722,7 +778,9 @@ public void ldsResponseErrorHandling_subscribedResourceInvalid() { List errorsV2 = ImmutableList.of("LDS response Listener 'B' validation error: "); verifyResourceMetadataAcked(LDS, "A", resourcesV2.get("A"), VERSION_2, TIME_INCREMENT * 2); verifyResourceMetadataNacked(LDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT, - VERSION_2, TIME_INCREMENT * 2, errorsV2); + VERSION_2, TIME_INCREMENT * 2, errorsV2, true); + // Check metric data. + verifyResourceValidInvalidCount(1, 1, 1, xdsServerInfo.target(), LDS.typeUrl()); if (!ignoreResourceDeletion()) { verifyResourceMetadataDoesNotExist(LDS, "C"); } else { @@ -738,6 +796,8 @@ public void ldsResponseErrorHandling_subscribedResourceInvalid() { call.sendResponse(LDS, resourcesV3.values().asList(), VERSION_3, "0002"); // {A} -> does not exist // {B, C} -> ACK, version 3 + // Check metric data. + verifyResourceValidInvalidCount(1, 2, 0, xdsServerInfo.target(), LDS.typeUrl()); if (!ignoreResourceDeletion()) { verifyResourceMetadataDoesNotExist(LDS, "A"); } else { @@ -753,12 +813,12 @@ public void ldsResponseErrorHandling_subscribedResourceInvalid() { @Test public void ldsResponseErrorHandling_subscribedResourceInvalid_withRdsSubscription() { List subscribedResourceNames = ImmutableList.of("A", "B", "C"); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),"A", ldsResourceWatcher); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),"A.1", rdsResourceWatcher); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),"B", ldsResourceWatcher); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),"B.1", rdsResourceWatcher); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),"C", ldsResourceWatcher); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),"C.1", rdsResourceWatcher); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), "A", ldsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), "A.1", rdsResourceWatcher); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), "B", ldsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), "B.1", rdsResourceWatcher); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), "C", ldsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), "C.1", rdsResourceWatcher); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); assertThat(call).isNotNull(); verifyResourceMetadataRequested(LDS, "A"); @@ -776,6 +836,7 @@ public void ldsResponseErrorHandling_subscribedResourceInvalid_withRdsSubscripti "C", Any.pack(mf.buildListenerWithApiListenerForRds("C", "C.1"))); call.sendResponse(LDS, resourcesV1.values().asList(), VERSION_1, "0000"); // {A, B, C} -> ACK, version 1 + verifyResourceValidInvalidCount(1, 3, 0, xdsServerInfo.target(), LDS.typeUrl()); verifyResourceMetadataAcked(LDS, "A", resourcesV1.get("A"), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(LDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(LDS, "C", resourcesV1.get("C"), VERSION_1, TIME_INCREMENT); @@ -792,6 +853,8 @@ public void ldsResponseErrorHandling_subscribedResourceInvalid_withRdsSubscripti verifyResourceMetadataAcked(RDS, "A.1", resourcesV11.get("A.1"), VERSION_1, TIME_INCREMENT * 2); verifyResourceMetadataAcked(RDS, "B.1", resourcesV11.get("B.1"), VERSION_1, TIME_INCREMENT * 2); verifyResourceMetadataAcked(RDS, "C.1", resourcesV11.get("C.1"), VERSION_1, TIME_INCREMENT * 2); + // Check metric data. + verifyResourceValidInvalidCount(1, 3, 0, xdsServerInfo.target(), RDS.typeUrl()); // LDS -> {A, B}, version 2 // Failed to parse endpoint B @@ -802,11 +865,13 @@ public void ldsResponseErrorHandling_subscribedResourceInvalid_withRdsSubscripti // {A} -> ACK, version 2 // {B} -> NACK, version 1, rejected version 2, rejected reason: Failed to parse B // {C} -> does not exist + // Check metric data. + verifyResourceValidInvalidCount(1, 1, 1, xdsServerInfo.target(), LDS.typeUrl()); List errorsV2 = ImmutableList.of("LDS response Listener 'B' validation error: "); verifyResourceMetadataAcked(LDS, "A", resourcesV2.get("A"), VERSION_2, TIME_INCREMENT * 3); verifyResourceMetadataNacked( LDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT, VERSION_2, TIME_INCREMENT * 3, - errorsV2); + errorsV2, true); if (!ignoreResourceDeletion()) { verifyResourceMetadataDoesNotExist(LDS, "C"); } else { @@ -860,11 +925,11 @@ public void wrappedLdsResource_preferWrappedName() { ldsResourceWatcher); Any innerResource = Any.pack(mf.buildListenerWithApiListener("random_name" /* name */, - mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); + mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); // Client sends an ACK LDS request. call.sendResponse(LDS, mf.buildWrappedResourceWithName(innerResource, LDS_RESOURCE), VERSION_1, - "0000"); + "0000"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); @@ -899,7 +964,7 @@ public void cachedLdsResource_data() { call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); ResourceWatcher watcher = mock(ResourceWatcher.class); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),LDS_RESOURCE, watcher); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, watcher); verify(watcher).onChanged(ldsUpdateCaptor.capture()); verifyGoldenListenerRds(ldsUpdateCaptor.getValue()); call.verifyNoMoreRequest(); @@ -916,7 +981,7 @@ public void cachedLdsResource_absent() { verify(ldsResourceWatcher).onResourceDoesNotExist(LDS_RESOURCE); // Add another watcher. ResourceWatcher watcher = mock(ResourceWatcher.class); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),LDS_RESOURCE, watcher); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, watcher); verify(watcher).onResourceDoesNotExist(LDS_RESOURCE); call.verifyNoMoreRequest(); verifyResourceMetadataDoesNotExist(LDS, LDS_RESOURCE); @@ -1048,7 +1113,7 @@ public void ldsResourceUpdated_withXdstpResourceName_withWrongType() { call.verifyRequestNack( LDS, ldsResourceName, "", "0000", NODE, ImmutableList.of( - "Unsupported resource name: " + ldsResourceNameWithWrongType + " for type: LDS")); + "Unsupported resource name: " + ldsResourceNameWithWrongType + " for type: LDS")); } @Test @@ -1074,7 +1139,7 @@ public void rdsResourceUpdated_withXdstpResourceName_withWrongType() { public void rdsResourceUpdated_withXdstpResourceName_unknownAuthority() { String rdsResourceName = "xdstp://unknown.example.com/envoy.config.route.v3.RouteConfiguration/route1"; - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),rdsResourceName, + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), rdsResourceName, rdsResourceWatcher); verify(rdsResourceWatcher).onError(errorCaptor.capture()); Status error = errorCaptor.getValue(); @@ -1083,7 +1148,7 @@ public void rdsResourceUpdated_withXdstpResourceName_unknownAuthority() { "Wrong configuration: xds server does not exist for resource " + rdsResourceName); assertThat(resourceDiscoveryCalls.size()).isEqualTo(0); xdsClient.cancelXdsResourceWatch( - XdsRouteConfigureResource.getInstance(),rdsResourceName, rdsResourceWatcher); + XdsRouteConfigureResource.getInstance(), rdsResourceName, rdsResourceWatcher); assertThat(resourceDiscoveryCalls.size()).isEqualTo(0); } @@ -1109,7 +1174,7 @@ public void cdsResourceUpdated_withXdstpResourceName_withWrongType() { @Test public void cdsResourceUpdated_withXdstpResourceName_unknownAuthority() { String cdsResourceName = "xdstp://unknown.example.com/envoy.config.cluster.v3.Cluster/cluster1"; - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),cdsResourceName, + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), cdsResourceName, cdsResourceWatcher); verify(cdsResourceWatcher).onError(errorCaptor.capture()); Status error = errorCaptor.getValue(); @@ -1117,7 +1182,7 @@ public void cdsResourceUpdated_withXdstpResourceName_unknownAuthority() { assertThat(error.getDescription()).isEqualTo( "Wrong configuration: xds server does not exist for resource " + cdsResourceName); assertThat(resourceDiscoveryCalls.poll()).isNull(); - xdsClient.cancelXdsResourceWatch(XdsClusterResource.getInstance(),cdsResourceName, + xdsClient.cancelXdsResourceWatch(XdsClusterResource.getInstance(), cdsResourceName, cdsResourceWatcher); assertThat(resourceDiscoveryCalls.poll()).isNull(); } @@ -1254,7 +1319,7 @@ public void ldsResourceDeleted() { /** * When ignore_resource_deletion server feature is on, xDS client should keep the deleted listener * on empty response, and resume the normal work when LDS contains the listener again. - * */ + */ @Test public void ldsResourceDeleted_ignoreResourceDeletion() { Assume.assumeTrue(ignoreResourceDeletion()); @@ -1298,9 +1363,9 @@ public void multipleLdsWatchers() { String ldsResourceTwo = "bar.googleapis.com"; ResourceWatcher watcher1 = mock(ResourceWatcher.class); ResourceWatcher watcher2 = mock(ResourceWatcher.class); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),LDS_RESOURCE, ldsResourceWatcher); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),ldsResourceTwo, watcher1); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),ldsResourceTwo, watcher2); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), ldsResourceTwo, watcher1); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), ldsResourceTwo, watcher2); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); call.verifyRequest(LDS, ImmutableList.of(LDS_RESOURCE, ldsResourceTwo), "", "", NODE); // Both LDS resources were requested. @@ -1340,7 +1405,7 @@ public void rdsResourceNotFound() { DiscoveryRpcCall call = startResourceWatcher(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, rdsResourceWatcher); Any routeConfig = Any.pack(mf.buildRouteConfiguration("route-bar.googleapis.com", - mf.buildOpaqueVirtualHosts(2))); + mf.buildOpaqueVirtualHosts(2))); call.sendResponse(RDS, routeConfig, VERSION_1, "0000"); // Client sends an ACK RDS request. @@ -1442,15 +1507,16 @@ public void rdsResponseErrorHandling_nackWeightedSumZero() { /** * Tests a subscribed RDS resource transitioned to and from the invalid state. * - * @see - * A40-csds-support.md + * @see + * A40-csds-support.md */ @Test public void rdsResponseErrorHandling_subscribedResourceInvalid() { List subscribedResourceNames = ImmutableList.of("A", "B", "C"); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),"A", rdsResourceWatcher); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),"B", rdsResourceWatcher); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),"C", rdsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), "A", rdsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), "B", rdsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), "C", rdsResourceWatcher); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); assertThat(call).isNotNull(); verifyResourceMetadataRequested(RDS, "A"); @@ -1469,6 +1535,8 @@ public void rdsResponseErrorHandling_subscribedResourceInvalid() { verifyResourceMetadataAcked(RDS, "A", resourcesV1.get("A"), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(RDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(RDS, "C", resourcesV1.get("C"), VERSION_1, TIME_INCREMENT); + // Check metric data. + verifyResourceValidInvalidCount(1, 3, 0, xdsServerInfo.target(), RDS.typeUrl()); call.verifyRequest(RDS, subscribedResourceNames, VERSION_1, "0000", NODE); // RDS -> {A, B}, version 2 @@ -1480,11 +1548,13 @@ public void rdsResponseErrorHandling_subscribedResourceInvalid() { // {A} -> ACK, version 2 // {B} -> NACK, version 1, rejected version 2, rejected reason: Failed to parse B // {C} -> ACK, version 1 + verifyResourceValidInvalidCount(1, 1, 1, xdsServerInfo.target(), + RDS.typeUrl()); List errorsV2 = ImmutableList.of("RDS response RouteConfiguration 'B' validation error: "); verifyResourceMetadataAcked(RDS, "A", resourcesV2.get("A"), VERSION_2, TIME_INCREMENT * 2); verifyResourceMetadataNacked(RDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT, - VERSION_2, TIME_INCREMENT * 2, errorsV2); + VERSION_2, TIME_INCREMENT * 2, errorsV2, true); verifyResourceMetadataAcked(RDS, "C", resourcesV1.get("C"), VERSION_1, TIME_INCREMENT); call.verifyRequestNack(RDS, subscribedResourceNames, VERSION_1, "0001", NODE, errorsV2); @@ -1496,6 +1566,8 @@ public void rdsResponseErrorHandling_subscribedResourceInvalid() { call.sendResponse(RDS, resourcesV3.values().asList(), VERSION_3, "0002"); // {A} -> ACK, version 2 // {B, C} -> ACK, version 3 + verifyResourceValidInvalidCount(1, 2, 0, xdsServerInfo.target(), + RDS.typeUrl()); verifyResourceMetadataAcked(RDS, "A", resourcesV2.get("A"), VERSION_2, TIME_INCREMENT * 2); verifyResourceMetadataAcked(RDS, "B", resourcesV3.get("B"), VERSION_3, TIME_INCREMENT * 3); verifyResourceMetadataAcked(RDS, "C", resourcesV3.get("C"), VERSION_3, TIME_INCREMENT * 3); @@ -1544,7 +1616,7 @@ public void cachedRdsResource_data() { call.verifyRequest(RDS, RDS_RESOURCE, VERSION_1, "0000", NODE); ResourceWatcher watcher = mock(ResourceWatcher.class); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),RDS_RESOURCE, watcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, watcher); verify(watcher).onChanged(rdsUpdateCaptor.capture()); verifyGoldenRouteConfig(rdsUpdateCaptor.getValue()); call.verifyNoMoreRequest(); @@ -1561,7 +1633,7 @@ public void cachedRdsResource_absent() { verify(rdsResourceWatcher).onResourceDoesNotExist(RDS_RESOURCE); // Add another watcher. ResourceWatcher watcher = mock(ResourceWatcher.class); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),RDS_RESOURCE, watcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, watcher); verify(watcher).onResourceDoesNotExist(RDS_RESOURCE); call.verifyNoMoreRequest(); verifyResourceMetadataDoesNotExist(RDS, RDS_RESOURCE); @@ -1592,14 +1664,43 @@ public void rdsResourceUpdated() { assertThat(rdsUpdateCaptor.getValue().virtualHosts).hasSize(4); verifyResourceMetadataAcked(RDS, RDS_RESOURCE, routeConfigUpdated, VERSION_2, TIME_INCREMENT * 2); - verifySubscribedResourcesMetadataSizes(0, 0, 1, 0); + } + + @Test + public void rdsResourceInvalid() { + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), "A", rdsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), "B", rdsResourceWatcher); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + assertThat(call).isNotNull(); + verifyResourceMetadataRequested(RDS, "A"); + verifyResourceMetadataRequested(RDS, "B"); + verifySubscribedResourcesMetadataSizes(0, 0, 2, 0); + + // RDS -> {A, B}, version 1 + // Failed to parse endpoint B + List vhostsV1 = mf.buildOpaqueVirtualHosts(1); + ImmutableMap resourcesV1 = ImmutableMap.of( + "A", Any.pack(mf.buildRouteConfiguration("A", vhostsV1)), + "B", Any.pack(mf.buildRouteConfigurationInvalid("B"))); + call.sendResponse(RDS, resourcesV1.values().asList(), VERSION_1, "0000"); + + // {A} -> ACK, version 1 + // {B} -> NACK, version 1, rejected version 1, rejected reason: Failed to parse B + List errorsV1 = + ImmutableList.of("RDS response RouteConfiguration 'B' validation error: "); + verifyResourceMetadataAcked(RDS, "A", resourcesV1.get("A"), VERSION_1, TIME_INCREMENT); + verifyResourceMetadataNacked(RDS, "B", null, "", 0, + VERSION_1, TIME_INCREMENT, errorsV1, false); + // Check metric data. + verifyResourceValidInvalidCount(1, 1, 1, xdsServerInfo.target(), RDS.typeUrl()); + verifySubscribedResourcesMetadataSizes(0, 0, 2, 0); } @Test public void rdsResourceDeletedByLdsApiListener() { - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),LDS_RESOURCE, + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),RDS_RESOURCE, + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, rdsResourceWatcher); verifyResourceMetadataRequested(LDS, LDS_RESOURCE); verifyResourceMetadataRequested(RDS, RDS_RESOURCE); @@ -1707,10 +1808,10 @@ public void multipleRdsWatchers() { String rdsResourceTwo = "route-bar.googleapis.com"; ResourceWatcher watcher1 = mock(ResourceWatcher.class); ResourceWatcher watcher2 = mock(ResourceWatcher.class); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),RDS_RESOURCE, + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, rdsResourceWatcher); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),rdsResourceTwo, watcher1); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),rdsResourceTwo, watcher2); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), rdsResourceTwo, watcher1); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), rdsResourceTwo, watcher2); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); call.verifyRequest(RDS, Arrays.asList(RDS_RESOURCE, rdsResourceTwo), "", "", NODE); // Both RDS resources were requested. @@ -1814,15 +1915,16 @@ public void cdsResponseErrorHandling_someResourcesFailedUnpack() { /** * Tests a subscribed CDS resource transitioned to and from the invalid state. * - * @see - * A40-csds-support.md + * @see + * A40-csds-support.md */ @Test public void cdsResponseErrorHandling_subscribedResourceInvalid() { List subscribedResourceNames = ImmutableList.of("A", "B", "C"); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),"A", cdsResourceWatcher); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),"B", cdsResourceWatcher); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),"C", cdsResourceWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), "A", cdsResourceWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), "B", cdsResourceWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), "C", cdsResourceWatcher); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); assertThat(call).isNotNull(); verifyResourceMetadataRequested(CDS, "A"); @@ -1843,6 +1945,8 @@ public void cdsResponseErrorHandling_subscribedResourceInvalid() { ))); call.sendResponse(CDS, resourcesV1.values().asList(), VERSION_1, "0000"); // {A, B, C} -> ACK, version 1 + verifyResourceValidInvalidCount(1, 3, 0, xdsServerInfo.target(), + CDS.typeUrl()); verifyResourceMetadataAcked(CDS, "A", resourcesV1.get("A"), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(CDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(CDS, "C", resourcesV1.get("C"), VERSION_1, TIME_INCREMENT); @@ -1859,10 +1963,12 @@ public void cdsResponseErrorHandling_subscribedResourceInvalid() { // {A} -> ACK, version 2 // {B} -> NACK, version 1, rejected version 2, rejected reason: Failed to parse B // {C} -> does not exist + verifyResourceValidInvalidCount(1, 1, 1, xdsServerInfo.target(), + CDS.typeUrl()); List errorsV2 = ImmutableList.of("CDS response Cluster 'B' validation error: "); verifyResourceMetadataAcked(CDS, "A", resourcesV2.get("A"), VERSION_2, TIME_INCREMENT * 2); verifyResourceMetadataNacked(CDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT, - VERSION_2, TIME_INCREMENT * 2, errorsV2); + VERSION_2, TIME_INCREMENT * 2, errorsV2, true); if (!ignoreResourceDeletion()) { verifyResourceMetadataDoesNotExist(CDS, "C"); } else { @@ -1882,6 +1988,8 @@ public void cdsResponseErrorHandling_subscribedResourceInvalid() { call.sendResponse(CDS, resourcesV3.values().asList(), VERSION_3, "0002"); // {A} -> does not exit // {B, C} -> ACK, version 3 + verifyResourceValidInvalidCount(1, 2, 0, xdsServerInfo.target(), + CDS.typeUrl()); if (!ignoreResourceDeletion()) { verifyResourceMetadataDoesNotExist(CDS, "A"); } else { @@ -1890,18 +1998,19 @@ public void cdsResponseErrorHandling_subscribedResourceInvalid() { } verifyResourceMetadataAcked(CDS, "B", resourcesV3.get("B"), VERSION_3, TIME_INCREMENT * 3); verifyResourceMetadataAcked(CDS, "C", resourcesV3.get("C"), VERSION_3, TIME_INCREMENT * 3); + call.verifyRequest(CDS, subscribedResourceNames, VERSION_3, "0002", NODE); } @Test public void cdsResponseErrorHandling_subscribedResourceInvalid_withEdsSubscription() { List subscribedResourceNames = ImmutableList.of("A", "B", "C"); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),"A", cdsResourceWatcher); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),"A.1", edsResourceWatcher); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),"B", cdsResourceWatcher); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),"B.1", edsResourceWatcher); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),"C", cdsResourceWatcher); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),"C.1", edsResourceWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), "A", cdsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), "A.1", edsResourceWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), "B", cdsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), "B.1", edsResourceWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), "C", cdsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), "C.1", edsResourceWatcher); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); assertThat(call).isNotNull(); verifyResourceMetadataRequested(CDS, "A"); @@ -1925,6 +2034,8 @@ public void cdsResponseErrorHandling_subscribedResourceInvalid_withEdsSubscripti ))); call.sendResponse(CDS, resourcesV1.values().asList(), VERSION_1, "0000"); // {A, B, C} -> ACK, version 1 + verifyResourceValidInvalidCount(1, 3, 0, xdsServerInfo.target(), + CDS.typeUrl()); verifyResourceMetadataAcked(CDS, "A", resourcesV1.get("A"), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(CDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(CDS, "C", resourcesV1.get("C"), VERSION_1, TIME_INCREMENT); @@ -1939,6 +2050,8 @@ public void cdsResponseErrorHandling_subscribedResourceInvalid_withEdsSubscripti "C.1", Any.pack(mf.buildClusterLoadAssignment("C.1", endpointsV1, dropOverloads))); call.sendResponse(EDS, resourcesV11.values().asList(), VERSION_1, "0000"); // {A.1, B.1, C.1} -> ACK, version 1 + verifyResourceValidInvalidCount(1, 3, 0, xdsServerInfo.target(), + EDS.typeUrl()); verifyResourceMetadataAcked(EDS, "A.1", resourcesV11.get("A.1"), VERSION_1, TIME_INCREMENT * 2); verifyResourceMetadataAcked(EDS, "B.1", resourcesV11.get("B.1"), VERSION_1, TIME_INCREMENT * 2); verifyResourceMetadataAcked(EDS, "C.1", resourcesV11.get("C.1"), VERSION_1, TIME_INCREMENT * 2); @@ -1954,11 +2067,13 @@ public void cdsResponseErrorHandling_subscribedResourceInvalid_withEdsSubscripti // {A} -> ACK, version 2 // {B} -> NACK, version 1, rejected version 2, rejected reason: Failed to parse B // {C} -> does not exist + // Check metric data. + verifyResourceValidInvalidCount(1, 1, 1, xdsServerInfo.target(), CDS.typeUrl()); List errorsV2 = ImmutableList.of("CDS response Cluster 'B' validation error: "); verifyResourceMetadataAcked(CDS, "A", resourcesV2.get("A"), VERSION_2, TIME_INCREMENT * 3); verifyResourceMetadataNacked( CDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT, VERSION_2, TIME_INCREMENT * 3, - errorsV2); + errorsV2, true); if (!ignoreResourceDeletion()) { verifyResourceMetadataDoesNotExist(CDS, "C"); } else { @@ -2144,7 +2259,7 @@ public void cdsResponseWithUpstreamTlsContext() { "envoy.transport_sockets.tls", null, null)); List clusters = ImmutableList.of( Any.pack(mf.buildLogicalDnsCluster("cluster-bar.googleapis.com", - "dns-service-bar.googleapis.com", 443, "round_robin", null, null,false, null, null)), + "dns-service-bar.googleapis.com", 443, "round_robin", null, null, false, null, null)), clusterEds, Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null, null))); @@ -2176,7 +2291,7 @@ public void cdsResponseWithNewUpstreamTlsContext() { // Management server sends back CDS response with UpstreamTlsContext. Any clusterEds = Any.pack(mf.buildEdsCluster(CDS_RESOURCE, "eds-cluster-foo.googleapis.com", "round_robin", - null, null,true, + null, null, true, mf.buildNewUpstreamTlsContext("cert-instance-name", "cert1"), "envoy.transport_sockets.tls", null, null)); List clusters = ImmutableList.of( @@ -2217,10 +2332,10 @@ public void cdsResponseErrorHandling_badUpstreamTlsContext() { // The response NACKed with errors indicating indices of the failed resources. String errorMsg = "CDS response Cluster 'cluster.googleapis.com' validation error: " - + "Cluster cluster.googleapis.com: malformed UpstreamTlsContext: " - + "io.grpc.xds.client.XdsResourceType$ResourceInvalidException: " - + "ca_certificate_provider_instance or system_root_certs is required in " - + "upstream-tls-context"; + + "Cluster cluster.googleapis.com: malformed UpstreamTlsContext: " + + "io.grpc.xds.client.XdsResourceType$ResourceInvalidException: " + + "ca_certificate_provider_instance or system_root_certs is required in " + + "upstream-tls-context"; call.verifyRequestNack(CDS, CDS_RESOURCE, "", "0000", NODE, ImmutableList.of(errorMsg)); verify(cdsResourceWatcher).onError(errorCaptor.capture()); verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); @@ -2257,7 +2372,7 @@ public void cdsResponseWithOutlierDetection() { "envoy.transport_sockets.tls", null, outlierDetectionXds)); List clusters = ImmutableList.of( Any.pack(mf.buildLogicalDnsCluster("cluster-bar.googleapis.com", - "dns-service-bar.googleapis.com", 443, "round_robin", null, null,false, null, null)), + "dns-service-bar.googleapis.com", 443, "round_robin", null, null, false, null, null)), clusterEds, Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null, outlierDetectionXds))); @@ -2316,7 +2431,7 @@ public void cdsResponseWithInvalidOutlierDetectionNacks() { "envoy.transport_sockets.tls", null, outlierDetectionXds)); List clusters = ImmutableList.of( Any.pack(mf.buildLogicalDnsCluster("cluster-bar.googleapis.com", - "dns-service-bar.googleapis.com", 443, "round_robin", null, null,false, null, null)), + "dns-service-bar.googleapis.com", 443, "round_robin", null, null, false, null, null)), clusterEds, Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null, outlierDetectionXds))); @@ -2436,8 +2551,7 @@ public void cdsResponseErrorHandling_xdstpWithoutEdsConfig() { )); final Any okClusterRoundRobin = Any.pack(mf.buildEdsCluster(cdsResourceName, "eds-service-bar.googleapis.com", - "round_robin", null,null, false, null, "envoy.transport_sockets.tls", null, null)); - + "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null, null)); DiscoveryRpcCall call = startResourceWatcher(XdsClusterResource.getInstance(), cdsResourceName, cdsResourceWatcher); @@ -2483,7 +2597,7 @@ public void cachedCdsResource_absent() { fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); verify(cdsResourceWatcher).onResourceDoesNotExist(CDS_RESOURCE); ResourceWatcher watcher = mock(ResourceWatcher.class); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),CDS_RESOURCE, watcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CDS_RESOURCE, watcher); verify(watcher).onResourceDoesNotExist(CDS_RESOURCE); call.verifyNoMoreRequest(); verifyResourceMetadataDoesNotExist(CDS, CDS_RESOURCE); @@ -2620,7 +2734,7 @@ public void cdsResourceDeleted() { /** * When ignore_resource_deletion server feature is on, xDS client should keep the deleted cluster * on empty response, and resume the normal work when CDS contains the cluster again. - * */ + */ @Test public void cdsResourceDeleted_ignoreResourceDeletion() { Assume.assumeTrue(ignoreResourceDeletion()); @@ -2666,9 +2780,9 @@ public void multipleCdsWatchers() { String cdsResourceTwo = "cluster-bar.googleapis.com"; ResourceWatcher watcher1 = mock(ResourceWatcher.class); ResourceWatcher watcher2 = mock(ResourceWatcher.class); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),CDS_RESOURCE, cdsResourceWatcher); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),cdsResourceTwo, watcher1); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),cdsResourceTwo, watcher2); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CDS_RESOURCE, cdsResourceWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), cdsResourceTwo, watcher1); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), cdsResourceTwo, watcher2); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); call.verifyRequest(CDS, Arrays.asList(CDS_RESOURCE, cdsResourceTwo), "", "", NODE); verifyResourceMetadataRequested(CDS, CDS_RESOURCE); @@ -2774,7 +2888,7 @@ public void edsCleanupNonceAfterUnsubscription() { List dropOverloads = ImmutableList.of(); List endpointsV1 = ImmutableList.of(lbEndpointHealthy); ImmutableMap resourcesV1 = ImmutableMap.of( - "A.1", Any.pack(mf.buildClusterLoadAssignment("A.1", endpointsV1, dropOverloads))); + "A.1", Any.pack(mf.buildClusterLoadAssignment("A.1", endpointsV1, dropOverloads))); call.sendResponse(EDS, resourcesV1.values().asList(), VERSION_1, "0000"); // {A.1} -> ACK, version 1 call.verifyRequest(EDS, "A.1", VERSION_1, "0000", NODE); @@ -2835,21 +2949,21 @@ public void edsResponseErrorHandling_someResourcesFailedUnpack() { /** * Tests a subscribed EDS resource transitioned to and from the invalid state. * - * @see - * A40-csds-support.md + * @see + * A40-csds-support.md */ @Test public void edsResponseErrorHandling_subscribedResourceInvalid() { List subscribedResourceNames = ImmutableList.of("A", "B", "C"); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),"A", edsResourceWatcher); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),"B", edsResourceWatcher); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),"C", edsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), "A", edsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), "B", edsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), "C", edsResourceWatcher); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); assertThat(call).isNotNull(); verifyResourceMetadataRequested(EDS, "A"); verifyResourceMetadataRequested(EDS, "B"); verifyResourceMetadataRequested(EDS, "C"); - verifySubscribedResourcesMetadataSizes(0, 0, 0, 3); // EDS -> {A, B, C}, version 1 List dropOverloads = ImmutableList.of(mf.buildDropOverload("lb", 200)); @@ -2860,6 +2974,7 @@ public void edsResponseErrorHandling_subscribedResourceInvalid() { "C", Any.pack(mf.buildClusterLoadAssignment("C", endpointsV1, dropOverloads))); call.sendResponse(EDS, resourcesV1.values().asList(), VERSION_1, "0000"); // {A, B, C} -> ACK, version 1 + verifyResourceValidInvalidCount(1, 3, 0, xdsServerInfo.target(), EDS.typeUrl()); verifyResourceMetadataAcked(EDS, "A", resourcesV1.get("A"), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(EDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(EDS, "C", resourcesV1.get("C"), VERSION_1, TIME_INCREMENT); @@ -2875,11 +2990,13 @@ public void edsResponseErrorHandling_subscribedResourceInvalid() { // {A} -> ACK, version 2 // {B} -> NACK, version 1, rejected version 2, rejected reason: Failed to parse B // {C} -> ACK, version 1 + // Check metric data. + verifyResourceValidInvalidCount(1, 1, 1, xdsServerInfo.target(), EDS.typeUrl()); List errorsV2 = ImmutableList.of("EDS response ClusterLoadAssignment 'B' validation error: "); verifyResourceMetadataAcked(EDS, "A", resourcesV2.get("A"), VERSION_2, TIME_INCREMENT * 2); verifyResourceMetadataNacked(EDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT, - VERSION_2, TIME_INCREMENT * 2, errorsV2); + VERSION_2, TIME_INCREMENT * 2, errorsV2, true); verifyResourceMetadataAcked(EDS, "C", resourcesV1.get("C"), VERSION_1, TIME_INCREMENT); call.verifyRequestNack(EDS, subscribedResourceNames, VERSION_1, "0001", NODE, errorsV2); @@ -2892,6 +3009,8 @@ public void edsResponseErrorHandling_subscribedResourceInvalid() { call.sendResponse(EDS, resourcesV3.values().asList(), VERSION_3, "0002"); // {A} -> ACK, version 2 // {B, C} -> ACK, version 3 + // Check metric data. + verifyResourceValidInvalidCount(1, 2, 0, xdsServerInfo.target(), EDS.typeUrl()); verifyResourceMetadataAcked(EDS, "A", resourcesV2.get("A"), VERSION_2, TIME_INCREMENT * 2); verifyResourceMetadataAcked(EDS, "B", resourcesV3.get("B"), VERSION_3, TIME_INCREMENT * 3); verifyResourceMetadataAcked(EDS, "C", resourcesV3.get("C"), VERSION_3, TIME_INCREMENT * 3); @@ -2940,7 +3059,7 @@ public void cachedEdsResource_data() { call.verifyRequest(EDS, EDS_RESOURCE, VERSION_1, "0000", NODE); // Add another watcher. ResourceWatcher watcher = mock(ResourceWatcher.class); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),EDS_RESOURCE, watcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), EDS_RESOURCE, watcher); verify(watcher).onChanged(edsUpdateCaptor.capture()); validateGoldenClusterLoadAssignment(edsUpdateCaptor.getValue()); call.verifyNoMoreRequest(); @@ -2957,7 +3076,7 @@ public void cachedEdsResource_absent() { fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); verify(edsResourceWatcher).onResourceDoesNotExist(EDS_RESOURCE); ResourceWatcher watcher = mock(ResourceWatcher.class); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),EDS_RESOURCE, watcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), EDS_RESOURCE, watcher); verify(watcher).onResourceDoesNotExist(EDS_RESOURCE); call.verifyNoMoreRequest(); verifyResourceMetadataDoesNotExist(EDS, EDS_RESOURCE); @@ -3161,10 +3280,10 @@ public void edsResourceDeletedByCds() { String resource = "backend-service.googleapis.com"; ResourceWatcher cdsWatcher = mock(ResourceWatcher.class); ResourceWatcher edsWatcher = mock(ResourceWatcher.class); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),resource, cdsWatcher); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),resource, edsWatcher); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),CDS_RESOURCE, cdsResourceWatcher); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),EDS_RESOURCE, edsResourceWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), resource, cdsWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), resource, edsWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CDS_RESOURCE, cdsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), EDS_RESOURCE, edsResourceWatcher); verifyResourceMetadataRequested(CDS, CDS_RESOURCE); verifyResourceMetadataRequested(CDS, resource); verifyResourceMetadataRequested(EDS, EDS_RESOURCE); @@ -3242,7 +3361,6 @@ public void edsResourceDeletedByCds() { EDS, resource, clusterLoadAssignments.get(1), VERSION_1, TIME_INCREMENT * 2); // no change verifyResourceMetadataAcked(CDS, resource, clusters.get(0), VERSION_2, TIME_INCREMENT * 3); verifyResourceMetadataAcked(CDS, CDS_RESOURCE, clusters.get(1), VERSION_2, TIME_INCREMENT * 3); - verifySubscribedResourcesMetadataSizes(0, 2, 0, 2); } @Test @@ -3251,9 +3369,9 @@ public void multipleEdsWatchers() { String edsResourceTwo = "cluster-load-assignment-bar.googleapis.com"; ResourceWatcher watcher1 = mock(ResourceWatcher.class); ResourceWatcher watcher2 = mock(ResourceWatcher.class); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),EDS_RESOURCE, edsResourceWatcher); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),edsResourceTwo, watcher1); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),edsResourceTwo, watcher2); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), EDS_RESOURCE, edsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), edsResourceTwo, watcher1); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), edsResourceTwo, watcher2); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); call.verifyRequest(EDS, Arrays.asList(EDS_RESOURCE, edsResourceTwo), "", "", NODE); verifyResourceMetadataRequested(EDS, EDS_RESOURCE); @@ -3338,12 +3456,18 @@ public void useIndependentRpcContext() { @Test public void streamClosedWithNoResponse() { - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),LDS_RESOURCE, ldsResourceWatcher); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),RDS_RESOURCE, + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, rdsResourceWatcher); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(1, true, xdsServerInfo.target()); // Management server closes the RPC stream before sending any response. call.sendCompleted(); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(1, false, xdsServerInfo.target()); verify(ldsResourceWatcher, Mockito.timeout(1000).times(1)) .onError(errorCaptor.capture()); verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, @@ -3355,20 +3479,29 @@ public void streamClosedWithNoResponse() { @Test public void streamClosedAfterSendingResponses() { - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),LDS_RESOURCE, ldsResourceWatcher); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),RDS_RESOURCE, + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, rdsResourceWatcher); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(1, true, xdsServerInfo.target()); ScheduledTask ldsResourceTimeout = Iterables.getOnlyElement(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); ScheduledTask rdsResourceTimeout = Iterables.getOnlyElement(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); call.sendResponse(LDS, testListenerRds, VERSION_1, "0000"); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(2, true, xdsServerInfo.target()); assertThat(ldsResourceTimeout.isCancelled()).isTrue(); call.sendResponse(RDS, testRouteConfig, VERSION_1, "0000"); assertThat(rdsResourceTimeout.isCancelled()).isTrue(); // Management server closes the RPC stream after sending responses. call.sendCompleted(); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(3, true, xdsServerInfo.target()); verify(ldsResourceWatcher, never()).onError(errorCaptor.capture()); verify(rdsResourceWatcher, never()).onError(errorCaptor.capture()); } @@ -3376,11 +3509,14 @@ public void streamClosedAfterSendingResponses() { @Test public void streamClosedAndRetryWithBackoff() { InOrder inOrder = Mockito.inOrder(backoffPolicyProvider, backoffPolicy1, backoffPolicy2); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(),LDS_RESOURCE, ldsResourceWatcher); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),RDS_RESOURCE, + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(1, true, xdsServerInfo.target()); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, rdsResourceWatcher); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(),CDS_RESOURCE, cdsResourceWatcher); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(),EDS_RESOURCE, edsResourceWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CDS_RESOURCE, cdsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), EDS_RESOURCE, edsResourceWatcher); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); call.verifyRequest(LDS, LDS_RESOURCE, "", "", NODE); call.verifyRequest(RDS, RDS_RESOURCE, "", "", NODE); @@ -3399,6 +3535,10 @@ public void streamClosedAndRetryWithBackoff() { verify(edsResourceWatcher).onError(errorCaptor.capture()); verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNKNOWN, ""); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(1, false, xdsServerInfo.target()); + // Retry after backoff. inOrder.verify(backoffPolicyProvider).get(); inOrder.verify(backoffPolicy1).nextBackoffNanos(); @@ -3412,6 +3552,10 @@ public void streamClosedAndRetryWithBackoff() { call.verifyRequest(CDS, CDS_RESOURCE, "", "", NODE); call.verifyRequest(EDS, EDS_RESOURCE, "", "", NODE); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(2, false, xdsServerInfo.target()); + // Management server becomes unreachable. String errorMsg = "my fault"; call.sendError(Status.UNAVAILABLE.withDescription(errorMsg).asException()); @@ -3424,6 +3568,10 @@ public void streamClosedAndRetryWithBackoff() { verify(edsResourceWatcher, times(2)).onError(errorCaptor.capture()); verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(3, false, xdsServerInfo.target()); + // Retry after backoff. inOrder.verify(backoffPolicy1).nextBackoffNanos(); retryTask = @@ -3441,6 +3589,9 @@ public void streamClosedAndRetryWithBackoff() { mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(2))))); call.sendResponse(LDS, listeners, "63", "3242"); call.verifyRequest(LDS, LDS_RESOURCE, "63", "3242", NODE); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(2, true, xdsServerInfo.target()); List routeConfigs = ImmutableList.of( Any.pack(mf.buildRouteConfiguration(RDS_RESOURCE, mf.buildOpaqueVirtualHosts(2)))); @@ -3455,6 +3606,10 @@ public void streamClosedAndRetryWithBackoff() { verify(edsResourceWatcher, times(2)).onError(errorCaptor.capture()); verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(3, true, xdsServerInfo.target()); + // Reset backoff sequence and retry after backoff. inOrder.verify(backoffPolicyProvider).get(); inOrder.verify(backoffPolicy2).nextBackoffNanos(); @@ -3477,6 +3632,10 @@ public void streamClosedAndRetryWithBackoff() { verify(edsResourceWatcher, times(3)).onError(errorCaptor.capture()); verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(4, false, xdsServerInfo.target()); + // Retry after backoff. inOrder.verify(backoffPolicy2).nextBackoffNanos(); retryTask = @@ -3489,6 +3648,10 @@ public void streamClosedAndRetryWithBackoff() { call.verifyRequest(CDS, CDS_RESOURCE, "", "", NODE); call.verifyRequest(EDS, EDS_RESOURCE, "", "", NODE); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(5, false, xdsServerInfo.target()); + inOrder.verifyNoMoreInteractions(); } @@ -3499,6 +3662,9 @@ public void streamClosedAndRetryRaceWithAddRemoveWatchers() { xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, rdsResourceWatcher); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(1, true, xdsServerInfo.target()); call.sendError(Status.UNAVAILABLE.asException()); verify(ldsResourceWatcher, Mockito.timeout(1000).times(1)) .onError(errorCaptor.capture()); @@ -3509,6 +3675,10 @@ public void streamClosedAndRetryRaceWithAddRemoveWatchers() { Iterables.getOnlyElement(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)); assertThat(retryTask.getDelay(TimeUnit.NANOSECONDS)).isEqualTo(10L); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(1, false, xdsServerInfo.target()); + xdsClient.cancelXdsResourceWatch(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); xdsClient.cancelXdsResourceWatch(XdsRouteConfigureResource.getInstance(), @@ -3523,11 +3693,19 @@ public void streamClosedAndRetryRaceWithAddRemoveWatchers() { call.verifyRequest(EDS, EDS_RESOURCE, "", "", NODE); call.verifyNoMoreRequest(); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(2,false, xdsServerInfo.target()); + call.sendResponse(LDS, testListenerRds, VERSION_1, "0000"); List routeConfigs = ImmutableList.of( Any.pack(mf.buildRouteConfiguration(RDS_RESOURCE, mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); call.sendResponse(RDS, routeConfigs, VERSION_1, "0000"); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(2, true, xdsServerInfo.target()); + verifyNoMoreInteractions(ldsResourceWatcher, rdsResourceWatcher); } @@ -3539,6 +3717,9 @@ public void streamClosedAndRetryRestartsResourceInitialFetchTimerForUnresolvedRe xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CDS_RESOURCE, cdsResourceWatcher); xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), EDS_RESOURCE, edsResourceWatcher); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(1, true, xdsServerInfo.target()); ScheduledTask ldsResourceTimeout = Iterables.getOnlyElement(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); ScheduledTask rdsResourceTimeout = @@ -3549,9 +3730,15 @@ public void streamClosedAndRetryRestartsResourceInitialFetchTimerForUnresolvedRe Iterables.getOnlyElement(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); call.sendResponse(LDS, testListenerRds, VERSION_1, "0000"); assertThat(ldsResourceTimeout.isCancelled()).isTrue(); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(2, true, xdsServerInfo.target()); call.sendResponse(RDS, testRouteConfig, VERSION_1, "0000"); assertThat(rdsResourceTimeout.isCancelled()).isTrue(); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(3, true, xdsServerInfo.target()); call.sendError(Status.UNAVAILABLE.asException()); assertThat(cdsResourceTimeout.isCancelled()).isTrue(); @@ -3560,6 +3747,9 @@ public void streamClosedAndRetryRestartsResourceInitialFetchTimerForUnresolvedRe verify(rdsResourceWatcher, never()).onError(errorCaptor.capture()); verify(cdsResourceWatcher, never()).onError(errorCaptor.capture()); verify(edsResourceWatcher, never()).onError(errorCaptor.capture()); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(4, true, xdsServerInfo.target()); fakeClock.forwardNanos(10L); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(0); @@ -3578,13 +3768,13 @@ public void reportLoadStatsToServer() { lrsCall.sendResponse(Collections.singletonList(clusterName), 1000L); fakeClock.forwardNanos(1000L); - lrsCall.verifyNextReportClusters(Collections.singletonList(new String[] {clusterName, null})); + lrsCall.verifyNextReportClusters(Collections.singletonList(new String[]{clusterName, null})); dropStats.release(); fakeClock.forwardNanos(1000L); // In case of having unreported cluster stats, one last report will be sent after corresponding // stats object released. - lrsCall.verifyNextReportClusters(Collections.singletonList(new String[] {clusterName, null})); + lrsCall.verifyNextReportClusters(Collections.singletonList(new String[]{clusterName, null})); fakeClock.forwardNanos(1000L); // Currently load reporting continues (with empty stats) even if all stats objects have been @@ -3658,18 +3848,18 @@ public void serverSideListenerNotFound() { @Test public void serverSideListenerResponseErrorHandling_badDownstreamTlsContext() { GrpcXdsClientImplTestBase.DiscoveryRpcCall call = - startResourceWatcher(XdsListenerResource.getInstance(), LISTENER_RESOURCE, - ldsResourceWatcher); + startResourceWatcher(XdsListenerResource.getInstance(), LISTENER_RESOURCE, + ldsResourceWatcher); Message hcmFilter = mf.buildHttpConnectionManagerFilter( - "route-foo.googleapis.com", null, + "route-foo.googleapis.com", null, Collections.singletonList(mf.buildTerminalFilter())); Message downstreamTlsContext = CommonTlsContextTestsUtil.buildTestDownstreamTlsContext( - null, null,false); + null, null, false); Message filterChain = mf.buildFilterChain( - Collections.emptyList(), downstreamTlsContext, "envoy.transport_sockets.tls", + Collections.emptyList(), downstreamTlsContext, "envoy.transport_sockets.tls", hcmFilter); Message listener = - mf.buildListenerWithFilterChain(LISTENER_RESOURCE, 7000, "0.0.0.0", filterChain); + mf.buildListenerWithFilterChain(LISTENER_RESOURCE, 7000, "0.0.0.0", filterChain); List listeners = ImmutableList.of(Any.pack(listener)); call.sendResponse(LDS, listeners, "0", "0000"); // The response NACKed with errors indicating indices of the failed resources. @@ -3690,7 +3880,7 @@ public void serverSideListenerResponseErrorHandling_badTransportSocketName() { "route-foo.googleapis.com", null, Collections.singletonList(mf.buildTerminalFilter())); Message downstreamTlsContext = CommonTlsContextTestsUtil.buildTestDownstreamTlsContext( - "cert1", "cert2",false); + "cert1", "cert2", false); Message filterChain = mf.buildFilterChain( Collections.emptyList(), downstreamTlsContext, "envoy.transport_sockets.bad1", hcmFilter); @@ -3721,6 +3911,9 @@ public void sendingToStoppedServer() throws Exception { xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); fakeClock.forwardTime(14, TimeUnit.SECONDS); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(1, false, xdsServerInfo.target()); // Restart the server xdsServer = cleanupRule.register( @@ -3735,11 +3928,18 @@ public void sendingToStoppedServer() throws Exception { verify(ldsResourceWatcher, never()).onResourceDoesNotExist(LDS_RESOURCE); fakeClock.forwardTime(20, TimeUnit.SECONDS); // Trigger rpcRetryTimer DiscoveryRpcCall call = resourceDiscoveryCalls.poll(3, TimeUnit.SECONDS); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(2, false, xdsServerInfo.target()); if (call == null) { // The first rpcRetry may have happened before the channel was ready fakeClock.forwardTime(50, TimeUnit.SECONDS); call = resourceDiscoveryCalls.poll(3, TimeUnit.SECONDS); } + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(3, false, xdsServerInfo.target()); + // NOTE: There is a ScheduledExecutorService that may get involved due to the reconnect // so you cannot rely on the logic being single threaded. The timeout() in verifyRequest // is therefore necessary to avoid flakiness. @@ -3751,6 +3951,9 @@ public void sendingToStoppedServer() throws Exception { assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(1, 1, 0, 0); + // Check metric data. + callback_ReportServerConnection(); + verifyServerConnection(1, true, xdsServerInfo.target()); } catch (Throwable t) { throw t; // This allows putting a breakpoint here for debugging } @@ -3782,6 +3985,152 @@ public void sendToNonexistentServer() throws Exception { client.shutdown(); } + @Test + public void validAndInvalidResourceMetricReport() { + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), "A", cdsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), "A.1", edsResourceWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), "B", cdsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), "B.1", edsResourceWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), "C", cdsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), "C.1", edsResourceWatcher); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + assertThat(call).isNotNull(); + + // CDS -> {A, B, C}, version 1 + ImmutableMap resourcesV1 = ImmutableMap.of( + "A", Any.pack(mf.buildEdsCluster("A", "A.1", "round_robin", null, null, false, null, + "envoy.transport_sockets.tls", null, null + )), + "B", Any.pack(mf.buildEdsCluster("B", "B.1", "round_robin", null, null, false, null, + "envoy.transport_sockets.tls", null, null + )), + "C", Any.pack(mf.buildEdsCluster("C", "C.1", "round_robin", null, null, false, null, + "envoy.transport_sockets.tls", null, null + ))); + call.sendResponse(CDS, resourcesV1.values().asList(), VERSION_1, "0000"); + // {A, B, C} -> ACK, version 1 + verifyResourceValidInvalidCount(1, 3, 0, xdsServerInfo.target(), CDS.typeUrl()); + + // EDS -> {A.1, B.1, C.1}, version 1 + List dropOverloads = ImmutableList.of(); + List endpointsV1 = ImmutableList.of(lbEndpointHealthy); + ImmutableMap resourcesV11 = ImmutableMap.of( + "A.1", Any.pack(mf.buildClusterLoadAssignment("A.1", endpointsV1, dropOverloads)), + "B.1", Any.pack(mf.buildClusterLoadAssignment("B.1", endpointsV1, dropOverloads)), + "C.1", Any.pack(mf.buildClusterLoadAssignment("C.1", endpointsV1, dropOverloads))); + call.sendResponse(EDS, resourcesV11.values().asList(), VERSION_1, "0000"); + // {A.1, B.1, C.1} -> ACK, version 1 + verifyResourceValidInvalidCount(1, 3, 0, xdsServerInfo.target(), EDS.typeUrl()); + + // CDS -> {A, B}, version 2 + // Failed to parse endpoint B + ImmutableMap resourcesV2 = ImmutableMap.of( + "A", Any.pack(mf.buildEdsCluster("A", "A.2", "round_robin", null, null, false, null, + "envoy.transport_sockets.tls", null, null + )), + "B", Any.pack(mf.buildClusterInvalid("B"))); + call.sendResponse(CDS, resourcesV2.values().asList(), VERSION_2, "0001"); + // {A} -> ACK, version 2 + // {B} -> NACK, version 1, rejected version 2, rejected reason: Failed to parse B + // {C} -> does not exist + verifyResourceValidInvalidCount(1, 1, 1, xdsServerInfo.target(), CDS.typeUrl()); + } + + @Test + public void serverFailureMetricReport() { + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, + rdsResourceWatcher); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + // Management server closes the RPC stream before sending any response. + call.sendCompleted(); + verify(ldsResourceWatcher, Mockito.timeout(1000).times(1)) + .onError(errorCaptor.capture()); + verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, + "ADS stream closed with OK before receiving a response"); + verify(rdsResourceWatcher).onError(errorCaptor.capture()); + verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, + "ADS stream closed with OK before receiving a response"); + verifyServerFailureCount(1, 1, xdsServerInfo.target()); + } + + @Test + public void serverFailureMetricReport_forRetryAndBackoff() { + InOrder inOrder = Mockito.inOrder(backoffPolicyProvider, backoffPolicy1, backoffPolicy2); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, + rdsResourceWatcher); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CDS_RESOURCE, cdsResourceWatcher); + xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), EDS_RESOURCE, edsResourceWatcher); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + + // Management server closes the RPC stream with an error. + call.sendError(Status.UNKNOWN.asException()); + verifyServerFailureCount(1, 1, xdsServerInfo.target()); + + // Retry after backoff. + inOrder.verify(backoffPolicyProvider).get(); + inOrder.verify(backoffPolicy1).nextBackoffNanos(); + ScheduledTask retryTask = + Iterables.getOnlyElement(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)); + assertThat(retryTask.getDelay(TimeUnit.NANOSECONDS)).isEqualTo(10L); + fakeClock.forwardNanos(10L); + call = resourceDiscoveryCalls.poll(); + + // Management server becomes unreachable. + String errorMsg = "my fault"; + call.sendError(Status.UNAVAILABLE.withDescription(errorMsg).asException()); + verifyServerFailureCount(2, 1, xdsServerInfo.target()); + + // Retry after backoff. + inOrder.verify(backoffPolicy1).nextBackoffNanos(); + retryTask = + Iterables.getOnlyElement(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)); + assertThat(retryTask.getDelay(TimeUnit.NANOSECONDS)).isEqualTo(100L); + fakeClock.forwardNanos(100L); + call = resourceDiscoveryCalls.poll(); + + List resources = ImmutableList.of(FAILING_ANY, testListenerRds, FAILING_ANY); + call.sendResponse(LDS, resources, "63", "3242"); + + List routeConfigs = ImmutableList.of(FAILING_ANY, testRouteConfig, FAILING_ANY); + call.sendResponse(RDS, routeConfigs, "5", "6764"); + + call.sendError(Status.DEADLINE_EXCEEDED.asException()); + // Server Failure metric will not be reported, as stream is closed with an error after receiving + // a response + verifyServerFailureCount(2, 1, xdsServerInfo.target()); + + // Reset backoff sequence and retry after backoff. + inOrder.verify(backoffPolicyProvider).get(); + inOrder.verify(backoffPolicy2).nextBackoffNanos(); + retryTask = + Iterables.getOnlyElement(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)); + assertThat(retryTask.getDelay(TimeUnit.NANOSECONDS)).isEqualTo(20L); + fakeClock.forwardNanos(20L); + call = resourceDiscoveryCalls.poll(); + + // Management server becomes unreachable again. + call.sendError(Status.UNAVAILABLE.asException()); + verifyServerFailureCount(3, 1, xdsServerInfo.target()); + + // Retry after backoff. + inOrder.verify(backoffPolicy2).nextBackoffNanos(); + retryTask = + Iterables.getOnlyElement(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)); + assertThat(retryTask.getDelay(TimeUnit.NANOSECONDS)).isEqualTo(200L); + fakeClock.forwardNanos(200L); + call = resourceDiscoveryCalls.poll(); + + List clusters = ImmutableList.of(FAILING_ANY, testClusterRoundRobin); + call.sendResponse(CDS, clusters, VERSION_1, "0000"); + call.sendCompleted(); + // Server Failure metric will not be reported once again, as stream is closed after receiving a + // response + verifyServerFailureCount(3, 1, xdsServerInfo.target()); + } + + private XdsClientImpl createXdsClient(String serverUri) { BootstrapInfo bootstrapInfo = buildBootStrap(serverUri); return new XdsClientImpl( @@ -3792,10 +4141,11 @@ private XdsClientImpl createXdsClient(String serverUri) { fakeClock.getStopwatchSupplier(), timeProvider, MessagePrinter.INSTANCE, - new TlsContextManagerImpl(bootstrapInfo)); + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); } - private BootstrapInfo buildBootStrap(String serverUri) { + private BootstrapInfo buildBootStrap(String serverUri) { ServerInfo xdsServerInfo = ServerInfo.create(serverUri, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), true); @@ -3902,7 +4252,7 @@ protected void sendResponse( } protected void sendResponse(XdsResourceType type, Any resource, String versionInfo, - String nonce) { + String nonce) { sendResponse(type, ImmutableList.of(resource), versionInfo, nonce); } @@ -3934,6 +4284,7 @@ protected void sendResponse(List clusters, long loadReportIntervalNano) } protected abstract static class MessageFactory { + /** Throws {@link InvalidProtocolBufferException} on {@link Any#unpack(Class)}. */ protected static final Any FAILING_ANY = Any.newBuilder().setTypeUrl("fake").build(); diff --git a/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java b/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java index ee164938b2d..4fb77f0be42 100644 --- a/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.when; import io.grpc.InsecureChannelCredentials; +import io.grpc.MetricRecorder; import io.grpc.internal.ObjectPool; import io.grpc.xds.SharedXdsClientPoolProvider.RefCountedXdsClientObjectPool; import io.grpc.xds.client.Bootstrapper.BootstrapInfo; @@ -51,6 +52,7 @@ public class SharedXdsClientPoolProviderTest { @Rule public final ExpectedException thrown = ExpectedException.none(); private final Node node = Node.newBuilder().setId("SharedXdsClientPoolProviderTest").build(); + private final MetricRecorder metricRecorder = new MetricRecorder() {}; private static final String DUMMY_TARGET = "dummy"; @Mock @@ -64,7 +66,7 @@ public void noServer() throws XdsInitializationException { SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); thrown.expect(XdsInitializationException.class); thrown.expectMessage("No xDS server provided"); - provider.getOrCreate(DUMMY_TARGET); + provider.getOrCreate(DUMMY_TARGET, metricRecorder); assertThat(provider.get(DUMMY_TARGET)).isNull(); } @@ -77,9 +79,9 @@ public void sharedXdsClientObjectPool() throws XdsInitializationException { SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); assertThat(provider.get(DUMMY_TARGET)).isNull(); - ObjectPool xdsClientPool = provider.getOrCreate(DUMMY_TARGET); + ObjectPool xdsClientPool = provider.getOrCreate(DUMMY_TARGET, metricRecorder); verify(bootstrapper).bootstrap(); - assertThat(provider.getOrCreate(DUMMY_TARGET)).isSameInstanceAs(xdsClientPool); + assertThat(provider.getOrCreate(DUMMY_TARGET, metricRecorder)).isSameInstanceAs(xdsClientPool); assertThat(provider.get(DUMMY_TARGET)).isNotNull(); assertThat(provider.get(DUMMY_TARGET)).isSameInstanceAs(xdsClientPool); verifyNoMoreInteractions(bootstrapper); @@ -90,8 +92,9 @@ public void refCountedXdsClientObjectPool_delayedCreation() { ServerInfo server = ServerInfo.create(SERVER_URI, InsecureChannelCredentials.create()); BootstrapInfo bootstrapInfo = BootstrapInfo.builder().servers(Collections.singletonList(server)).node(node).build(); + SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); RefCountedXdsClientObjectPool xdsClientPool = - new RefCountedXdsClientObjectPool(bootstrapInfo, DUMMY_TARGET); + provider.new RefCountedXdsClientObjectPool(bootstrapInfo, DUMMY_TARGET, metricRecorder); assertThat(xdsClientPool.getXdsClientForTest()).isNull(); XdsClient xdsClient = xdsClientPool.getObject(); assertThat(xdsClientPool.getXdsClientForTest()).isNotNull(); @@ -103,8 +106,9 @@ public void refCountedXdsClientObjectPool_refCounted() { ServerInfo server = ServerInfo.create(SERVER_URI, InsecureChannelCredentials.create()); BootstrapInfo bootstrapInfo = BootstrapInfo.builder().servers(Collections.singletonList(server)).node(node).build(); + SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); RefCountedXdsClientObjectPool xdsClientPool = - new RefCountedXdsClientObjectPool(bootstrapInfo, DUMMY_TARGET); + provider.new RefCountedXdsClientObjectPool(bootstrapInfo, DUMMY_TARGET, metricRecorder); // getObject once XdsClient xdsClient = xdsClientPool.getObject(); assertThat(xdsClient).isNotNull(); @@ -123,8 +127,9 @@ public void refCountedXdsClientObjectPool_getObjectCreatesNewInstanceIfAlreadySh ServerInfo server = ServerInfo.create(SERVER_URI, InsecureChannelCredentials.create()); BootstrapInfo bootstrapInfo = BootstrapInfo.builder().servers(Collections.singletonList(server)).node(node).build(); + SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); RefCountedXdsClientObjectPool xdsClientPool = - new RefCountedXdsClientObjectPool(bootstrapInfo, DUMMY_TARGET); + provider.new RefCountedXdsClientObjectPool(bootstrapInfo, DUMMY_TARGET, metricRecorder); XdsClient xdsClient1 = xdsClientPool.getObject(); assertThat(xdsClientPool.returnObject(xdsClient1)).isNull(); assertThat(xdsClient1.isShutDown()).isTrue(); diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java index 0b8e89de721..b2b713e9a8e 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import io.grpc.MetricRecorder; import io.grpc.internal.ObjectPool; import io.grpc.xds.Filter.NamedFilterConfig; import io.grpc.xds.XdsListenerResource.LdsUpdate; @@ -73,12 +74,13 @@ public class XdsClientFederationTest { private ObjectPool xdsClientPool; private XdsClient xdsClient; private static final String DUMMY_TARGET = "dummy"; + private final MetricRecorder metricRecorder = new MetricRecorder() {}; @Before public void setUp() throws XdsInitializationException { SharedXdsClientPoolProvider clientPoolProvider = new SharedXdsClientPoolProvider(); clientPoolProvider.setBootstrapOverride(defaultBootstrapOverride()); - xdsClientPool = clientPoolProvider.getOrCreate(DUMMY_TARGET); + xdsClientPool = clientPoolProvider.getOrCreate(DUMMY_TARGET, metricRecorder); xdsClient = xdsClientPool.getObject(); } diff --git a/xds/src/test/java/io/grpc/xds/XdsClientMetricReporterImplTest.java b/xds/src/test/java/io/grpc/xds/XdsClientMetricReporterImplTest.java new file mode 100644 index 00000000000..9ee3f88d921 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/XdsClientMetricReporterImplTest.java @@ -0,0 +1,394 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.google.protobuf.Any; +import io.envoyproxy.envoy.config.listener.v3.Listener; +import io.grpc.MetricInstrument; +import io.grpc.MetricRecorder; +import io.grpc.MetricRecorder.BatchCallback; +import io.grpc.MetricRecorder.BatchRecorder; +import io.grpc.MetricSink; +import io.grpc.xds.XdsClientMetricReporterImpl.MetricReporterCallback; +import io.grpc.xds.client.XdsClient; +import io.grpc.xds.client.XdsClient.ResourceMetadata; +import io.grpc.xds.client.XdsClient.ServerConnectionCallback; +import io.grpc.xds.client.XdsResourceType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** + * Unit tests for {@link XdsClientMetricReporterImpl}. + */ +@RunWith(JUnit4.class) +public class XdsClientMetricReporterImplTest { + + private static final String target = "test-target"; + private static final String server = "trafficdirector.googleapis.com"; + private static final String resourceTypeUrl = + "resourceTypeUrl.googleapis.com/envoy.config.cluster.v3.Cluster"; + + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + + @Mock + private MetricRecorder mockMetricRecorder; + @Mock + private XdsClient mockXdsClient; + @Mock + private BatchRecorder mockBatchRecorder; + @Captor + private ArgumentCaptor gaugeBatchCallbackCaptor; + + private XdsClientMetricReporterImpl reporter; + + @Before + public void setUp() { + reporter = new XdsClientMetricReporterImpl(mockMetricRecorder, target); + } + + @Test + public void reportResourceUpdates() { + // TODO(dnvindhya): add the "authority" label once available. + reporter.reportResourceUpdates(10, 5, server, resourceTypeUrl); + verify(mockMetricRecorder).addLongCounter( + eqMetricInstrumentName("grpc.xds_client.resource_updates_valid"), eq((long) 10), + eq(Lists.newArrayList(target, server, resourceTypeUrl)), + eq(Lists.newArrayList())); + verify(mockMetricRecorder).addLongCounter( + eqMetricInstrumentName("grpc.xds_client.resource_updates_invalid"), + eq((long) 5), + eq(Lists.newArrayList(target, server, resourceTypeUrl)), + eq(Lists.newArrayList())); + } + + @Test + public void reportServerFailure() { + reporter.reportServerFailure(1, server); + verify(mockMetricRecorder).addLongCounter( + eqMetricInstrumentName("grpc.xds_client.server_failure"), eq((long) 1), + eq(Lists.newArrayList(target, server)), + eq(Lists.newArrayList())); + } + + @Test + public void setXdsClient_reportMetrics() throws Exception { + SettableFuture future = SettableFuture.create(); + future.set(null); + when(mockXdsClient.getSubscribedResourcesMetadataSnapshot()).thenReturn(Futures.immediateFuture( + ImmutableMap.of())); + when(mockXdsClient.reportServerConnections(any(ServerConnectionCallback.class))) + .thenReturn(future); + reporter.setXdsClient(mockXdsClient); + verify(mockMetricRecorder).registerBatchCallback(gaugeBatchCallbackCaptor.capture(), + eqMetricInstrumentName("grpc.xds_client.connected"), + eqMetricInstrumentName("grpc.xds_client.resources")); + gaugeBatchCallbackCaptor.getValue().accept(mockBatchRecorder); + verify(mockXdsClient).reportServerConnections(any(ServerConnectionCallback.class)); + } + + @Test + public void setXdsClient_reportCallbackMetrics_resourceCountsFails() { + TestlogHandler testLogHandler = new TestlogHandler(); + Logger logger = Logger.getLogger(XdsClientMetricReporterImpl.class.getName()); + logger.addHandler(testLogHandler); + + // For reporting resource counts connections, return a normally completed future + SettableFuture future = SettableFuture.create(); + future.set(null); + when(mockXdsClient.getSubscribedResourcesMetadataSnapshot()).thenReturn(Futures.immediateFuture( + ImmutableMap.of())); + + // Create a future that will throw an exception + SettableFuture serverConnectionsFeature = SettableFuture.create(); + serverConnectionsFeature.setException(new Exception("test")); + when(mockXdsClient.reportServerConnections(any())).thenReturn(serverConnectionsFeature); + + reporter.setXdsClient(mockXdsClient); + verify(mockMetricRecorder) + .registerBatchCallback(gaugeBatchCallbackCaptor.capture(), any(), any()); + gaugeBatchCallbackCaptor.getValue().accept(mockBatchRecorder); + // Verify that the xdsClient methods were called + // verify(mockXdsClient).reportResourceCounts(any()); + verify(mockXdsClient).reportServerConnections(any()); + + assertThat(testLogHandler.getLogs().size()).isEqualTo(1); + assertThat(testLogHandler.getLogs().get(0).getLevel()).isEqualTo(Level.WARNING); + assertThat(testLogHandler.getLogs().get(0).getMessage()).isEqualTo( + "Failed to report gauge metrics"); + logger.removeHandler(testLogHandler); + } + + @Test + public void metricGauges() { + SettableFuture future = SettableFuture.create(); + future.set(null); + when(mockXdsClient.getSubscribedResourcesMetadataSnapshot()).thenReturn(Futures.immediateFuture( + ImmutableMap.of())); + when(mockXdsClient.reportServerConnections(any(ServerConnectionCallback.class))) + .thenReturn(future); + reporter.setXdsClient(mockXdsClient); + verify(mockMetricRecorder).registerBatchCallback(gaugeBatchCallbackCaptor.capture(), + eqMetricInstrumentName("grpc.xds_client.connected"), + eqMetricInstrumentName("grpc.xds_client.resources")); + BatchCallback gaugeBatchCallback = gaugeBatchCallbackCaptor.getValue(); + InOrder inOrder = inOrder(mockBatchRecorder); + // Trigger the internal call to reportCallbackMetrics() + gaugeBatchCallback.accept(mockBatchRecorder); + + ArgumentCaptor serverConnectionCallbackCaptor = + ArgumentCaptor.forClass(ServerConnectionCallback.class); + // verify(mockXdsClient).reportResourceCounts(resourceCallbackCaptor.capture()); + verify(mockXdsClient).reportServerConnections(serverConnectionCallbackCaptor.capture()); + + // Get the captured callback + MetricReporterCallback callback = (MetricReporterCallback) + serverConnectionCallbackCaptor.getValue(); + + // Verify that reportResourceCounts and reportServerConnections were called + // with the captured callback + callback.reportResourceCountGauge(10, "acked", resourceTypeUrl); + inOrder.verify(mockBatchRecorder) + .recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), eq(10L), any(), + any()); + callback.reportServerConnectionGauge(true, "xdsServer"); + inOrder.verify(mockBatchRecorder) + .recordLongGauge(eqMetricInstrumentName("grpc.xds_client.connected"), eq(1L), any(), any()); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void metricReporterCallback() { + MetricReporterCallback callback = + new MetricReporterCallback(mockBatchRecorder, target); + + callback.reportServerConnectionGauge(true, server); + verify(mockBatchRecorder, times(1)).recordLongGauge( + eqMetricInstrumentName("grpc.xds_client.connected"), eq(1L), + eq(Lists.newArrayList(target, server)), + eq(Lists.newArrayList())); + + String cacheState = "requested"; + callback.reportResourceCountGauge(10, cacheState, resourceTypeUrl); + verify(mockBatchRecorder, times(1)).recordLongGauge( + eqMetricInstrumentName("grpc.xds_client.resources"), eq(10L), + eq(Arrays.asList(target, cacheState, resourceTypeUrl)), + eq(Collections.emptyList())); + } + + @Test + public void reportCallbackMetrics_computeAndReportResourceCounts() { + Map, Map> metadataByType = new HashMap<>(); + XdsResourceType listenerResource = XdsListenerResource.getInstance(); + XdsResourceType routeConfigResource = XdsRouteConfigureResource.getInstance(); + XdsResourceType clusterResource = XdsClusterResource.getInstance(); + + Any rawListener = + Any.pack(Listener.newBuilder().setName("listener.googleapis.com").build()); + long nanosLastUpdate = 1577923199_606042047L; + + Map ldsResourceMetadataMap = new HashMap<>(); + ldsResourceMetadataMap.put("resource1", + ResourceMetadata.newResourceMetadataRequested()); + ResourceMetadata ackedLdsResource = ResourceMetadata.newResourceMetadataAcked(rawListener, "42", + nanosLastUpdate); + ldsResourceMetadataMap.put("resource2", ackedLdsResource); + ldsResourceMetadataMap.put("resource3", + ResourceMetadata.newResourceMetadataAcked(rawListener, "43", nanosLastUpdate)); + ldsResourceMetadataMap.put("resource4", + ResourceMetadata.newResourceMetadataNacked(ackedLdsResource, "44", nanosLastUpdate, + "nacked after previous ack", true)); + + Map rdsResourceMetadataMap = new HashMap<>(); + ResourceMetadata requestedRdsResourceMetadata = ResourceMetadata.newResourceMetadataRequested(); + rdsResourceMetadataMap.put("resource5", + ResourceMetadata.newResourceMetadataNacked(requestedRdsResourceMetadata, "24", + nanosLastUpdate, "nacked after request", false)); + rdsResourceMetadataMap.put("resource6", + ResourceMetadata.newResourceMetadataDoesNotExist()); + + Map cdsResourceMetadataMap = new HashMap<>(); + cdsResourceMetadataMap.put("resource7", ResourceMetadata.newResourceMetadataUnknown()); + + metadataByType.put(listenerResource, ldsResourceMetadataMap); + metadataByType.put(routeConfigResource, rdsResourceMetadataMap); + metadataByType.put(clusterResource, cdsResourceMetadataMap); + + SettableFuture reportServerConnectionsCompleted = SettableFuture.create(); + reportServerConnectionsCompleted.set(null); + when(mockXdsClient.reportServerConnections(any(MetricReporterCallback.class))) + .thenReturn(reportServerConnectionsCompleted); + + ListenableFuture, Map>> + getResourceMetadataCompleted = Futures.immediateFuture(metadataByType); + when(mockXdsClient.getSubscribedResourcesMetadataSnapshot()) + .thenReturn(getResourceMetadataCompleted); + + reporter.reportCallbackMetrics(mockBatchRecorder, mockXdsClient); + + // LDS resource requested + verify(mockBatchRecorder).recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), + eq(1L), eq(Arrays.asList(target, "requested", listenerResource.typeUrl())), any()); + // LDS resources acked + verify(mockBatchRecorder).recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), + eq(2L), eq(Arrays.asList(target, "acked", listenerResource.typeUrl())), any()); + // LDS resource nacked but cached + verify(mockBatchRecorder).recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), + eq(1L), eq(Arrays.asList(target, "nacked_but_cached", listenerResource.typeUrl())), any()); + + // RDS resource nacked + verify(mockBatchRecorder).recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), + eq(1L), eq(Arrays.asList(target, "nacked", routeConfigResource.typeUrl())), any()); + // RDS resource does not exist + verify(mockBatchRecorder).recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), + eq(1L), eq(Arrays.asList(target, "does_not_exist", routeConfigResource.typeUrl())), any()); + + // CDS resource unknown + verify(mockBatchRecorder).recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), + eq(1L), eq(Arrays.asList(target, "unknown", clusterResource.typeUrl())), any()); + verifyNoMoreInteractions(mockBatchRecorder); + } + + @Test + public void reportCallbackMetrics_computeAndReportResourceCounts_emptyResources() { + Map, Map> metadataByType = new HashMap<>(); + XdsResourceType listenerResource = XdsListenerResource.getInstance(); + metadataByType.put(listenerResource, Collections.emptyMap()); + + SettableFuture reportServerConnectionsCompleted = SettableFuture.create(); + reportServerConnectionsCompleted.set(null); + when(mockXdsClient.reportServerConnections(any(MetricReporterCallback.class))) + .thenReturn(reportServerConnectionsCompleted); + + ListenableFuture, Map>> + getResourceMetadataCompleted = Futures.immediateFuture(metadataByType); + when(mockXdsClient.getSubscribedResourcesMetadataSnapshot()) + .thenReturn(getResourceMetadataCompleted); + + reporter.reportCallbackMetrics(mockBatchRecorder, mockXdsClient); + + // Verify that reportResourceCountGauge is never called + verifyNoInteractions(mockBatchRecorder); + } + + @Test + public void reportCallbackMetrics_computeAndReportResourceCounts_nullMetadata() { + TestlogHandler testLogHandler = new TestlogHandler(); + Logger logger = Logger.getLogger(XdsClientMetricReporterImpl.class.getName()); + logger.addHandler(testLogHandler); + + SettableFuture reportServerConnectionsCompleted = SettableFuture.create(); + reportServerConnectionsCompleted.set(null); + when(mockXdsClient.reportServerConnections(any(MetricReporterCallback.class))) + .thenReturn(reportServerConnectionsCompleted); + + ListenableFuture, Map>> + getResourceMetadataCompleted = Futures.immediateFailedFuture( + new Exception("Error generating metadata snapshot")); + when(mockXdsClient.getSubscribedResourcesMetadataSnapshot()) + .thenReturn(getResourceMetadataCompleted); + + reporter.reportCallbackMetrics(mockBatchRecorder, mockXdsClient); + assertThat(testLogHandler.getLogs().size()).isEqualTo(1); + assertThat(testLogHandler.getLogs().get(0).getLevel()).isEqualTo(Level.WARNING); + assertThat(testLogHandler.getLogs().get(0).getMessage()).isEqualTo( + "Failed to report gauge metrics"); + logger.removeHandler(testLogHandler); + } + + @Test + public void close_closesGaugeRegistration() { + MetricSink.Registration mockRegistration = mock(MetricSink.Registration.class); + when(mockMetricRecorder.registerBatchCallback(any(MetricRecorder.BatchCallback.class), + eqMetricInstrumentName("grpc.xds_client.connected"), + eqMetricInstrumentName("grpc.xds_client.resources"))).thenReturn(mockRegistration); + + // Sets XdsClient and register the gauges + reporter.setXdsClient(mockXdsClient); + // Closes registered gauges + reporter.close(); + verify(mockRegistration, times(1)).close(); + } + + @SuppressWarnings("TypeParameterUnusedInFormals") + private T eqMetricInstrumentName(String name) { + return argThat(new ArgumentMatcher() { + @Override + public boolean matches(T instrument) { + return instrument.getName().equals(name); + } + }); + } + + static class TestlogHandler extends Handler { + List logs = new ArrayList<>(); + + @Override + public void publish(LogRecord record) { + logs.add(record); + } + + @Override + public void close() {} + + @Override + public void flush() {} + + public List getLogs() { + return logs; + } + } + +} diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverProviderTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverProviderTest.java index a216c3de028..33aae268c60 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverProviderTest.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap; import io.grpc.ChannelLogger; import io.grpc.InternalServiceProviders; +import io.grpc.MetricRecorder; import io.grpc.NameResolver; import io.grpc.NameResolver.ServiceConfigParser; import io.grpc.NameResolverProvider; @@ -57,6 +58,7 @@ public void uncaughtException(Thread t, Throwable e) { .setServiceConfigParser(mock(ServiceConfigParser.class)) .setScheduledExecutorService(fakeClock.getScheduledExecutorService()) .setChannelLogger(mock(ChannelLogger.class)) + .setMetricRecorder(mock(MetricRecorder.class)) .build(); private XdsNameResolverProvider provider = new XdsNameResolverProvider(); diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index f32c198f21f..6f4c1503cee 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -55,6 +55,7 @@ import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.MethodDescriptor.MethodType; +import io.grpc.MetricRecorder; import io.grpc.NameResolver; import io.grpc.NameResolver.ConfigOrError; import io.grpc.NameResolver.ResolutionResult; @@ -152,6 +153,7 @@ public ConfigOrError parseServiceConfig(Map rawServiceConfig) { private final CallInfo call1 = new CallInfo("HelloService", "hi"); private final CallInfo call2 = new CallInfo("GreetService", "bye"); private final TestChannel channel = new TestChannel(); + private final MetricRecorder metricRecorder = new MetricRecorder() {}; private BootstrapInfo bootstrapInfo = BootstrapInfo.builder() .servers(ImmutableList.of(ServerInfo.create( "td.googleapis.com", InsecureChannelCredentials.create()))) @@ -187,7 +189,7 @@ public void setUp() { RouterFilter.INSTANCE); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, filterRegistry, null); + xdsClientPoolFactory, mockRandom, filterRegistry, null, metricRecorder); } @After @@ -215,7 +217,8 @@ public ObjectPool get(String target) { } @Override - public ObjectPool getOrCreate(String target) throws XdsInitializationException { + public ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) + throws XdsInitializationException { throw new XdsInitializationException("Fail to read bootstrap file"); } @@ -227,7 +230,8 @@ public List getTargets() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + metricRecorder); resolver.start(mockListener); verify(mockListener).onError(errorCaptor.capture()); Status error = errorCaptor.getValue(); @@ -240,7 +244,8 @@ public List getTargets() { public void resolving_withTargetAuthorityNotFound() { resolver = new XdsNameResolver(targetUri, "notfound.google.com", AUTHORITY, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + metricRecorder); resolver.start(mockListener); verify(mockListener).onError(errorCaptor.capture()); Status error = errorCaptor.getValue(); @@ -262,7 +267,7 @@ public void resolving_noTargetAuthority_templateWithoutXdstp() { resolver = new XdsNameResolver( targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, - mockRandom, FilterRegistry.getDefaultRegistry(), null); + mockRandom, FilterRegistry.getDefaultRegistry(), null, metricRecorder); resolver.start(mockListener); verify(mockListener, never()).onError(any(Status.class)); } @@ -282,7 +287,8 @@ public void resolving_noTargetAuthority_templateWithXdstp() { + "%5B::FFFF:129.144.52.38%5D:80?id=1"; resolver = new XdsNameResolver( targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + metricRecorder); resolver.start(mockListener); verify(mockListener, never()).onError(any(Status.class)); } @@ -302,7 +308,8 @@ public void resolving_noTargetAuthority_xdstpWithMultipleSlashes() { + "path/to/service?id=1"; resolver = new XdsNameResolver( targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + metricRecorder); // The Service Authority must be URL encoded, but unlike the LDS resource name. @@ -330,7 +337,8 @@ public void resolving_targetAuthorityInAuthoritiesMap() { + "%5B::FFFF:129.144.52.38%5D:80?bar=2&foo=1"; // query param canonified resolver = new XdsNameResolver(targetUri, "xds.authority.com", serviceAuthority, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + metricRecorder); resolver.start(mockListener); verify(mockListener, never()).onError(any(Status.class)); } @@ -362,7 +370,8 @@ public void resolving_ldsResourceUpdateRdsName() { .build(); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + metricRecorder); // use different ldsResourceName and service authority. The virtualhost lookup should use // service authority. expectedLdsResourceName = "test-" + expectedLdsResourceName; @@ -543,7 +552,8 @@ public void resolving_matchingVirtualHostNotFound_matchingOverrideAuthority() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, "random", serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + metricRecorder); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate(0L, Arrays.asList(virtualHost)); @@ -566,7 +576,8 @@ public void resolving_matchingVirtualHostNotFound_notMatchingOverrideAuthority() resolver = new XdsNameResolver(targetUri, null, AUTHORITY, "random", serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + metricRecorder); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate(0L, Arrays.asList(virtualHost)); @@ -577,7 +588,8 @@ public void resolving_matchingVirtualHostNotFound_notMatchingOverrideAuthority() public void resolving_matchingVirtualHostNotFoundForOverrideAuthority() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, AUTHORITY, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + metricRecorder); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate(0L, buildUnmatchedVirtualHosts()); @@ -661,7 +673,8 @@ public void retryPolicyInPerMethodConfigGeneratedByResolverIsValid() { ServiceConfigParser realParser = new ScParser( true, 5, 5, new AutoConfiguredLoadBalancerFactory("pick-first")); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, realParser, syncContext, - scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + metricRecorder); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); RetryPolicy retryPolicy = RetryPolicy.create( @@ -871,7 +884,8 @@ public void resolved_rpcHashingByChannelId() { when(mockRandom.nextLong()).thenReturn(123L); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + metricRecorder); resolver.start(mockListener); xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( @@ -904,7 +918,7 @@ public void resolved_rpcHashingByChannelId() { public void resolved_routeActionHasAutoHostRewrite_emitsCallOptionForTheSame() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, - FilterRegistry.getDefaultRegistry(), null); + FilterRegistry.getDefaultRegistry(), null, metricRecorder); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( @@ -935,7 +949,7 @@ public void resolved_routeActionHasAutoHostRewrite_emitsCallOptionForTheSame() { public void resolved_routeActionNoAutoHostRewrite_doesntEmitCallOptionForTheSame() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, - FilterRegistry.getDefaultRegistry(), null); + FilterRegistry.getDefaultRegistry(), null, metricRecorder); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( @@ -1994,7 +2008,8 @@ public ObjectPool get(String target) { } @Override - public ObjectPool getOrCreate(String target) throws XdsInitializationException { + public ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) + throws XdsInitializationException { targets.add(target); return new ObjectPool() { @Override diff --git a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java index 791318c5355..a27c2917712 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java @@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.SettableFuture; import io.grpc.InsecureChannelCredentials; +import io.grpc.MetricRecorder; import io.grpc.internal.ObjectPool; import io.grpc.xds.EnvoyServerProtoData.ConnectionSourceType; import io.grpc.xds.EnvoyServerProtoData.FilterChain; @@ -151,7 +152,8 @@ public ObjectPool get(String target) { } @Override - public ObjectPool getOrCreate(String target) throws XdsInitializationException { + public ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) + throws XdsInitializationException { return new ObjectPool() { @Override public XdsClient getObject() { From a79982c7fded85ac6f53896f045e959b6cbd7c43 Mon Sep 17 00:00:00 2001 From: Yash Tibrewal Date: Mon, 25 Nov 2024 21:07:52 -0800 Subject: [PATCH 094/591] [CSM] Use xds-enabled server and xds credentials in examples (#11706) --- examples/example-gcp-csm-observability/build.gradle | 1 + .../csmobservability/CsmObservabilityClient.java | 7 +++++-- .../csmobservability/CsmObservabilityServer.java | 12 ++++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index 7a72ebcdcd9..87bdc32f64f 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-gcp-csm-observability:${grpcVersion}" + implementation "io.grpc:grpc-xds:${grpcVersion}" implementation "io.opentelemetry:opentelemetry-sdk:${openTelemetryVersion}" implementation "io.opentelemetry:opentelemetry-sdk-metrics:${openTelemetryVersion}" implementation "io.opentelemetry:opentelemetry-exporter-prometheus:${openTelemetryPrometheusVersion}" diff --git a/examples/example-gcp-csm-observability/src/main/java/io/grpc/examples/csmobservability/CsmObservabilityClient.java b/examples/example-gcp-csm-observability/src/main/java/io/grpc/examples/csmobservability/CsmObservabilityClient.java index 7387c18da96..dd0ab7eb546 100644 --- a/examples/example-gcp-csm-observability/src/main/java/io/grpc/examples/csmobservability/CsmObservabilityClient.java +++ b/examples/example-gcp-csm-observability/src/main/java/io/grpc/examples/csmobservability/CsmObservabilityClient.java @@ -25,6 +25,7 @@ import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloRequest; import io.grpc.gcp.csm.observability.CsmObservability; +import io.grpc.xds.XdsChannelCredentials; import io.opentelemetry.exporter.prometheus.PrometheusHttpServer; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.metrics.SdkMeterProvider; @@ -127,8 +128,10 @@ public void run() { observability.registerGlobal(); // Create a communication channel to the server, known as a Channel. - ManagedChannel channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()) - .build(); + ManagedChannel channel = + Grpc.newChannelBuilder( + target, XdsChannelCredentials.create(InsecureChannelCredentials.create())) + .build(); CsmObservabilityClient client = new CsmObservabilityClient(channel); try { diff --git a/examples/example-gcp-csm-observability/src/main/java/io/grpc/examples/csmobservability/CsmObservabilityServer.java b/examples/example-gcp-csm-observability/src/main/java/io/grpc/examples/csmobservability/CsmObservabilityServer.java index 78df71b65a9..589753b1a4c 100644 --- a/examples/example-gcp-csm-observability/src/main/java/io/grpc/examples/csmobservability/CsmObservabilityServer.java +++ b/examples/example-gcp-csm-observability/src/main/java/io/grpc/examples/csmobservability/CsmObservabilityServer.java @@ -24,6 +24,8 @@ import io.grpc.examples.helloworld.HelloRequest; import io.grpc.gcp.csm.observability.CsmObservability; import io.grpc.stub.StreamObserver; +import io.grpc.xds.XdsServerBuilder; +import io.grpc.xds.XdsServerCredentials; import io.opentelemetry.exporter.prometheus.PrometheusHttpServer; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.metrics.SdkMeterProvider; @@ -40,10 +42,12 @@ public class CsmObservabilityServer { private Server server; private void start(int port) throws IOException { - server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()) - .addService(new GreeterImpl()) - .build() - .start(); + server = + XdsServerBuilder.forPort( + port, XdsServerCredentials.create(InsecureServerCredentials.create())) + .addService(new GreeterImpl()) + .build() + .start(); logger.info("Server started, listening on " + port); } From 29dd9bad3f19f948a8d157449b022b96b5b8d6b6 Mon Sep 17 00:00:00 2001 From: Riya Mehta Date: Mon, 25 Nov 2024 16:30:56 -0800 Subject: [PATCH 095/591] change s2av2_credentials to s2a --- s2a/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/s2a/BUILD.bazel b/s2a/BUILD.bazel index 283828a9f7e..f8fb7f1df5e 100644 --- a/s2a/BUILD.bazel +++ b/s2a/BUILD.bazel @@ -79,7 +79,7 @@ java_library( ) java_library( - name = "s2av2_credentials", + name = "s2a", srcs = ["src/main/java/io/grpc/s2a/S2AChannelCredentials.java"], visibility = ["//visibility:public"], deps = [ From 229a010f55cbb729489df195035c08749f9deb2a Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 26 Nov 2024 21:36:50 +0530 Subject: [PATCH 096/591] Start 1.70.0 development cycle (#11708) * Start 1.70.0 development cycle --- MODULE.bazel | 2 +- build.gradle | 2 +- .../src/test/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/test/golden/TestService.java.txt | 2 +- core/src/main/java/io/grpc/internal/GrpcUtil.java | 2 +- examples/MODULE.bazel | 2 +- examples/android/clientcache/app/build.gradle | 10 +++++----- examples/android/helloworld/app/build.gradle | 8 ++++---- examples/android/routeguide/app/build.gradle | 8 ++++---- examples/android/strictmode/app/build.gradle | 8 ++++---- examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 4 ++-- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 4 ++-- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 4 ++-- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 4 ++-- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 4 ++-- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- 34 files changed, 55 insertions(+), 55 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 724f537ee6f..edb5284a23d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "grpc-java", compatibility_level = 0, repo_name = "io_grpc_grpc_java", - version = "1.69.0-SNAPSHOT", # CURRENT_GRPC_VERSION + version = "1.70.0-SNAPSHOT", # CURRENT_GRPC_VERSION ) # GRPC_DEPS_START diff --git a/build.gradle b/build.gradle index 1013a2a4386..52ca19ab4ba 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.69.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.70.0-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index adfe415c845..36ff11a160a 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.69.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.70.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index 438d067ac0d..69a1b0947b5 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.69.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.70.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index f519b502c1f..8a65c7d6c2b 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -219,7 +219,7 @@ public byte[] parseAsciiString(byte[] serialized) { public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); - public static final String IMPLEMENTATION_VERSION = "1.69.0-SNAPSHOT"; // CURRENT_GRPC_VERSION + public static final String IMPLEMENTATION_VERSION = "1.70.0-SNAPSHOT"; // CURRENT_GRPC_VERSION /** * The default timeout in nanos for a keepalive ping request. diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index e7b65724a5b..72541817979 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,5 +1,5 @@ bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") -bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.69.0-SNAPSHOT") # CURRENT_GRPC_VERSION +bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.70.0-SNAPSHOT") # CURRENT_GRPC_VERSION bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") bazel_dep(name = "rules_jvm_external", version = "6.0") diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index 271d29cfb4b..d279155b64e 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -34,7 +34,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -54,12 +54,12 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.5' - testImplementation 'io.grpc:grpc-testing:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + testImplementation 'io.grpc:grpc-testing:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index e51176cc4f6..d56a447377b 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index 3209937ba70..1dceb21b5c9 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index 1fdf43d1f3f..2bb696027fd 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -33,7 +33,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -53,8 +53,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/build.gradle b/examples/build.gradle index 83877cbb8d9..756a9c47bb1 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index e0d565a41f2..f0dc0bd8850 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index 7926d53138b..7cd3b5de38e 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -25,7 +25,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index d08ec7787eb..2239bc03d5a 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -6,13 +6,13 @@ jar - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT example-debug https://github.com/grpc/grpc-java UTF-8 - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index ca9ad9a60b8..875bdfda623 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -25,7 +25,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index 94691b1d1d2..3c7f14cacde 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -6,13 +6,13 @@ jar - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT example-dualstack https://github.com/grpc/grpc-java UTF-8 - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 98841c20ef9..3ff0284c5a1 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index 6a1b11d5e20..0aeedccbda3 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,13 +6,13 @@ jar - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index 87bdc32f64f..c72f1ba9753 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -25,7 +25,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 3365b844895..5199105baae 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -25,7 +25,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 54407994dab..784b0c71a47 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index baebfc6fc50..b22318f4d53 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,13 +6,13 @@ jar - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index 0e14f369ebe..ba7aeb05b19 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index d20358f9266..f90da62fcd8 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,13 +7,13 @@ jar - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index ee46d69cdaf..6e07cbc7447 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index fe48c0db6ee..22d42abf642 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -7,13 +7,13 @@ jar - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT example-oauth https://github.com/grpc/grpc-java UTF-8 - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index ccdcf93ebfd..5c1e419c82e 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index 43df71e5ed7..babd0c7d18e 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -18,7 +18,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index 769e9bddee8..d82f5ecda7e 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -18,7 +18,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index e4a5128898f..50b9d47632c 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index 1eb51182309..2d61862700b 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index de32a5f9807..e16ce1018a8 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,13 +6,13 @@ jar - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT example-tls https://github.com/grpc/grpc-java UTF-8 - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index a77e71b253c..bb404bdb07c 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.69.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/pom.xml b/examples/pom.xml index 475747a5b5e..04cd792cd3e 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,13 +6,13 @@ jar - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT examples https://github.com/grpc/grpc-java UTF-8 - 1.69.0-SNAPSHOT + 1.70.0-SNAPSHOT 3.25.5 3.25.5 From 55cef6330f78f6e60419c0ec345328e222fbacb9 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Wed, 27 Nov 2024 10:48:59 -0800 Subject: [PATCH 097/591] s2a: Load resources from classpath instead of from disk --- .../grpc/s2a/S2AChannelCredentialsTest.java | 16 +- .../S2AHandshakerServiceChannelTest.java | 16 +- .../handshaker/FakeS2AServerTest.java | 25 +++- .../s2a/internal/handshaker/FakeWriter.java | 141 ++++++++++-------- .../internal/handshaker/IntegrationTest.java | 39 +++-- .../handshaker/S2APrivateKeyMethodTest.java | 10 +- .../s2a/internal/handshaker/S2AStubTest.java | 26 +++- 7 files changed, 156 insertions(+), 117 deletions(-) diff --git a/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java b/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java index fd5bfd654f3..3e6eef7f470 100644 --- a/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java +++ b/s2a/src/test/java/io/grpc/s2a/S2AChannelCredentialsTest.java @@ -22,7 +22,7 @@ import io.grpc.ChannelCredentials; import io.grpc.InsecureChannelCredentials; import io.grpc.TlsChannelCredentials; -import java.io.File; +import java.io.InputStream; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -124,15 +124,13 @@ public void build_withUseMtlsToS2AWithLocalUid_success() throws Exception { } private static ChannelCredentials getTlsChannelCredentials() throws Exception { - String privateKeyPath = "src/test/resources/client_key.pem"; - String certChainPath = "src/test/resources/client_cert.pem"; - String trustBundlePath = "src/test/resources/root_cert.pem"; - File privateKeyFile = new File(privateKeyPath); - File certChainFile = new File(certChainPath); - File trustBundleFile = new File(trustBundlePath); + ClassLoader classLoader = S2AChannelCredentialsTest.class.getClassLoader(); + InputStream privateKey = classLoader.getResourceAsStream("client_key.pem"); + InputStream certChain = classLoader.getResourceAsStream("client_cert.pem"); + InputStream trustBundle = classLoader.getResourceAsStream("root_cert.pem"); return TlsChannelCredentials.newBuilder() - .keyManager(certChainFile, privateKeyFile) - .trustManager(trustBundleFile) + .keyManager(certChain, privateKey) + .trustManager(trustBundle) .build(); } } \ No newline at end of file diff --git a/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java b/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java index 46f705d2f77..9ba3caaf99e 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannelTest.java @@ -37,7 +37,7 @@ import io.grpc.testing.protobuf.SimpleRequest; import io.grpc.testing.protobuf.SimpleResponse; import io.grpc.testing.protobuf.SimpleServiceGrpc; -import java.io.File; +import java.io.InputStream; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -218,9 +218,10 @@ public void create_mtlsSucceedsAfterCloseIsCalledOnce() throws Exception { private static Server createMtlsServer() throws Exception { SimpleServiceImpl service = new SimpleServiceImpl(); - File serverCert = new File("src/test/resources/server_cert.pem"); - File serverKey = new File("src/test/resources/server_key.pem"); - File rootCert = new File("src/test/resources/root_cert.pem"); + ClassLoader classLoader = S2AHandshakerServiceChannelTest.class.getClassLoader(); + InputStream serverCert = classLoader.getResourceAsStream("server_cert.pem"); + InputStream serverKey = classLoader.getResourceAsStream("server_key.pem"); + InputStream rootCert = classLoader.getResourceAsStream("root_cert.pem"); ServerCredentials creds = TlsServerCredentials.newBuilder() .keyManager(serverCert, serverKey) @@ -238,9 +239,10 @@ private static Server createPlaintextServer() { } private static ChannelCredentials getTlsChannelCredentials() throws Exception { - File clientCert = new File("src/test/resources/client_cert.pem"); - File clientKey = new File("src/test/resources/client_key.pem"); - File rootCert = new File("src/test/resources/root_cert.pem"); + ClassLoader classLoader = S2AHandshakerServiceChannelTest.class.getClassLoader(); + InputStream clientCert = classLoader.getResourceAsStream("client_cert.pem"); + InputStream clientKey = classLoader.getResourceAsStream("client_key.pem"); + InputStream rootCert = classLoader.getResourceAsStream("root_cert.pem"); return TlsChannelCredentials.newBuilder() .keyManager(clientCert, clientKey) .trustManager(rootCert) diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java index e1ee878b9cc..fc3bbba9e39 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java @@ -30,8 +30,8 @@ import io.grpc.s2a.internal.handshaker.ValidatePeerCertificateChainReq.VerificationMode; import io.grpc.stub.StreamObserver; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -118,18 +118,29 @@ public void onCompleted() { executor.awaitTermination(1, SECONDS); } + String leafCertString = ""; + String cert2String = ""; + String cert1String = ""; + ClassLoader classLoader = FakeS2AServerTest.class.getClassLoader(); + try ( + InputStream leafCert = classLoader.getResourceAsStream("leaf_cert_ec.pem"); + InputStream cert2 = classLoader.getResourceAsStream("int_cert2_ec.pem"); + InputStream cert1 = classLoader.getResourceAsStream("int_cert1_ec.pem"); + ) { + leafCertString = FakeWriter.convertInputStreamToString(leafCert); + cert2String = FakeWriter.convertInputStreamToString(cert2); + cert1String = FakeWriter.convertInputStreamToString(cert1); + } + SessionResp expected = SessionResp.newBuilder() .setGetTlsConfigurationResp( GetTlsConfigurationResp.newBuilder() .setClientTlsConfiguration( GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.leafCertFile.toPath()), StandardCharsets.UTF_8)) - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.cert1File.toPath()), StandardCharsets.UTF_8)) - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.cert2File.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(leafCertString) + .addCertificateChain(cert1String) + .addCertificateChain(cert2String) .setMinTlsVersion(TLSVersion.TLS_VERSION_1_3) .setMaxTlsVersion(TLSVersion.TLS_VERSION_1_3) .addCiphersuites( diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java index 4455e77b1e2..0b3ecff3f8e 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java @@ -20,17 +20,17 @@ import static io.grpc.s2a.internal.handshaker.TLSVersion.TLS_VERSION_1_3; import com.google.common.collect.ImmutableMap; +import com.google.common.io.CharStreams; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.ByteString; import io.grpc.stub.StreamObserver; import io.grpc.util.CertificateUtils; -import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.Signature; @@ -54,14 +54,7 @@ enum VerificationResult { FAILURE } - public static final File leafCertFile = - new File("src/test/resources/leaf_cert_ec.pem"); - public static final File cert2File = - new File("src/test/resources/int_cert2_ec.pem"); - public static final File cert1File = - new File("src/test/resources/int_cert1_ec.pem"); - public static final File keyFile = - new File("src/test/resources/leaf_key_ec.pem"); + private static final ClassLoader classLoader = FakeWriter.class.getClassLoader(); private static final ImmutableMap ALGORITHM_TO_SIGNATURE_INSTANCE_IDENTIFIER = ImmutableMap.of( @@ -79,6 +72,10 @@ enum VerificationResult { private String failureReason; private PrivateKey privateKey; + public static String convertInputStreamToString(InputStream is) throws IOException { + return CharStreams.toString(new InputStreamReader(is, StandardCharsets.UTF_8)); + } + @CanIgnoreReturnValue FakeWriter setReader(StreamObserver reader) { this.reader = reader; @@ -106,11 +103,10 @@ FakeWriter setFailureReason(String failureReason) { @CanIgnoreReturnValue FakeWriter initializePrivateKey() throws InvalidKeySpecException, NoSuchAlgorithmException, IOException, FileNotFoundException, UnsupportedEncodingException { - FileInputStream keyInputStream = new FileInputStream(keyFile); - try { + try ( + InputStream keyInputStream = classLoader.getResourceAsStream("leaf_key_ec.pem"); + ) { privateKey = CertificateUtils.getPrivateKey(keyInputStream); - } finally { - keyInputStream.close(); } return this; } @@ -130,32 +126,39 @@ void sendIoError() { } void sendGetTlsConfigResp() { - try { - reader.onNext( - SessionResp.newBuilder() - .setGetTlsConfigurationResp( - GetTlsConfigurationResp.newBuilder() - .setClientTlsConfiguration( - GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.leafCertFile.toPath()), StandardCharsets.UTF_8)) - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.cert1File.toPath()), StandardCharsets.UTF_8)) - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.cert2File.toPath()), StandardCharsets.UTF_8)) - .setMinTlsVersion(TLS_VERSION_1_3) - .setMaxTlsVersion(TLS_VERSION_1_3) - .addCiphersuites( - Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) - .addCiphersuites( - Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) - .addCiphersuites( - Ciphersuite - .CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256))) - .build()); + String leafCertString = ""; + String cert2String = ""; + String cert1String = ""; + try ( + InputStream leafCert = classLoader.getResourceAsStream("leaf_cert_ec.pem"); + InputStream cert2 = classLoader.getResourceAsStream("int_cert2_ec.pem"); + InputStream cert1 = classLoader.getResourceAsStream("int_cert1_ec.pem"); + ) { + leafCertString = FakeWriter.convertInputStreamToString(leafCert); + cert2String = FakeWriter.convertInputStreamToString(cert2); + cert1String = FakeWriter.convertInputStreamToString(cert1); } catch (IOException e) { reader.onError(e); } + reader.onNext( + SessionResp.newBuilder() + .setGetTlsConfigurationResp( + GetTlsConfigurationResp.newBuilder() + .setClientTlsConfiguration( + GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() + .addCertificateChain(leafCertString) + .addCertificateChain(cert1String) + .addCertificateChain(cert2String) + .setMinTlsVersion(TLS_VERSION_1_3) + .setMaxTlsVersion(TLS_VERSION_1_3) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) + .addCiphersuites( + Ciphersuite.CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) + .addCiphersuites( + Ciphersuite + .CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256))) + .build()); } boolean isFakeWriterClosed() { @@ -191,25 +194,32 @@ public void onNext(SessionReq sessionReq) { reader.onCompleted(); break; case BAD_TLS_VERSION_RESPONSE: - try { - reader.onNext( - SessionResp.newBuilder() - .setGetTlsConfigurationResp( - GetTlsConfigurationResp.newBuilder() - .setClientTlsConfiguration( - GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.leafCertFile.toPath()), StandardCharsets.UTF_8)) - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.cert1File.toPath()), StandardCharsets.UTF_8)) - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.cert2File.toPath()), StandardCharsets.UTF_8)) - .setMinTlsVersion(TLS_VERSION_1_3) - .setMaxTlsVersion(TLS_VERSION_1_2))) - .build()); + String leafCertString = ""; + String cert2String = ""; + String cert1String = ""; + try ( + InputStream leafCert = classLoader.getResourceAsStream("leaf_cert_ec.pem"); + InputStream cert2 = classLoader.getResourceAsStream("int_cert2_ec.pem"); + InputStream cert1 = classLoader.getResourceAsStream("int_cert1_ec.pem"); + ) { + leafCertString = FakeWriter.convertInputStreamToString(leafCert); + cert2String = FakeWriter.convertInputStreamToString(cert2); + cert1String = FakeWriter.convertInputStreamToString(cert1); } catch (IOException e) { reader.onError(e); } + reader.onNext( + SessionResp.newBuilder() + .setGetTlsConfigurationResp( + GetTlsConfigurationResp.newBuilder() + .setClientTlsConfiguration( + GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() + .addCertificateChain(leafCertString) + .addCertificateChain(cert1String) + .addCertificateChain(cert2String) + .setMinTlsVersion(TLS_VERSION_1_3) + .setMaxTlsVersion(TLS_VERSION_1_2))) + .build()); break; default: try { @@ -249,17 +259,28 @@ private SessionResp handleGetTlsConfigurationReq(GetTlsConfigurationReq req) .setDetails("No TLS configuration for the server side.")) .build(); } + String leafCertString = ""; + String cert2String = ""; + String cert1String = ""; + try ( + InputStream leafCert = classLoader.getResourceAsStream("leaf_cert_ec.pem"); + InputStream cert2 = classLoader.getResourceAsStream("int_cert2_ec.pem"); + InputStream cert1 = classLoader.getResourceAsStream("int_cert1_ec.pem"); + ) { + leafCertString = FakeWriter.convertInputStreamToString(leafCert); + cert2String = FakeWriter.convertInputStreamToString(cert2); + cert1String = FakeWriter.convertInputStreamToString(cert1); + } catch (IOException e) { + reader.onError(e); + } return SessionResp.newBuilder() .setGetTlsConfigurationResp( GetTlsConfigurationResp.newBuilder() .setClientTlsConfiguration( GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.leafCertFile.toPath()), StandardCharsets.UTF_8)) - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.cert1File.toPath()), StandardCharsets.UTF_8)) - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.cert2File.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(leafCertString) + .addCertificateChain(cert1String) + .addCertificateChain(cert2String) .setMinTlsVersion(TLS_VERSION_1_3) .setMaxTlsVersion(TLS_VERSION_1_3) .addCiphersuites( diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java index d842fde8dea..613983d9b39 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java @@ -46,7 +46,7 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; -import java.io.File; +import java.io.InputStream; import java.util.concurrent.FutureTask; import java.util.logging.Logger; import javax.net.ssl.SSLException; @@ -60,13 +60,6 @@ @RunWith(JUnit4.class) public final class IntegrationTest { private static final Logger logger = Logger.getLogger(FakeS2AServer.class.getName()); - - public static final File privateKeyFile = - new File("src/test/resources/leaf_key_ec.pem"); - public static final File rootCertFile = - new File("src/test/resources/root_cert_ec.pem"); - public static final File certChainFile = - new File("src/test/resources/cert_chain_ec.pem"); private String s2aAddress; private Server s2aServer; private String s2aDelayAddress; @@ -82,10 +75,10 @@ public void setUp() throws Exception { int s2aPort = s2aServer.getPort(); s2aAddress = "localhost:" + s2aPort; logger.info("S2A service listening on localhost:" + s2aPort); - - File s2aCert = new File("src/test/resources/server_cert.pem"); - File s2aKey = new File("src/test/resources/server_key.pem"); - File rootCert = new File("src/test/resources/root_cert.pem"); + ClassLoader classLoader = IntegrationTest.class.getClassLoader(); + InputStream s2aCert = classLoader.getResourceAsStream("server_cert.pem"); + InputStream s2aKey = classLoader.getResourceAsStream("server_key.pem"); + InputStream rootCert = classLoader.getResourceAsStream("root_cert.pem"); ServerCredentials s2aCreds = TlsServerCredentials.newBuilder() .keyManager(s2aCert, s2aKey) @@ -166,16 +159,14 @@ public void clientCommunicateUsingS2ACredentialsSucceeds_verifyStreamToS2AClosed @Test public void clientCommunicateUsingMtlsToS2ACredentials_succeeds() throws Exception { - String privateKeyPath = "src/test/resources/client_key.pem"; - String certChainPath = "src/test/resources/client_cert.pem"; - String trustBundlePath = "src/test/resources/root_cert.pem"; - File privateKeyFile = new File(privateKeyPath); - File certChainFile = new File(certChainPath); - File trustBundleFile = new File(trustBundlePath); + ClassLoader classLoader = IntegrationTest.class.getClassLoader(); + InputStream privateKey = classLoader.getResourceAsStream("client_key.pem"); + InputStream certChain = classLoader.getResourceAsStream("client_cert.pem"); + InputStream trustBundle = classLoader.getResourceAsStream("root_cert.pem"); ChannelCredentials s2aChannelCredentials = TlsChannelCredentials.newBuilder() - .keyManager(certChainFile, privateKeyFile) - .trustManager(trustBundleFile) + .keyManager(certChain, privateKey) + .trustManager(trustBundle) .build(); ChannelCredentials credentials = @@ -223,12 +214,16 @@ public static boolean doUnaryRpc(ManagedChannel channel) throws InterruptedExcep } private static SslContext buildSslContext() throws SSLException { + ClassLoader classLoader = IntegrationTest.class.getClassLoader(); + InputStream privateKey = classLoader.getResourceAsStream("leaf_key_ec.pem"); + InputStream rootCert = classLoader.getResourceAsStream("root_cert_ec.pem"); + InputStream certChain = classLoader.getResourceAsStream("cert_chain_ec.pem"); SslContextBuilder sslServerContextBuilder = - SslContextBuilder.forServer(certChainFile, privateKeyFile); + SslContextBuilder.forServer(certChain, privateKey); SslContext sslServerContext = GrpcSslContexts.configure(sslServerContextBuilder, SslProvider.OPENSSL) .protocols("TLSv1.3", "TLSv1.2") - .trustManager(rootCertFile) + .trustManager(rootCert) .clientAuth(ClientAuth.REQUIRE) .build(); diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethodTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethodTest.java index c885783be99..1aceb9518c3 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethodTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethodTest.java @@ -30,8 +30,7 @@ import io.netty.handler.ssl.OpenSslPrivateKeyMethod; import io.netty.handler.ssl.SslContextBuilder; import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; +import java.io.InputStream; import java.security.PublicKey; import java.security.Signature; import java.security.cert.CertificateFactory; @@ -63,8 +62,11 @@ private static PublicKey extractPublicKeyFromPem(String pem) throws Exception { private static boolean verifySignature( byte[] dataToSign, byte[] signature, String signatureAlgorithm) throws Exception { Signature sig = Signature.getInstance(signatureAlgorithm); - sig.initVerify(extractPublicKeyFromPem(new String( - Files.readAllBytes(FakeWriter.leafCertFile.toPath()), StandardCharsets.UTF_8))); + InputStream leafCert = + S2APrivateKeyMethodTest.class.getClassLoader().getResourceAsStream("leaf_cert_ec.pem"); + sig.initVerify(extractPublicKeyFromPem(FakeWriter.convertInputStreamToString( + leafCert))); + leafCert.close(); sig.update(dataToSign); return sig.verify(signature); } diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java index bf99ef3f944..c912faecd48 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java @@ -28,8 +28,7 @@ import io.grpc.s2a.internal.channel.S2AHandshakerServiceChannel; import io.grpc.stub.StreamObserver; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; +import java.io.InputStream; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -77,18 +76,29 @@ public void send_clientTlsConfiguration_receiveOkStatus() throws Exception { SessionResp resp = stub.send(req); + String leafCertString = ""; + String cert2String = ""; + String cert1String = ""; + ClassLoader classLoader = S2AStubTest.class.getClassLoader(); + try ( + InputStream leafCert = classLoader.getResourceAsStream("leaf_cert_ec.pem"); + InputStream cert2 = classLoader.getResourceAsStream("int_cert2_ec.pem"); + InputStream cert1 = classLoader.getResourceAsStream("int_cert1_ec.pem"); + ) { + leafCertString = FakeWriter.convertInputStreamToString(leafCert); + cert2String = FakeWriter.convertInputStreamToString(cert2); + cert1String = FakeWriter.convertInputStreamToString(cert1); + } + SessionResp expected = SessionResp.newBuilder() .setGetTlsConfigurationResp( GetTlsConfigurationResp.newBuilder() .setClientTlsConfiguration( GetTlsConfigurationResp.ClientTlsConfiguration.newBuilder() - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.leafCertFile.toPath()), StandardCharsets.UTF_8)) - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.cert1File.toPath()), StandardCharsets.UTF_8)) - .addCertificateChain(new String(Files.readAllBytes( - FakeWriter.cert2File.toPath()), StandardCharsets.UTF_8)) + .addCertificateChain(leafCertString) + .addCertificateChain(cert1String) + .addCertificateChain(cert2String) .setMinTlsVersion(TLSVersion.TLS_VERSION_1_3) .setMaxTlsVersion(TLSVersion.TLS_VERSION_1_3) .addCiphersuites( From 0192bece47d25d8052a1cc87ebe630f1494cf890 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 27 Nov 2024 09:40:31 -0800 Subject: [PATCH 098/591] api: DeadlineSubject should include actual on failure This was noticed because of a CallOptionsTest flake that had a surprising error: ``` expected : 59.983387319 but was : 59.983387319 outside tolerance in seconds: 0.01 ``` --- api/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java b/api/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java index 5d4e86fac15..94ab8fb9b18 100644 --- a/api/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java +++ b/api/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java @@ -67,7 +67,7 @@ public void of(Deadline expected) { if (Math.abs(actualNanos - expectedNanos) > deltaNanos) { failWithoutActual( fact("expected", expectedNanos / NANOSECONDS_IN_A_SECOND), - fact("but was", expectedNanos / NANOSECONDS_IN_A_SECOND), + fact("but was", actualNanos / NANOSECONDS_IN_A_SECOND), fact("outside tolerance in seconds", deltaNanos / NANOSECONDS_IN_A_SECOND)); } } From ebb43a69e7a46202462ec811af6ca51eae98d933 Mon Sep 17 00:00:00 2001 From: Vindhya Ningegowda Date: Wed, 27 Nov 2024 10:59:54 -0800 Subject: [PATCH 099/591] Add "#server" as dataplane target value for xDS enabled gRPC servers. (#11715) As mentioned in [A71 xDS Fallback]( https://github.com/grpc/proposal/blob/master/A71-xds-fallback.md#update-csds-to-aggregate-configs-from-multiple-xdsclient-instances): updated dataplane target to "#server" for xDS-enabled gRPC servers. --- xds/src/main/java/io/grpc/xds/XdsServerWrapper.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index 8c03e64f185..392f4c1a313 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -172,9 +172,7 @@ public void run() { private void internalStart() { try { - // TODO(dnvindhya): Add "#server" as "grpc.target" attribute value for - // xDS enabled servers. - xdsClientPool = xdsClientPoolFactory.getOrCreate("", new MetricRecorder() {}); + xdsClientPool = xdsClientPoolFactory.getOrCreate("#server", new MetricRecorder() {}); } catch (Exception e) { StatusException statusException = Status.UNAVAILABLE.withDescription( "Failed to initialize xDS").withCause(e).asException(); From 7f9c1f39f38647a23f9321fe3ab0f72648055bfa Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 27 Nov 2024 11:37:45 -0800 Subject: [PATCH 100/591] rls: Reduce RLS channel logging The channel log is shared by many components and is poorly suited to the noise of per-RPC events. This commit restricts RLS usage of the logger to no more frequent than cache entry events. This may still be too frequent, but should substantially improve the signal-to-noise and we can do further rework as needed. Many of the log entries were poor because they lacked enough context. They weren't even clear they were from RLS. The cache entry events now regularly include the request key in the logs, allowing you to follow events for specific keys. I would have preferred using the hash code, but NumberFormat is annoying and toString() may be acceptable given its convenience. This commit reverts much of eba699ad. Those logs have not proven to be helpful as they produce more output than can be reasonably stored. --- .../java/io/grpc/rls/CachingRlsLbClient.java | 59 ++++++------------- .../java/io/grpc/rls/RlsLoadBalancer.java | 10 +--- 2 files changed, 19 insertions(+), 50 deletions(-) diff --git a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java index d0661ba3be8..3d52187a158 100644 --- a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java +++ b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java @@ -278,9 +278,8 @@ private void periodicClean() { @GuardedBy("lock") private CachedRouteLookupResponse asyncRlsCall( RouteLookupRequest request, @Nullable BackoffPolicy backoffPolicy) { - logger.log(ChannelLogLevel.DEBUG, "Making an async call to RLS"); if (throttler.shouldThrottle()) { - logger.log(ChannelLogLevel.DEBUG, "Request is throttled"); + logger.log(ChannelLogLevel.DEBUG, "[RLS Entry {0}] Throttled RouteLookup", request); // Cache updated, but no need to call updateBalancingState because no RPCs were queued waiting // on this result return CachedRouteLookupResponse.backoffEntry(createBackOffEntry( @@ -288,27 +287,29 @@ private CachedRouteLookupResponse asyncRlsCall( } final SettableFuture response = SettableFuture.create(); io.grpc.lookup.v1.RouteLookupRequest routeLookupRequest = REQUEST_CONVERTER.convert(request); - logger.log(ChannelLogLevel.DEBUG, "Sending RouteLookupRequest: {0}", routeLookupRequest); + logger.log(ChannelLogLevel.DEBUG, + "[RLS Entry {0}] Starting RouteLookup: {1}", request, routeLookupRequest); rlsStub.withDeadlineAfter(callTimeoutNanos, TimeUnit.NANOSECONDS) .routeLookup( routeLookupRequest, new StreamObserver() { @Override public void onNext(io.grpc.lookup.v1.RouteLookupResponse value) { - logger.log(ChannelLogLevel.DEBUG, "Received RouteLookupResponse: {0}", value); + logger.log(ChannelLogLevel.DEBUG, + "[RLS Entry {0}] RouteLookup succeeded: {1}", request, value); response.set(RESPONSE_CONVERTER.reverse().convert(value)); } @Override public void onError(Throwable t) { - logger.log(ChannelLogLevel.DEBUG, "Error looking up route:", t); + logger.log(ChannelLogLevel.DEBUG, + "[RLS Entry {0}] RouteLookup failed: {1}", request, t); response.setException(t); throttler.registerBackendResponse(true); } @Override public void onCompleted() { - logger.log(ChannelLogLevel.DEBUG, "routeLookup call completed"); throttler.registerBackendResponse(false); } }); @@ -323,13 +324,10 @@ public void onCompleted() { */ @CheckReturnValue final CachedRouteLookupResponse get(final RouteLookupRequest request) { - logger.log(ChannelLogLevel.DEBUG, "Acquiring lock to get cached entry"); synchronized (lock) { - logger.log(ChannelLogLevel.DEBUG, "Acquired lock to get cached entry"); final CacheEntry cacheEntry; cacheEntry = linkedHashLruCache.read(request); if (cacheEntry == null) { - logger.log(ChannelLogLevel.DEBUG, "No cache entry found, making a new RLS request"); PendingCacheEntry pendingEntry = pendingCallCache.get(request); if (pendingEntry != null) { return CachedRouteLookupResponse.pendingResponse(pendingEntry); @@ -339,15 +337,12 @@ final CachedRouteLookupResponse get(final RouteLookupRequest request) { if (cacheEntry instanceof DataCacheEntry) { // cache hit, initiate async-refresh if entry is staled - logger.log(ChannelLogLevel.DEBUG, "Cache hit for the request"); DataCacheEntry dataEntry = ((DataCacheEntry) cacheEntry); if (dataEntry.isStaled(ticker.read())) { - logger.log(ChannelLogLevel.DEBUG, "Cache entry is stale"); dataEntry.maybeRefresh(); } return CachedRouteLookupResponse.dataEntry((DataCacheEntry) cacheEntry); } - logger.log(ChannelLogLevel.DEBUG, "Cache hit for a backup entry"); return CachedRouteLookupResponse.backoffEntry((BackoffCacheEntry) cacheEntry); } } @@ -409,8 +404,8 @@ private DataCacheEntry createDataEntry( RouteLookupRequest request, RouteLookupResponse routeLookupResponse) { logger.log( ChannelLogLevel.DEBUG, - "Transition to data cache: routeLookupResponse={0}", - routeLookupResponse); + "[RLS Entry {0}] Transition to data cache: routeLookupResponse={1}", + request, routeLookupResponse); DataCacheEntry entry = new DataCacheEntry(request, routeLookupResponse); // Constructor for DataCacheEntry causes updateBalancingState, but the picks can't happen until // this cache update because the lock is held @@ -421,18 +416,19 @@ private DataCacheEntry createDataEntry( @GuardedBy("lock") private BackoffCacheEntry createBackOffEntry( RouteLookupRequest request, Status status, @Nullable BackoffPolicy backoffPolicy) { - logger.log(ChannelLogLevel.DEBUG, "Transition to back off: status={0}", status); if (backoffPolicy == null) { backoffPolicy = backoffProvider.get(); } long delayNanos = backoffPolicy.nextBackoffNanos(); + logger.log( + ChannelLogLevel.DEBUG, + "[RLS Entry {0}] Transition to back off: status={1}, delayNanos={2}", + request, status, delayNanos); BackoffCacheEntry entry = new BackoffCacheEntry(request, status, backoffPolicy); // Lock is held, so the task can't execute before the assignment entry.scheduledFuture = scheduledExecutorService.schedule( () -> refreshBackoffEntry(entry), delayNanos, TimeUnit.NANOSECONDS); linkedHashLruCache.cacheAndClean(request, entry); - logger.log(ChannelLogLevel.DEBUG, "BackoffCacheEntry created with a delay of {0} nanos", - delayNanos); return entry; } @@ -443,7 +439,8 @@ private void refreshBackoffEntry(BackoffCacheEntry entry) { // Future was previously cancelled return; } - logger.log(ChannelLogLevel.DEBUG, "Calling RLS for transition to pending"); + logger.log(ChannelLogLevel.DEBUG, + "[RLS Entry {0}] Calling RLS for transition to pending", entry.request); linkedHashLruCache.invalidate(entry.request); asyncRlsCall(entry.request, entry.backoffPolicy); } @@ -659,10 +656,10 @@ void maybeRefresh() { synchronized (lock) { // Lock is already held, but ErrorProne can't tell if (pendingCallCache.containsKey(request)) { // pending already requested - logger.log(ChannelLogLevel.DEBUG, - "A pending refresh request already created, no need to proceed with refresh"); return; } + logger.log(ChannelLogLevel.DEBUG, + "[RLS Entry {0}] Cache entry is stale, refreshing", request); asyncRlsCall(request, /* backoffPolicy= */ null); } } @@ -943,13 +940,10 @@ private final class BackoffRefreshListener implements ChildLbStatusListener { @Override public void onStatusChanged(ConnectivityState newState) { - logger.log(ChannelLogLevel.DEBUG, "LB status changed to: {0}", newState); if (prevState == ConnectivityState.TRANSIENT_FAILURE && newState == ConnectivityState.READY) { logger.log(ChannelLogLevel.DEBUG, "Transitioning from TRANSIENT_FAILURE to READY"); - logger.log(ChannelLogLevel.DEBUG, "Acquiring lock force refresh backoff cache entries"); synchronized (lock) { - logger.log(ChannelLogLevel.DEBUG, "Lock acquired for refreshing backoff cache entries"); for (CacheEntry value : linkedHashLruCache.values()) { if (value instanceof BackoffCacheEntry) { refreshBackoffEntry((BackoffCacheEntry) value); @@ -983,31 +977,22 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { RouteLookupRequest request = requestFactory.create(serviceName, methodName, args.getHeaders()); final CachedRouteLookupResponse response = CachingRlsLbClient.this.get(request); - logger.log(ChannelLogLevel.DEBUG, - "Got route lookup cache entry for service={0}, method={1}, headers={2}:\n {3}", - new Object[]{serviceName, methodName, args.getHeaders(), response}); if (response.getHeaderData() != null && !response.getHeaderData().isEmpty()) { - logger.log(ChannelLogLevel.DEBUG, "Updating RLS metadata from the RLS response headers"); Metadata headers = args.getHeaders(); headers.discardAll(RLS_DATA_KEY); headers.put(RLS_DATA_KEY, response.getHeaderData()); } String defaultTarget = lbPolicyConfig.getRouteLookupConfig().defaultTarget(); - logger.log(ChannelLogLevel.DEBUG, "defaultTarget = {0}", defaultTarget); boolean hasFallback = defaultTarget != null && !defaultTarget.isEmpty(); if (response.hasData()) { - logger.log(ChannelLogLevel.DEBUG, "RLS response has data, proceed with selecting a picker"); ChildPolicyWrapper childPolicyWrapper = response.getChildPolicyWrapper(); SubchannelPicker picker = (childPolicyWrapper != null) ? childPolicyWrapper.getPicker() : null; if (picker == null) { - logger.log(ChannelLogLevel.DEBUG, - "Child policy wrapper didn't return a picker, returning PickResult with no results"); return PickResult.withNoResult(); } // Happy path - logger.log(ChannelLogLevel.DEBUG, "Returning PickResult"); PickResult pickResult = picker.pickSubchannel(args); if (pickResult.hasResult()) { helper.getMetricRecorder().addLongCounter(TARGET_PICKS_COUNTER, 1, @@ -1017,20 +1002,15 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { } return pickResult; } else if (response.hasError()) { - logger.log(ChannelLogLevel.DEBUG, "RLS response has errors"); if (hasFallback) { - logger.log(ChannelLogLevel.DEBUG, "Using RLS fallback"); return useFallback(args); } - logger.log(ChannelLogLevel.DEBUG, "No RLS fallback, returning PickResult with an error"); helper.getMetricRecorder().addLongCounter(FAILED_PICKS_COUNTER, 1, Arrays.asList(helper.getChannelTarget(), lookupService), Collections.emptyList()); return PickResult.withError( convertRlsServerStatus(response.getStatus(), lbPolicyConfig.getRouteLookupConfig().lookupService())); } else { - logger.log(ChannelLogLevel.DEBUG, - "RLS response had no data, return a PickResult with no data"); return PickResult.withNoResult(); } } @@ -1067,13 +1047,11 @@ private String determineMetricsPickResult(PickResult pickResult) { private void startFallbackChildPolicy() { String defaultTarget = lbPolicyConfig.getRouteLookupConfig().defaultTarget(); - logger.log(ChannelLogLevel.DEBUG, "starting fallback to {0}", defaultTarget); - logger.log(ChannelLogLevel.DEBUG, "Acquiring lock to start fallback child policy"); synchronized (lock) { - logger.log(ChannelLogLevel.DEBUG, "Acquired lock for starting fallback child policy"); if (fallbackChildPolicyWrapper != null) { return; } + logger.log(ChannelLogLevel.DEBUG, "starting fallback to {0}", defaultTarget); fallbackChildPolicyWrapper = refCountedChildPolicyWrapperFactory.createOrGet(defaultTarget); } } @@ -1081,7 +1059,6 @@ private void startFallbackChildPolicy() { // GuardedBy CachingRlsLbClient.lock void close() { synchronized (lock) { // Lock is already held, but ErrorProne can't tell - logger.log(ChannelLogLevel.DEBUG, "Closing RLS picker"); if (fallbackChildPolicyWrapper != null) { refCountedChildPolicyWrapperFactory.release(fallbackChildPolicyWrapper); } diff --git a/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java b/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java index d1e537f1482..81ef8fdb31a 100644 --- a/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java +++ b/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java @@ -50,12 +50,11 @@ final class RlsLoadBalancer extends LoadBalancer { @Override public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - logger.log(ChannelLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses); LbPolicyConfiguration lbPolicyConfiguration = (LbPolicyConfiguration) resolvedAddresses.getLoadBalancingPolicyConfig(); checkNotNull(lbPolicyConfiguration, "Missing RLS LB config"); if (!lbPolicyConfiguration.equals(this.lbPolicyConfiguration)) { - logger.log(ChannelLogLevel.DEBUG, "A new RLS LB config received"); + logger.log(ChannelLogLevel.DEBUG, "A new RLS LB config received: {0}", lbPolicyConfiguration); boolean needToConnect = this.lbPolicyConfiguration == null || !this.lbPolicyConfiguration.getRouteLookupConfig().lookupService().equals( lbPolicyConfiguration.getRouteLookupConfig().lookupService()); @@ -80,22 +79,18 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { // not required. this.lbPolicyConfiguration = lbPolicyConfiguration; } - logger.log(ChannelLogLevel.DEBUG, "RLS LB accepted resolved addresses successfully"); return Status.OK; } @Override public void requestConnection() { - logger.log(ChannelLogLevel.DEBUG, "connection requested from RLS LB"); if (routeLookupClient != null) { - logger.log(ChannelLogLevel.DEBUG, "requesting a connection from the routeLookupClient"); routeLookupClient.requestConnection(); } } @Override public void handleNameResolutionError(final Status error) { - logger.log(ChannelLogLevel.DEBUG, "Received resolution error: {0}", error); class ErrorPicker extends SubchannelPicker { @Override public PickResult pickSubchannel(PickSubchannelArgs args) { @@ -116,14 +111,11 @@ public String toString() { routeLookupClient = null; lbPolicyConfiguration = null; } - logger.log(ChannelLogLevel.DEBUG, - "Updating balancing state to TRANSIENT_FAILURE with an error picker"); helper.updateBalancingState(ConnectivityState.TRANSIENT_FAILURE, new ErrorPicker()); } @Override public void shutdown() { - logger.log(ChannelLogLevel.DEBUG, "Rls lb shutdown"); if (routeLookupClient != null) { logger.log(ChannelLogLevel.DEBUG, "closing the routeLookupClient because of RLS LB shutdown"); routeLookupClient.close(); From f66d7fc54d23fa441d0feb9873952a325346cbc4 Mon Sep 17 00:00:00 2001 From: vinodhabib <47808007+vinodhabib@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:39:25 +0530 Subject: [PATCH 101/591] netty: Fix ByteBuf leaks in tests (#11593) Part of #3353 --- .../io/grpc/netty/NettyClientHandlerTest.java | 6 ++- .../io/grpc/netty/NettyHandlerTestBase.java | 41 +++++++++++-------- .../io/grpc/netty/NettyServerHandlerTest.java | 28 +++++-------- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java index 6c5dd6b18bc..e5c97e9efd9 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java @@ -217,6 +217,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { // Simulate receipt of initial remote settings. ByteBuf serializedSettings = serializeSettings(new Http2Settings()); channelRead(serializedSettings); + channel().releaseOutbound(); } @Test @@ -342,11 +343,12 @@ public void sendFrameShouldSucceed() throws Exception { createStream(); // Send a frame and verify that it was written. + ByteBuf content = content(); ChannelFuture future - = enqueue(new SendGrpcFrameCommand(streamTransportState, content(), true)); + = enqueue(new SendGrpcFrameCommand(streamTransportState, content, true)); assertTrue(future.isSuccess()); - verifyWrite().writeData(eq(ctx()), eq(STREAM_ID), eq(content()), eq(0), eq(true), + verifyWrite().writeData(eq(ctx()), eq(STREAM_ID), same(content), eq(0), eq(true), any(ChannelPromise.class)); verify(mockKeepAliveManager, times(1)).onTransportActive(); // onStreamActive verifyNoMoreInteractions(mockKeepAliveManager); diff --git a/netty/src/test/java/io/grpc/netty/NettyHandlerTestBase.java b/netty/src/test/java/io/grpc/netty/NettyHandlerTestBase.java index eef8d30e05a..c971294fbb6 100644 --- a/netty/src/test/java/io/grpc/netty/NettyHandlerTestBase.java +++ b/netty/src/test/java/io/grpc/netty/NettyHandlerTestBase.java @@ -38,7 +38,6 @@ import io.grpc.internal.WritableBuffer; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.netty.buffer.UnpooledByteBufAllocator; @@ -68,6 +67,7 @@ import java.nio.ByteBuffer; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; +import org.junit.After; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -84,7 +84,6 @@ public abstract class NettyHandlerTestBase { protected static final int STREAM_ID = 3; - private ByteBuf content; private EmbeddedChannel channel; @@ -106,18 +105,24 @@ protected void manualSetUp() throws Exception {} protected final TransportTracer transportTracer = new TransportTracer(); protected int flowControlWindow = DEFAULT_WINDOW_SIZE; protected boolean autoFlowControl = false; - private final FakeClock fakeClock = new FakeClock(); FakeClock fakeClock() { return fakeClock; } + @After + public void tearDown() throws Exception { + if (channel() != null) { + channel().releaseInbound(); + channel().releaseOutbound(); + } + } + /** * Must be called by subclasses to initialize the handler and channel. */ protected final void initChannel(Http2HeadersDecoder headersDecoder) throws Exception { - content = Unpooled.copiedBuffer("hello world", UTF_8); frameWriter = mock(Http2FrameWriter.class, delegatesTo(new DefaultHttp2FrameWriter())); frameReader = new DefaultHttp2FrameReader(headersDecoder); @@ -233,11 +238,11 @@ protected final Http2FrameReader frameReader() { } protected final ByteBuf content() { - return content; + return Unpooled.copiedBuffer(contentAsArray()); } protected final byte[] contentAsArray() { - return ByteBufUtil.getBytes(content()); + return "\000\000\000\000\rhello world".getBytes(UTF_8); } protected final Http2FrameWriter verifyWrite() { @@ -252,8 +257,8 @@ protected final void channelRead(Object obj) throws Exception { channel.writeInbound(obj); } - protected ByteBuf grpcDataFrame(int streamId, boolean endStream, byte[] content) { - final ByteBuf compressionFrame = Unpooled.buffer(content.length); + protected ByteBuf grpcFrame(byte[] message) { + final ByteBuf compressionFrame = Unpooled.buffer(message.length); MessageFramer framer = new MessageFramer( new MessageFramer.Sink() { @Override @@ -262,23 +267,22 @@ public void deliverFrame( if (frame != null) { ByteBuf bytebuf = ((NettyWritableBuffer) frame).bytebuf(); compressionFrame.writeBytes(bytebuf); + bytebuf.release(); } } }, new NettyWritableBufferAllocator(ByteBufAllocator.DEFAULT), StatsTraceContext.NOOP); - framer.writePayload(new ByteArrayInputStream(content)); - framer.flush(); - ChannelHandlerContext ctx = newMockContext(); - new DefaultHttp2FrameWriter().writeData(ctx, streamId, compressionFrame, 0, endStream, - newPromise()); - return captureWrite(ctx); + framer.writePayload(new ByteArrayInputStream(message)); + framer.close(); + return compressionFrame; } - protected final ByteBuf dataFrame(int streamId, boolean endStream, ByteBuf content) { - // Need to retain the content since the frameWriter releases it. - content.retain(); + protected final ByteBuf grpcDataFrame(int streamId, boolean endStream, byte[] content) { + return dataFrame(streamId, endStream, grpcFrame(content)); + } + protected final ByteBuf dataFrame(int streamId, boolean endStream, ByteBuf content) { ChannelHandlerContext ctx = newMockContext(); new DefaultHttp2FrameWriter().writeData(ctx, streamId, content, 0, endStream, newPromise()); return captureWrite(ctx); @@ -410,6 +414,7 @@ public void dataSizeSincePingAccumulates() throws Exception { channelRead(dataFrame(3, false, buff.copy())); assertEquals(length * 3, handler.flowControlPing().getDataSincePing()); + buff.release(); } @Test @@ -608,12 +613,14 @@ public void bdpPingWindowResizing() throws Exception { private void readPingAck(long pingData) throws Exception { channelRead(pingFrame(true, pingData)); + channel().releaseOutbound(); } private void readXCopies(int copies, byte[] data) throws Exception { for (int i = 0; i < copies; i++) { channelRead(grpcDataFrame(STREAM_ID, false, data)); // buffer it stream().request(1); // consume it + channel().releaseOutbound(); } } diff --git a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java index 308079ff62f..54c1375eef2 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java @@ -43,6 +43,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; @@ -74,7 +75,6 @@ import io.grpc.internal.testing.TestServerStreamTracer; import io.grpc.netty.GrpcHttp2HeadersUtils.GrpcHttp2ServerHeadersDecoder; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; @@ -120,23 +120,16 @@ public class NettyServerHandlerTest extends NettyHandlerTestBase streamListenerMessageQueue = new LinkedList<>(); private int maxConcurrentStreams = Integer.MAX_VALUE; @@ -208,6 +201,7 @@ protected void manualSetUp() throws Exception { // Simulate receipt of initial remote settings. ByteBuf serializedSettings = serializeSettings(new Http2Settings()); channelRead(serializedSettings); + channel().releaseOutbound(); } @Test @@ -229,10 +223,11 @@ public void sendFrameShouldSucceed() throws Exception { createStream(); // Send a frame and verify that it was written. + ByteBuf content = content(); ChannelFuture future = enqueue( - new SendGrpcFrameCommand(stream.transportState(), content(), false)); + new SendGrpcFrameCommand(stream.transportState(), content, false)); assertTrue(future.isSuccess()); - verifyWrite().writeData(eq(ctx()), eq(STREAM_ID), eq(content()), eq(0), eq(false), + verifyWrite().writeData(eq(ctx()), eq(STREAM_ID), same(content), eq(0), eq(false), any(ChannelPromise.class)); } @@ -267,10 +262,11 @@ private void inboundDataShouldForwardToStreamListener(boolean endStream) throws // Create a data frame and then trigger the handler to read it. ByteBuf frame = grpcDataFrame(STREAM_ID, endStream, contentAsArray()); channelRead(frame); + channel().releaseOutbound(); verify(streamListener, atLeastOnce()) .messagesAvailable(any(StreamListener.MessageProducer.class)); InputStream message = streamListenerMessageQueue.poll(); - assertArrayEquals(ByteBufUtil.getBytes(content()), ByteStreams.toByteArray(message)); + assertArrayEquals(contentAsArray(), ByteStreams.toByteArray(message)); message.close(); assertNull("no additional message expected", streamListenerMessageQueue.poll()); @@ -870,7 +866,7 @@ public void keepAliveEnforcer_sendingDataResetsCounters() throws Exception { future.get(); for (int i = 0; i < 10; i++) { future = enqueue( - new SendGrpcFrameCommand(stream.transportState(), content().retainedSlice(), false)); + new SendGrpcFrameCommand(stream.transportState(), content(), false)); future.get(); channel().releaseOutbound(); channelRead(pingFrame(false /* isAck */, 1L)); @@ -1293,6 +1289,7 @@ public void maxRstCount_withinLimit_succeeds() throws Exception { maxRstPeriodNanos = TimeUnit.MILLISECONDS.toNanos(100); manualSetUp(); rapidReset(maxRstCount); + assertTrue(channel().isOpen()); } @@ -1302,6 +1299,7 @@ public void maxRstCount_exceedsLimit_fails() throws Exception { maxRstPeriodNanos = TimeUnit.MILLISECONDS.toNanos(100); manualSetUp(); assertThrows(ClosedChannelException.class, () -> rapidReset(maxRstCount + 1)); + assertFalse(channel().isOpen()); } @@ -1344,11 +1342,7 @@ private void createStream() throws Exception { private ByteBuf emptyGrpcFrame(int streamId, boolean endStream) throws Exception { ByteBuf buf = NettyTestUtil.messageFrame(""); - try { - return dataFrame(streamId, endStream, buf); - } finally { - buf.release(); - } + return dataFrame(streamId, endStream, buf); } @Override From c080b52f95abfa48eb9ea684a09e38ec3d57a767 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 2 Dec 2024 16:31:04 -0800 Subject: [PATCH 102/591] .github/workflows: Split Bazel into two jobs The two Bazel versions are completely separate; no need to run them serially. --- .github/workflows/testing.yml | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 8c639cf14ed..d68d85eb0cd 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -77,8 +77,16 @@ jobs: bazel: runs-on: ubuntu-latest + strategy: + matrix: + include: + # Test with and without bzlmod. Bazel 6 doesn't support bzlmod, so use Bazel 7 instead + - bazel: 6.0.0 + bzlmod: false + - bazel: 7.0.0 + bzlmod: true env: - USE_BAZEL_VERSION: 6.0.0 + USE_BAZEL_VERSION: ${{ matrix.bazel }} steps: - uses: actions/checkout@v4 @@ -97,19 +105,8 @@ jobs: key: ${{ runner.os }}-bazel-${{ env.USE_BAZEL_VERSION }}-${{ hashFiles('WORKSPACE', 'repositories.bzl') }} - name: Run bazel build - run: bazelisk build //... --enable_bzlmod=false + run: bazelisk build //... --enable_bzlmod=${{ matrix.bzlmod }} - name: Run example bazel build - run: bazelisk build //... --enable_bzlmod=false - working-directory: ./examples - - - name: Run bazel build (bzlmod) - env: - USE_BAZEL_VERSION: 7.0.0 - run: bazelisk build //... --enable_bzlmod=true - - - name: Run example bazel build (bzlmod) - env: - USE_BAZEL_VERSION: 7.0.0 - run: bazelisk build //... --enable_bzlmod=true + run: bazelisk build //... --enable_bzlmod=${{ matrix.bzlmod }} working-directory: ./examples From 65b32e60e0545f2c8404df9e29d985c241d26066 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Thu, 5 Dec 2024 23:07:32 +0530 Subject: [PATCH 103/591] okhttp: Fix for ipv6 link local with scope (#11725) --- .../main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java index d09d6cccedd..0706a39d028 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java @@ -19,6 +19,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HostAndPort; +import com.google.common.net.InetAddresses; import io.grpc.internal.GrpcUtil; import io.grpc.okhttp.internal.OptionalMethod; import io.grpc.okhttp.internal.Platform; @@ -247,7 +249,9 @@ protected void configureTlsExtensions( } else { SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true); } - if (SET_SERVER_NAMES != null && SNI_HOST_NAME != null) { + if (SET_SERVER_NAMES != null + && SNI_HOST_NAME != null + && !InetAddresses.isInetAddress(HostAndPort.fromString(hostname).getHost())) { SET_SERVER_NAMES .invoke(sslParams, Collections.singletonList(SNI_HOST_NAME.newInstance(hostname))); } else { From 99a2696c48250cbc7ba279a2863f4e8df53b05e4 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 9 Dec 2024 13:25:46 -0800 Subject: [PATCH 104/591] s2a: Restore static token state mutated in tests This is the easy-to-fix state, but GetAuthenticationMechanisms can save these temporary states used in tests, so more fixes will be necessary. --- .../handshaker/tokenmanager/SingleTokenFetcher.java | 7 ++++++- .../handshaker/GetAuthenticationMechanismsTest.java | 10 +++++++++- .../SingleTokenAccessTokenManagerTest.java | 11 ++++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenFetcher.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenFetcher.java index a5402af9db2..28aa0f87ba1 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenFetcher.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenFetcher.java @@ -41,6 +41,11 @@ public static void setAccessToken(String token) { accessToken = token; } + @VisibleForTesting + public static String getAccessToken() { + return accessToken; + } + private SingleTokenFetcher(String token) { this.token = token; } @@ -54,4 +59,4 @@ public String getDefaultToken() { public String getToken(S2AIdentity identity) { return token; } -} \ No newline at end of file +} diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java index 4c00b0746fc..d17d9ba99e8 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java @@ -20,6 +20,7 @@ import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.grpc.s2a.internal.handshaker.tokenmanager.SingleTokenFetcher; import java.util.Optional; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -31,13 +32,20 @@ public final class GetAuthenticationMechanismsTest { @Rule public final Expect expect = Expect.create(); private static final String TOKEN = "access_token"; + private static String originalAccessToken; @BeforeClass public static void setUpClass() { + originalAccessToken = SingleTokenFetcher.getAccessToken(); // Set the token that the client will use to authenticate to the S2A. SingleTokenFetcher.setAccessToken(TOKEN); } + @AfterClass + public static void tearDownClass() { + SingleTokenFetcher.setAccessToken(originalAccessToken); + } + @Test public void getAuthMechanisms_emptyIdentity_success() { expect @@ -58,4 +66,4 @@ public void getAuthMechanisms_nonEmptyIdentity_success() { .setToken("access_token") .build())); } -} \ No newline at end of file +} diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java index 5bf2ce05259..9fd33fe9070 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/tokenmanager/SingleTokenAccessTokenManagerTest.java @@ -20,6 +20,7 @@ import io.grpc.s2a.internal.handshaker.S2AIdentity; import java.util.Optional; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -30,11 +31,19 @@ public final class SingleTokenAccessTokenManagerTest { private static final S2AIdentity IDENTITY = S2AIdentity.fromSpiffeId("spiffe_id"); private static final String TOKEN = "token"; + private String originalAccessToken; + @Before public void setUp() { + originalAccessToken = SingleTokenFetcher.getAccessToken(); SingleTokenFetcher.setAccessToken(null); } + @After + public void tearDown() { + SingleTokenFetcher.setAccessToken(originalAccessToken); + } + @Test public void getDefaultToken_success() throws Exception { SingleTokenFetcher.setAccessToken(TOKEN); @@ -68,4 +77,4 @@ public void create_success() throws Exception { public void create_noEnvironmentVariable() throws Exception { assertThat(AccessTokenManager.create()).isEmpty(); } -} \ No newline at end of file +} From 210f9c083e7ce6c831b2b65f0ee7f9c1384a88f9 Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Mon, 9 Dec 2024 15:42:27 -0800 Subject: [PATCH 105/591] Xds fallback (#11254) * XDS Client Fallback --- .../io/grpc/xds/client/BootstrapperImpl.java | 13 + .../grpc/xds/client/ControlPlaneClient.java | 226 +++++-- .../java/io/grpc/xds/client/XdsClient.java | 37 +- .../io/grpc/xds/client/XdsClientImpl.java | 556 ++++++++++++------ .../java/io/grpc/xds/ControlPlaneRule.java | 109 +++- .../java/io/grpc/xds/CsdsServiceTest.java | 15 +- .../io/grpc/xds/GrpcBootstrapperImplTest.java | 4 + .../grpc/xds/GrpcXdsClientImplTestBase.java | 59 +- .../io/grpc/xds/XdsClientFallbackTest.java | 522 ++++++++++++++++ .../grpc/xds/XdsSecurityClientServerTest.java | 1 + .../grpc/xds/XdsTestControlPlaneService.java | 8 + .../CommonBootstrapperTestUtils.java | 84 ++- .../ClientSslContextProviderFactoryTest.java | 2 +- .../SecurityProtocolNegotiatorsTest.java | 2 +- .../ServerSslContextProviderFactoryTest.java | 2 +- .../security/TlsContextManagerTest.java | 2 +- ...tProviderClientSslContextProviderTest.java | 2 +- ...tProviderServerSslContextProviderTest.java | 2 +- 18 files changed, 1354 insertions(+), 292 deletions(-) create mode 100644 xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java rename xds/src/test/java/io/grpc/xds/{ => client}/CommonBootstrapperTestUtils.java (65%) diff --git a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java index 9930417348b..a48313fd21e 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -41,6 +41,9 @@ @Internal public abstract class BootstrapperImpl extends Bootstrapper { + public static final String GRPC_EXPERIMENTAL_XDS_FALLBACK = + "GRPC_EXPERIMENTAL_XDS_FALLBACK"; + // Client features. @VisibleForTesting public static final String CLIENT_FEATURE_DISABLE_OVERPROVISIONING = @@ -52,6 +55,9 @@ public abstract class BootstrapperImpl extends Bootstrapper { private static final String SERVER_FEATURE_IGNORE_RESOURCE_DELETION = "ignore_resource_deletion"; private static final String SERVER_FEATURE_TRUSTED_XDS_SERVER = "trusted_xds_server"; + @VisibleForTesting + static boolean enableXdsFallback = GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_FALLBACK, false); + protected final XdsLogger logger; protected FileReader reader = LocalFileReader.INSTANCE; @@ -65,6 +71,7 @@ protected BootstrapperImpl() { protected abstract Object getImplSpecificConfig(Map serverConfig, String serverUri) throws XdsInitializationException; + /** * Reads and parses bootstrap config. The config is expected to be in JSON format. */ @@ -103,6 +110,9 @@ protected BootstrapInfo.Builder bootstrapBuilder(Map rawData) throw new XdsInitializationException("Invalid bootstrap: 'xds_servers' does not exist."); } List servers = parseServerInfos(rawServerConfigs, logger); + if (servers.size() > 1 && !enableXdsFallback) { + servers = ImmutableList.of(servers.get(0)); + } builder.servers(servers); Node.Builder nodeBuilder = Node.newBuilder(); @@ -209,6 +219,9 @@ protected BootstrapInfo.Builder bootstrapBuilder(Map rawData) if (rawAuthorityServers == null || rawAuthorityServers.isEmpty()) { authorityServers = servers; } else { + if (rawAuthorityServers.size() > 1 && !enableXdsFallback) { + rawAuthorityServers = ImmutableList.of(rawAuthorityServers.get(0)); + } authorityServers = parseServerInfos(rawAuthorityServers, logger); } authorityInfoMapBuilder.put( diff --git a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java index 62076fb8bf1..047f8a2e315 100644 --- a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java +++ b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java @@ -39,7 +39,6 @@ import io.grpc.xds.client.XdsClient.ResourceStore; import io.grpc.xds.client.XdsClient.XdsResponseHandler; import io.grpc.xds.client.XdsLogger.XdsLogLevel; -import io.grpc.xds.client.XdsTransportFactory.EventHandler; import io.grpc.xds.client.XdsTransportFactory.StreamingCall; import io.grpc.xds.client.XdsTransportFactory.XdsTransport; import java.util.Collection; @@ -70,7 +69,6 @@ final class ControlPlaneClient { private final BackoffPolicy.Provider backoffPolicyProvider; private final Stopwatch stopwatch; private final Node bootstrapNode; - private final XdsClient xdsClient; // Last successfully applied version_info for each resource type. Starts with empty string. // A version_info is used to update management server with client's most recent knowledge of @@ -78,14 +76,15 @@ final class ControlPlaneClient { private final Map, String> versions = new HashMap<>(); private boolean shutdown; - private boolean streamClosedNoResponse; + private boolean inError; + @Nullable private AdsStream adsStream; @Nullable private BackoffPolicy retryBackoffPolicy; @Nullable private ScheduledHandle rpcRetryTimer; - private MessagePrettyPrinter messagePrinter; + private final MessagePrettyPrinter messagePrinter; /** An entity that manages ADS RPCs over a single channel. */ ControlPlaneClient( @@ -99,7 +98,6 @@ final class ControlPlaneClient { SynchronizationContext syncContext, BackoffPolicy.Provider backoffPolicyProvider, Supplier stopwatchSupplier, - XdsClient xdsClient, MessagePrettyPrinter messagePrinter) { this.serverInfo = checkNotNull(serverInfo, "serverInfo"); this.xdsTransport = checkNotNull(xdsTransport, "xdsTransport"); @@ -109,7 +107,6 @@ final class ControlPlaneClient { this.timeService = checkNotNull(timeService, "timeService"); this.syncContext = checkNotNull(syncContext, "syncContext"); this.backoffPolicyProvider = checkNotNull(backoffPolicyProvider, "backoffPolicyProvider"); - this.xdsClient = checkNotNull(xdsClient, "xdsClient"); this.messagePrinter = checkNotNull(messagePrinter, "messagePrinter"); stopwatch = checkNotNull(stopwatchSupplier, "stopwatchSupplier").get(); logId = InternalLogId.allocate("xds-client", serverInfo.target()); @@ -139,22 +136,36 @@ public String toString() { return logId.toString(); } + public ServerInfo getServerInfo() { + return serverInfo; + } + /** * Updates the resource subscription for the given resource type. */ // Must be synchronized. void adjustResourceSubscription(XdsResourceType resourceType) { - if (isInBackoff()) { + if (rpcRetryTimer != null && rpcRetryTimer.isPending()) { return; } if (adsStream == null) { startRpcStream(); + // when the stream becomes ready, it will send the discovery requests + return; + } + + // We will do the rest of the method as part of the readyHandler when the stream is ready. + if (!isConnected()) { + return; } + Collection resources = resourceStore.getSubscribedResources(serverInfo, resourceType); if (resources == null) { resources = Collections.emptyList(); } adsStream.sendDiscoveryRequest(resourceType, resources); + resourceStore.startMissingResourceTimers(resources, resourceType); + if (resources.isEmpty()) { // The resource type no longer has subscribing resources; clean up references to it versions.remove(resourceType); @@ -194,50 +205,44 @@ void nackResponse(XdsResourceType type, String nonce, String errorDetail) { adsStream.sendDiscoveryRequest(type, versionInfo, resources, nonce, errorDetail); } - /** - * Returns {@code true} if the resource discovery is currently in backoff. - */ // Must be synchronized. - boolean isInBackoff() { - return rpcRetryTimer != null && rpcRetryTimer.isPending(); + boolean isReady() { + return adsStream != null && adsStream.call != null + && adsStream.call.isReady() && !adsStream.closed; } - // Must be synchronized. - boolean isReady() { - return adsStream != null && adsStream.call != null && adsStream.call.isReady(); + boolean isConnected() { + return adsStream != null && adsStream.sentInitialRequest; } /** - * Starts a timer for each requested resource that hasn't been responded to and - * has been waiting for the channel to get ready. + * Used for identifying whether or not when getting a control plane for authority that this + * control plane should be skipped over if there is a fallback. + * + *

Also used by metric to consider this control plane to not be "active". + * + *

A ControlPlaneClient is considered to be in error during the time from when an + * {@link AdsStream} closed without having received a response to the time an AdsStream does + * receive a response. */ - // Must be synchronized. - void readyHandler() { - if (!isReady()) { - return; - } - - if (isInBackoff()) { - rpcRetryTimer.cancel(); - rpcRetryTimer = null; - } - - xdsClient.startSubscriberTimersIfNeeded(serverInfo); + boolean isInError() { + return inError; } + /** - * Indicates whether there is an active ADS stream. - * - *

Return {@code true} when the {@code AdsStream} is created. - * {@code false} when the ADS stream fails without a response. Resets to true - * upon receiving the first response on a new ADS stream. + * Cleans up outstanding rpcRetryTimer if present, since we are communicating. + * If we haven't sent the initial discovery request for this RPC stream, we will delegate to + * xdsResponseHandler (in practice XdsClientImpl) to do any initialization for a new active + * stream such as starting timers. We then send the initial discovery request. */ - // Must be synchronized - boolean hasWorkingAdsStream() { - return !streamClosedNoResponse; + // Must be synchronized. + void readyHandler(boolean shouldSendInitialRequest) { + if (shouldSendInitialRequest) { + sendDiscoveryRequests(); + } } - /** * Establishes the RPC connection by creating a new RPC stream on the given channel for * xDS protocol communication. @@ -245,28 +250,51 @@ boolean hasWorkingAdsStream() { // Must be synchronized. private void startRpcStream() { checkState(adsStream == null, "Previous adsStream has not been cleared yet"); + + if (rpcRetryTimer != null) { + rpcRetryTimer.cancel(); + rpcRetryTimer = null; + } + adsStream = new AdsStream(); + adsStream.start(); logger.log(XdsLogLevel.INFO, "ADS stream started"); stopwatch.reset().start(); } + void sendDiscoveryRequests() { + if (rpcRetryTimer != null && rpcRetryTimer.isPending()) { + return; + } + + if (adsStream == null) { + startRpcStream(); + // when the stream becomes ready, it will send the discovery requests + return; + } + + if (isConnected()) { + Set> subscribedResourceTypes = + new HashSet<>(resourceStore.getSubscribedResourceTypesWithTypeUrl().values()); + + for (XdsResourceType type : subscribedResourceTypes) { + adjustResourceSubscription(type); + } + } + } + @VisibleForTesting public final class RpcRetryTask implements Runnable { @Override public void run() { + logger.log(XdsLogLevel.DEBUG, "Retry timeout. Restart ADS stream {0}", logId); if (shutdown) { return; } + startRpcStream(); - Set> subscribedResourceTypes = - new HashSet<>(resourceStore.getSubscribedResourceTypesWithTypeUrl().values()); - for (XdsResourceType type : subscribedResourceTypes) { - Collection resources = resourceStore.getSubscribedResources(serverInfo, type); - if (resources != null) { - adsStream.sendDiscoveryRequest(type, resources); - } - } - xdsResponseHandler.handleStreamRestarted(serverInfo); + + // handling CPC management is triggered in readyHandler } } @@ -276,8 +304,9 @@ XdsResourceType fromTypeUrl(String typeUrl) { return resourceStore.getSubscribedResourceTypesWithTypeUrl().get(typeUrl); } - private class AdsStream implements EventHandler { + private class AdsStream implements XdsTransportFactory.EventHandler { private boolean responseReceived; + private boolean sentInitialRequest; private boolean closed; // Response nonce for the most recently received discovery responses of each resource type. // Client initiated requests start response nonce with empty string. @@ -293,6 +322,9 @@ private class AdsStream implements EventHandler { private AdsStream() { this.call = xdsTransport.createStreamingCall(methodDescriptor.getFullMethodName(), methodDescriptor.getRequestMarshaller(), methodDescriptor.getResponseMarshaller()); + } + + void start() { call.start(this); } @@ -338,7 +370,19 @@ final void sendDiscoveryRequest(XdsResourceType type, Collection reso @Override public void onReady() { - syncContext.execute(ControlPlaneClient.this::readyHandler); + syncContext.execute(() -> { + if (!isReady()) { + logger.log(XdsLogLevel.DEBUG, + "ADS stream ready handler called, but not ready {0}", logId); + return; + } + + logger.log(XdsLogLevel.DEBUG, "ADS stream ready {0}", logId); + + boolean hadSentInitialRequest = sentInitialRequest; + sentInitialRequest = true; + readyHandler(!hadSentInitialRequest); + }); } @Override @@ -346,8 +390,13 @@ public void onRecvMessage(DiscoveryResponse response) { syncContext.execute(new Runnable() { @Override public void run() { - // Reset flag as message has been received on a stream - streamClosedNoResponse = false; + if (closed) { + return; + } + boolean isFirstResponse = !responseReceived; + responseReceived = true; + inError = false; + XdsResourceType type = fromTypeUrl(response.getTypeUrl()); if (logger.isLoggable(XdsLogLevel.DEBUG)) { logger.log( @@ -364,7 +413,7 @@ public void run() { return; } handleRpcResponse(type, response.getVersionInfo(), response.getResourcesList(), - response.getNonce()); + response.getNonce(), isFirstResponse); } }); } @@ -377,17 +426,14 @@ public void onStatusReceived(final Status status) { } final void handleRpcResponse(XdsResourceType type, String versionInfo, List resources, - String nonce) { + String nonce, boolean isFirstResponse) { checkNotNull(type, "type"); - if (closed) { - return; - } - responseReceived = true; + respNonces.put(type, nonce); ProcessingTracker processingTracker = new ProcessingTracker( () -> call.startRecvMessage(), syncContext); xdsResponseHandler.handleResourceResponse(type, serverInfo, versionInfo, resources, nonce, - processingTracker); + isFirstResponse, processingTracker); processingTracker.onComplete(); } @@ -401,13 +447,16 @@ private void handleRpcStreamClosed(Status status) { // has never been initialized. retryBackoffPolicy = backoffPolicyProvider.get(); } + // FakeClock in tests isn't thread-safe. Schedule the retry timer before notifying callbacks // to avoid TSAN races, since tests may wait until callbacks are called but then would run // concurrently with the stopwatch and schedule. + long elapsed = stopwatch.elapsed(TimeUnit.NANOSECONDS); long delayNanos = Math.max(0, retryBackoffPolicy.nextBackoffNanos() - elapsed); - rpcRetryTimer = syncContext.schedule( - new RpcRetryTask(), delayNanos, TimeUnit.NANOSECONDS, timeService); + + rpcRetryTimer = + syncContext.schedule(new RpcRetryTask(), delayNanos, TimeUnit.NANOSECONDS, timeService); Status newStatus = status; if (responseReceived) { @@ -424,9 +473,9 @@ private void handleRpcStreamClosed(Status status) { "ADS stream closed by server after a response was received"); } } else { - streamClosedNoResponse = true; // If the ADS stream is closed without ever having received a response from the server, then // the XdsClient should consider that a connectivity error (see gRFC A57). + inError = true; if (status.isOk()) { newStatus = Status.UNAVAILABLE.withDescription( "ADS stream closed with OK before receiving a response"); @@ -437,10 +486,8 @@ private void handleRpcStreamClosed(Status status) { } closed = true; - xdsResponseHandler.handleStreamClosed(newStatus); + xdsResponseHandler.handleStreamClosed(newStatus, !responseReceived); cleanUp(); - - logger.log(XdsLogLevel.INFO, "Retry ADS stream in {0} ns", delayNanos); } private void close(Exception error) { @@ -458,4 +505,55 @@ private void cleanUp() { } } } + + @VisibleForTesting + static class FailingXdsTransport implements XdsTransport { + Status error; + + public FailingXdsTransport(Status error) { + this.error = error; + } + + @Override + public StreamingCall + createStreamingCall(String fullMethodName, + MethodDescriptor.Marshaller reqMarshaller, + MethodDescriptor.Marshaller respMarshaller) { + return new FailingXdsStreamingCall<>(); + } + + @Override + public void shutdown() { + // no-op + } + + private class FailingXdsStreamingCall implements StreamingCall { + + @Override + public void start(XdsTransportFactory.EventHandler eventHandler) { + eventHandler.onStatusReceived(error); + } + + @Override + public void sendMessage(ReqT message) { + // no-op + } + + @Override + public void startRecvMessage() { + // no-op + } + + @Override + public void sendError(Exception e) { + // no-op + } + + @Override + public boolean isReady() { + return false; + } + } + } + } diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClient.java b/xds/src/main/java/io/grpc/xds/client/XdsClient.java index 06f15005c22..36f8bd591c7 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClient.java @@ -306,14 +306,6 @@ public Object getSecurityConfig() { throw new UnsupportedOperationException(); } - /** - * For all subscriber's for the specified server, if the resource hasn't yet been - * resolved then start a timer for it. - */ - protected void startSubscriberTimersIfNeeded(ServerInfo serverInfo) { - throw new UnsupportedOperationException(); - } - /** * Returns a {@link ListenableFuture} to the snapshot of the subscribed resources as * they are at the moment of the call. @@ -428,30 +420,39 @@ interface XdsResponseHandler { /** Called when a xds response is received. */ void handleResourceResponse( XdsResourceType resourceType, ServerInfo serverInfo, String versionInfo, - List resources, String nonce, ProcessingTracker processingTracker); + List resources, String nonce, boolean isFirstResponse, + ProcessingTracker processingTracker); /** Called when the ADS stream is closed passively. */ // Must be synchronized. - void handleStreamClosed(Status error); - - /** Called when the ADS stream has been recreated. */ - // Must be synchronized. - void handleStreamRestarted(ServerInfo serverInfo); + void handleStreamClosed(Status error, boolean shouldTryFallback); } public interface ResourceStore { + /** - * Returns the collection of resources currently subscribing to or {@code null} if not - * subscribing to any resources for the given type. + * Returns the collection of resources currently subscribed to which have an authority matching + * one of those for which the ControlPlaneClient associated with the specified ServerInfo is + * the active one, or {@code null} if no such resources are currently subscribed to. * *

Note an empty collection indicates subscribing to resources of the given type with * wildcard mode. + * + * @param serverInfo the xds server to get the resources from + * @param type the type of the resources that should be retrieved */ // Must be synchronized. @Nullable - Collection getSubscribedResources(ServerInfo serverInfo, - XdsResourceType type); + Collection getSubscribedResources( + ServerInfo serverInfo, XdsResourceType type); Map> getSubscribedResourceTypesWithTypeUrl(); + + /** + * For any of the subscribers to one of the specified resources, if there isn't a result or + * an existing timer for the resource, start a timer for the resource. + */ + void startMissingResourceTimers(Collection resourceNames, + XdsResourceType resourceType); } } diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index 529ac2747df..791ba3cc62d 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -26,8 +26,8 @@ import com.google.common.base.Joiner; import com.google.common.base.Stopwatch; import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.google.protobuf.Any; @@ -42,10 +42,13 @@ import io.grpc.xds.client.Bootstrapper.ServerInfo; import io.grpc.xds.client.XdsClient.ResourceStore; import io.grpc.xds.client.XdsLogger.XdsLogLevel; +import java.io.IOException; import java.net.URI; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -54,6 +57,7 @@ import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** @@ -74,21 +78,25 @@ public void uncaughtException(Thread t, Throwable e) { XdsLogLevel.ERROR, "Uncaught exception in XdsClient SynchronizationContext. Panic!", e); - // TODO(chengyuanzhang): better error handling. + // TODO: better error handling. throw new AssertionError(e); } }); - private final Map loadStatsManagerMap = - new HashMap<>(); - final Map serverLrsClientMap = - new HashMap<>(); - + private final Map loadStatsManagerMap = new HashMap<>(); + final Map serverLrsClientMap = new HashMap<>(); + /** Map of authority to its activated control plane client (affected by xds fallback). + * The last entry in the list for each value is the "active" CPC for the matching key */ + private final Map> activatedCpClients = new HashMap<>(); private final Map serverCpClientMap = new HashMap<>(); + + /** Maps resource type to the corresponding map of subscribers (keyed by resource name). */ private final Map, Map>> resourceSubscribers = new HashMap<>(); + /** Maps typeUrl to the corresponding XdsResourceType. */ private final Map> subscribedResourceTypeUrls = new HashMap<>(); + private final XdsTransportFactory xdsTransportFactory; private final Bootstrapper.BootstrapInfo bootstrapInfo; private final ScheduledExecutorService timeService; @@ -126,48 +134,6 @@ public XdsClientImpl( logger.log(XdsLogLevel.INFO, "Created"); } - private void handleResourceResponse( - XdsResourceType xdsResourceType, ServerInfo serverInfo, String versionInfo, - List resources, String nonce, ProcessingTracker processingTracker) { - checkNotNull(xdsResourceType, "xdsResourceType"); - syncContext.throwIfNotInThisSynchronizationContext(); - Set toParseResourceNames = - xdsResourceType.shouldRetrieveResourceKeysForArgs() - ? getResourceKeys(xdsResourceType) - : null; - XdsResourceType.Args args = new XdsResourceType.Args(serverInfo, versionInfo, nonce, - bootstrapInfo, securityConfig, toParseResourceNames); - handleResourceUpdate(args, resources, xdsResourceType, processingTracker); - } - - private void handleStreamClosed(Status error, ServerInfo serverInfo) { - syncContext.throwIfNotInThisSynchronizationContext(); - cleanUpResourceTimers(); - if (!error.isOk()) { - metricReporter.reportServerFailure(1L, serverInfo.target()); - for (Map> subscriberMap : - resourceSubscribers.values()) { - for (ResourceSubscriber subscriber : subscriberMap.values()) { - if (!subscriber.hasResult()) { - subscriber.onError(error, null); - } - } - } - } - } - - private void handleStreamRestarted(ServerInfo serverInfo) { - syncContext.throwIfNotInThisSynchronizationContext(); - for (Map> subscriberMap : - resourceSubscribers.values()) { - for (ResourceSubscriber subscriber : subscriberMap.values()) { - if (subscriber.serverInfo.equals(serverInfo)) { - subscriber.restartTimer(); - } - } - } - } - @Override public void shutdown() { syncContext.execute( @@ -184,7 +150,8 @@ public void run() { for (final LoadReportClient lrsClient : serverLrsClientMap.values()) { lrsClient.stopLoadReporting(); } - cleanUpResourceTimers(); + cleanUpResourceTimers(null); + activatedCpClients.clear(); } }); } @@ -199,20 +166,53 @@ public Map> getSubscribedResourceTypesWithTypeUrl() { return Collections.unmodifiableMap(subscribedResourceTypeUrls); } + private ControlPlaneClient getActiveCpc(String authority) { + List controlPlaneClients = activatedCpClients.get(authority); + if (controlPlaneClients == null || controlPlaneClients.isEmpty()) { + return null; + } + + return controlPlaneClients.get(controlPlaneClients.size() - 1); + } + @Nullable @Override - public Collection getSubscribedResources(ServerInfo serverInfo, - XdsResourceType type) { + public Collection getSubscribedResources( + ServerInfo serverInfo, XdsResourceType type) { + ControlPlaneClient targetCpc = serverCpClientMap.get(serverInfo); + if (targetCpc == null) { + return null; + } + + // This should include all of the authorities that targetCpc or a fallback from it is serving + List authorities = activatedCpClients.entrySet().stream() + .filter(entry -> entry.getValue().contains(targetCpc)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + Map> resources = resourceSubscribers.getOrDefault(type, Collections.emptyMap()); - ImmutableSet.Builder builder = ImmutableSet.builder(); - for (String key : resources.keySet()) { - if (resources.get(key).serverInfo.equals(serverInfo)) { - builder.add(key); + + Collection retVal = resources.entrySet().stream() + .filter(entry -> authorities.contains(entry.getValue().authority)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + return retVal.isEmpty() ? null : retVal; + } + + @Override + public void startMissingResourceTimers(Collection resourceNames, + XdsResourceType resourceType) { + Map> subscriberMap = + resourceSubscribers.get(resourceType); + + for (String resourceName : resourceNames) { + ResourceSubscriber subscriber = subscriberMap.get(resourceName); + if (subscriber.respTimer == null && !subscriber.hasResult()) { + subscriber.restartTimer(); } } - Collection retVal = builder.build(); - return retVal.isEmpty() ? null : retVal; } // As XdsClient APIs becomes resource agnostic, subscribed resource types are dynamic. @@ -228,7 +228,7 @@ public void run() { // A map from a "resource type" to a map ("resource name": "resource metadata") ImmutableMap.Builder, Map> metadataSnapshot = ImmutableMap.builder(); - for (XdsResourceType resourceType: resourceSubscribers.keySet()) { + for (XdsResourceType resourceType : resourceSubscribers.keySet()) { ImmutableMap.Builder metadataMap = ImmutableMap.builder(); for (Map.Entry> resourceEntry : resourceSubscribers.get(resourceType).entrySet()) { @@ -249,9 +249,9 @@ public Object getSecurityConfig() { @Override public void watchXdsResource(XdsResourceType type, - String resourceName, - ResourceWatcher watcher, - Executor watcherExecutor) { + String resourceName, + ResourceWatcher watcher, + Executor watcherExecutor) { syncContext.execute(new Runnable() { @Override @SuppressWarnings("unchecked") @@ -262,36 +262,125 @@ public void run() { } ResourceSubscriber subscriber = (ResourceSubscriber) resourceSubscribers.get(type).get(resourceName); + if (subscriber == null) { logger.log(XdsLogLevel.INFO, "Subscribe {0} resource {1}", type, resourceName); subscriber = new ResourceSubscriber<>(type, resourceName); resourceSubscribers.get(type).put(resourceName, subscriber); - if (subscriber.controlPlaneClient != null) { - subscriber.controlPlaneClient.adjustResourceSubscription(type); + + if (subscriber.errorDescription == null) { + CpcWithFallbackState cpcToUse = manageControlPlaneClient(subscriber); + if (cpcToUse.cpc != null) { + cpcToUse.cpc.adjustResourceSubscription(type); + } } } + subscriber.addWatcher(watcher, watcherExecutor); } }); } + /** + * Gets a ControlPlaneClient for the subscriber's authority, creating one if necessary. + * If there already was an active CPC for this authority, and it is different from the one + * identified, then do fallback to the identified one (cpcToUse). + * + * @return identified CPC or {@code null} (if there are no valid ServerInfos associated with the + * subscriber's authority or CPC's for all are in backoff), and whether did a fallback. + */ + @VisibleForTesting + private CpcWithFallbackState manageControlPlaneClient( + ResourceSubscriber subscriber) { + + ControlPlaneClient cpcToUse; + boolean didFallback = false; + try { + cpcToUse = getOrCreateControlPlaneClient(subscriber.authority); + } catch (IllegalArgumentException e) { + if (subscriber.errorDescription == null) { + subscriber.errorDescription = "Bad configuration: " + e.getMessage(); + } + + subscriber.onError( + Status.INVALID_ARGUMENT.withDescription(subscriber.errorDescription), null); + return new CpcWithFallbackState(null, false); + } catch (IOException e) { + logger.log(XdsLogLevel.DEBUG, + "Could not create a control plane client for authority {0}: {1}", + subscriber.authority, e.getMessage()); + return new CpcWithFallbackState(null, false); + } + + ControlPlaneClient activeCpClient = getActiveCpc(subscriber.authority); + if (cpcToUse != activeCpClient) { + addCpcToAuthority(subscriber.authority, cpcToUse); // makes it active + if (activeCpClient != null) { + didFallback = cpcToUse != null && !cpcToUse.isInError(); + if (didFallback) { + logger.log(XdsLogLevel.INFO, "Falling back to XDS server {0}", + cpcToUse.getServerInfo().target()); + } else { + logger.log(XdsLogLevel.WARNING, "No working fallback XDS Servers found from {0}", + activeCpClient.getServerInfo().target()); + } + } + } + + return new CpcWithFallbackState(cpcToUse, didFallback); + } + + private void addCpcToAuthority(String authority, ControlPlaneClient cpcToUse) { + List controlPlaneClients = + activatedCpClients.computeIfAbsent(authority, k -> new ArrayList<>()); + + if (controlPlaneClients.contains(cpcToUse)) { + return; + } + + // if there are any missing CPCs between the last one and cpcToUse, add them + add cpcToUse + ImmutableList serverInfos = getServerInfos(authority); + for (int i = controlPlaneClients.size(); i < serverInfos.size(); i++) { + ServerInfo serverInfo = serverInfos.get(i); + ControlPlaneClient cpc = serverCpClientMap.get(serverInfo); + controlPlaneClients.add(cpc); + logger.log(XdsLogLevel.DEBUG, "Adding control plane client {0} to authority {1}", + cpc, authority); + cpcToUse.sendDiscoveryRequests(); + if (cpc == cpcToUse) { + break; + } + } + } + @Override public void cancelXdsResourceWatch(XdsResourceType type, - String resourceName, - ResourceWatcher watcher) { + String resourceName, + ResourceWatcher watcher) { syncContext.execute(new Runnable() { @Override @SuppressWarnings("unchecked") public void run() { ResourceSubscriber subscriber = (ResourceSubscriber) resourceSubscribers.get(type).get(resourceName); + if (subscriber == null) { + logger.log(XdsLogLevel.WARNING, "double cancel of resource watch for {0}:{1}", + type.typeName(), resourceName); + return; + } subscriber.removeWatcher(watcher); if (!subscriber.isWatched()) { subscriber.cancelResourceWatch(); resourceSubscribers.get(type).remove(resourceName); - if (subscriber.controlPlaneClient != null) { - subscriber.controlPlaneClient.adjustResourceSubscription(type); + + List controlPlaneClients = + activatedCpClients.get(subscriber.authority); + if (controlPlaneClients != null) { + controlPlaneClients.forEach((cpc) -> { + cpc.adjustResourceSubscription(type); + }); } + if (resourceSubscribers.get(type).isEmpty()) { resourceSubscribers.remove(type); subscribedResourceTypeUrls.remove(type.typeUrl()); @@ -344,30 +433,6 @@ public String toString() { return logId.toString(); } - @Override - protected void startSubscriberTimersIfNeeded(ServerInfo serverInfo) { - if (isShutDown()) { - return; - } - - syncContext.execute(new Runnable() { - @Override - public void run() { - if (isShutDown()) { - return; - } - - for (Map> subscriberMap : resourceSubscribers.values()) { - for (ResourceSubscriber subscriber : subscriberMap.values()) { - if (subscriber.serverInfo.equals(serverInfo) && subscriber.respTimer == null) { - subscriber.restartTimer(); - } - } - } - } - }); - } - private Set getResourceKeys(XdsResourceType xdsResourceType) { if (!resourceSubscribers.containsKey(xdsResourceType)) { return null; @@ -376,53 +441,74 @@ private Set getResourceKeys(XdsResourceType xdsResourceType) { return resourceSubscribers.get(xdsResourceType).keySet(); } - private void cleanUpResourceTimers() { + // cpcForThisStream is null when doing shutdown + private void cleanUpResourceTimers(ControlPlaneClient cpcForThisStream) { + Collection authoritiesForCpc = getActiveAuthorities(cpcForThisStream); + for (Map> subscriberMap : resourceSubscribers.values()) { for (ResourceSubscriber subscriber : subscriberMap.values()) { - subscriber.stopTimer(); + if (cpcForThisStream == null || authoritiesForCpc.contains(subscriber.authority)) { + subscriber.stopTimer(); + } + } + } + } + + private ControlPlaneClient getOrCreateControlPlaneClient(String authority) throws IOException { + // Optimize for the common case of a working ads stream already exists for the authority + ControlPlaneClient activeCpc = getActiveCpc(authority); + if (activeCpc != null && !activeCpc.isInError()) { + return activeCpc; + } + + ImmutableList serverInfos = getServerInfos(authority); + if (serverInfos == null) { + throw new IllegalArgumentException("No xds servers found for authority " + authority); + } + + for (ServerInfo serverInfo : serverInfos) { + ControlPlaneClient cpc = getOrCreateControlPlaneClient(serverInfo); + if (cpc.isInError()) { + continue; } + return cpc; } + + // Everything existed and is in backoff so throw + throw new IOException("All xds transports for authority " + authority + " are in backoff"); } - public ControlPlaneClient getOrCreateControlPlaneClient(ServerInfo serverInfo) { + private ControlPlaneClient getOrCreateControlPlaneClient(ServerInfo serverInfo) { syncContext.throwIfNotInThisSynchronizationContext(); if (serverCpClientMap.containsKey(serverInfo)) { return serverCpClientMap.get(serverInfo); } - XdsTransportFactory.XdsTransport xdsTransport = xdsTransportFactory.create(serverInfo); + logger.log(XdsLogLevel.DEBUG, "Creating control plane client for {0}", serverInfo.target()); + XdsTransportFactory.XdsTransport xdsTransport; + try { + xdsTransport = xdsTransportFactory.create(serverInfo); + } catch (Exception e) { + String msg = String.format("Failed to create xds transport for %s: %s", + serverInfo.target(), e.getMessage()); + logger.log(XdsLogLevel.WARNING, msg); + xdsTransport = + new ControlPlaneClient.FailingXdsTransport(Status.UNAVAILABLE.withDescription(msg)); + } + ControlPlaneClient controlPlaneClient = new ControlPlaneClient( xdsTransport, serverInfo, bootstrapInfo.node(), - new XdsResponseHandler() { - - @Override - public void handleResourceResponse( - XdsResourceType resourceType, ServerInfo serverInfo, String versionInfo, - List resources, String nonce, ProcessingTracker processingTracker) { - XdsClientImpl.this.handleResourceResponse(resourceType, serverInfo, versionInfo, - resources, nonce, - processingTracker); - } - - @Override - public void handleStreamClosed(Status error) { - XdsClientImpl.this.handleStreamClosed(error, serverInfo); - } - - @Override - public void handleStreamRestarted(ServerInfo serverInfo) { - XdsClientImpl.this.handleStreamRestarted(serverInfo); - } - }, + new ResponseHandler(serverInfo), this, timeService, syncContext, backoffPolicyProvider, stopwatchSupplier, - this, - messagePrinter); + messagePrinter + ); + serverCpClientMap.put(serverInfo, controlPlaneClient); LoadStatsManager2 loadStatsManager = new LoadStatsManager2(stopwatchSupplier); @@ -441,32 +527,48 @@ public Map getServerLrsClientMap() { return ImmutableMap.copyOf(serverLrsClientMap); } - @Nullable - private ServerInfo getServerInfo(String resource) { + private String getAuthority(String resource) { + String authority; if (resource.startsWith(XDSTP_SCHEME)) { URI uri = URI.create(resource); - String authority = uri.getAuthority(); + authority = uri.getAuthority(); if (authority == null) { authority = ""; } + } else { + authority = null; + } + + return authority; + } + + @Nullable + private ImmutableList getServerInfos(String authority) { + if (authority != null) { AuthorityInfo authorityInfo = bootstrapInfo.authorities().get(authority); if (authorityInfo == null || authorityInfo.xdsServers().isEmpty()) { return null; } - return authorityInfo.xdsServers().get(0); + return authorityInfo.xdsServers(); } else { - return bootstrapInfo.servers().get(0); // use first server + return bootstrapInfo.servers(); } } @SuppressWarnings("unchecked") private void handleResourceUpdate( XdsResourceType.Args args, List resources, XdsResourceType xdsResourceType, - ProcessingTracker processingTracker) { + boolean isFirstResponse, ProcessingTracker processingTracker) { + ControlPlaneClient controlPlaneClient = serverCpClientMap.get(args.serverInfo); + + if (isFirstResponse) { + shutdownLowerPriorityCpcs(controlPlaneClient); + } + ValidatedResourceUpdate result = xdsResourceType.parse(args, resources); logger.log(XdsLogger.XdsLogLevel.INFO, "Received {0} Response version {1} nonce {2}. Parsed resources: {3}", - xdsResourceType.typeName(), args.versionInfo, args.nonce, result.unpackedResources); + xdsResourceType.typeName(), args.versionInfo, args.nonce, result.unpackedResources); Map> parsedResources = result.parsedResources; Set invalidResources = result.invalidResources; metricReporter.reportResourceUpdates(Long.valueOf(parsedResources.size()), @@ -477,14 +579,14 @@ private void handleResourceUpdate( String errorDetail = null; if (errors.isEmpty()) { checkArgument(invalidResources.isEmpty(), "found invalid resources but missing errors"); - serverCpClientMap.get(args.serverInfo).ackResponse(xdsResourceType, args.versionInfo, + controlPlaneClient.ackResponse(xdsResourceType, args.versionInfo, args.nonce); } else { errorDetail = Joiner.on('\n').join(errors); logger.log(XdsLogLevel.WARNING, "Failed processing {0} Response version {1} nonce {2}. Errors:\n{3}", xdsResourceType.typeName(), args.versionInfo, args.nonce, errorDetail); - serverCpClientMap.get(args.serverInfo).nackResponse(xdsResourceType, args.nonce, errorDetail); + controlPlaneClient.nackResponse(xdsResourceType, args.nonce, errorDetail); } long updateTime = timeProvider.currentTimeNanos(); @@ -523,8 +625,8 @@ private void handleResourceUpdate( // For State of the World services, notify watchers when their watched resource is missing // from the ADS update. Note that we can only do this if the resource update is coming from // the same xDS server that the ResourceSubscriber is subscribed to. - if (subscriber.serverInfo.equals(args.serverInfo)) { - subscriber.onAbsent(processingTracker); + if (getActiveCpc(subscriber.authority) == controlPlaneClient) { + subscriber.onAbsent(processingTracker, args.serverInfo); } } } @@ -535,58 +637,89 @@ public Future reportServerConnections(ServerConnectionCallback callback) { syncContext.execute(() -> { serverCpClientMap.forEach((serverInfo, controlPlaneClient) -> callback.reportServerConnectionGauge( - controlPlaneClient.hasWorkingAdsStream(), serverInfo.target())); + !controlPlaneClient.isInError(), serverInfo.target())); future.set(null); }); return future; } + private void shutdownLowerPriorityCpcs(ControlPlaneClient activatedCpc) { + // For each authority, remove any control plane clients, with lower priority than the activated + // one, from activatedCpClients storing them all in cpcsToShutdown. + Set cpcsToShutdown = new HashSet<>(); + for ( List cpcsForAuth : activatedCpClients.values()) { + if (cpcsForAuth == null) { + continue; + } + int index = cpcsForAuth.indexOf(activatedCpc); + if (index > -1) { + cpcsToShutdown.addAll(cpcsForAuth.subList(index + 1, cpcsForAuth.size())); + cpcsForAuth.subList(index + 1, cpcsForAuth.size()).clear(); // remove lower priority cpcs + } + } + + // Shutdown any lower priority control plane clients identified above that aren't still being + // used by another authority. If they are still being used let the XDS server know that we + // no longer are interested in subscriptions for authorities we are no longer responsible for. + for (ControlPlaneClient cpc : cpcsToShutdown) { + if (activatedCpClients.values().stream().noneMatch(list -> list.contains(cpc))) { + cpc.shutdown(); + serverCpClientMap.remove(cpc.getServerInfo()); + } else { + cpc.sendDiscoveryRequests(); + } + } + } + + /** Tracks a single subscribed resource. */ private final class ResourceSubscriber { - @Nullable private final ServerInfo serverInfo; - @Nullable private final ControlPlaneClient controlPlaneClient; + @Nullable + private final String authority; private final XdsResourceType type; private final String resource; private final Map, Executor> watchers = new HashMap<>(); - @Nullable private T data; + @Nullable + private T data; private boolean absent; // Tracks whether the deletion has been ignored per bootstrap server feature. // See https://github.com/grpc/proposal/blob/master/A53-xds-ignore-resource-deletion.md private boolean resourceDeletionIgnored; - @Nullable private ScheduledHandle respTimer; - @Nullable private ResourceMetadata metadata; - @Nullable private String errorDescription; + @Nullable + private ScheduledHandle respTimer; + @Nullable + private ResourceMetadata metadata; + @Nullable + private String errorDescription; ResourceSubscriber(XdsResourceType type, String resource) { syncContext.throwIfNotInThisSynchronizationContext(); this.type = type; this.resource = resource; - this.serverInfo = getServerInfo(resource); - if (serverInfo == null) { + this.authority = getAuthority(resource); + if (getServerInfos(authority) == null) { this.errorDescription = "Wrong configuration: xds server does not exist for resource " + resource; - this.controlPlaneClient = null; return; } + // Initialize metadata in UNKNOWN state to cover the case when resource subscriber, // is created but not yet requested because the client is in backoff. this.metadata = ResourceMetadata.newResourceMetadataUnknown(); + } - ControlPlaneClient controlPlaneClient = null; - try { - controlPlaneClient = getOrCreateControlPlaneClient(serverInfo); - if (controlPlaneClient.isInBackoff()) { - return; - } - } catch (IllegalArgumentException e) { - controlPlaneClient = null; - this.errorDescription = "Bad configuration: " + e.getMessage(); - return; - } finally { - this.controlPlaneClient = controlPlaneClient; - } - - restartTimer(); + @Override + public String toString() { + return "ResourceSubscriber{" + + "resource='" + resource + '\'' + + ", authority='" + authority + '\'' + + ", type=" + type + + ", watchers=" + watchers.size() + + ", data=" + data + + ", absent=" + absent + + ", resourceDeletionIgnored=" + resourceDeletionIgnored + + ", errorDescription='" + errorDescription + '\'' + + '}'; } void addWatcher(ResourceWatcher watcher, Executor watcherExecutor) { @@ -607,7 +740,7 @@ void addWatcher(ResourceWatcher watcher, Executor watcherExecutor) { }); } - void removeWatcher(ResourceWatcher watcher) { + void removeWatcher(ResourceWatcher watcher) { checkArgument(watchers.containsKey(watcher), "watcher %s not registered", watcher); watchers.remove(watcher); } @@ -616,7 +749,9 @@ void restartTimer() { if (data != null || absent) { // resource already resolved return; } - if (!controlPlaneClient.isReady()) { // When client becomes ready, it triggers a restartTimer + ControlPlaneClient activeCpc = getActiveCpc(authority); + if (activeCpc == null || !activeCpc.isReady()) { + // When client becomes ready, it triggers a restartTimer for all relevant subscribers. return; } @@ -626,7 +761,7 @@ public void run() { logger.log(XdsLogLevel.INFO, "{0} resource {1} initial fetch timeout", type, resource); respTimer = null; - onAbsent(null); + onAbsent(null, activeCpc.getServerInfo()); } @Override @@ -638,6 +773,9 @@ public String toString() { // Initial fetch scheduled or rescheduled, transition metadata state to REQUESTED. metadata = ResourceMetadata.newResourceMetadataRequested(); + if (respTimer != null) { + respTimer.cancel(); + } respTimer = syncContext.schedule( new ResourceNotFound(), INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS, timeService); @@ -661,8 +799,7 @@ void cancelResourceWatch() { message += " for which we previously ignored a deletion"; logLevel = XdsLogLevel.FORCE_INFO; } - logger.log(logLevel, message, type, resource, - serverInfo != null ? serverInfo.target() : "unknown"); + logger.log(logLevel, message, type, resource, getTarget()); } boolean isWatched() { @@ -687,7 +824,7 @@ void onData(ParsedResource parsedResource, String version, long updateTime, if (resourceDeletionIgnored) { logger.log(XdsLogLevel.FORCE_INFO, "xds server {0}: server returned new version " + "of resource for which we previously ignored a deletion: type {1} name {2}", - serverInfo != null ? serverInfo.target() : "unknown", type, resource); + getTarget(), type, resource); resourceDeletionIgnored = false; } if (!Objects.equals(oldData, data)) { @@ -704,15 +841,21 @@ void onData(ParsedResource parsedResource, String version, long updateTime, } } - void onAbsent(@Nullable ProcessingTracker processingTracker) { + private String getTarget() { + ControlPlaneClient activeCpc = getActiveCpc(authority); + return (activeCpc != null) + ? activeCpc.getServerInfo().target() + : "unknown"; + } + + void onAbsent(@Nullable ProcessingTracker processingTracker, ServerInfo serverInfo) { if (respTimer != null && respTimer.isPending()) { // too early to conclude absence return; } // Ignore deletion of State of the World resources when this feature is on, // and the resource is reusable. - boolean ignoreResourceDeletionEnabled = - serverInfo != null && serverInfo.ignoreResourceDeletion(); + boolean ignoreResourceDeletionEnabled = serverInfo.ignoreResourceDeletion(); if (ignoreResourceDeletionEnabled && type.isFullStateOfTheWorld() && data != null) { if (!resourceDeletionIgnored) { logger.log(XdsLogLevel.FORCE_WARNING, @@ -785,4 +928,89 @@ private void notifyWatcher(ResourceWatcher watcher, T update) { } } + private class ResponseHandler implements XdsResponseHandler { + final ServerInfo serverInfo; + + ResponseHandler(ServerInfo serverInfo) { + this.serverInfo = serverInfo; + } + + @Override + public void handleResourceResponse( + XdsResourceType xdsResourceType, ServerInfo serverInfo, String versionInfo, + List resources, String nonce, boolean isFirstResponse, + ProcessingTracker processingTracker) { + checkNotNull(xdsResourceType, "xdsResourceType"); + syncContext.throwIfNotInThisSynchronizationContext(); + Set toParseResourceNames = + xdsResourceType.shouldRetrieveResourceKeysForArgs() + ? getResourceKeys(xdsResourceType) + : null; + XdsResourceType.Args args = new XdsResourceType.Args(serverInfo, versionInfo, nonce, + bootstrapInfo, securityConfig, toParseResourceNames); + handleResourceUpdate(args, resources, xdsResourceType, isFirstResponse, processingTracker); + } + + @Override + public void handleStreamClosed(Status status, boolean shouldTryFallback) { + syncContext.throwIfNotInThisSynchronizationContext(); + + ControlPlaneClient cpcClosed = serverCpClientMap.get(serverInfo); + if (cpcClosed == null) { + return; + } + + cleanUpResourceTimers(cpcClosed); + + if (status.isOk()) { + return; // Not considered an error + } + + metricReporter.reportServerFailure(1L, serverInfo.target()); + + Collection authoritiesForClosedCpc = getActiveAuthorities(cpcClosed); + for (Map> subscriberMap : + resourceSubscribers.values()) { + for (ResourceSubscriber subscriber : subscriberMap.values()) { + if (subscriber.hasResult() || !authoritiesForClosedCpc.contains(subscriber.authority)) { + continue; + } + + // try to fallback to lower priority control plane client + if (shouldTryFallback && manageControlPlaneClient(subscriber).didFallback) { + authoritiesForClosedCpc.remove(subscriber.authority); + if (authoritiesForClosedCpc.isEmpty()) { + return; // optimization: no need to continue once all authorities have done fallback + } + continue; // since we did fallback, don't consider it an error + } + + subscriber.onError(status, null); + } + } + } + + } + + private static class CpcWithFallbackState { + ControlPlaneClient cpc; + boolean didFallback; + + private CpcWithFallbackState(ControlPlaneClient cpc, boolean didFallback) { + this.cpc = cpc; + this.didFallback = didFallback; + } + } + + private Collection getActiveAuthorities(ControlPlaneClient cpc) { + List asList = activatedCpClients.entrySet().stream() + .filter(entry -> !entry.getValue().isEmpty() + && cpc == entry.getValue().get(entry.getValue().size() - 1)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + // Since this is usually used for contains, use a set when the list is large + return (asList.size() < 100) ? asList : new HashSet<>(asList); + } + } diff --git a/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java b/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java index 69fde29a0a9..ac1c4829c74 100644 --- a/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java +++ b/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java @@ -57,6 +57,7 @@ import io.grpc.InsecureServerCredentials; import io.grpc.NameResolverRegistry; import io.grpc.Server; +import java.io.IOException; import java.util.Collections; import java.util.Map; import java.util.UUID; @@ -88,9 +89,11 @@ public class ControlPlaneRule extends TestWatcher { private XdsTestControlPlaneService controlPlaneService; private XdsTestLoadReportingService loadReportingService; private XdsNameResolverProvider nameResolverProvider; + private int port; // Only change from 0 to actual port used in the server. public ControlPlaneRule() { serverHostName = "test-server"; + this.port = 0; } public ControlPlaneRule setServerHostName(String serverHostName) { @@ -117,11 +120,7 @@ public Server getServer() { try { controlPlaneService = new XdsTestControlPlaneService(); loadReportingService = new XdsTestLoadReportingService(); - server = Grpc.newServerBuilderForPort(0, InsecureServerCredentials.create()) - .addService(controlPlaneService) - .addService(loadReportingService) - .build() - .start(); + createAndStartXdsServer(); } catch (Exception e) { throw new AssertionError("unable to start the control plane server", e); } @@ -146,6 +145,42 @@ public Server getServer() { NameResolverRegistry.getDefaultRegistry().deregister(nameResolverProvider); } + /** + * Will shutdown existing server if needed. + * Then creates a new server in the same way as {@link #starting(Description)} and starts it. + */ + public void restartXdsServer() { + + if (getServer() != null && !getServer().isTerminated()) { + getServer().shutdownNow(); + try { + if (!getServer().awaitTermination(5, TimeUnit.SECONDS)) { + logger.log(Level.SEVERE, "Timed out waiting for server shutdown"); + } + } catch (InterruptedException e) { + throw new AssertionError("unable to shut down control plane server", e); + } + } + + try { + createAndStartXdsServer(); + } catch (Exception e) { + throw new AssertionError("unable to restart the control plane server", e); + } + } + + private void createAndStartXdsServer() throws IOException { + server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()) + .addService(controlPlaneService) + .addService(loadReportingService) + .build() + .start(); + + if (port == 0) { + port = server.getPort(); + } + } + /** * For test purpose, use boostrapOverride to programmatically provide bootstrap info. */ @@ -175,46 +210,69 @@ void setLdsConfig(Listener serverListener, Listener clientListener) { } void setRdsConfig(RouteConfiguration routeConfiguration) { - getService().setXdsConfig(ADS_TYPE_URL_RDS, ImmutableMap.of(RDS_NAME, routeConfiguration)); + setRdsConfig(RDS_NAME, routeConfiguration); + } + + public void setRdsConfig(String rdsName, RouteConfiguration routeConfiguration) { + getService().setXdsConfig(ADS_TYPE_URL_RDS, ImmutableMap.of(rdsName, routeConfiguration)); } void setCdsConfig(Cluster cluster) { + setCdsConfig(CLUSTER_NAME, cluster); + } + + void setCdsConfig(String clusterName, Cluster cluster) { getService().setXdsConfig(ADS_TYPE_URL_CDS, - ImmutableMap.of(CLUSTER_NAME, cluster)); + ImmutableMap.of(clusterName, cluster)); } void setEdsConfig(ClusterLoadAssignment clusterLoadAssignment) { + setEdsConfig(EDS_NAME, clusterLoadAssignment); + } + + void setEdsConfig(String edsName, ClusterLoadAssignment clusterLoadAssignment) { getService().setXdsConfig(ADS_TYPE_URL_EDS, - ImmutableMap.of(EDS_NAME, clusterLoadAssignment)); + ImmutableMap.of(edsName, clusterLoadAssignment)); } /** * Builds a new default RDS configuration. */ static RouteConfiguration buildRouteConfiguration(String authority) { - io.envoyproxy.envoy.config.route.v3.VirtualHost virtualHost = VirtualHost.newBuilder() + return buildRouteConfiguration(authority, RDS_NAME, CLUSTER_NAME); + } + + static RouteConfiguration buildRouteConfiguration(String authority, String rdsName, + String clusterName) { + VirtualHost.Builder vhBuilder = VirtualHost.newBuilder() + .setName(rdsName) .addDomains(authority) .addRoutes( Route.newBuilder() .setMatch( RouteMatch.newBuilder().setPrefix("/").build()) .setRoute( - RouteAction.newBuilder().setCluster(CLUSTER_NAME) + RouteAction.newBuilder().setCluster(clusterName) .setAutoHostRewrite(BoolValue.newBuilder().setValue(true).build()) - .build()).build()).build(); - return RouteConfiguration.newBuilder().setName(RDS_NAME).addVirtualHosts(virtualHost).build(); + .build())); + VirtualHost virtualHost = vhBuilder.build(); + return RouteConfiguration.newBuilder().setName(rdsName).addVirtualHosts(virtualHost).build(); } /** * Builds a new default CDS configuration. */ static Cluster buildCluster() { + return buildCluster(CLUSTER_NAME, EDS_NAME); + } + + static Cluster buildCluster(String clusterName, String edsName) { return Cluster.newBuilder() - .setName(CLUSTER_NAME) + .setName(clusterName) .setType(Cluster.DiscoveryType.EDS) .setEdsClusterConfig( Cluster.EdsClusterConfig.newBuilder() - .setServiceName(EDS_NAME) + .setServiceName(edsName) .setEdsConfig( ConfigSource.newBuilder() .setAds(AggregatedConfigSource.newBuilder().build()) @@ -228,7 +286,13 @@ static Cluster buildCluster() { * Builds a new default EDS configuration. */ static ClusterLoadAssignment buildClusterLoadAssignment(String hostName, String endpointHostname, - int port) { + int port) { + return buildClusterLoadAssignment(hostName, endpointHostname, port, EDS_NAME); + } + + static ClusterLoadAssignment buildClusterLoadAssignment(String hostName, String endpointHostname, + int port, String edsName) { + Address address = Address.newBuilder() .setSocketAddress( SocketAddress.newBuilder().setAddress(hostName).setPortValue(port).build()).build(); @@ -243,7 +307,7 @@ static ClusterLoadAssignment buildClusterLoadAssignment(String hostName, String .setHealthStatus(HealthStatus.HEALTHY) .build()).build(); return ClusterLoadAssignment.newBuilder() - .setClusterName(EDS_NAME) + .setClusterName(edsName) .addEndpoints(endpoints) .build(); } @@ -252,8 +316,17 @@ static ClusterLoadAssignment buildClusterLoadAssignment(String hostName, String * Builds a new client listener. */ static Listener buildClientListener(String name) { + return buildClientListener(name, "terminal-filter"); + } + + + static Listener buildClientListener(String name, String identifier) { + return buildClientListener(name, identifier, RDS_NAME); + } + + static Listener buildClientListener(String name, String identifier, String rdsName) { HttpFilter httpFilter = HttpFilter.newBuilder() - .setName("terminal-filter") + .setName(identifier) .setTypedConfig(Any.pack(Router.newBuilder().build())) .setIsOptional(true) .build(); @@ -262,7 +335,7 @@ static Listener buildClientListener(String name) { .HttpConnectionManager.newBuilder() .setRds( Rds.newBuilder() - .setRouteConfigName(RDS_NAME) + .setRouteConfigName(rdsName) .setConfigSource( ConfigSource.newBuilder() .setAds(AggregatedConfigSource.getDefaultInstance()))) diff --git a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java index 7c6821dc560..df0687f6706 100644 --- a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java +++ b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java @@ -108,7 +108,7 @@ public void setUp() { // because true->false return mutation prevents fetchClientStatus from completing the request. csdsStub = ClientStatusDiscoveryServiceGrpc .newBlockingStub(grpcServerRule.getChannel()) - .withDeadline(Deadline.after(3, TimeUnit.SECONDS)); + .withDeadline(Deadline.after(30, TimeUnit.SECONDS)); csdsAsyncStub = ClientStatusDiscoveryServiceGrpc.newStub(grpcServerRule.getChannel()); } @@ -498,11 +498,17 @@ public BootstrapInfo getBootstrapInfo() { @Nullable @Override - public Collection getSubscribedResources(ServerInfo serverInfo, - XdsResourceType type) { + public Collection getSubscribedResources( + ServerInfo serverInfo, XdsResourceType type) { return null; } + @Override + public void startMissingResourceTimers(Collection resourceNames, + XdsResourceType resourceType) { + // do nothing + } + @Override public Map> getSubscribedResourceTypesWithTypeUrl() { return ImmutableMap.of(); @@ -511,8 +517,7 @@ public Map> getSubscribedResourceTypesWithTypeUrl() { private static class FakeXdsClientPoolFactory implements XdsClientPoolFactory { private final Map xdsClientMap = new HashMap<>(); - private boolean isOldStyle - ; + private boolean isOldStyle; private FakeXdsClientPoolFactory(@Nullable XdsClient xdsClient) { if (xdsClient != null) { diff --git a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java index 475b6e00a07..d2a9bf3316d 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -32,6 +32,7 @@ import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.Bootstrapper.ServerInfo; import io.grpc.xds.client.BootstrapperImpl; +import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.client.EnvoyProtoData.Node; import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsInitializationException; @@ -61,10 +62,12 @@ public class GrpcBootstrapperImplTest { private String originalBootstrapPathFromSysProp; private String originalBootstrapConfigFromEnvVar; private String originalBootstrapConfigFromSysProp; + private boolean originalExperimentalXdsFallbackFlag; @Before public void setUp() { saveEnvironment(); + originalExperimentalXdsFallbackFlag = CommonBootstrapperTestUtils.setEnableXdsFallback(true); bootstrapper.bootstrapPathFromEnvVar = BOOTSTRAP_FILE_PATH; } @@ -81,6 +84,7 @@ public void restoreEnvironment() { bootstrapper.bootstrapPathFromSysProp = originalBootstrapPathFromSysProp; bootstrapper.bootstrapConfigFromEnvVar = originalBootstrapConfigFromEnvVar; bootstrapper.bootstrapConfigFromSysProp = originalBootstrapConfigFromSysProp; + CommonBootstrapperTestUtils.setEnableXdsFallback(originalExperimentalXdsFallbackFlag); } @Test diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 198faea7fdc..b326eb7d02d 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -3599,42 +3599,52 @@ public void streamClosedAndRetryWithBackoff() { call.verifyRequest(RDS, RDS_RESOURCE, "5", "6764", NODE); call.sendError(Status.DEADLINE_EXCEEDED.asException()); + fakeClock.forwardNanos(100L); + call = resourceDiscoveryCalls.poll(); + call.sendError(Status.DEADLINE_EXCEEDED.asException()); + + // Already received LDS and RDS, so they only error twice. verify(ldsResourceWatcher, times(2)).onError(errorCaptor.capture()); verify(rdsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verify(cdsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); - verify(edsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + verify(cdsResourceWatcher, times(3)).onError(errorCaptor.capture()); + verifyStatusWithNodeId(errorCaptor.getValue(), Code.DEADLINE_EXCEEDED, ""); + verify(edsResourceWatcher, times(3)).onError(errorCaptor.capture()); + verifyStatusWithNodeId(errorCaptor.getValue(), Code.DEADLINE_EXCEEDED, ""); // Check metric data. callback_ReportServerConnection(); - verifyServerConnection(3, true, xdsServerInfo.target()); + verifyServerConnection(2, true, xdsServerInfo.target()); + verifyServerConnection(4, false, xdsServerInfo.target()); // Reset backoff sequence and retry after backoff. inOrder.verify(backoffPolicyProvider).get(); - inOrder.verify(backoffPolicy2).nextBackoffNanos(); + inOrder.verify(backoffPolicy2, times(2)).nextBackoffNanos(); retryTask = Iterables.getOnlyElement(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)); - assertThat(retryTask.getDelay(TimeUnit.NANOSECONDS)).isEqualTo(20L); - fakeClock.forwardNanos(20L); + fakeClock.forwardNanos(retryTask.getDelay(TimeUnit.NANOSECONDS)); call = resourceDiscoveryCalls.poll(); call.verifyRequest(LDS, LDS_RESOURCE, "63", "", NODE); call.verifyRequest(RDS, RDS_RESOURCE, "5", "", NODE); call.verifyRequest(CDS, CDS_RESOURCE, "", "", NODE); call.verifyRequest(EDS, EDS_RESOURCE, "", "", NODE); + // Check metric data, should be in error since haven't gotten a response. + callback_ReportServerConnection(); + verifyServerConnection(2, true, xdsServerInfo.target()); + verifyServerConnection(5, false, xdsServerInfo.target()); + // Management server becomes unreachable again. call.sendError(Status.UNAVAILABLE.asException()); verify(ldsResourceWatcher, times(2)).onError(errorCaptor.capture()); verify(rdsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verify(cdsResourceWatcher, times(3)).onError(errorCaptor.capture()); + verify(cdsResourceWatcher, times(4)).onError(errorCaptor.capture()); verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); - verify(edsResourceWatcher, times(3)).onError(errorCaptor.capture()); + verify(edsResourceWatcher, times(4)).onError(errorCaptor.capture()); verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); // Check metric data. callback_ReportServerConnection(); - verifyServerConnection(4, false, xdsServerInfo.target()); + verifyServerConnection(6, false, xdsServerInfo.target()); // Retry after backoff. inOrder.verify(backoffPolicy2).nextBackoffNanos(); @@ -3650,7 +3660,12 @@ public void streamClosedAndRetryWithBackoff() { // Check metric data. callback_ReportServerConnection(); - verifyServerConnection(5, false, xdsServerInfo.target()); + verifyServerConnection(7, false, xdsServerInfo.target()); + + // Send a response so CPC is considered working + call.sendResponse(LDS, listeners, "63", "3242"); + callback_ReportServerConnection(); + verifyServerConnection(3, true, xdsServerInfo.target()); inOrder.verifyNoMoreInteractions(); } @@ -3750,6 +3765,19 @@ public void streamClosedAndRetryRestartsResourceInitialFetchTimerForUnresolvedRe // Check metric data. callback_ReportServerConnection(); verifyServerConnection(4, true, xdsServerInfo.target()); + verify(cdsResourceWatcher, never()).onError(errorCaptor.capture()); // We had a response + + fakeClock.forwardTime(5, TimeUnit.SECONDS); + DiscoveryRpcCall call2 = resourceDiscoveryCalls.poll(); + call2.sendError(Status.UNAVAILABLE.asException()); + verify(cdsResourceWatcher).onError(errorCaptor.capture()); + verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); + verify(edsResourceWatcher).onError(errorCaptor.capture()); + verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); + + fakeClock.forwardTime(5, TimeUnit.SECONDS); + DiscoveryRpcCall call3 = resourceDiscoveryCalls.poll(); + assertThat(call3).isNotNull(); fakeClock.forwardNanos(10L); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(0); @@ -3962,11 +3990,14 @@ public void sendingToStoppedServer() throws Exception { @Test public void sendToBadUrl() throws Exception { // Setup xdsClient to fail on stream creation - XdsClientImpl client = createXdsClient("some. garbage"); + String garbageUri = "some. garbage"; + XdsClientImpl client = createXdsClient(garbageUri); client.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); fakeClock.forwardTime(20, TimeUnit.SECONDS); - verify(ldsResourceWatcher, Mockito.timeout(5000).times(1)).onError(ArgumentMatchers.any()); + verify(ldsResourceWatcher, Mockito.timeout(5000).atLeastOnce()) + .onError(errorCaptor.capture()); + assertThat(errorCaptor.getValue().getDescription()).contains(garbageUri); client.shutdown(); } diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java new file mode 100644 index 00000000000..6df27db0450 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java @@ -0,0 +1,522 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static io.grpc.xds.GrpcXdsTransportFactory.DEFAULT_XDS_TRANSPORT_FACTORY; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.grpc.MetricRecorder; +import io.grpc.Status; +import io.grpc.internal.ExponentialBackoffPolicy; +import io.grpc.internal.FakeClock; +import io.grpc.internal.ObjectPool; +import io.grpc.xds.client.Bootstrapper; +import io.grpc.xds.client.CommonBootstrapperTestUtils; +import io.grpc.xds.client.LoadReportClient; +import io.grpc.xds.client.XdsClient; +import io.grpc.xds.client.XdsClientImpl; +import io.grpc.xds.client.XdsClientMetricReporter; +import io.grpc.xds.client.XdsInitializationException; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class XdsClientFallbackTest { + private static final Logger log = Logger.getLogger(XdsClientFallbackTest.class.getName()); + + private static final String MAIN_SERVER = "main-server"; + private static final String FALLBACK_SERVER = "fallback-server"; + private static final String DUMMY_TARGET = "TEST_TARGET"; + private static final String RDS_NAME = "route-config.googleapis.com"; + private static final String FALLBACK_RDS_NAME = "fallback-" + RDS_NAME; + private static final String CLUSTER_NAME = "cluster0"; + private static final String FALLBACK_CLUSTER_NAME = "fallback-" + CLUSTER_NAME; + private static final String EDS_NAME = "eds-service-0"; + private static final String FALLBACK_EDS_NAME = "fallback-" + EDS_NAME; + private static final HttpConnectionManager MAIN_HTTP_CONNECTION_MANAGER = + HttpConnectionManager.forRdsName(0, RDS_NAME, ImmutableList.of( + new Filter.NamedFilterConfig(MAIN_SERVER, RouterFilter.ROUTER_CONFIG))); + private static final HttpConnectionManager FALLBACK_HTTP_CONNECTION_MANAGER = + HttpConnectionManager.forRdsName(0, RDS_NAME, ImmutableList.of( + new Filter.NamedFilterConfig(FALLBACK_SERVER, RouterFilter.ROUTER_CONFIG))); + private ObjectPool xdsClientPool; + private XdsClient xdsClient; + private boolean originalEnableXdsFallback; + private final FakeClock fakeClock = new FakeClock(); + private final MetricRecorder metricRecorder = new MetricRecorder() {}; + + @Mock + private XdsClientMetricReporter xdsClientMetricReporter; + + @Captor + private ArgumentCaptor errorCaptor; + + + private final XdsClient.ResourceWatcher raalLdsWatcher = + new XdsClient.ResourceWatcher() { + + @Override + public void onChanged(XdsListenerResource.LdsUpdate update) { + log.log(Level.FINE, "LDS update: " + update); + } + + @Override + public void onError(Status error) { + log.log(Level.FINE, "LDS update error: " + error.getDescription()); + } + + @Override + public void onResourceDoesNotExist(String resourceName) { + log.log(Level.FINE, "LDS resource does not exist: " + resourceName); + } + }; + + @SuppressWarnings("unchecked") + private final XdsClient.ResourceWatcher ldsWatcher = + mock(XdsClient.ResourceWatcher.class, delegatesTo(raalLdsWatcher)); + @Mock + private XdsClient.ResourceWatcher ldsWatcher2; + + @Mock + private XdsClient.ResourceWatcher rdsWatcher; + @Mock + private XdsClient.ResourceWatcher rdsWatcher2; + @Mock + private XdsClient.ResourceWatcher rdsWatcher3; + + private final XdsClient.ResourceWatcher raalCdsWatcher = + new XdsClient.ResourceWatcher() { + + @Override + public void onChanged(XdsClusterResource.CdsUpdate update) { + log.log(Level.FINE, "CDS update: " + update); + } + + @Override + public void onError(Status error) { + log.log(Level.FINE, "CDS update error: " + error.getDescription()); + } + + @Override + public void onResourceDoesNotExist(String resourceName) { + log.log(Level.FINE, "CDS resource does not exist: " + resourceName); + } + }; + + @SuppressWarnings("unchecked") + private final XdsClient.ResourceWatcher cdsWatcher = + mock(XdsClient.ResourceWatcher.class, delegatesTo(raalCdsWatcher)); + @Mock + private XdsClient.ResourceWatcher cdsWatcher2; + + @Rule(order = 0) + public ControlPlaneRule mainXdsServer = + new ControlPlaneRule().setServerHostName(MAIN_SERVER); + + @Rule(order = 1) + public ControlPlaneRule fallbackServer = + new ControlPlaneRule().setServerHostName(MAIN_SERVER); + + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + @Before + public void setUp() throws XdsInitializationException { + originalEnableXdsFallback = CommonBootstrapperTestUtils.setEnableXdsFallback(true); + if (mainXdsServer == null) { + throw new XdsInitializationException("Failed to create ControlPlaneRule for main TD server"); + } + setAdsConfig(mainXdsServer, MAIN_SERVER); + setAdsConfig(fallbackServer, FALLBACK_SERVER); + + SharedXdsClientPoolProvider clientPoolProvider = new SharedXdsClientPoolProvider(); + clientPoolProvider.setBootstrapOverride(defaultBootstrapOverride()); + xdsClientPool = clientPoolProvider.getOrCreate(DUMMY_TARGET, metricRecorder); + } + + @After + public void cleanUp() { + if (xdsClientPool != null) { + xdsClientPool.returnObject(xdsClient); + } + CommonBootstrapperTestUtils.setEnableXdsFallback(originalEnableXdsFallback); + } + + private static void setAdsConfig(ControlPlaneRule controlPlane, String serverName) { + InetSocketAddress edsInetSocketAddress = + (InetSocketAddress) controlPlane.getServer().getListenSockets().get(0); + boolean isMainServer = serverName.equals(MAIN_SERVER); + String rdsName = isMainServer + ? RDS_NAME + : FALLBACK_RDS_NAME; + String clusterName = isMainServer ? CLUSTER_NAME : FALLBACK_CLUSTER_NAME; + String edsName = isMainServer ? EDS_NAME : FALLBACK_EDS_NAME; + + controlPlane.setLdsConfig(ControlPlaneRule.buildServerListener(), + ControlPlaneRule.buildClientListener(MAIN_SERVER, serverName)); + + controlPlane.setRdsConfig(rdsName, + ControlPlaneRule.buildRouteConfiguration(MAIN_SERVER, rdsName, clusterName)); + controlPlane.setCdsConfig(clusterName, ControlPlaneRule.buildCluster(clusterName, edsName)); + + controlPlane.setEdsConfig(edsName, + ControlPlaneRule.buildClusterLoadAssignment(edsInetSocketAddress.getHostName(), + DataPlaneRule.ENDPOINT_HOST_NAME, edsInetSocketAddress.getPort(), edsName)); + log.log(Level.FINE, + String.format("Set ADS config for %s with address %s", serverName, edsInetSocketAddress)); + } + + // This is basically a control test to make sure everything is set up correctly. + @Test + public void everything_okay() { + mainXdsServer.restartXdsServer(); + fallbackServer.restartXdsServer(); + xdsClient = xdsClientPool.getObject(); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); + verify(ldsWatcher, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener( + MAIN_HTTP_CONNECTION_MANAGER)); + + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_NAME, rdsWatcher); + verify(rdsWatcher, timeout(5000)).onChanged(any()); + } + + @Test + public void mainServerDown_fallbackServerUp() { + mainXdsServer.getServer().shutdownNow(); + fallbackServer.restartXdsServer(); + xdsClient = xdsClientPool.getObject(); + log.log(Level.FINE, "Fallback port = " + fallbackServer.getServer().getPort()); + + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); + + verify(ldsWatcher, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener( + FALLBACK_HTTP_CONNECTION_MANAGER)); + } + + @Test + public void useBadAuthority() { + xdsClient = xdsClientPool.getObject(); + InOrder inOrder = inOrder(ldsWatcher, rdsWatcher, rdsWatcher2, rdsWatcher3); + + String badPrefix = "xdstp://authority.xds.bad/envoy.config.listener.v3.Listener/"; + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), + badPrefix + "listener.googleapis.com", ldsWatcher); + inOrder.verify(ldsWatcher, timeout(5000)).onError(any()); + + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), + badPrefix + "route-config.googleapis.bad", rdsWatcher); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), + badPrefix + "route-config2.googleapis.bad", rdsWatcher2); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), + badPrefix + "route-config3.googleapis.bad", rdsWatcher3); + inOrder.verify(rdsWatcher, timeout(5000).times(1)).onError(any()); + inOrder.verify(rdsWatcher2, timeout(5000).times(1)).onError(any()); + inOrder.verify(rdsWatcher3, timeout(5000).times(1)).onError(any()); + verify(rdsWatcher, never()).onChanged(any()); + + // even after an error, a valid one will still work + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher2); + verify(ldsWatcher2, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + } + + @Test + public void both_down_restart_main() { + mainXdsServer.getServer().shutdownNow(); + fallbackServer.getServer().shutdownNow(); + xdsClient = xdsClientPool.getObject(); + + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); + verify(ldsWatcher, timeout(5000).atLeastOnce()).onError(any()); + verify(ldsWatcher, timeout(5000).times(0)).onChanged(any()); + xdsClient.watchXdsResource( + XdsRouteConfigureResource.getInstance(), RDS_NAME, rdsWatcher2); + verify(rdsWatcher2, timeout(5000).atLeastOnce()).onError(any()); + + mainXdsServer.restartXdsServer(); + + xdsClient.watchXdsResource( + XdsRouteConfigureResource.getInstance(), RDS_NAME, rdsWatcher); + + verify(ldsWatcher, timeout(16000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + verify(rdsWatcher, timeout(5000)).onChanged(any()); + verify(rdsWatcher2, timeout(5000)).onChanged(any()); + } + + @Test + public void mainDown_fallbackUp_restart_main() { + mainXdsServer.getServer().shutdownNow(); + fallbackServer.restartXdsServer(); + xdsClient = xdsClientPool.getObject(); + InOrder inOrder = inOrder(ldsWatcher, rdsWatcher, cdsWatcher, cdsWatcher2); + + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); + inOrder.verify(ldsWatcher, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER)); + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), FALLBACK_CLUSTER_NAME, cdsWatcher); + inOrder.verify(cdsWatcher, timeout(5000)).onChanged(any()); + + assertThat(fallbackServer.getService().getSubscriberCounts() + .get("type.googleapis.com/envoy.config.listener.v3.Listener")).isEqualTo(1); + verifyNoSubscribers(mainXdsServer); + + mainXdsServer.restartXdsServer(); + + verify(ldsWatcher, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_NAME, rdsWatcher); + inOrder.verify(rdsWatcher, timeout(5000)).onChanged(any()); + verifyNoSubscribers(fallbackServer); + + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CLUSTER_NAME, cdsWatcher2); + inOrder.verify(cdsWatcher2, timeout(5000)).onChanged(any()); + + verifyNoSubscribers(fallbackServer); + assertThat(mainXdsServer.getService().getSubscriberCounts() + .get("type.googleapis.com/envoy.config.listener.v3.Listener")).isEqualTo(1); + } + + private static void verifyNoSubscribers(ControlPlaneRule rule) { + for (Map.Entry me : rule.getService().getSubscriberCounts().entrySet()) { + String type = me.getKey(); + Integer count = me.getValue(); + assertWithMessage("Type with non-zero subscribers is: %s", type) + .that(count).isEqualTo(0); + } + } + + // This test takes a long time because of the 16 sec timeout for non-existent resource + @Test + public void connect_then_mainServerDown_fallbackServerUp() throws InterruptedException { + mainXdsServer.restartXdsServer(); + fallbackServer.restartXdsServer(); + xdsClient = xdsClientPool.getObject(); + + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); + + verify(ldsWatcher, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_NAME, rdsWatcher); + verify(rdsWatcher, timeout(5000)).onChanged(any()); + + mainXdsServer.getServer().shutdownNow(); + TimeUnit.SECONDS.sleep(5); // TODO(lsafran) Use FakeClock so test runs faster + + // Shouldn't do fallback since all watchers are loaded + verify(ldsWatcher, never()).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER)); + + // Should just get from cache + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher2); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_NAME, rdsWatcher2); + verify(ldsWatcher2, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + verify(ldsWatcher, never()).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER)); + // Make sure that rdsWatcher wasn't called again + verify(rdsWatcher, times(1)).onChanged(any()); + verify(rdsWatcher2, timeout(5000)).onChanged(any()); + + // Asking for something not in cache should force a fallback + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), FALLBACK_CLUSTER_NAME, cdsWatcher); + verify(ldsWatcher, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER)); + verify(ldsWatcher2, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER)); + verify(cdsWatcher, timeout(16000)).onChanged(any()); + + xdsClient.watchXdsResource( + XdsRouteConfigureResource.getInstance(), FALLBACK_RDS_NAME, rdsWatcher3); + verify(rdsWatcher3, timeout(5000)).onChanged(any()); + + // Test that resource defined in main but not fallback is handled correctly + xdsClient.watchXdsResource( + XdsClusterResource.getInstance(), CLUSTER_NAME, cdsWatcher2); + verify(cdsWatcher2, timeout(16000)).onResourceDoesNotExist(eq(CLUSTER_NAME)); + } + + @Test + public void connect_then_mainServerRestart_fallbackServerdown() { + mainXdsServer.restartXdsServer(); + xdsClient = xdsClientPool.getObject(); + + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); + + verify(ldsWatcher, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + + mainXdsServer.getServer().shutdownNow(); + fallbackServer.getServer().shutdownNow(); + + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CLUSTER_NAME, cdsWatcher); + + mainXdsServer.restartXdsServer(); + + verify(cdsWatcher, timeout(5000)).onChanged(any()); + verify(ldsWatcher, timeout(5000).atLeastOnce()).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + } + + @Test + public void fallbackFromBadUrlToGoodOne() { + // Setup xdsClient to fail on stream creation + String garbageUri = "some. garbage"; + + String validUri = "localhost:" + mainXdsServer.getServer().getPort(); + XdsClientImpl client = CommonBootstrapperTestUtils.createXdsClient( + Arrays.asList(garbageUri, validUri), DEFAULT_XDS_TRANSPORT_FACTORY, fakeClock, + new ExponentialBackoffPolicy.Provider(), MessagePrinter.INSTANCE, xdsClientMetricReporter); + + client.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); + fakeClock.forwardTime(20, TimeUnit.SECONDS); + verify(ldsWatcher, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener( + MAIN_HTTP_CONNECTION_MANAGER)); + verify(ldsWatcher, never()).onError(any()); + + client.shutdown(); + } + + @Test + public void testGoodUrlFollowedByBadUrl() { + // Setup xdsClient to fail on stream creation + String garbageUri = "some. garbage"; + String validUri = "localhost:" + mainXdsServer.getServer().getPort(); + + XdsClientImpl client = CommonBootstrapperTestUtils.createXdsClient( + Arrays.asList(validUri, garbageUri), DEFAULT_XDS_TRANSPORT_FACTORY, fakeClock, + new ExponentialBackoffPolicy.Provider(), MessagePrinter.INSTANCE, xdsClientMetricReporter); + + client.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); + fakeClock.forwardTime(20, TimeUnit.SECONDS); + verify(ldsWatcher, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener( + MAIN_HTTP_CONNECTION_MANAGER)); + verify(ldsWatcher, never()).onError(any()); + + client.shutdown(); + } + + @Test + public void testTwoBadUrl() { + // Setup xdsClient to fail on stream creation + String garbageUri1 = "some. garbage"; + String garbageUri2 = "other garbage"; + + XdsClientImpl client = CommonBootstrapperTestUtils.createXdsClient( + Arrays.asList(garbageUri1, garbageUri2), DEFAULT_XDS_TRANSPORT_FACTORY, fakeClock, + new ExponentialBackoffPolicy.Provider(), MessagePrinter.INSTANCE, xdsClientMetricReporter); + + client.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); + fakeClock.forwardTime(20, TimeUnit.SECONDS); + verify(ldsWatcher, Mockito.timeout(5000).atLeastOnce()).onError(errorCaptor.capture()); + assertThat(errorCaptor.getValue().getDescription()).contains(garbageUri2); + verify(ldsWatcher, never()).onChanged(any()); + client.shutdown(); + } + + private Bootstrapper.ServerInfo getLrsServerInfo(String target) { + for (Map.Entry entry + : xdsClient.getServerLrsClientMap().entrySet()) { + if (entry.getKey().target().equals(target)) { + return entry.getKey(); + } + } + return null; + } + + @Test + public void used_then_mainServerRestart_fallbackServerUp() { + xdsClient = xdsClientPool.getObject(); + + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); + + verify(ldsWatcher, timeout(5000)).onChanged( + XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + + mainXdsServer.restartXdsServer(); + + assertThat(getLrsServerInfo("localhost:" + fallbackServer.getServer().getPort())).isNull(); + assertThat(getLrsServerInfo("localhost:" + mainXdsServer.getServer().getPort())).isNotNull(); + + xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CLUSTER_NAME, cdsWatcher); + + verify(cdsWatcher, timeout(5000)).onChanged(any()); + assertThat(getLrsServerInfo("localhost:" + fallbackServer.getServer().getPort())).isNull(); + } + + private Map defaultBootstrapOverride() { + return ImmutableMap.of( + "node", ImmutableMap.of( + "id", UUID.randomUUID().toString(), + "cluster", CLUSTER_NAME), + "xds_servers", ImmutableList.of( + ImmutableMap.of( + "server_uri", "localhost:" + mainXdsServer.getServer().getPort(), + "channel_creds", Collections.singletonList( + ImmutableMap.of("type", "insecure") + ), + "server_features", Collections.singletonList("xds_v3") + ), + ImmutableMap.of( + "server_uri", "localhost:" + fallbackServer.getServer().getPort(), + "channel_creds", Collections.singletonList( + ImmutableMap.of("type", "insecure") + ), + "server_features", Collections.singletonList("xds_v3") + ) + ), + "fallback-policy", "fallback" + ); + } + +} diff --git a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java index 590b6c79a10..6915ac6c13e 100644 --- a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java @@ -67,6 +67,7 @@ import io.grpc.xds.XdsServerTestHelper.FakeXdsClient; import io.grpc.xds.XdsServerTestHelper.FakeXdsClientPoolFactory; import io.grpc.xds.client.Bootstrapper; +import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.internal.Matchers.HeaderMatcher; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import io.grpc.xds.internal.security.SslContextProviderSupplier; diff --git a/xds/src/test/java/io/grpc/xds/XdsTestControlPlaneService.java b/xds/src/test/java/io/grpc/xds/XdsTestControlPlaneService.java index cc12e3863ba..98f5fcbfef9 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestControlPlaneService.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestControlPlaneService.java @@ -202,4 +202,12 @@ private DiscoveryResponse generateResponse(String resourceType, String version, } return responseBuilder.build(); } + + public Map getSubscriberCounts() { + Map subscriberCounts = new HashMap<>(); + for (String type : subscribers.keySet()) { + subscriberCounts.put(type, subscribers.get(type).size()); + } + return subscriberCounts; + } } diff --git a/xds/src/test/java/io/grpc/xds/CommonBootstrapperTestUtils.java b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java similarity index 65% rename from xds/src/test/java/io/grpc/xds/CommonBootstrapperTestUtils.java rename to xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java index 6a8eced298c..27a0d4ba1d9 100644 --- a/xds/src/test/java/io/grpc/xds/CommonBootstrapperTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java @@ -14,21 +14,46 @@ * limitations under the License. */ -package io.grpc.xds; +package io.grpc.xds.client; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import io.grpc.ChannelCredentials; +import io.grpc.InsecureChannelCredentials; +import io.grpc.internal.BackoffPolicy; +import io.grpc.internal.FakeClock; import io.grpc.internal.JsonParser; -import io.grpc.xds.client.Bootstrapper; +import io.grpc.internal.TimeProvider; import io.grpc.xds.client.Bootstrapper.ServerInfo; -import io.grpc.xds.client.EnvoyProtoData; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; +import io.grpc.xds.internal.security.TlsContextManagerImpl; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; public class CommonBootstrapperTestUtils { + private static final ChannelCredentials CHANNEL_CREDENTIALS = InsecureChannelCredentials.create(); + private static final String SERVER_URI_CUSTOM_AUTHORITY = "trafficdirector2.googleapis.com"; + private static final String SERVER_URI_EMPTY_AUTHORITY = "trafficdirector3.googleapis.com"; + + private static final long TIME_INCREMENT = TimeUnit.SECONDS.toNanos(1); + + /** Fake time provider increments time TIME_INCREMENT each call. */ + private static TimeProvider newTimeProvider() { + return new TimeProvider() { + private long count; + + @Override + public long currentTimeNanos() { + return ++count * TIME_INCREMENT; + } + }; + } + private static final String FILE_WATCHER_CONFIG = "{\"path\": \"/etc/secret/certs\"}"; private static final String MESHCA_CONFIG = "{\n" @@ -145,4 +170,57 @@ public static Bootstrapper.BootstrapInfo buildBootstrapInfo( .certProviders(certProviders) .build(); } + + public static boolean setEnableXdsFallback(boolean target) { + boolean oldValue = BootstrapperImpl.enableXdsFallback; + BootstrapperImpl.enableXdsFallback = target; + return oldValue; + } + + public static XdsClientImpl createXdsClient(List serverUris, + XdsTransportFactory xdsTransportFactory, + FakeClock fakeClock, + BackoffPolicy.Provider backoffPolicyProvider, + MessagePrettyPrinter messagePrinter, + XdsClientMetricReporter xdsClientMetricReporter) { + Bootstrapper.BootstrapInfo bootstrapInfo = buildBootStrap(serverUris); + return new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + newTimeProvider(), + messagePrinter, + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); + } + + public static Bootstrapper.BootstrapInfo buildBootStrap(List serverUris) { + + List serverInfos = new ArrayList<>(); + for (String uri : serverUris) { + serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, false, true)); + } + EnvoyProtoData.Node node = EnvoyProtoData.Node.newBuilder().setId("node-id").build(); + + return Bootstrapper.BootstrapInfo.builder() + .servers(serverInfos) + .node(node) + .authorities(ImmutableMap.of( + "authority.xds.com", + Bootstrapper.AuthorityInfo.create( + "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_CUSTOM_AUTHORITY, CHANNEL_CREDENTIALS))), + "", + Bootstrapper.AuthorityInfo.create( + "xdstp:///envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS))))) + .certProviders(ImmutableMap.of("cert-instance-name", + Bootstrapper.CertificateProviderInfo.create("file-watcher", ImmutableMap.of()))) + .build(); + } + } diff --git a/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java index 23e30883307..9c8340123da 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java @@ -28,9 +28,9 @@ import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.TlsCertificate; import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; -import io.grpc.xds.CommonBootstrapperTestUtils; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.client.Bootstrapper; +import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.client.XdsInitializationException; import io.grpc.xds.internal.security.certprovider.CertProviderClientSslContextProviderFactory; import io.grpc.xds.internal.security.certprovider.CertificateProvider; diff --git a/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java b/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java index fdfcd369937..955c812233a 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java @@ -45,12 +45,12 @@ import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; import io.grpc.netty.InternalProtocolNegotiators; import io.grpc.netty.ProtocolNegotiationEvent; -import io.grpc.xds.CommonBootstrapperTestUtils; import io.grpc.xds.EnvoyServerProtoData.DownstreamTlsContext; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.InternalXdsAttributes; import io.grpc.xds.TlsContextManager; import io.grpc.xds.client.Bootstrapper; +import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.internal.security.SecurityProtocolNegotiators.ClientSecurityHandler; import io.grpc.xds.internal.security.SecurityProtocolNegotiators.ClientSecurityProtocolNegotiator; import io.grpc.xds.internal.security.certprovider.CommonCertProviderTestUtils; diff --git a/xds/src/test/java/io/grpc/xds/internal/security/ServerSslContextProviderFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/security/ServerSslContextProviderFactoryTest.java index c455385dae9..cf86b511f1f 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/ServerSslContextProviderFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/ServerSslContextProviderFactoryTest.java @@ -24,10 +24,10 @@ import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; -import io.grpc.xds.CommonBootstrapperTestUtils; import io.grpc.xds.EnvoyServerProtoData; import io.grpc.xds.EnvoyServerProtoData.DownstreamTlsContext; import io.grpc.xds.client.Bootstrapper; +import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.client.XdsInitializationException; import io.grpc.xds.internal.security.certprovider.CertProviderServerSslContextProviderFactory; import io.grpc.xds.internal.security.certprovider.CertificateProvider; diff --git a/xds/src/test/java/io/grpc/xds/internal/security/TlsContextManagerTest.java b/xds/src/test/java/io/grpc/xds/internal/security/TlsContextManagerTest.java index 29d131cb8d7..035096a3528 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/TlsContextManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/TlsContextManagerTest.java @@ -30,10 +30,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.grpc.xds.CommonBootstrapperTestUtils; import io.grpc.xds.EnvoyServerProtoData.DownstreamTlsContext; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.client.Bootstrapper; +import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.internal.security.ReferenceCountingMap.ValueFactory; import org.junit.Rule; import org.junit.Test; diff --git a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java index 7c300c88297..b0800458d66 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java @@ -33,9 +33,9 @@ import com.google.common.util.concurrent.MoreExecutors; import io.envoyproxy.envoy.config.core.v3.DataSource; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; -import io.grpc.xds.CommonBootstrapperTestUtils; import io.grpc.xds.EnvoyServerProtoData; import io.grpc.xds.client.Bootstrapper; +import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil.TestCallback; import java.util.Queue; diff --git a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProviderTest.java index 82af7d1dc27..423829ff5af 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProviderTest.java @@ -32,9 +32,9 @@ import io.envoyproxy.envoy.config.core.v3.DataSource; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; -import io.grpc.xds.CommonBootstrapperTestUtils; import io.grpc.xds.EnvoyServerProtoData; import io.grpc.xds.client.Bootstrapper; +import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil.TestCallback; import io.grpc.xds.internal.security.certprovider.CertProviderClientSslContextProviderTest.QueuedExecutor; From 6055adca5ad020bd4b2b724d15ebf98c8f3c0084 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 10 Dec 2024 12:53:24 -0800 Subject: [PATCH 106/591] core: Simplify DnsNameResolver by using ObjectPool ObjectPool is our standard solution for dealing with the sometimes-shutdown resources. This was implemented by a contributor not familiar with regular tools. There are wider changes that can be made here, but I chose to just do a smaller change because this class is used by GrpclbNameResolver. --- .../io/grpc/internal/DnsNameResolver.java | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/DnsNameResolver.java b/core/src/main/java/io/grpc/internal/DnsNameResolver.java index b59de833d7c..6f1cf4cd900 100644 --- a/core/src/main/java/io/grpc/internal/DnsNameResolver.java +++ b/core/src/main/java/io/grpc/internal/DnsNameResolver.java @@ -134,10 +134,10 @@ public class DnsNameResolver extends NameResolver { private final String host; private final int port; - /** Executor that will be used if an Executor is not provide via {@link NameResolver.Args}. */ - private final Resource executorResource; + private final ObjectPool executorPool; private final long cacheTtlNanos; private final SynchronizationContext syncContext; + private final ServiceConfigParser serviceConfigParser; // Following fields must be accessed from syncContext private final Stopwatch stopwatch; @@ -145,10 +145,6 @@ public class DnsNameResolver extends NameResolver { private boolean shutdown; private Executor executor; - /** True if using an executor resource that should be released after use. */ - private final boolean usingExecutorResource; - private final ServiceConfigParser serviceConfigParser; - private boolean resolving; // The field must be accessed from syncContext, although the methods on an Listener2 can be called @@ -165,7 +161,7 @@ protected DnsNameResolver( checkNotNull(args, "args"); // TODO: if a DNS server is provided as nsAuthority, use it. // https://www.captechconsulting.com/blogs/accessing-the-dusty-corners-of-dns-with-java - this.executorResource = executorResource; + // Must prepend a "//" to the name when constructing a URI, otherwise it will be treated as an // opaque URI, thus the authority and host of the resulted URI would be null. URI nameUri = URI.create("//" + checkNotNull(name, "name")); @@ -179,11 +175,15 @@ protected DnsNameResolver( port = nameUri.getPort(); } this.proxyDetector = checkNotNull(args.getProxyDetector(), "proxyDetector"); + Executor offloadExecutor = args.getOffloadExecutor(); + if (offloadExecutor != null) { + this.executorPool = new FixedObjectPool<>(offloadExecutor); + } else { + this.executorPool = SharedResourcePool.forResource(executorResource); + } this.cacheTtlNanos = getNetworkAddressCacheTtlNanos(isAndroid); this.stopwatch = checkNotNull(stopwatch, "stopwatch"); this.syncContext = checkNotNull(args.getSynchronizationContext(), "syncContext"); - this.executor = args.getOffloadExecutor(); - this.usingExecutorResource = executor == null; this.serviceConfigParser = checkNotNull(args.getServiceConfigParser(), "serviceConfigParser"); } @@ -200,9 +200,7 @@ protected String getHost() { @Override public void start(Listener2 listener) { Preconditions.checkState(this.listener == null, "already started"); - if (usingExecutorResource) { - executor = SharedResourceHolder.get(executorResource); - } + executor = executorPool.getObject(); this.listener = checkNotNull(listener, "listener"); resolve(); } @@ -413,8 +411,8 @@ public void shutdown() { return; } shutdown = true; - if (executor != null && usingExecutorResource) { - executor = SharedResourceHolder.release(executorResource, executor); + if (executor != null) { + executor = executorPool.returnObject(executor); } } From f1109e421534108f482ae5022f47fe6cd5a83871 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 10 Dec 2024 15:21:15 -0800 Subject: [PATCH 107/591] examples: Simplify graceful shutdown in Hostname example I've slept since I wrote the original code, so now I see a less repetitive implementation. --- .../grpc/examples/hostname/HostnameServer.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/example-hostname/src/main/java/io/grpc/examples/hostname/HostnameServer.java b/examples/example-hostname/src/main/java/io/grpc/examples/hostname/HostnameServer.java index ca38c7cdc7b..7baa2d4733d 100644 --- a/examples/example-hostname/src/main/java/io/grpc/examples/hostname/HostnameServer.java +++ b/examples/example-hostname/src/main/java/io/grpc/examples/hostname/HostnameServer.java @@ -64,17 +64,17 @@ public void run() { // Start graceful shutdown server.shutdown(); try { - // Wait for RPCs to complete processing - if (!server.awaitTermination(30, TimeUnit.SECONDS)) { - // That was plenty of time. Let's cancel the remaining RPCs - server.shutdownNow(); - // shutdownNow isn't instantaneous, so give a bit of time to clean resources up - // gracefully. Normally this will be well under a second. - server.awaitTermination(5, TimeUnit.SECONDS); - } + // Wait up to 30 seconds for RPCs to complete processing. + server.awaitTermination(30, TimeUnit.SECONDS); } catch (InterruptedException ex) { - server.shutdownNow(); + Thread.currentThread().interrupt(); } + // Cancel any remaining RPCs. If awaitTermination() returned true above, then there are no + // RPCs and the server is already terminated. But it is safe to call even when terminated. + server.shutdownNow(); + // shutdownNow isn't instantaneous, so you want an additional awaitTermination() to give + // time to clean resources up gracefully. Normally it will return in well under a second. In + // this example, the server.awaitTermination() in main() provides that delay. } }); // This would normally be tied to the service's dependencies. For example, if HostnameGreeter From 486b8ba67f914f7f46f3549b48abe2c1ea2cc5d8 Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Wed, 11 Dec 2024 17:48:19 -0800 Subject: [PATCH 108/591] Fix tsan error (#11742) Eliminate unneeded fakeClock.forwardTime() that was causing the conflict. --- xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java index 6df27db0450..39379d43aba 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java @@ -428,7 +428,7 @@ public void fallbackFromBadUrlToGoodOne() { @Test public void testGoodUrlFollowedByBadUrl() { - // Setup xdsClient to fail on stream creation + // xdsClient should succeed in stream creation as it doesn't need to use the bad url String garbageUri = "some. garbage"; String validUri = "localhost:" + mainXdsServer.getServer().getPort(); @@ -437,7 +437,6 @@ public void testGoodUrlFollowedByBadUrl() { new ExponentialBackoffPolicy.Provider(), MessagePrinter.INSTANCE, xdsClientMetricReporter); client.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); - fakeClock.forwardTime(20, TimeUnit.SECONDS); verify(ldsWatcher, timeout(5000)).onChanged( XdsListenerResource.LdsUpdate.forApiListener( MAIN_HTTP_CONNECTION_MANAGER)); From 3b39a83621626c844b16e64ec6389511903ee075 Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Fri, 13 Dec 2024 18:09:30 -0800 Subject: [PATCH 109/591] Add cfg for java psm-interop fallback test (#11743) --- buildscripts/kokoro/psm-fallback.cfg | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 buildscripts/kokoro/psm-fallback.cfg diff --git a/buildscripts/kokoro/psm-fallback.cfg b/buildscripts/kokoro/psm-fallback.cfg new file mode 100644 index 00000000000..7335d1d9fd9 --- /dev/null +++ b/buildscripts/kokoro/psm-fallback.cfg @@ -0,0 +1,17 @@ +# Config file for internal CI + +# Location of the continuous shell script in repository. +build_file: "grpc-java/buildscripts/kokoro/psm-interop-test-java.sh" +timeout_mins: 120 + +action { + define_artifacts { + regex: "artifacts/**/*sponge_log.xml" + regex: "artifacts/**/*.log" + strip_prefix: "artifacts" + } +} +env_vars { + key: "PSM_TEST_SUITE" + value: "fallback" +} From e8ff6da2cf57a39a62497e9f317e6976b5bfb98c Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 13 Dec 2024 15:54:19 -0800 Subject: [PATCH 110/591] xds: Unexpected types in server_features should be ignored It was clearly defined in gRFC A30. The relevant text was copied as a comment in the code. As discovered due to grpc/grpc-go#7932 --- .../io/grpc/xds/client/BootstrapperImpl.java | 4 +++- .../io/grpc/xds/GrpcBootstrapperImplTest.java | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java index a48313fd21e..dffe19f9256 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -248,7 +248,9 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo Object implSpecificConfig = getImplSpecificConfig(serverConfig, serverUri); boolean ignoreResourceDeletion = false; - List serverFeatures = JsonUtil.getListOfStrings(serverConfig, "server_features"); + // "For forward compatibility reasons, the client will ignore any entry in the list that it + // does not understand, regardless of type." + List serverFeatures = JsonUtil.getList(serverConfig, "server_features"); if (serverFeatures != null) { logger.log(XdsLogLevel.INFO, "Server features: {0}", serverFeatures); ignoreResourceDeletion = serverFeatures.contains(SERVER_FEATURE_IGNORE_RESOURCE_DELETION); diff --git a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java index d2a9bf3316d..192d88177eb 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -676,6 +676,26 @@ public void serverFeatureIgnoreResourceDeletion_xdsV3() throws XdsInitialization assertThat(serverInfo.ignoreResourceDeletion()).isTrue(); } + @Test + public void serverFeatures_ignoresUnknownValues() throws XdsInitializationException { + String rawData = "{\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\"type\": \"insecure\"}\n" + + " ],\n" + + " \"server_features\": [null, {}, 3, true, \"unexpected\", \"trusted_xds_server\"]\n" + + " }\n" + + " ]\n" + + "}"; + + bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); + BootstrapInfo info = bootstrapper.bootstrap(); + ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); + assertThat(serverInfo.isTrustedXdsServer()).isTrue(); + } + @Test public void notFound() { bootstrapper.bootstrapPathFromEnvVar = null; From a0982ca0a156f26457df087b41d00e2c6369dcbe Mon Sep 17 00:00:00 2001 From: ZachChuba <49295341+ZachChuba@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:31:36 -0500 Subject: [PATCH 111/591] fix security issue with okhttp (#11749) * Validate that hostname is ascii in OkHostnameVerifier.java --- .../okhttp/internal/OkHostnameVerifier.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/OkHostnameVerifier.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/OkHostnameVerifier.java index 34bb56ee2d6..f6efb2d90e7 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/OkHostnameVerifier.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/OkHostnameVerifier.java @@ -29,10 +29,13 @@ import java.util.List; import java.util.Locale; import java.util.regex.Pattern; +import java.nio.charset.StandardCharsets; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import javax.security.auth.x500.X500Principal; +import com.google.common.base.Utf8; +import com.google.common.base.Ascii; /** * A HostnameVerifier consistent with altNames = getSubjectAltNames(certificate, ALT_DNS_NAME); for (int i = 0, size = altNames.size(); i < size; i++) { @@ -198,7 +204,7 @@ private boolean verifyHostName(String hostName, String pattern) { } // hostName and pattern are now absolute domain names. - pattern = pattern.toLowerCase(Locale.US); + pattern = Ascii.toLowerCase(pattern); // hostName and pattern are now in lower case -- domain names are case-insensitive. if (!pattern.contains("*")) { @@ -254,4 +260,13 @@ private boolean verifyHostName(String hostName, String pattern) { // hostName matches pattern return true; } + + /** + * Returns true if {@code input} is an ASCII string. + * @param input the string to check. + */ + private static boolean isAscii(String input) { + // Only ASCII characters are 1 byte in UTF-8. + return Utf8.encodedLength(input) == input.length(); + } } From fe752a290e30c575bbb532d114080f149272ca23 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 16 Dec 2024 07:27:37 -0800 Subject: [PATCH 112/591] xds: Move specialized APIs out of XdsResourceType StructOrError is a more generic API, but we have StatusOr now so we don't want new usages of StructOrError. Moving StructOrError out of io.grpc.xds.client will make it easier to delete StructOrError once we've migrated to StatusOr in the future. TRANSPORT_SOCKET_NAME_TLS should also move, but it wasn't immediately clear to me where it should go. --- .../main/java/io/grpc/xds/StructOrError.java | 72 +++++++++++++++++++ .../java/io/grpc/xds/XdsClusterResource.java | 2 + .../grpc/xds/XdsRouteConfigureResource.java | 2 + .../io/grpc/xds/client/XdsResourceType.java | 54 -------------- .../grpc/xds/GrpcXdsClientImplDataTest.java | 3 +- 5 files changed, 77 insertions(+), 56 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/StructOrError.java diff --git a/xds/src/main/java/io/grpc/xds/StructOrError.java b/xds/src/main/java/io/grpc/xds/StructOrError.java new file mode 100644 index 00000000000..14f008d191e --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/StructOrError.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import javax.annotation.Nullable; + +/** An object or a String error. */ +final class StructOrError { + + /** + * Returns a {@link StructOrError} for the successfully converted data object. + */ + public static StructOrError fromStruct(T struct) { + return new StructOrError<>(struct); + } + + /** + * Returns a {@link StructOrError} for the failure to convert the data object. + */ + public static StructOrError fromError(String errorDetail) { + return new StructOrError<>(errorDetail); + } + + private final String errorDetail; + private final T struct; + + private StructOrError(T struct) { + this.struct = checkNotNull(struct, "struct"); + this.errorDetail = null; + } + + private StructOrError(String errorDetail) { + this.struct = null; + this.errorDetail = checkNotNull(errorDetail, "errorDetail"); + } + + /** + * Returns struct if exists, otherwise null. + */ + @VisibleForTesting + @Nullable + public T getStruct() { + return struct; + } + + /** + * Returns error detail if exists, otherwise null. + */ + @VisibleForTesting + @Nullable + public String getErrorDetail() { + return errorDetail; + } +} + diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index c7789f3d7dd..e1645063c48 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -67,6 +67,8 @@ class XdsClusterResource extends XdsResourceType { static final String AGGREGATE_CLUSTER_TYPE_NAME = "envoy.clusters.aggregate"; static final String ADS_TYPE_URL_CDS = "type.googleapis.com/envoy.config.cluster.v3.Cluster"; + private static final String TYPE_URL_CLUSTER_CONFIG = + "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig"; private static final String TYPE_URL_UPSTREAM_TLS_CONTEXT = "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext"; private static final String TYPE_URL_UPSTREAM_TLS_CONTEXT_V2 = diff --git a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java index 0e065c6ba9a..587c7a437ad 100644 --- a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java @@ -78,6 +78,8 @@ class XdsRouteConfigureResource extends XdsResourceType { "type.googleapis.com/envoy.config.route.v3.RouteConfiguration"; private static final String TYPE_URL_FILTER_CONFIG = "type.googleapis.com/envoy.config.route.v3.FilterConfig"; + @VisibleForTesting + static final String HASH_POLICY_FILTER_STATE_KEY = "io.grpc.channel_id"; // TODO(zdapeng): need to discuss how to handle unsupported values. private static final Set SUPPORTED_RETRYABLE_CODES = Collections.unmodifiableSet(EnumSet.of( diff --git a/xds/src/main/java/io/grpc/xds/client/XdsResourceType.java b/xds/src/main/java/io/grpc/xds/client/XdsResourceType.java index 8c3d31604e4..ccb622ff168 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsResourceType.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsResourceType.java @@ -20,7 +20,6 @@ import static io.grpc.xds.client.XdsClient.canonifyResourceName; import static io.grpc.xds.client.XdsClient.isResourceNameValid; -import com.google.common.annotations.VisibleForTesting; import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; @@ -41,11 +40,7 @@ public abstract class XdsResourceType { static final String TYPE_URL_RESOURCE = "type.googleapis.com/envoy.service.discovery.v3.Resource"; protected static final String TRANSPORT_SOCKET_NAME_TLS = "envoy.transport_sockets.tls"; - @VisibleForTesting - public static final String HASH_POLICY_FILTER_STATE_KEY = "io.grpc.channel_id"; - protected static final String TYPE_URL_CLUSTER_CONFIG = - "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig"; protected static final String TYPE_URL_TYPED_STRUCT_UDPA = "type.googleapis.com/udpa.type.v1.TypedStruct"; protected static final String TYPE_URL_TYPED_STRUCT = @@ -249,53 +244,4 @@ public ValidatedResourceUpdate(Map> parsedResources, this.errors = errors; } } - - @VisibleForTesting - public static final class StructOrError { - - /** - * Returns a {@link StructOrError} for the successfully converted data object. - */ - public static StructOrError fromStruct(T struct) { - return new StructOrError<>(struct); - } - - /** - * Returns a {@link StructOrError} for the failure to convert the data object. - */ - public static StructOrError fromError(String errorDetail) { - return new StructOrError<>(errorDetail); - } - - private final String errorDetail; - private final T struct; - - private StructOrError(T struct) { - this.struct = checkNotNull(struct, "struct"); - this.errorDetail = null; - } - - private StructOrError(String errorDetail) { - this.struct = null; - this.errorDetail = checkNotNull(errorDetail, "errorDetail"); - } - - /** - * Returns struct if exists, otherwise null. - */ - @VisibleForTesting - @Nullable - public T getStruct() { - return struct; - } - - /** - * Returns error detail if exists, otherwise null. - */ - @VisibleForTesting - @Nullable - public String getErrorDetail() { - return errorDetail; - } - } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index b0ef0131a6d..6b905c4e2ba 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -140,7 +140,6 @@ import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsResourceType; import io.grpc.xds.client.XdsResourceType.ResourceInvalidException; -import io.grpc.xds.client.XdsResourceType.StructOrError; import io.grpc.xds.internal.Matchers; import io.grpc.xds.internal.Matchers.FractionMatcher; import io.grpc.xds.internal.Matchers.HeaderMatcher; @@ -939,7 +938,7 @@ public void parseRouteAction_withHashPolicies() { io.envoyproxy.envoy.config.route.v3.RouteAction.HashPolicy.newBuilder() .setFilterState( FilterState.newBuilder() - .setKey(XdsResourceType.HASH_POLICY_FILTER_STATE_KEY))) + .setKey(XdsRouteConfigureResource.HASH_POLICY_FILTER_STATE_KEY))) .addHashPolicy( io.envoyproxy.envoy.config.route.v3.RouteAction.HashPolicy.newBuilder() .setQueryParameter( From 8a5f7776dba4bc126614c04a52879bccdc11c070 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 16 Dec 2024 15:59:10 -0800 Subject: [PATCH 113/591] .github/workflows: Stop testing Bazel 6 Bazel 8 is now out. We support the two most recent releases. FWIW, I have compiled with Bazel 8 and didn't experience any problems. --- .github/workflows/testing.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d68d85eb0cd..8c934f24f3e 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -79,14 +79,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - include: - # Test with and without bzlmod. Bazel 6 doesn't support bzlmod, so use Bazel 7 instead - - bazel: 6.0.0 - bzlmod: false - - bazel: 7.0.0 - bzlmod: true + bzlmod: [true, false] env: - USE_BAZEL_VERSION: ${{ matrix.bazel }} + USE_BAZEL_VERSION: 7.0.0 steps: - uses: actions/checkout@v4 From f8f613984fe1c57171977e948315449e3d013ebc Mon Sep 17 00:00:00 2001 From: vinodhabib <47808007+vinodhabib@users.noreply.github.com> Date: Tue, 17 Dec 2024 05:37:22 +0000 Subject: [PATCH 114/591] xds: fixed unsupported unsigned 32 bits issue for circuit breaker (#11735) Added change for circuit breaking by converting signed 32-bit Int to Unsigned 64-bit Long For MaxRequest negative value ( -1) Fixes #11695 --- .../java/io/grpc/xds/XdsClusterResource.java | 2 +- .../grpc/xds/GrpcXdsClientImplTestBase.java | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index e1645063c48..1afe865d10a 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -215,7 +215,7 @@ private static StructOrError parseNonAggregateCluster( continue; } if (threshold.hasMaxRequests()) { - maxConcurrentRequests = (long) threshold.getMaxRequests().getValue(); + maxConcurrentRequests = Integer.toUnsignedLong(threshold.getMaxRequests().getValue()); } } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index b326eb7d02d..f1c114673b5 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -4001,6 +4001,25 @@ public void sendToBadUrl() throws Exception { client.shutdown(); } + @Test + public void circuitBreakingConversionOf32bitIntTo64bitLongForMaxRequestNegativeValue() { + DiscoveryRpcCall call = startResourceWatcher(XdsClusterResource.getInstance(), CDS_RESOURCE, + cdsResourceWatcher); + Any clusterCircuitBreakers = Any.pack( + mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, null, false, null, + "envoy.transport_sockets.tls", mf.buildCircuitBreakers(50, -1), null)); + call.sendResponse(CDS, clusterCircuitBreakers, VERSION_1, "0000"); + + // Client sent an ACK CDS request. + call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); + verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); + CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + + assertThat(cdsUpdate.clusterName()).isEqualTo(CDS_RESOURCE); + assertThat(cdsUpdate.clusterType()).isEqualTo(ClusterType.EDS); + assertThat(cdsUpdate.maxConcurrentRequests()).isEqualTo(4294967295L); + } + @Test public void sendToNonexistentServer() throws Exception { // Setup xdsClient to fail on stream creation From 8ea36293780a49d5cbcb3424e27357caa353d73a Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 19 Dec 2024 07:54:54 -0800 Subject: [PATCH 115/591] Re-enable animalsniffer, fixing violations In 61f19d707a I swapped the signatures to use the version catalog. But I failed to preserve the `@signature` extension and it all seemed to work... But in fact all the animalsniffer tasks were completing as SKIPPED as they lacked signatures. The build.gradle changes in this commit are to fix that while still using version catalog. But while it was broken violations crept in. Most violations weren't too important and we're not surprised went unnoticed. For example, Netty with TLS has long required the Java 8 API `setEndpointIdentificationAlgorithm()`, so using `Optional` in the same code path didn't harm anything in particular. I still swapped it to Guava's `Optional` to avoid overuse of `@IgnoreJRERequirement`. One important violation has not been fixed and instead I've disabled the android signature in api/build.gradle for the moment. The violation is in StatusException using the `fillInStackTrace` overload of Exception. This problem [had been noticed][PR11066], but we couldn't figure out what was going on. AnimalSniffer is now noticing this and agreeing with the internal linter. There is still a question of why our interop tests failed to notice this, but given they are no longer running on pre-API level 24, that may forever be a mystery. [PR11066]: https://github.com/grpc/grpc-java/pull/11066 --- alts/build.gradle | 6 ++++- api/BUILD.bazel | 1 + api/build.gradle | 15 +++++++++-- api/src/main/java/io/grpc/TimeUtils.java | 2 ++ .../test/java/io/grpc/CallOptionsTest.java | 2 ++ .../io/grpc/SynchronizationContextTest.java | 5 +++- api/src/test/java/io/grpc/TimeUtilsTest.java | 4 ++- auth/build.gradle | 12 +++++++-- authz/build.gradle | 6 ++++- benchmarks/build.gradle | 6 ++++- census/build.gradle | 12 +++++++-- contextstorage/build.gradle | 12 +++++++-- core/build.gradle | 12 +++++++-- .../java/io/grpc/internal/SpiffeUtil.java | 9 +++---- .../internal/ConcurrentTimeProviderTest.java | 7 ++--- .../internal/InstantTimeProviderTest.java | 2 ++ .../grpc/internal/ManagedChannelImplTest.java | 7 +++-- .../java/io/grpc/internal/SpiffeUtilTest.java | 27 +++++++++++-------- gae-interop-testing/gae-jdk8/build.gradle | 6 ++++- gcp-csm-observability/build.gradle | 6 ++++- gcp-observability/build.gradle | 6 ++++- gcp-observability/interop/build.gradle | 6 ++++- googleapis/build.gradle | 6 ++++- grpclb/build.gradle | 6 ++++- inprocess/build.gradle | 12 +++++++-- interop-testing/build.gradle | 12 +++++++-- .../testing/integration/XdsTestClient.java | 2 ++ .../testing/integration/XdsTestServer.java | 2 ++ istio-interop-testing/build.gradle | 6 ++++- netty/BUILD.bazel | 1 + netty/build.gradle | 12 +++++++-- netty/shaded/build.gradle | 12 +++++++-- .../netty/InternalProtocolNegotiators.java | 6 ++--- .../io/grpc/netty/NettyChannelBuilder.java | 4 +-- .../io/grpc/netty/ProtocolNegotiators.java | 6 +++-- .../netty/NettyAdaptiveCumulatorTest.java | 3 +-- .../io/grpc/netty/NettyClientStreamTest.java | 11 +++++--- .../grpc/netty/NettyClientTransportTest.java | 4 +-- .../io/grpc/netty/NettyServerStreamTest.java | 20 +++++++------- .../grpc/netty/ProtocolNegotiatorsTest.java | 12 ++++----- okhttp/build.gradle | 12 +++++++-- opentelemetry/build.gradle | 7 +++-- protobuf-lite/build.gradle | 12 +++++++-- protobuf/build.gradle | 12 +++++++-- rls/build.gradle | 6 ++++- s2a/build.gradle | 6 ++++- .../S2AProtocolNegotiatorFactory.java | 4 +-- services/build.gradle | 6 ++++- stub/BUILD.bazel | 1 + stub/build.gradle | 12 +++++++-- .../main/java/io/grpc/stub/AbstractStub.java | 2 ++ .../java/io/grpc/stub/AbstractStubTest.java | 2 ++ testing-proto/build.gradle | 6 ++++- testing/build.gradle | 12 +++++++-- util/build.gradle | 12 +++++++-- .../util/OutlierDetectionLoadBalancer.java | 6 ++++- .../util/AdvancedTlsX509TrustManagerTest.java | 2 ++ xds/build.gradle | 6 ++++- 58 files changed, 329 insertions(+), 105 deletions(-) diff --git a/alts/build.gradle b/alts/build.gradle index de93c90546f..3e472d9cea6 100644 --- a/alts/build.gradle +++ b/alts/build.gradle @@ -44,7 +44,11 @@ dependencies { classifier = "linux-x86_64" } } - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } configureProtoCompilation() diff --git a/api/BUILD.bazel b/api/BUILD.bazel index 6bf3375e9f0..34e8de95335 100644 --- a/api/BUILD.bazel +++ b/api/BUILD.bazel @@ -13,5 +13,6 @@ java_library( artifact("com.google.errorprone:error_prone_annotations"), artifact("com.google.guava:failureaccess"), # future transitive dep of Guava. See #5214 artifact("com.google.guava:guava"), + artifact("org.codehaus.mojo:animal-sniffer-annotations"), ], ) diff --git a/api/build.gradle b/api/build.gradle index 1d21c7bdcb6..4edfba7c0d8 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -36,6 +36,7 @@ tasks.named("jar").configure { dependencies { compileOnly sourceSets.context.output api libraries.jsr305, + libraries.animalsniffer.annotations, libraries.errorprone.annotations implementation libraries.guava @@ -48,8 +49,18 @@ dependencies { testImplementation project(':grpc-testing') testImplementation libraries.guava.testlib - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + // TODO: Temporarily disabled until StatusException is fixed. + // Context: https://github.com/grpc/grpc-java/pull/11066 + //signature (libraries.signature.android) { + // artifact { + // extension = "signature" + // } + //} } tasks.named("javadoc").configure { diff --git a/api/src/main/java/io/grpc/TimeUtils.java b/api/src/main/java/io/grpc/TimeUtils.java index c3031f13d94..c3cdf843d79 100644 --- a/api/src/main/java/io/grpc/TimeUtils.java +++ b/api/src/main/java/io/grpc/TimeUtils.java @@ -17,10 +17,12 @@ package io.grpc; import java.time.Duration; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; final class TimeUtils { private TimeUtils() {} + @IgnoreJRERequirement static long convertToNanos(Duration duration) { try { return duration.toNanos(); diff --git a/api/src/test/java/io/grpc/CallOptionsTest.java b/api/src/test/java/io/grpc/CallOptionsTest.java index d74c74ccd66..051c1b2d851 100644 --- a/api/src/test/java/io/grpc/CallOptionsTest.java +++ b/api/src/test/java/io/grpc/CallOptionsTest.java @@ -34,6 +34,7 @@ import io.grpc.internal.SerializingExecutor; import java.time.Duration; import java.util.concurrent.Executor; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -152,6 +153,7 @@ public void withDeadlineAfter() { } @Test + @IgnoreJRERequirement public void withDeadlineAfterDuration() { Deadline actual = CallOptions.DEFAULT.withDeadlineAfter(Duration.ofMinutes(1L)).getDeadline(); Deadline expected = Deadline.after(1, MINUTES); diff --git a/api/src/test/java/io/grpc/SynchronizationContextTest.java b/api/src/test/java/io/grpc/SynchronizationContextTest.java index f0797df227e..d063c58a978 100644 --- a/api/src/test/java/io/grpc/SynchronizationContextTest.java +++ b/api/src/test/java/io/grpc/SynchronizationContextTest.java @@ -36,6 +36,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.junit.After; import org.junit.Rule; import org.junit.Test; @@ -248,6 +249,7 @@ public void schedule() { } @Test + @IgnoreJRERequirement public void scheduleDuration() { MockScheduledExecutorService executorService = new MockScheduledExecutorService(); ScheduledHandle handle = @@ -265,6 +267,7 @@ public void scheduleDuration() { } @Test + @IgnoreJRERequirement public void scheduleWithFixedDelayDuration() { MockScheduledExecutorService executorService = new MockScheduledExecutorService(); ScheduledHandle handle = @@ -402,4 +405,4 @@ static class MockScheduledExecutorService extends ForwardingScheduledExecutorSer return future = super.scheduleWithFixedDelay(command, intialDelay, delay, unit); } } -} \ No newline at end of file +} diff --git a/api/src/test/java/io/grpc/TimeUtilsTest.java b/api/src/test/java/io/grpc/TimeUtilsTest.java index 4faaa9cbf6d..75c0437ce26 100644 --- a/api/src/test/java/io/grpc/TimeUtilsTest.java +++ b/api/src/test/java/io/grpc/TimeUtilsTest.java @@ -19,12 +19,14 @@ import static org.junit.Assert.assertEquals; import java.time.Duration; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link TimeUtils}. */ @RunWith(JUnit4.class) +@IgnoreJRERequirement public class TimeUtilsTest { @Test @@ -56,4 +58,4 @@ public void testConvertTooLargeNegativeDuration() { assertEquals(Long.MIN_VALUE, TimeUtils.convertToNanos(duration)); } -} \ No newline at end of file +} diff --git a/auth/build.gradle b/auth/build.gradle index 78bb720601b..d56802c14ca 100644 --- a/auth/build.gradle +++ b/auth/build.gradle @@ -22,6 +22,14 @@ dependencies { project(':grpc-core'), project(":grpc-context"), // Override google-auth dependency with our newer version libraries.google.auth.oauth2Http - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } diff --git a/authz/build.gradle b/authz/build.gradle index 60c86ca0dba..b72088bfbaa 100644 --- a/authz/build.gradle +++ b/authz/build.gradle @@ -26,7 +26,11 @@ dependencies { shadow configurations.implementation.getDependencies().minus([xdsDependency]) shadow project(path: ':grpc-xds', configuration: 'shadow') - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } tasks.named("jar").configure { diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index d00c0d76ebe..bf043106050 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -43,7 +43,11 @@ dependencies { testImplementation libraries.junit, libraries.mockito.core - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } import net.ltgt.gradle.errorprone.CheckSeverity diff --git a/census/build.gradle b/census/build.gradle index c1dc53e4c05..0993488ff83 100644 --- a/census/build.gradle +++ b/census/build.gradle @@ -27,8 +27,16 @@ dependencies { project(':grpc-testing'), libraries.opencensus.impl - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } tasks.named("javadoc").configure { diff --git a/contextstorage/build.gradle b/contextstorage/build.gradle index 39ea003d462..b1e78ea0e17 100644 --- a/contextstorage/build.gradle +++ b/contextstorage/build.gradle @@ -16,8 +16,16 @@ dependencies { libraries.assertj.core testImplementation 'junit:junit:4.13.1'// opentelemetry.sdk.testing uses compileOnly for assertj - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } tasks.named("jar").configure { diff --git a/core/build.gradle b/core/build.gradle index f8a95c37286..2fac9ddba04 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -39,8 +39,16 @@ dependencies { jmh project(':grpc-testing') - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } tasks.named("javadoc").configure { diff --git a/core/src/main/java/io/grpc/internal/SpiffeUtil.java b/core/src/main/java/io/grpc/internal/SpiffeUtil.java index 57e201d19ba..44ef343b6dc 100644 --- a/core/src/main/java/io/grpc/internal/SpiffeUtil.java +++ b/core/src/main/java/io/grpc/internal/SpiffeUtil.java @@ -23,13 +23,12 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -188,8 +187,8 @@ public static SpiffeBundle loadTrustBundleFromFile(String trustBundleFile) throw } private static Map readTrustDomainsFromFile(String filePath) throws IOException { - Path path = Paths.get(checkNotNull(filePath, "trustBundleFile")); - String json = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + File file = new File(checkNotNull(filePath, "trustBundleFile")); + String json = new String(Files.toByteArray(file), StandardCharsets.UTF_8); Object jsonObject = JsonParser.parse(json); if (!(jsonObject instanceof Map)) { throw new IllegalArgumentException( diff --git a/core/src/test/java/io/grpc/internal/ConcurrentTimeProviderTest.java b/core/src/test/java/io/grpc/internal/ConcurrentTimeProviderTest.java index a02cb6a6e42..7983530456c 100644 --- a/core/src/test/java/io/grpc/internal/ConcurrentTimeProviderTest.java +++ b/core/src/test/java/io/grpc/internal/ConcurrentTimeProviderTest.java @@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat; -import java.time.Instant; import java.util.concurrent.TimeUnit; import org.junit.Test; import org.junit.runner.RunWith; @@ -36,10 +35,8 @@ public void testConcurrentCurrentTimeNanos() { // Get the current time from the ConcurrentTimeProvider long actualTimeNanos = concurrentTimeProvider.currentTimeNanos(); - // Get the current time from Instant for comparison - Instant instantNow = Instant.now(); - long expectedTimeNanos = TimeUnit.SECONDS.toNanos(instantNow.getEpochSecond()) - + instantNow.getNano(); + // Get the current time from System for comparison + long expectedTimeNanos = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); // Validate the time returned is close to the expected value within a tolerance // (i,e 10 millisecond tolerance in nanoseconds). diff --git a/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java b/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java index 46d1891cbb9..ac9a02fa936 100644 --- a/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java +++ b/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java @@ -20,6 +20,7 @@ import java.time.Instant; import java.util.concurrent.TimeUnit; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -28,6 +29,7 @@ * Unit tests for {@link InstantTimeProvider}. */ @RunWith(JUnit4.class) +@IgnoreJRERequirement public class InstantTimeProviderTest { @Test public void testInstantCurrentTimeNanos() throws Exception { diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 535086716bd..2fe82aea972 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -57,6 +57,7 @@ import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; @@ -1199,8 +1200,10 @@ public void addressResolutionError_noPriorNameResolution_usesDefaultServiceConfi // config simply gets ignored and not gets reassigned. resolver.resolved(); timer.forwardNanos(1234); - assertThat(getStats(channel).channelTrace.events.stream().filter( - event -> event.description.equals("Service config changed")).count()).isEqualTo(0); + assertThat(Iterables.filter( + getStats(channel).channelTrace.events, + event -> event.description.equals("Service config changed"))) + .isEmpty(); } @Test diff --git a/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java b/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java index 244539501a6..d5155728936 100644 --- a/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java +++ b/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java @@ -22,15 +22,16 @@ import static org.junit.Assert.assertTrue; import com.google.common.base.Optional; +import com.google.common.io.ByteStreams; import io.grpc.internal.SpiffeUtil.SpiffeBundle; import io.grpc.internal.SpiffeUtil.SpiffeId; import io.grpc.testing.TlsTesting; import io.grpc.util.CertificateUtils; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +import java.io.OutputStream; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collection; @@ -247,12 +248,14 @@ public void setUp() throws Exception { } private String copyFileToTmp(String fileName) throws Exception { - Path tempFilePath = tempFolder.newFile(fileName).toPath(); + File tempFile = tempFolder.newFile(fileName); try (InputStream resourceStream = SpiffeUtilTest.class.getClassLoader() - .getResourceAsStream(TEST_DIRECTORY_PREFIX + fileName)) { - Files.copy(resourceStream, tempFilePath, StandardCopyOption.REPLACE_EXISTING); + .getResourceAsStream(TEST_DIRECTORY_PREFIX + fileName); + OutputStream fileStream = new FileOutputStream(tempFile)) { + ByteStreams.copy(resourceStream, fileStream); + fileStream.flush(); } - return tempFilePath.toString(); + return tempFile.toString(); } @Test @@ -358,9 +361,11 @@ public void loadTrustBundleFromFileParameterValidityTest() { NullPointerException npe = assertThrows(NullPointerException.class, () -> SpiffeUtil .loadTrustBundleFromFile(null)); assertEquals("trustBundleFile", npe.getMessage()); - NoSuchFileException nsfe = assertThrows(NoSuchFileException.class, () -> SpiffeUtil + FileNotFoundException nsfe = assertThrows(FileNotFoundException.class, () -> SpiffeUtil .loadTrustBundleFromFile("i_do_not_exist")); - assertEquals("i_do_not_exist", nsfe.getMessage()); + assertTrue( + "Did not contain expected substring: " + nsfe.getMessage(), + nsfe.getMessage().contains("i_do_not_exist")); } } -} \ No newline at end of file +} diff --git a/gae-interop-testing/gae-jdk8/build.gradle b/gae-interop-testing/gae-jdk8/build.gradle index a09a8e793c0..3ed395198c2 100644 --- a/gae-interop-testing/gae-jdk8/build.gradle +++ b/gae-interop-testing/gae-jdk8/build.gradle @@ -53,7 +53,11 @@ dependencies { implementation libraries.junit implementation libraries.protobuf.java runtimeOnly libraries.netty.tcnative, libraries.netty.tcnative.classes - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } tasks.named("compileJava").configure { diff --git a/gcp-csm-observability/build.gradle b/gcp-csm-observability/build.gradle index e29a56b1052..bda54ca8146 100644 --- a/gcp-csm-observability/build.gradle +++ b/gcp-csm-observability/build.gradle @@ -28,5 +28,9 @@ dependencies { libraries.opentelemetry.sdk.testing, libraries.assertj.core // opentelemetry.sdk.testing uses compileOnly for this dep - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } diff --git a/gcp-observability/build.gradle b/gcp-observability/build.gradle index f869bd61a76..c6d6fa28ddc 100644 --- a/gcp-observability/build.gradle +++ b/gcp-observability/build.gradle @@ -74,7 +74,11 @@ dependencies { exclude group: 'junit', module: 'junit' } - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } configureProtoCompilation() diff --git a/gcp-observability/interop/build.gradle b/gcp-observability/interop/build.gradle index 4a78c056eac..7e17624995a 100644 --- a/gcp-observability/interop/build.gradle +++ b/gcp-observability/interop/build.gradle @@ -10,7 +10,11 @@ dependencies { implementation project(':grpc-interop-testing'), project(':grpc-gcp-observability') - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } application { diff --git a/googleapis/build.gradle b/googleapis/build.gradle index 435e552d47d..3a7a3a2766a 100644 --- a/googleapis/build.gradle +++ b/googleapis/build.gradle @@ -21,5 +21,9 @@ dependencies { libraries.guava.jre // JRE required by transitive protobuf-java-util testImplementation testFixtures(project(':grpc-core')) - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } diff --git a/grpclb/build.gradle b/grpclb/build.gradle index 93331053b09..3f67181372b 100644 --- a/grpclb/build.gradle +++ b/grpclb/build.gradle @@ -28,7 +28,11 @@ dependencies { project(':grpc-inprocess'), testFixtures(project(':grpc-core')) - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } configureProtoCompilation() diff --git a/inprocess/build.gradle b/inprocess/build.gradle index edc97883b50..075968ccb9a 100644 --- a/inprocess/build.gradle +++ b/inprocess/build.gradle @@ -22,8 +22,16 @@ dependencies { testFixtures(project(':grpc-core')) testImplementation libraries.guava.testlib - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } tasks.named("javadoc").configure { diff --git a/interop-testing/build.gradle b/interop-testing/build.gradle index a85aec97adf..97e7c69533a 100644 --- a/interop-testing/build.gradle +++ b/interop-testing/build.gradle @@ -53,8 +53,16 @@ dependencies { libraries.mockito.core, libraries.okhttp - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } configureProtoCompilation() diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestClient.java index 23bc12a6b65..89519041a79 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestClient.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestClient.java @@ -78,6 +78,7 @@ import java.util.logging.Logger; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; /** Client for xDS interop tests. */ public final class XdsTestClient { @@ -261,6 +262,7 @@ private static RpcType parseRpc(String rpc) { } } + @IgnoreJRERequirement // OpenTelemetry uses Java 8+ APIs private void run() { if (enableCsmObservability) { csmObservability = CsmObservability.newBuilder() diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java b/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java index 1bc4ff88981..88f1bf468b6 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java @@ -57,6 +57,7 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; /** Interop test server that implements the xDS testing service. */ public final class XdsTestServer { @@ -193,6 +194,7 @@ void parseArgs(String[] args) { } @SuppressWarnings("AddressSelection") + @IgnoreJRERequirement // OpenTelemetry uses Java 8+ APIs void start() throws Exception { if (enableCsmObservability) { csmObservability = CsmObservability.newBuilder() diff --git a/istio-interop-testing/build.gradle b/istio-interop-testing/build.gradle index e2fe228f13b..4550f0ea202 100644 --- a/istio-interop-testing/build.gradle +++ b/istio-interop-testing/build.gradle @@ -28,7 +28,11 @@ dependencies { libraries.junit, libraries.truth - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } sourceSets { diff --git a/netty/BUILD.bazel b/netty/BUILD.bazel index 9fe52ea5868..52fdcb19fd8 100644 --- a/netty/BUILD.bazel +++ b/netty/BUILD.bazel @@ -27,6 +27,7 @@ java_library( artifact("io.netty:netty-transport"), artifact("io.netty:netty-transport-native-unix-common"), artifact("io.perfmark:perfmark-api"), + artifact("org.codehaus.mojo:animal-sniffer-annotations"), ], ) diff --git a/netty/build.gradle b/netty/build.gradle index 5533038c85b..3662f8ec399 100644 --- a/netty/build.gradle +++ b/netty/build.gradle @@ -65,8 +65,16 @@ dependencies { classifier = "linux-x86_64" } } - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } import net.ltgt.gradle.errorprone.CheckSeverity diff --git a/netty/shaded/build.gradle b/netty/shaded/build.gradle index 25682d60b75..f805a4f4a1f 100644 --- a/netty/shaded/build.gradle +++ b/netty/shaded/build.gradle @@ -65,8 +65,16 @@ dependencies { shadow project(':grpc-netty').configurations.runtimeClasspath.allDependencies.matching { it.group != 'io.netty' } - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } tasks.named("jar").configure { diff --git a/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java index 8aeb44d0fc2..3d1aa83d9ff 100644 --- a/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java @@ -16,6 +16,7 @@ package io.grpc.netty; +import com.google.common.base.Optional; import io.grpc.ChannelLogger; import io.grpc.internal.ObjectPool; import io.grpc.netty.ProtocolNegotiators.ClientTlsHandler; @@ -24,7 +25,6 @@ import io.netty.channel.ChannelHandler; import io.netty.handler.ssl.SslContext; import io.netty.util.AsciiString; -import java.util.Optional; import java.util.concurrent.Executor; /** @@ -72,7 +72,7 @@ public void close() { * may happen immediately, even before the TLS Handshake is complete. */ public static InternalProtocolNegotiator.ProtocolNegotiator tls(SslContext sslContext) { - return tls(sslContext, null, Optional.empty()); + return tls(sslContext, null, Optional.absent()); } /** @@ -170,7 +170,7 @@ public static ChannelHandler clientTlsHandler( ChannelHandler next, SslContext sslContext, String authority, ChannelLogger negotiationLogger) { return new ClientTlsHandler(next, sslContext, authority, null, negotiationLogger, - Optional.empty()); + Optional.absent()); } public static class ProtocolNegotiationHandler diff --git a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java index fe226ec2ba9..8b80f7b4e46 100644 --- a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java +++ b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java @@ -23,6 +23,7 @@ import static io.grpc.internal.GrpcUtil.KEEPALIVE_TIME_NANOS_DISABLED; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; import com.google.common.base.Ticker; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CheckReturnValue; @@ -63,7 +64,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -652,7 +652,7 @@ static ProtocolNegotiator createProtocolNegotiatorByType( case PLAINTEXT_UPGRADE: return ProtocolNegotiators.plaintextUpgrade(); case TLS: - return ProtocolNegotiators.tls(sslContext, executorPool, Optional.empty()); + return ProtocolNegotiators.tls(sslContext, executorPool, Optional.absent()); default: throw new IllegalArgumentException("Unsupported negotiationType: " + negotiationType); } diff --git a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java index 84370ac8153..3f4d59bb334 100644 --- a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.errorprone.annotations.ForOverride; import io.grpc.Attributes; @@ -72,7 +73,6 @@ import java.nio.channels.ClosedChannelException; import java.util.Arrays; import java.util.EnumSet; -import java.util.Optional; import java.util.Set; import java.util.concurrent.Executor; import java.util.logging.Level; @@ -82,6 +82,7 @@ import javax.net.ssl.SSLException; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; /** * Common {@link ProtocolNegotiator}s used by gRPC. @@ -601,6 +602,7 @@ static final class ClientTlsHandler extends ProtocolNegotiationHandler { } @Override + @IgnoreJRERequirement protected void handlerAdded0(ChannelHandlerContext ctx) { SSLEngine sslEngine = sslContext.newEngine(ctx.alloc(), host, port); SSLParameters sslParams = sslEngine.getSSLParameters(); @@ -708,7 +710,7 @@ public static ProtocolNegotiator tls(SslContext sslContext, * may happen immediately, even before the TLS Handshake is complete. */ public static ProtocolNegotiator tls(SslContext sslContext) { - return tls(sslContext, null, Optional.empty()); + return tls(sslContext, null, Optional.absent()); } public static ProtocolNegotiator.ClientFactory tlsClientFactory(SslContext sslContext) { diff --git a/netty/src/test/java/io/grpc/netty/NettyAdaptiveCumulatorTest.java b/netty/src/test/java/io/grpc/netty/NettyAdaptiveCumulatorTest.java index 1037dab4712..68dc6dc0c80 100644 --- a/netty/src/test/java/io/grpc/netty/NettyAdaptiveCumulatorTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyAdaptiveCumulatorTest.java @@ -40,7 +40,6 @@ import io.netty.buffer.UnpooledByteBufAllocator; import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -55,7 +54,7 @@ public class NettyAdaptiveCumulatorTest { private static Collection cartesianProductParams(List... lists) { - return Lists.cartesianProduct(lists).stream().map(List::toArray).collect(Collectors.toList()); + return Lists.transform(Lists.cartesianProduct(lists), List::toArray); } @RunWith(JUnit4.class) diff --git a/netty/src/test/java/io/grpc/netty/NettyClientStreamTest.java b/netty/src/test/java/io/grpc/netty/NettyClientStreamTest.java index 2a5a0df279a..a44a196ac8c 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientStreamTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientStreamTest.java @@ -46,6 +46,7 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Iterables; import com.google.common.io.BaseEncoding; import io.grpc.CallOptions; import io.grpc.InternalStatus; @@ -239,11 +240,13 @@ public void writeFrameFutureFailedShouldCancelRpc() { // Get the CancelClientStreamCommand written to the queue. Above we verified that there is // only one CancelClientStreamCommand enqueued, and is the third enqueued command (create, // frame write failure, cancel). - CancelClientStreamCommand cancelCommand = Mockito.mockingDetails(writeQueue).getInvocations() - // Get enqueue() innovations only - .stream().filter(invocation -> invocation.getMethod().getName().equals("enqueue")) + CancelClientStreamCommand cancelCommand = Iterables.get( + Iterables.filter( + Mockito.mockingDetails(writeQueue).getInvocations(), + // Get enqueue() innovations only + invocation -> invocation.getMethod().getName().equals("enqueue")), // Get the third invocation of enqueue() - .skip(2).findFirst().get() + 2) // Get the first argument (QueuedCommand command) .getArgument(0); diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index b7a3ff13a59..8d0d656859f 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -39,6 +39,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.base.Ticker; import com.google.common.io.ByteStreams; @@ -105,7 +106,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -803,7 +803,7 @@ public void tlsNegotiationServerExecutorShouldSucceed() throws Exception { .keyManager(clientCert, clientKey) .build(); ProtocolNegotiator negotiator = ProtocolNegotiators.tls(clientContext, clientExecutorPool, - Optional.empty()); + Optional.absent()); // after starting the client, the Executor in the client pool should be used assertEquals(true, clientExecutorPool.isInUse()); final NettyClientTransport transport = newTransport(negotiator); diff --git a/netty/src/test/java/io/grpc/netty/NettyServerStreamTest.java b/netty/src/test/java/io/grpc/netty/NettyServerStreamTest.java index 452f68341b1..0723e359752 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerStreamTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerStreamTest.java @@ -38,7 +38,9 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; import io.grpc.Attributes; import io.grpc.Metadata; import io.grpc.Status; @@ -57,7 +59,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Queue; -import java.util.stream.Collectors; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -217,14 +218,15 @@ private CancelServerStreamCommand findCancelServerStreamCommand() { // Ensure there's no CancelServerStreamCommand enqueued with flush=false. verify(writeQueue, never()).enqueue(any(CancelServerStreamCommand.class), eq(false)); - List commands = Mockito.mockingDetails(writeQueue).getInvocations() - .stream() - // Get enqueue() innovations only. - .filter(invocation -> invocation.getMethod().getName().equals("enqueue")) - // Find the cancel commands. - .filter(invocation -> invocation.getArgument(0) instanceof CancelServerStreamCommand) - .map(invocation -> invocation.getArgument(0, CancelServerStreamCommand.class)) - .collect(Collectors.toList()); + List commands = Lists.newArrayList( + Iterables.transform( + Iterables.filter( + Mockito.mockingDetails(writeQueue).getInvocations(), + // Get enqueue() innovations only + invocation -> invocation.getMethod().getName().equals("enqueue") + // Find the cancel commands. + && invocation.getArgument(0) instanceof CancelServerStreamCommand), + invocation -> invocation.getArgument(0, CancelServerStreamCommand.class))); assertWithMessage("Expected exactly one CancelClientStreamCommand").that(commands).hasSize(1); return commands.get(0); diff --git a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java index 2ccdb2de543..6dff3de2b2a 100644 --- a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java +++ b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import com.google.common.base.Optional; import io.grpc.Attributes; import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; @@ -120,7 +121,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.Queue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -877,7 +877,7 @@ public String applicationProtocol() { DefaultEventLoopGroup elg = new DefaultEventLoopGroup(1); ClientTlsHandler handler = new ClientTlsHandler(grpcHandler, sslContext, - "authority", elg, noopLogger, Optional.empty()); + "authority", elg, noopLogger, Optional.absent()); pipeline.addLast(handler); pipeline.replace(SslHandler.class, null, goodSslHandler); pipeline.fireUserEventTriggered(ProtocolNegotiationEvent.DEFAULT); @@ -915,7 +915,7 @@ public String applicationProtocol() { .applicationProtocolConfig(apn).build(); ClientTlsHandler handler = new ClientTlsHandler(grpcHandler, sslContext, - "authority", elg, noopLogger, Optional.empty()); + "authority", elg, noopLogger, Optional.absent()); pipeline.addLast(handler); pipeline.replace(SslHandler.class, null, goodSslHandler); pipeline.fireUserEventTriggered(ProtocolNegotiationEvent.DEFAULT); @@ -939,7 +939,7 @@ public String applicationProtocol() { DefaultEventLoopGroup elg = new DefaultEventLoopGroup(1); ClientTlsHandler handler = new ClientTlsHandler(grpcHandler, sslContext, - "authority", elg, noopLogger, Optional.empty()); + "authority", elg, noopLogger, Optional.absent()); pipeline.addLast(handler); final AtomicReference error = new AtomicReference<>(); @@ -967,7 +967,7 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { @Test public void clientTlsHandler_closeDuringNegotiation() throws Exception { ClientTlsHandler handler = new ClientTlsHandler(grpcHandler, sslContext, - "authority", null, noopLogger, Optional.empty()); + "authority", null, noopLogger, Optional.absent()); pipeline.addLast(new WriteBufferingAndExceptionHandler(handler)); ChannelFuture pendingWrite = channel.writeAndFlush(NettyClientHandler.NOOP_MESSAGE); @@ -1230,7 +1230,7 @@ public void clientTlsHandler_firesNegotiation() throws Exception { } FakeGrpcHttp2ConnectionHandler gh = FakeGrpcHttp2ConnectionHandler.newHandler(); ClientTlsProtocolNegotiator pn = new ClientTlsProtocolNegotiator(clientSslContext, - null, Optional.empty()); + null, Optional.absent()); WriteBufferingAndExceptionHandler clientWbaeh = new WriteBufferingAndExceptionHandler(pn.newHandler(gh)); diff --git a/okhttp/build.gradle b/okhttp/build.gradle index 063e4775de1..6c542feec9c 100644 --- a/okhttp/build.gradle +++ b/okhttp/build.gradle @@ -31,8 +31,16 @@ dependencies { project(':grpc-testing-proto'), libraries.netty.codec.http2, libraries.okhttp - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } project.sourceSets { diff --git a/opentelemetry/build.gradle b/opentelemetry/build.gradle index 00d913c280d..b729f393e4b 100644 --- a/opentelemetry/build.gradle +++ b/opentelemetry/build.gradle @@ -23,8 +23,11 @@ dependencies { annotationProcessor libraries.auto.value - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } tasks.named("jar").configure { diff --git a/protobuf-lite/build.gradle b/protobuf-lite/build.gradle index 11a49d4816d..16b622535e8 100644 --- a/protobuf-lite/build.gradle +++ b/protobuf-lite/build.gradle @@ -17,8 +17,16 @@ dependencies { testImplementation project(':grpc-core') - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } tasks.named("jar").configure { diff --git a/protobuf/build.gradle b/protobuf/build.gradle index c88ae836e0f..c477e41dceb 100644 --- a/protobuf/build.gradle +++ b/protobuf/build.gradle @@ -31,8 +31,16 @@ dependencies { exclude group: 'com.google.protobuf', module: 'protobuf-javalite' } - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } tasks.named("javadoc").configure { diff --git a/rls/build.gradle b/rls/build.gradle index 0629dce64c1..1dbcd91ec3c 100644 --- a/rls/build.gradle +++ b/rls/build.gradle @@ -30,7 +30,11 @@ dependencies { project(':grpc-testing-proto'), testFixtures(project(':grpc-api')), testFixtures(project(':grpc-core')) - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } tasks.named("compileJava").configure { diff --git a/s2a/build.gradle b/s2a/build.gradle index 572c48a0c5b..6ac193938ca 100644 --- a/s2a/build.gradle +++ b/s2a/build.gradle @@ -62,7 +62,11 @@ dependencies { } } - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } configureProtoCompilation() diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java index 7ad9de991cf..0822399aad6 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java @@ -253,7 +253,7 @@ public void onSuccess(SslContext sslContext) { InternalProtocolNegotiators.tls( sslContext, SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR), - Optional.of(new Runnable() { + com.google.common.base.Optional.of(new Runnable() { @Override public void run() { s2aStub.close(); @@ -278,4 +278,4 @@ public void onFailure(Throwable t) { } private S2AProtocolNegotiatorFactory() {} -} \ No newline at end of file +} diff --git a/services/build.gradle b/services/build.gradle index 6daa7b0511d..758f2a5c899 100644 --- a/services/build.gradle +++ b/services/build.gradle @@ -39,7 +39,11 @@ dependencies { testFixtures(project(':grpc-core')), testFixtures(project(':grpc-api')) testCompileOnly libraries.javax.annotation - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } configureProtoCompilation() diff --git a/stub/BUILD.bazel b/stub/BUILD.bazel index 6d06e01f918..572ea681ef3 100644 --- a/stub/BUILD.bazel +++ b/stub/BUILD.bazel @@ -12,6 +12,7 @@ java_library( artifact("com.google.code.findbugs:jsr305"), artifact("com.google.errorprone:error_prone_annotations"), artifact("com.google.guava:guava"), + artifact("org.codehaus.mojo:animal-sniffer-annotations"), ], ) diff --git a/stub/build.gradle b/stub/build.gradle index 867936f3ea3..a9a7cec5a0e 100644 --- a/stub/build.gradle +++ b/stub/build.gradle @@ -22,8 +22,16 @@ dependencies { project(':grpc-inprocess'), project(':grpc-testing'), testFixtures(project(':grpc-api')) - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } tasks.named("javadoc").configure { diff --git a/stub/src/main/java/io/grpc/stub/AbstractStub.java b/stub/src/main/java/io/grpc/stub/AbstractStub.java index 06dd55ff466..7b4bbed34a8 100644 --- a/stub/src/main/java/io/grpc/stub/AbstractStub.java +++ b/stub/src/main/java/io/grpc/stub/AbstractStub.java @@ -33,6 +33,7 @@ import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; /** * Common base type for stub implementations. Stub configuration is immutable; changing the @@ -152,6 +153,7 @@ public final S withDeadlineAfter(long duration, TimeUnit unit) { } @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657") + @IgnoreJRERequirement public final S withDeadlineAfter(Duration duration) { return withDeadlineAfter(convert(duration), TimeUnit.NANOSECONDS); } diff --git a/stub/src/test/java/io/grpc/stub/AbstractStubTest.java b/stub/src/test/java/io/grpc/stub/AbstractStubTest.java index a167c735160..352a2fb7fe2 100644 --- a/stub/src/test/java/io/grpc/stub/AbstractStubTest.java +++ b/stub/src/test/java/io/grpc/stub/AbstractStubTest.java @@ -28,6 +28,7 @@ import io.grpc.stub.AbstractStub.StubFactory; import io.grpc.stub.AbstractStubTest.NoopStub; import java.time.Duration; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -54,6 +55,7 @@ public NoopStub newStub(Channel channel, CallOptions callOptions) { } @Test + @IgnoreJRERequirement public void testDuration() { NoopStub stub = NoopStub.newStub(new StubFactory() { @Override diff --git a/testing-proto/build.gradle b/testing-proto/build.gradle index e6afce468f0..a34392b26d2 100644 --- a/testing-proto/build.gradle +++ b/testing-proto/build.gradle @@ -20,7 +20,11 @@ dependencies { compileOnly libraries.javax.annotation testImplementation libraries.truth testRuntimeOnly libraries.javax.annotation - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } } configureProtoCompilation() diff --git a/testing/build.gradle b/testing/build.gradle index cc83b7ad620..b92e39279c6 100644 --- a/testing/build.gradle +++ b/testing/build.gradle @@ -24,8 +24,16 @@ dependencies { testImplementation project(':grpc-testing-proto'), testFixtures(project(':grpc-core')) - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } tasks.named("javadoc").configure { exclude 'io/grpc/internal/**' } diff --git a/util/build.gradle b/util/build.gradle index 932ca66883e..6fbd6925c00 100644 --- a/util/build.gradle +++ b/util/build.gradle @@ -35,8 +35,16 @@ dependencies { project(':grpc-testing') jmh project(':grpc-testing') - signature libraries.signature.java - signature libraries.signature.android + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } animalsniffer { diff --git a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java index 1f0290e76d7..928592e5534 100644 --- a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java @@ -685,7 +685,11 @@ void updateTrackerConfigs(OutlierDetectionLoadBalancerConfig config) { /** Adds a new tracker for every given address. */ void putNewTrackers(OutlierDetectionLoadBalancerConfig config, Set> endpoints) { - endpoints.forEach(e -> trackerMap.putIfAbsent(e, new EndpointTracker(config))); + for (Set endpoint : endpoints) { + if (!trackerMap.containsKey(endpoint)) { + trackerMap.put(endpoint, new EndpointTracker(config)); + } + } } /** Resets the call counters for all the trackers in the map. */ diff --git a/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java b/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java index 36ef75abeaa..91159d121ad 100644 --- a/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java +++ b/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java @@ -44,6 +44,7 @@ import java.util.logging.LogRecord; import java.util.logging.Logger; import javax.net.ssl.SSLSocket; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -51,6 +52,7 @@ /** Unit tests for {@link AdvancedTlsX509TrustManager}. */ @RunWith(JUnit4.class) +@IgnoreJRERequirement public class AdvancedTlsX509TrustManagerTest { private static final String CA_PEM_FILE = "ca.pem"; diff --git a/xds/build.gradle b/xds/build.gradle index e09b42d06a9..c51fc2819d7 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -81,7 +81,11 @@ dependencies { shadow configurations.implementation.getDependencies().minus([nettyDependency]) shadow project(path: ':grpc-netty-shaded', configuration: 'shadow') - signature libraries.signature.java + signature (libraries.signature.java) { + artifact { + extension = "signature" + } + } testRuntimeOnly libraries.netty.tcnative, libraries.netty.tcnative.classes testRuntimeOnly (libraries.netty.tcnative) { From 7601afc21361c2444f9720a8c5c40db14a1bb659 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 19 Dec 2024 08:19:02 -0800 Subject: [PATCH 116/591] Bump android animalsniffer signature to API 21 This should have been done as part of 2b4f649b --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b9f9c5350e1..4d871239cc2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -97,7 +97,7 @@ protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version. protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } re2j = "com.google.re2j:re2j:1.7" robolectric = "org.robolectric:robolectric:4.13" -signature-android = "net.sf.androidscents.signature:android-api-level-19:4.4.2_r4" +signature-android = "net.sf.androidscents.signature:android-api-level-21:5.0.1_r2" signature-java = "org.codehaus.mojo.signature:java18:1.0" # 11.0.0+ require Java 17+ tomcat-embed-core = "org.apache.tomcat.embed:tomcat-embed-core:10.1.31" From ef7c2d59c1c9b3b74a5bd10d54f205c0a68016b9 Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Thu, 19 Dec 2024 14:46:58 -0800 Subject: [PATCH 117/591] xds: Fix XDS control plane client retry timer backoff duration when connection closes after results are received (#11766) * Fix retry timer backoff duration. * Reset stopwatch when we had results on AdsStream rather than change the delay calculation logic. --- xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java | 1 + xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java | 1 + 2 files changed, 2 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java index 047f8a2e315..9c7d744816a 100644 --- a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java +++ b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java @@ -446,6 +446,7 @@ private void handleRpcStreamClosed(Status status) { // Reset the backoff sequence if had received a response, or backoff sequence // has never been initialized. retryBackoffPolicy = backoffPolicyProvider.get(); + stopwatch.reset(); } // FakeClock in tests isn't thread-safe. Schedule the retry timer before notifying callbacks diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index f1c114673b5..1b0c363d94f 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -3524,6 +3524,7 @@ public void streamClosedAndRetryWithBackoff() { call.verifyRequest(EDS, EDS_RESOURCE, "", "", NODE); // Management server closes the RPC stream with an error. + fakeClock.forwardNanos(1000L); // Make sure retry isn't based on stopwatch 0 call.sendError(Status.UNKNOWN.asException()); verify(ldsResourceWatcher, Mockito.timeout(1000).times(1)) .onError(errorCaptor.capture()); From 0b2d44098fba13320ef03d7d4e4f891a9bf944b1 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 19 Dec 2024 23:32:47 -0800 Subject: [PATCH 118/591] Introduce custom NameResolver.Args (#11669) grpc-binder's upcoming AndroidIntentNameResolver needs to know the target Android user so it can resolve target URIs in the correct place. Unfortunately, Android's built in intent:// URI scheme has no way to specify a user and in fact the android.os.UserHandle object can't reasonably be encoded as a String at all. We solve this problem by extending NameResolver.Args with the same type-safe and domain-specific Key pattern used by CallOptions, Context and CreateSubchannelArgs. New "custom" arguments could apply to all NameResolvers of a certain URI scheme, to all NameResolvers producing a particular type of java.net.SocketAddress, or even to a specific NameResolver subclass. --- .../io/grpc/ForwardingChannelBuilder2.java | 6 + .../java/io/grpc/ManagedChannelBuilder.java | 17 +++ api/src/main/java/io/grpc/NameResolver.java | 125 ++++++++++++++---- .../test/java/io/grpc/NameResolverTest.java | 16 ++- .../io/grpc/internal/ManagedChannelImpl.java | 8 +- .../internal/ManagedChannelImplBuilder.java | 21 +++ .../ManagedChannelImplBuilderTest.java | 10 ++ .../grpc/internal/ManagedChannelImplTest.java | 9 +- 8 files changed, 179 insertions(+), 33 deletions(-) diff --git a/api/src/main/java/io/grpc/ForwardingChannelBuilder2.java b/api/src/main/java/io/grpc/ForwardingChannelBuilder2.java index 7f21a57ec80..78fe730d91a 100644 --- a/api/src/main/java/io/grpc/ForwardingChannelBuilder2.java +++ b/api/src/main/java/io/grpc/ForwardingChannelBuilder2.java @@ -263,6 +263,12 @@ protected T addMetricSink(MetricSink metricSink) { return thisT(); } + @Override + public T setNameResolverArg(NameResolver.Args.Key key, X value) { + delegate().setNameResolverArg(key, value); + return thisT(); + } + /** * Returns the {@link ManagedChannel} built by the delegate by default. Overriding method can * return different value. diff --git a/api/src/main/java/io/grpc/ManagedChannelBuilder.java b/api/src/main/java/io/grpc/ManagedChannelBuilder.java index 6e30d8eae04..df867d09ba1 100644 --- a/api/src/main/java/io/grpc/ManagedChannelBuilder.java +++ b/api/src/main/java/io/grpc/ManagedChannelBuilder.java @@ -633,6 +633,23 @@ protected T addMetricSink(MetricSink metricSink) { throw new UnsupportedOperationException(); } + /** + * Provides a "custom" argument for the {@link NameResolver}, if applicable, replacing any 'value' + * previously provided for 'key'. + * + *

NB: If the selected {@link NameResolver} does not understand 'key', or target URI resolution + * isn't needed at all, your custom argument will be silently ignored. + * + *

See {@link NameResolver.Args#getArg(NameResolver.Args.Key)} for more. + * + * @param key identifies the argument in a type-safe manner + * @param value the argument itself + * @return this + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770") + public T setNameResolverArg(NameResolver.Args.Key key, X value) { + throw new UnsupportedOperationException(); + } /** * Builds a channel using the given parameters. diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java index b35601289a3..e8295365ca8 100644 --- a/api/src/main/java/io/grpc/NameResolver.java +++ b/api/src/main/java/io/grpc/NameResolver.java @@ -28,11 +28,13 @@ import java.lang.annotation.RetentionPolicy; import java.net.URI; import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.ThreadSafe; /** @@ -276,7 +278,12 @@ public Status onResult2(ResolutionResult resolutionResult) { /** * Information that a {@link Factory} uses to create a {@link NameResolver}. * - *

Note this class doesn't override neither {@code equals()} nor {@code hashCode()}. + *

Args applicable to all {@link NameResolver}s are defined here using ordinary setters and + * getters. This container can also hold externally-defined "custom" args that aren't so widely + * useful or that would be inappropriate dependencies for this low level API. See {@link + * Args#getArg} for more. + * + *

Note this class overrides neither {@code equals()} nor {@code hashCode()}. * * @since 1.21.0 */ @@ -291,26 +298,20 @@ public static final class Args { @Nullable private final Executor executor; @Nullable private final String overrideAuthority; @Nullable private final MetricRecorder metricRecorder; + @Nullable private final IdentityHashMap, Object> customArgs; - private Args( - Integer defaultPort, - ProxyDetector proxyDetector, - SynchronizationContext syncContext, - ServiceConfigParser serviceConfigParser, - @Nullable ScheduledExecutorService scheduledExecutorService, - @Nullable ChannelLogger channelLogger, - @Nullable Executor executor, - @Nullable String overrideAuthority, - @Nullable MetricRecorder metricRecorder) { - this.defaultPort = checkNotNull(defaultPort, "defaultPort not set"); - this.proxyDetector = checkNotNull(proxyDetector, "proxyDetector not set"); - this.syncContext = checkNotNull(syncContext, "syncContext not set"); - this.serviceConfigParser = checkNotNull(serviceConfigParser, "serviceConfigParser not set"); - this.scheduledExecutorService = scheduledExecutorService; - this.channelLogger = channelLogger; - this.executor = executor; - this.overrideAuthority = overrideAuthority; - this.metricRecorder = metricRecorder; + private Args(Builder builder) { + this.defaultPort = checkNotNull(builder.defaultPort, "defaultPort not set"); + this.proxyDetector = checkNotNull(builder.proxyDetector, "proxyDetector not set"); + this.syncContext = checkNotNull(builder.syncContext, "syncContext not set"); + this.serviceConfigParser = + checkNotNull(builder.serviceConfigParser, "serviceConfigParser not set"); + this.scheduledExecutorService = builder.scheduledExecutorService; + this.channelLogger = builder.channelLogger; + this.executor = builder.executor; + this.overrideAuthority = builder.overrideAuthority; + this.metricRecorder = builder.metricRecorder; + this.customArgs = cloneCustomArgs(builder.customArgs); } /** @@ -319,6 +320,7 @@ private Args( * * @since 1.21.0 */ + //

TODO: Only meaningful for InetSocketAddress producers. Make this a custom arg? public int getDefaultPort() { return defaultPort; } @@ -371,6 +373,30 @@ public ServiceConfigParser getServiceConfigParser() { return serviceConfigParser; } + /** + * Returns the value of a custom arg named 'key', or {@code null} if it's not set. + * + *

While ordinary {@link Args} should be universally useful and meaningful, custom arguments + * can apply just to resolvers of a certain URI scheme, just to resolvers producing a particular + * type of {@link java.net.SocketAddress}, or even an individual {@link NameResolver} subclass. + * Custom args are identified by an instance of {@link Args.Key} which should be a constant + * defined in a java package and class appropriate for the argument's scope. + * + *

{@link Args} are normally reserved for information in *support* of name resolution, not + * the name to be resolved itself. However, there are rare cases where all or part of the target + * name can't be represented by any standard URI scheme or can't be encoded as a String at all. + * Custom args, in contrast, can hold arbitrary Java types, making them a useful work around in + * these cases. + * + *

Custom args can also be used simply to avoid adding inappropriate deps to the low level + * io.grpc package. + */ + @SuppressWarnings("unchecked") // Cast is safe because all put()s go through the setArg() API. + @Nullable + public T getArg(Key key) { + return customArgs != null ? (T) customArgs.get(key) : null; + } + /** * Returns the {@link ChannelLogger} for the Channel served by this NameResolver. * @@ -424,6 +450,7 @@ public String toString() { .add("proxyDetector", proxyDetector) .add("syncContext", syncContext) .add("serviceConfigParser", serviceConfigParser) + .add("customArgs", customArgs) .add("scheduledExecutorService", scheduledExecutorService) .add("channelLogger", channelLogger) .add("executor", executor) @@ -448,6 +475,7 @@ public Builder toBuilder() { builder.setOffloadExecutor(executor); builder.setOverrideAuthority(overrideAuthority); builder.setMetricRecorder(metricRecorder); + builder.customArgs = cloneCustomArgs(customArgs); return builder; } @@ -475,6 +503,7 @@ public static final class Builder { private Executor executor; private String overrideAuthority; private MetricRecorder metricRecorder; + private IdentityHashMap, Object> customArgs; Builder() { } @@ -561,6 +590,17 @@ public Builder setOverrideAuthority(String authority) { return this; } + /** See {@link Args#getArg(Key)}. */ + public Builder setArg(Key key, T value) { + checkNotNull(key, "key"); + checkNotNull(value, "value"); + if (customArgs == null) { + customArgs = new IdentityHashMap<>(); + } + customArgs.put(key, value); + return this; + } + /** * See {@link Args#getMetricRecorder()}. This is an optional field. */ @@ -575,11 +615,40 @@ public Builder setMetricRecorder(MetricRecorder metricRecorder) { * @since 1.21.0 */ public Args build() { - return - new Args( - defaultPort, proxyDetector, syncContext, serviceConfigParser, - scheduledExecutorService, channelLogger, executor, overrideAuthority, - metricRecorder); + return new Args(this); + } + } + + /** + * Identifies an externally-defined custom argument that can be stored in {@link Args}. + * + *

Uses reference equality so keys should be defined as global constants. + * + * @param type of values that can be stored under this key + */ + @Immutable + @SuppressWarnings("UnusedTypeParameter") + public static final class Key { + private final String debugString; + + private Key(String debugString) { + this.debugString = debugString; + } + + @Override + public String toString() { + return debugString; + } + + /** + * Creates a new instance of {@link Key}. + * + * @param debugString a string used to describe the key, used for debugging. + * @param Key type + * @return a new instance of Key + */ + public static Key create(String debugString) { + return new Key<>(debugString); } } } @@ -877,4 +946,10 @@ public String toString() { } } } + + @Nullable + private static IdentityHashMap, Object> cloneCustomArgs( + @Nullable IdentityHashMap, Object> customArgs) { + return customArgs != null ? new IdentityHashMap<>(customArgs) : null; + } } diff --git a/api/src/test/java/io/grpc/NameResolverTest.java b/api/src/test/java/io/grpc/NameResolverTest.java index 1a7c59f8df5..ae8c080bd5c 100644 --- a/api/src/test/java/io/grpc/NameResolverTest.java +++ b/api/src/test/java/io/grpc/NameResolverTest.java @@ -47,9 +47,13 @@ public class NameResolverTest { private static final List ADDRESSES = Collections.singletonList( new EquivalentAddressGroup(new FakeSocketAddress("fake-address-1"), Attributes.EMPTY)); - private static final Attributes.Key YOLO_KEY = Attributes.Key.create("yolo"); - private static Attributes ATTRIBUTES = Attributes.newBuilder() - .set(YOLO_KEY, "To be, or not to be?").build(); + private static final Attributes.Key YOLO_ATTR_KEY = Attributes.Key.create("yolo"); + private static Attributes ATTRIBUTES = + Attributes.newBuilder().set(YOLO_ATTR_KEY, "To be, or not to be?").build(); + private static final NameResolver.Args.Key FOO_ARG_KEY = + NameResolver.Args.Key.create("foo"); + private static final NameResolver.Args.Key BAR_ARG_KEY = + NameResolver.Args.Key.create("bar"); private static ConfigOrError CONFIG = ConfigOrError.fromConfig("foo"); @Rule @@ -65,6 +69,7 @@ public class NameResolverTest { private final Executor executor = Executors.newSingleThreadExecutor(); private final String overrideAuthority = "grpc.io"; private final MetricRecorder metricRecorder = new MetricRecorder() {}; + private final int customArgValue = 42; @Mock NameResolver.Listener mockListener; @Test @@ -79,6 +84,8 @@ public void args() { assertThat(args.getOffloadExecutor()).isSameInstanceAs(executor); assertThat(args.getOverrideAuthority()).isSameInstanceAs(overrideAuthority); assertThat(args.getMetricRecorder()).isSameInstanceAs(metricRecorder); + assertThat(args.getArg(FOO_ARG_KEY)).isEqualTo(customArgValue); + assertThat(args.getArg(BAR_ARG_KEY)).isNull(); NameResolver.Args args2 = args.toBuilder().build(); assertThat(args2.getDefaultPort()).isEqualTo(defaultPort); @@ -90,6 +97,8 @@ public void args() { assertThat(args2.getOffloadExecutor()).isSameInstanceAs(executor); assertThat(args2.getOverrideAuthority()).isSameInstanceAs(overrideAuthority); assertThat(args.getMetricRecorder()).isSameInstanceAs(metricRecorder); + assertThat(args.getArg(FOO_ARG_KEY)).isEqualTo(customArgValue); + assertThat(args.getArg(BAR_ARG_KEY)).isNull(); assertThat(args2).isNotSameInstanceAs(args); assertThat(args2).isNotEqualTo(args); @@ -106,6 +115,7 @@ private NameResolver.Args createArgs() { .setOffloadExecutor(executor) .setOverrideAuthority(overrideAuthority) .setMetricRecorder(metricRecorder) + .setArg(FOO_ARG_KEY, customArgValue) .build(); } diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 88fbf5e2c62..5c2871a1373 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -591,8 +591,7 @@ ClientStream newSubstream( this.authorityOverride = builder.authorityOverride; this.metricRecorder = new MetricRecorderImpl(builder.metricSinks, MetricInstrumentRegistry.getDefaultRegistry()); - this.nameResolverArgs = - NameResolver.Args.newBuilder() + NameResolver.Args.Builder nameResolverArgsBuilder = NameResolver.Args.newBuilder() .setDefaultPort(builder.getDefaultPort()) .setProxyDetector(proxyDetector) .setSynchronizationContext(syncContext) @@ -601,8 +600,9 @@ ClientStream newSubstream( .setChannelLogger(channelLogger) .setOffloadExecutor(this.offloadExecutorHolder) .setOverrideAuthority(this.authorityOverride) - .setMetricRecorder(this.metricRecorder) - .build(); + .setMetricRecorder(this.metricRecorder); + builder.copyAllNameResolverCustomArgsTo(nameResolverArgsBuilder); + this.nameResolverArgs = nameResolverArgsBuilder.build(); this.nameResolver = getNameResolver( targetUri, authorityOverride, nameResolverProvider, nameResolverArgs); this.balancerRpcExecutorPool = checkNotNull(balancerRpcExecutorPool, "balancerRpcExecutorPool"); diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java index 48a255472e1..20f7e901ddd 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java @@ -55,6 +55,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -159,6 +160,8 @@ public static ManagedChannelBuilder forTarget(String target) { final ChannelCredentials channelCredentials; @Nullable final CallCredentials callCredentials; + @Nullable + IdentityHashMap, Object> nameResolverCustomArgs; @Nullable private final SocketAddress directServerAddress; @@ -613,6 +616,24 @@ private static List checkListEntryTypes(List list) { return Collections.unmodifiableList(parsedList); } + @Override + public ManagedChannelImplBuilder setNameResolverArg(NameResolver.Args.Key key, X value) { + if (nameResolverCustomArgs == null) { + nameResolverCustomArgs = new IdentityHashMap<>(); + } + nameResolverCustomArgs.put(checkNotNull(key, "key"), checkNotNull(value, "value")); + return this; + } + + @SuppressWarnings("unchecked") // This cast is safe because of setNameResolverArg()'s signature. + void copyAllNameResolverCustomArgsTo(NameResolver.Args.Builder dest) { + if (nameResolverCustomArgs != null) { + for (Map.Entry, Object> entry : nameResolverCustomArgs.entrySet()) { + dest.setArg((NameResolver.Args.Key) entry.getKey(), entry.getValue()); + } + } + } + @Override public ManagedChannelImplBuilder disableServiceConfigLookUp() { this.lookUpServiceConfig = false; diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java index ddef7ef546f..cf131a79d87 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java @@ -764,6 +764,16 @@ public void disableNameResolverServiceConfig() { assertThat(builder.lookUpServiceConfig).isFalse(); } + @Test + public void setNameResolverExtArgs() { + assertThat(builder.nameResolverCustomArgs) + .isNull(); + + NameResolver.Args.Key testKey = NameResolver.Args.Key.create("test-key"); + builder.setNameResolverArg(testKey, 42); + assertThat(builder.nameResolverCustomArgs.get(testKey)).isEqualTo(42); + } + @Test public void metricSinks() { MetricSink mocksink = mock(MetricSink.class); diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 2fe82aea972..98900cecf2b 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -205,6 +205,8 @@ public class ManagedChannelImplTest { .setServiceConfigParser(mock(NameResolver.ServiceConfigParser.class)) .setScheduledExecutorService(new FakeClock().getScheduledExecutorService()) .build(); + private static final NameResolver.Args.Key TEST_RESOLVER_CUSTOM_ARG_KEY = + NameResolver.Args.Key.create("test-key"); private URI expectedUri; private final SocketAddress socketAddress = @@ -4271,13 +4273,18 @@ public String getDefaultScheme() { return "fake"; } }; - channelBuilder.nameResolverFactory(factory).proxyDetector(neverProxy); + channelBuilder + .nameResolverFactory(factory) + .proxyDetector(neverProxy) + .setNameResolverArg(TEST_RESOLVER_CUSTOM_ARG_KEY, "test-value"); + createChannel(); NameResolver.Args args = capturedArgs.get(); assertThat(args).isNotNull(); assertThat(args.getDefaultPort()).isEqualTo(DEFAULT_PORT); assertThat(args.getProxyDetector()).isSameInstanceAs(neverProxy); + assertThat(args.getArg(TEST_RESOLVER_CUSTOM_ARG_KEY)).isEqualTo("test-value"); verify(offloadExecutor, never()).execute(any(Runnable.class)); args.getOffloadExecutor() From 7b29111cd0a4bd605254b0b93bb45da590e1e245 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Sat, 21 Dec 2024 00:45:54 +0530 Subject: [PATCH 119/591] Update README etc to reference 1.69.0 --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 97b2bd6d5f9..c6a8f3bdd8a 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.68.1/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.68.1/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.69.0/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.69.0/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.68.1 + 1.69.0 runtime io.grpc grpc-protobuf - 1.68.1 + 1.69.0 io.grpc grpc-stub - 1.68.1 + 1.69.0 org.apache.tomcat @@ -79,18 +79,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.68.1' -implementation 'io.grpc:grpc-protobuf:1.68.1' -implementation 'io.grpc:grpc-stub:1.68.1' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.69.0' +implementation 'io.grpc:grpc-protobuf:1.69.0' +implementation 'io.grpc:grpc-stub:1.69.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.68.1' -implementation 'io.grpc:grpc-protobuf-lite:1.68.1' -implementation 'io.grpc:grpc-stub:1.68.1' +implementation 'io.grpc:grpc-okhttp:1.69.0' +implementation 'io.grpc:grpc-protobuf-lite:1.69.0' +implementation 'io.grpc:grpc-stub:1.69.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` @@ -99,7 +99,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.68.1 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.69.0 Development snapshots are available in [Sonatypes's snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/). @@ -131,7 +131,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.68.1:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.69.0:exe:${os.detected.classifier} @@ -161,7 +161,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.68.1' + artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0' } } generateProtoTasks { @@ -194,7 +194,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.68.1' + artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0' } } generateProtoTasks { From ea8c31c305eac5a6ac9a09de5ea2edb9ed719a31 Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Fri, 20 Dec 2024 16:16:17 -0800 Subject: [PATCH 120/591] Bidi Blocking Stub (#10318) --- .../alts/internal/HandshakerServiceGrpc.java | 49 ++ .../LoadBalancerStatsServiceGrpc.java | 55 ++ .../integration/MetricsServiceGrpc.java | 55 ++ .../integration/ReconnectServiceGrpc.java | 49 ++ .../testing/integration/TestServiceGrpc.java | 134 +++++ .../integration/UnimplementedServiceGrpc.java | 46 ++ .../XdsUpdateClientConfigureServiceGrpc.java | 45 ++ .../XdsUpdateHealthServiceGrpc.java | 49 ++ .../LoadBalancerStatsServiceGrpc.java | 55 ++ .../integration/MetricsServiceGrpc.java | 55 ++ .../integration/ReconnectServiceGrpc.java | 49 ++ .../testing/integration/TestServiceGrpc.java | 134 +++++ .../integration/UnimplementedServiceGrpc.java | 46 ++ .../XdsUpdateClientConfigureServiceGrpc.java | 45 ++ .../XdsUpdateHealthServiceGrpc.java | 49 ++ .../proto/BenchmarkServiceGrpc.java | 96 ++++ .../proto/ReportQpsScenarioServiceGrpc.java | 42 ++ .../benchmarks/proto/WorkerServiceGrpc.java | 86 +++ .../src/java_plugin/cpp/java_generator.cpp | 117 +++- .../golden/TestDeprecatedService.java.txt | 47 ++ compiler/src/test/golden/TestService.java.txt | 134 +++++ .../golden/TestDeprecatedService.java.txt | 47 ++ .../src/testLite/golden/TestService.java.txt | 134 +++++ examples/build.gradle | 1 + .../manualflowcontrol/BidiBlockingClient.java | 286 ++++++++++ .../ManualFlowControlServer.java | 4 +- examples/src/main/proto/hello_streaming.proto | 1 + .../grpc/io/grpc/lb/v1/LoadBalancerGrpc.java | 44 ++ .../LoadBalancerStatsServiceGrpc.java | 55 ++ .../integration/MetricsServiceGrpc.java | 55 ++ .../integration/ReconnectServiceGrpc.java | 49 ++ .../testing/integration/TestServiceGrpc.java | 134 +++++ .../integration/UnimplementedServiceGrpc.java | 46 ++ .../XdsUpdateClientConfigureServiceGrpc.java | 45 ++ .../XdsUpdateHealthServiceGrpc.java | 49 ++ .../io/istio/test/EchoTestServiceGrpc.java | 46 ++ .../lookup/v1/RouteLookupServiceGrpc.java | 42 ++ .../internal/handshaker/S2AServiceGrpc.java | 45 ++ .../io/grpc/channelz/v1/ChannelzGrpc.java | 107 ++++ .../grpc/io/grpc/health/v1/HealthGrpc.java | 67 +++ .../reflection/v1/ServerReflectionGrpc.java | 45 ++ .../v1alpha/ServerReflectionGrpc.java | 45 ++ .../testing/AnotherDynamicServiceGrpc.java | 45 ++ .../AnotherReflectableServiceGrpc.java | 39 ++ .../testing/DynamicServiceGrpc.java | 45 ++ .../testing/ReflectableServiceGrpc.java | 39 ++ .../java/io/grpc/stub/BlockingClientCall.java | 352 ++++++++++++ .../main/java/io/grpc/stub/ClientCalls.java | 230 +++++++- .../io/grpc/stub/BlockingClientCallTest.java | 520 ++++++++++++++++++ .../java/io/grpc/stub/ClientCallsTest.java | 2 +- .../testing/protobuf/SimpleServiceGrpc.java | 81 +++ .../service/orca/v3/OpenRcaServiceGrpc.java | 51 ++ .../v3/AggregatedDiscoveryServiceGrpc.java | 61 ++ .../v3/LoadReportingServiceGrpc.java | 70 +++ .../v3/RateLimitQuotaServiceGrpc.java | 48 ++ .../v3/ClientStatusDiscoveryServiceGrpc.java | 53 ++ 56 files changed, 4390 insertions(+), 30 deletions(-) create mode 100644 examples/src/main/java/io/grpc/examples/manualflowcontrol/BidiBlockingClient.java create mode 100644 stub/src/main/java/io/grpc/stub/BlockingClientCall.java create mode 100644 stub/src/test/java/io/grpc/stub/BlockingClientCallTest.java diff --git a/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java b/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java index 2caba4a0544..bfb98ea3e68 100644 --- a/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java +++ b/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java @@ -60,6 +60,21 @@ public HandshakerServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOption return HandshakerServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static HandshakerServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public HandshakerServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new HandshakerServiceBlockingV2Stub(channel, callOptions); + } + }; + return HandshakerServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -157,6 +172,40 @@ public io.grpc.stub.StreamObserver doHandsh /** * A stub to allow clients to do synchronous rpc calls to service HandshakerService. */ + public static final class HandshakerServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private HandshakerServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected HandshakerServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new HandshakerServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Handshaker service accepts a stream of handshaker request, returning a
+     * stream of handshaker response. Client is expected to send exactly one
+     * message with either client_start or server_start followed by one or more
+     * messages with next. Each time client sends a request, the handshaker
+     * service expects to respond. Client does not have to wait for service's
+     * response before sending next request.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + doHandshake() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getDoHandshakeMethod(), getCallOptions()); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service HandshakerService. + */ public static final class HandshakerServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private HandshakerServiceBlockingStub( diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java index e030fde13e3..fe8c30ba3c4 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java @@ -92,6 +92,21 @@ public LoadBalancerStatsServiceStub newStub(io.grpc.Channel channel, io.grpc.Cal return LoadBalancerStatsServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static LoadBalancerStatsServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public LoadBalancerStatsServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new LoadBalancerStatsServiceBlockingV2Stub(channel, callOptions); + } + }; + return LoadBalancerStatsServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -212,6 +227,46 @@ public void getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadB * A service used to obtain stats for verifying LB behavior. * */ + public static final class LoadBalancerStatsServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private LoadBalancerStatsServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected LoadBalancerStatsServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new LoadBalancerStatsServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Gets the backend distribution for RPCs sent by a test client.
+     * 
+ */ + public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetClientStatsMethod(), getCallOptions(), request); + } + + /** + *
+     * Gets the accumulated stats for RPCs sent by a test client.
+     * 
+ */ + public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetClientAccumulatedStatsMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service LoadBalancerStatsService. + *
+   * A service used to obtain stats for verifying LB behavior.
+   * 
+ */ public static final class LoadBalancerStatsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private LoadBalancerStatsServiceBlockingStub( diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java index e8726d5adc4..57a7db1ec89 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java @@ -89,6 +89,21 @@ public MetricsServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions c return MetricsServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static MetricsServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public MetricsServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new MetricsServiceBlockingV2Stub(channel, callOptions); + } + }; + return MetricsServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -199,6 +214,46 @@ public void getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request, /** * A stub to allow clients to do synchronous rpc calls to service MetricsService. */ + public static final class MetricsServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private MetricsServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected MetricsServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new MetricsServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Returns the values of all the gauges that are currently being maintained by
+     * the service
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + getAllGauges(io.grpc.testing.integration.Metrics.EmptyMessage request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getGetAllGaugesMethod(), getCallOptions(), request); + } + + /** + *
+     * Returns the value of one gauge
+     * 
+ */ + public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetGaugeMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service MetricsService. + */ public static final class MetricsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private MetricsServiceBlockingStub( diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java index 8ede6407cd0..79f0bdf0fab 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java @@ -92,6 +92,21 @@ public ReconnectServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions return ReconnectServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static ReconnectServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public ReconnectServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ReconnectServiceBlockingV2Stub(channel, callOptions); + } + }; + return ReconnectServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -200,6 +215,40 @@ public void stop(io.grpc.testing.integration.EmptyProtos.Empty request, * A service used to control reconnect server. * */ + public static final class ReconnectServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private ReconnectServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected ReconnectServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ReconnectServiceBlockingV2Stub(channel, callOptions); + } + + /** + */ + public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getStartMethod(), getCallOptions(), request); + } + + /** + */ + public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getStopMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service ReconnectService. + *
+   * A service used to control reconnect server.
+   * 
+ */ public static final class ReconnectServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private ReconnectServiceBlockingStub( diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java index 01e2678a12f..1086974d823 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java @@ -273,6 +273,21 @@ public TestServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions call return TestServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static TestServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public TestServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestServiceBlockingV2Stub(channel, callOptions); + } + }; + return TestServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -543,6 +558,125 @@ public void unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty requ * performance with various types of payload. * */ + public static final class TestServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private TestServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected TestServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * One empty request followed by one empty response.
+     * 
+ */ + public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getEmptyCallMethod(), getCallOptions(), request); + } + + /** + *
+     * One request followed by one response.
+     * 
+ */ + public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+     * One request followed by one response. Response has cache control
+     * headers set such that a caching HTTP proxy (such as GFE) can
+     * satisfy subsequent requests.
+     * 
+ */ + public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getCacheableUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+     * One request followed by a sequence of responses (streamed download).
+     * The server returns the payload with client desired type and sizes.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingOutputCall(io.grpc.testing.integration.Messages.StreamingOutputCallRequest request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getStreamingOutputCallMethod(), getCallOptions(), request); + } + + /** + *
+     * A sequence of requests followed by one response (streamed upload).
+     * The server returns the aggregated size of client payload as the result.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingInputCall() { + return io.grpc.stub.ClientCalls.blockingClientStreamingCall( + getChannel(), getStreamingInputCallMethod(), getCallOptions()); + } + + /** + *
+     * A sequence of requests with each request served by the server immediately.
+     * As one request could lead to multiple responses, this interface
+     * demonstrates the idea of full duplexing.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + fullDuplexCall() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getFullDuplexCallMethod(), getCallOptions()); + } + + /** + *
+     * A sequence of requests followed by a sequence of responses.
+     * The server buffers all the client requests and then serves them in order. A
+     * stream of responses are returned to the client when the server starts with
+     * first request.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + halfDuplexCall() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getHalfDuplexCallMethod(), getCallOptions()); + } + + /** + *
+     * The test server will not implement this method. It will be used
+     * to test the behavior when clients call unimplemented methods.
+     * 
+ */ + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service TestService. + *
+   * A simple service to test the various types of RPCs and experiment with
+   * performance with various types of payload.
+   * 
+ */ public static final class TestServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private TestServiceBlockingStub( diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java index 743d68c3828..be3d1eab13a 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java @@ -63,6 +63,21 @@ public UnimplementedServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOpt return UnimplementedServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static UnimplementedServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public UnimplementedServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new UnimplementedServiceBlockingV2Stub(channel, callOptions); + } + }; + return UnimplementedServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -166,6 +181,37 @@ public void unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty requ * that case. * */ + public static final class UnimplementedServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private UnimplementedServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected UnimplementedServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new UnimplementedServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * A call that no server should implement
+     * 
+ */ + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service UnimplementedService. + *
+   * A simple service NOT implemented at servers so clients can test for
+   * that case.
+   * 
+ */ public static final class UnimplementedServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private UnimplementedServiceBlockingStub( diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java index 61cfc19d29b..f72a09192f0 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java @@ -62,6 +62,21 @@ public XdsUpdateClientConfigureServiceStub newStub(io.grpc.Channel channel, io.g return XdsUpdateClientConfigureServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static XdsUpdateClientConfigureServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public XdsUpdateClientConfigureServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new XdsUpdateClientConfigureServiceBlockingV2Stub(channel, callOptions); + } + }; + return XdsUpdateClientConfigureServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -161,6 +176,36 @@ public void configure(io.grpc.testing.integration.Messages.ClientConfigureReques * A service to dynamically update the configuration of an xDS test client. * */ + public static final class XdsUpdateClientConfigureServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private XdsUpdateClientConfigureServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected XdsUpdateClientConfigureServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new XdsUpdateClientConfigureServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Update the tes client's configuration.
+     * 
+ */ + public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getConfigureMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service XdsUpdateClientConfigureService. + *
+   * A service to dynamically update the configuration of an xDS test client.
+   * 
+ */ public static final class XdsUpdateClientConfigureServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private XdsUpdateClientConfigureServiceBlockingStub( diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java index 6ba9419dedf..3258354cc20 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java @@ -92,6 +92,21 @@ public XdsUpdateHealthServiceStub newStub(io.grpc.Channel channel, io.grpc.CallO return XdsUpdateHealthServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static XdsUpdateHealthServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public XdsUpdateHealthServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new XdsUpdateHealthServiceBlockingV2Stub(channel, callOptions); + } + }; + return XdsUpdateHealthServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -200,6 +215,40 @@ public void setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request, * A service to remotely control health status of an xDS test server. * */ + public static final class XdsUpdateHealthServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private XdsUpdateHealthServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected XdsUpdateHealthServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new XdsUpdateHealthServiceBlockingV2Stub(channel, callOptions); + } + + /** + */ + public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getSetServingMethod(), getCallOptions(), request); + } + + /** + */ + public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getSetNotServingMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service XdsUpdateHealthService. + *
+   * A service to remotely control health status of an xDS test server.
+   * 
+ */ public static final class XdsUpdateHealthServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private XdsUpdateHealthServiceBlockingStub( diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java index e030fde13e3..fe8c30ba3c4 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java @@ -92,6 +92,21 @@ public LoadBalancerStatsServiceStub newStub(io.grpc.Channel channel, io.grpc.Cal return LoadBalancerStatsServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static LoadBalancerStatsServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public LoadBalancerStatsServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new LoadBalancerStatsServiceBlockingV2Stub(channel, callOptions); + } + }; + return LoadBalancerStatsServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -212,6 +227,46 @@ public void getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadB * A service used to obtain stats for verifying LB behavior. * */ + public static final class LoadBalancerStatsServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private LoadBalancerStatsServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected LoadBalancerStatsServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new LoadBalancerStatsServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Gets the backend distribution for RPCs sent by a test client.
+     * 
+ */ + public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetClientStatsMethod(), getCallOptions(), request); + } + + /** + *
+     * Gets the accumulated stats for RPCs sent by a test client.
+     * 
+ */ + public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetClientAccumulatedStatsMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service LoadBalancerStatsService. + *
+   * A service used to obtain stats for verifying LB behavior.
+   * 
+ */ public static final class LoadBalancerStatsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private LoadBalancerStatsServiceBlockingStub( diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java index e8726d5adc4..57a7db1ec89 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java @@ -89,6 +89,21 @@ public MetricsServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions c return MetricsServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static MetricsServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public MetricsServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new MetricsServiceBlockingV2Stub(channel, callOptions); + } + }; + return MetricsServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -199,6 +214,46 @@ public void getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request, /** * A stub to allow clients to do synchronous rpc calls to service MetricsService. */ + public static final class MetricsServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private MetricsServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected MetricsServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new MetricsServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Returns the values of all the gauges that are currently being maintained by
+     * the service
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + getAllGauges(io.grpc.testing.integration.Metrics.EmptyMessage request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getGetAllGaugesMethod(), getCallOptions(), request); + } + + /** + *
+     * Returns the value of one gauge
+     * 
+ */ + public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetGaugeMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service MetricsService. + */ public static final class MetricsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private MetricsServiceBlockingStub( diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java index 8ede6407cd0..79f0bdf0fab 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java @@ -92,6 +92,21 @@ public ReconnectServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions return ReconnectServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static ReconnectServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public ReconnectServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ReconnectServiceBlockingV2Stub(channel, callOptions); + } + }; + return ReconnectServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -200,6 +215,40 @@ public void stop(io.grpc.testing.integration.EmptyProtos.Empty request, * A service used to control reconnect server. * */ + public static final class ReconnectServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private ReconnectServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected ReconnectServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ReconnectServiceBlockingV2Stub(channel, callOptions); + } + + /** + */ + public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getStartMethod(), getCallOptions(), request); + } + + /** + */ + public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getStopMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service ReconnectService. + *
+   * A service used to control reconnect server.
+   * 
+ */ public static final class ReconnectServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private ReconnectServiceBlockingStub( diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java index 01e2678a12f..1086974d823 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java @@ -273,6 +273,21 @@ public TestServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions call return TestServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static TestServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public TestServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestServiceBlockingV2Stub(channel, callOptions); + } + }; + return TestServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -543,6 +558,125 @@ public void unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty requ * performance with various types of payload. * */ + public static final class TestServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private TestServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected TestServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * One empty request followed by one empty response.
+     * 
+ */ + public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getEmptyCallMethod(), getCallOptions(), request); + } + + /** + *
+     * One request followed by one response.
+     * 
+ */ + public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+     * One request followed by one response. Response has cache control
+     * headers set such that a caching HTTP proxy (such as GFE) can
+     * satisfy subsequent requests.
+     * 
+ */ + public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getCacheableUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+     * One request followed by a sequence of responses (streamed download).
+     * The server returns the payload with client desired type and sizes.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingOutputCall(io.grpc.testing.integration.Messages.StreamingOutputCallRequest request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getStreamingOutputCallMethod(), getCallOptions(), request); + } + + /** + *
+     * A sequence of requests followed by one response (streamed upload).
+     * The server returns the aggregated size of client payload as the result.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingInputCall() { + return io.grpc.stub.ClientCalls.blockingClientStreamingCall( + getChannel(), getStreamingInputCallMethod(), getCallOptions()); + } + + /** + *
+     * A sequence of requests with each request served by the server immediately.
+     * As one request could lead to multiple responses, this interface
+     * demonstrates the idea of full duplexing.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + fullDuplexCall() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getFullDuplexCallMethod(), getCallOptions()); + } + + /** + *
+     * A sequence of requests followed by a sequence of responses.
+     * The server buffers all the client requests and then serves them in order. A
+     * stream of responses are returned to the client when the server starts with
+     * first request.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + halfDuplexCall() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getHalfDuplexCallMethod(), getCallOptions()); + } + + /** + *
+     * The test server will not implement this method. It will be used
+     * to test the behavior when clients call unimplemented methods.
+     * 
+ */ + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service TestService. + *
+   * A simple service to test the various types of RPCs and experiment with
+   * performance with various types of payload.
+   * 
+ */ public static final class TestServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private TestServiceBlockingStub( diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java index 743d68c3828..be3d1eab13a 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java @@ -63,6 +63,21 @@ public UnimplementedServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOpt return UnimplementedServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static UnimplementedServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public UnimplementedServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new UnimplementedServiceBlockingV2Stub(channel, callOptions); + } + }; + return UnimplementedServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -166,6 +181,37 @@ public void unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty requ * that case. * */ + public static final class UnimplementedServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private UnimplementedServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected UnimplementedServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new UnimplementedServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * A call that no server should implement
+     * 
+ */ + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service UnimplementedService. + *
+   * A simple service NOT implemented at servers so clients can test for
+   * that case.
+   * 
+ */ public static final class UnimplementedServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private UnimplementedServiceBlockingStub( diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java index 61cfc19d29b..f72a09192f0 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java @@ -62,6 +62,21 @@ public XdsUpdateClientConfigureServiceStub newStub(io.grpc.Channel channel, io.g return XdsUpdateClientConfigureServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static XdsUpdateClientConfigureServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public XdsUpdateClientConfigureServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new XdsUpdateClientConfigureServiceBlockingV2Stub(channel, callOptions); + } + }; + return XdsUpdateClientConfigureServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -161,6 +176,36 @@ public void configure(io.grpc.testing.integration.Messages.ClientConfigureReques * A service to dynamically update the configuration of an xDS test client. * */ + public static final class XdsUpdateClientConfigureServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private XdsUpdateClientConfigureServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected XdsUpdateClientConfigureServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new XdsUpdateClientConfigureServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Update the tes client's configuration.
+     * 
+ */ + public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getConfigureMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service XdsUpdateClientConfigureService. + *
+   * A service to dynamically update the configuration of an xDS test client.
+   * 
+ */ public static final class XdsUpdateClientConfigureServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private XdsUpdateClientConfigureServiceBlockingStub( diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java index 6ba9419dedf..3258354cc20 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java @@ -92,6 +92,21 @@ public XdsUpdateHealthServiceStub newStub(io.grpc.Channel channel, io.grpc.CallO return XdsUpdateHealthServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static XdsUpdateHealthServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public XdsUpdateHealthServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new XdsUpdateHealthServiceBlockingV2Stub(channel, callOptions); + } + }; + return XdsUpdateHealthServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -200,6 +215,40 @@ public void setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request, * A service to remotely control health status of an xDS test server. * */ + public static final class XdsUpdateHealthServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private XdsUpdateHealthServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected XdsUpdateHealthServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new XdsUpdateHealthServiceBlockingV2Stub(channel, callOptions); + } + + /** + */ + public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getSetServingMethod(), getCallOptions(), request); + } + + /** + */ + public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getSetNotServingMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service XdsUpdateHealthService. + *
+   * A service to remotely control health status of an xDS test server.
+   * 
+ */ public static final class XdsUpdateHealthServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private XdsUpdateHealthServiceBlockingStub( diff --git a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java index e62c2274ee9..9422ed7b1fa 100644 --- a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java +++ b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java @@ -184,6 +184,21 @@ public BenchmarkServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions return BenchmarkServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static BenchmarkServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public BenchmarkServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new BenchmarkServiceBlockingV2Stub(channel, callOptions); + } + }; + return BenchmarkServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -367,6 +382,87 @@ public io.grpc.stub.StreamObserver { + private BenchmarkServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected BenchmarkServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new BenchmarkServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * One request followed by one response.
+     * The server returns the client payload as-is.
+     * 
+ */ + public io.grpc.benchmarks.proto.Messages.SimpleResponse unaryCall(io.grpc.benchmarks.proto.Messages.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+     * Repeated sequence of one request followed by one response.
+     * Should be called streaming ping-pong
+     * The server returns the client payload as-is on each response
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingCall() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getStreamingCallMethod(), getCallOptions()); + } + + /** + *
+     * Single-sided unbounded streaming from client to server
+     * The server returns the client payload as-is once the client does WritesDone
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingFromClient() { + return io.grpc.stub.ClientCalls.blockingClientStreamingCall( + getChannel(), getStreamingFromClientMethod(), getCallOptions()); + } + + /** + *
+     * Single-sided unbounded streaming from server to client
+     * The server repeatedly returns the client payload as-is
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingFromServer(io.grpc.benchmarks.proto.Messages.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getStreamingFromServerMethod(), getCallOptions(), request); + } + + /** + *
+     * Two-sided unbounded streaming between server to client
+     * Both sides send the content of their own choice to the other
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingBothWays() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getStreamingBothWaysMethod(), getCallOptions()); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service BenchmarkService. + */ public static final class BenchmarkServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private BenchmarkServiceBlockingStub( diff --git a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java index b24c3813c19..a7f2fdd3b5e 100644 --- a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java +++ b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java @@ -60,6 +60,21 @@ public ReportQpsScenarioServiceStub newStub(io.grpc.Channel channel, io.grpc.Cal return ReportQpsScenarioServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static ReportQpsScenarioServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public ReportQpsScenarioServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ReportQpsScenarioServiceBlockingV2Stub(channel, callOptions); + } + }; + return ReportQpsScenarioServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -147,6 +162,33 @@ public void reportScenario(io.grpc.benchmarks.proto.Control.ScenarioResult reque /** * A stub to allow clients to do synchronous rpc calls to service ReportQpsScenarioService. */ + public static final class ReportQpsScenarioServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private ReportQpsScenarioServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected ReportQpsScenarioServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ReportQpsScenarioServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Report results of a QPS test benchmark scenario.
+     * 
+ */ + public io.grpc.benchmarks.proto.Control.Void reportScenario(io.grpc.benchmarks.proto.Control.ScenarioResult request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getReportScenarioMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service ReportQpsScenarioService. + */ public static final class ReportQpsScenarioServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private ReportQpsScenarioServiceBlockingStub( diff --git a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java index 0ee6797c8e3..bf6c115be35 100644 --- a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java +++ b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java @@ -153,6 +153,21 @@ public WorkerServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions ca return WorkerServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static WorkerServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public WorkerServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new WorkerServiceBlockingV2Stub(channel, callOptions); + } + }; + return WorkerServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -323,6 +338,77 @@ public void quitWorker(io.grpc.benchmarks.proto.Control.Void request, /** * A stub to allow clients to do synchronous rpc calls to service WorkerService. */ + public static final class WorkerServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private WorkerServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected WorkerServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new WorkerServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Start server with specified workload.
+     * First request sent specifies the ServerConfig followed by ServerStatus
+     * response. After that, a "Mark" can be sent anytime to request the latest
+     * stats. Closing the stream will initiate shutdown of the test server
+     * and once the shutdown has finished, the OK status is sent to terminate
+     * this RPC.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + runServer() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getRunServerMethod(), getCallOptions()); + } + + /** + *
+     * Start client with specified workload.
+     * First request sent specifies the ClientConfig followed by ClientStatus
+     * response. After that, a "Mark" can be sent anytime to request the latest
+     * stats. Closing the stream will initiate shutdown of the test client
+     * and once the shutdown has finished, the OK status is sent to terminate
+     * this RPC.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + runClient() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getRunClientMethod(), getCallOptions()); + } + + /** + *
+     * Just return the core count - unary call
+     * 
+ */ + public io.grpc.benchmarks.proto.Control.CoreResponse coreCount(io.grpc.benchmarks.proto.Control.CoreRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getCoreCountMethod(), getCallOptions(), request); + } + + /** + *
+     * Quit this worker
+     * 
+ */ + public io.grpc.benchmarks.proto.Control.Void quitWorker(io.grpc.benchmarks.proto.Control.Void request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getQuitWorkerMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service WorkerService. + */ public static final class WorkerServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private WorkerServiceBlockingStub( diff --git a/compiler/src/java_plugin/cpp/java_generator.cpp b/compiler/src/java_plugin/cpp/java_generator.cpp index 8693fad1b66..5d12beb87c6 100644 --- a/compiler/src/java_plugin/cpp/java_generator.cpp +++ b/compiler/src/java_plugin/cpp/java_generator.cpp @@ -355,13 +355,15 @@ enum StubType { BLOCKING_CLIENT_IMPL = 5, FUTURE_CLIENT_IMPL = 6, ABSTRACT_CLASS = 7, - NONE = 8, + BLOCKING_V2_CLIENT_IMPL = 8, + NONE = 999, }; enum CallType { ASYNC_CALL = 0, BLOCKING_CALL = 1, - FUTURE_CALL = 2 + FUTURE_CALL = 2, + BLOCKING_V2_CALL = 3, }; // TODO(nmittler): Remove once protobuf includes javadoc methods in distribution. @@ -410,6 +412,9 @@ static void GrpcWriteServiceDocComment(Printer* printer, printer->Print(vars, " * A stub to allow clients to do asynchronous rpc calls to service $service$.\n"); break; case BLOCKING_CLIENT_IMPL: + printer->Print(vars, " * A stub to allow clients to do llimited synchronous rpc calls to service $service$.\n"); + break; + case BLOCKING_V2_CLIENT_IMPL: printer->Print(vars, " * A stub to allow clients to do synchronous rpc calls to service $service$.\n"); break; case FUTURE_CLIENT_IMPL: @@ -555,6 +560,9 @@ static void PrintStubFactory( case BLOCKING_CLIENT_IMPL: stub_type_name = "Blocking"; break; + case BLOCKING_V2_CLIENT_IMPL: + stub_type_name = "BlockingV2"; + break; default: GRPC_CODEGEN_FAIL << "Cannot generate StubFactory for StubType: " << type; } @@ -597,6 +605,11 @@ static void PrintStub( stub_name += "BlockingStub"; stub_base_class_name = "AbstractBlockingStub"; break; + case BLOCKING_V2_CLIENT_IMPL: + call_type = BLOCKING_V2_CALL; + stub_name += "BlockingV2Stub"; + stub_base_class_name = "AbstractBlockingStub"; + break; case FUTURE_CLIENT_IMPL: call_type = FUTURE_CALL; stub_name += "FutureStub"; @@ -679,13 +692,17 @@ static void PrintStub( // Method signature p->Print("\n"); - // TODO(nmittler): Replace with WriteMethodDocComment once included by the protobuf distro. GrpcWriteMethodDocComment(p, method); if (method->options().deprecated()) { p->Print(*vars, "@$Deprecated$\n"); } + if ((call_type == BLOCKING_CALL && client_streaming && server_streaming) + || (call_type == BLOCKING_V2_CALL && (client_streaming || server_streaming))) { + p->Print(*vars, "@io.grpc.ExperimentalApi(\"https://github.com/grpc/grpc-java/issues/10918\")\n"); + } + if (!interface) { p->Print("public "); } else { @@ -695,7 +712,12 @@ static void PrintStub( case BLOCKING_CALL: GRPC_CODEGEN_CHECK(!client_streaming) << "Blocking client interface with client streaming is unavailable"; - if (server_streaming) { + if (client_streaming && server_streaming) { + p->Print( + *vars, + "$BlockingClientCall$<$input_type$, $output_type$>\n" + " $lower_method_name$()"); + } else if (server_streaming) { // Server streaming p->Print( *vars, @@ -708,6 +730,25 @@ static void PrintStub( "$output_type$ $lower_method_name$($input_type$ request)"); } break; + case BLOCKING_V2_CALL: + if (client_streaming) { // Both Bidi and Client Streaming + p->Print( + *vars, + "$BlockingClientCall$<$input_type$, $output_type$>\n" + " $lower_method_name$()"); + } else if (server_streaming) { + // Server streaming + p->Print( + *vars, + "$BlockingClientCall$\n" + " $lower_method_name$($input_type$ request)"); + } else { + // Simple RPC + p->Print( + *vars, + "$output_type$ $lower_method_name$($input_type$ request)"); + } + break; case ASYNC_CALL: if (client_streaming) { // Bidirectional streaming or client streaming @@ -753,21 +794,46 @@ static void PrintStub( "$method_method_name$(), responseObserver);\n"); } } else if (!interface) { - switch (call_type) { + switch (call_type) { case BLOCKING_CALL: GRPC_CODEGEN_CHECK(!client_streaming) - << "Blocking client streaming interface is not available"; - if (server_streaming) { - (*vars)["calls_method"] = "io.grpc.stub.ClientCalls.blockingServerStreamingCall"; - (*vars)["params"] = "request"; - } else { - (*vars)["calls_method"] = "io.grpc.stub.ClientCalls.blockingUnaryCall"; - (*vars)["params"] = "request"; + << "Blocking client and bidi streaming interface are not available"; + if (server_streaming) { + (*vars)["calls_method"] = "io.grpc.stub.ClientCalls.blockingServerStreamingCall"; + (*vars)["params"] = "request"; + } else { + (*vars)["calls_method"] = "io.grpc.stub.ClientCalls.blockingUnaryCall"; + (*vars)["params"] = "request"; + } + p->Print( + *vars, + "return $calls_method$(\n" + " getChannel(), $method_method_name$(), getCallOptions(), $params$);\n"); + break; + case BLOCKING_V2_CALL: + if (client_streaming) { // client and bidi streaming + if (server_streaming) { + (*vars)["calls_method"] = "io.grpc.stub.ClientCalls.blockingBidiStreamingCall"; + } else { + (*vars)["calls_method"] = "io.grpc.stub.ClientCalls.blockingClientStreamingCall"; + } + p->Print( + *vars, + "return $calls_method$(\n" + " getChannel(), $method_method_name$(), getCallOptions());\n"); + } else { // server streaming and unary + (*vars)["params"] = "request"; + if (server_streaming) { + (*vars)["calls_method"] = "io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall"; + } else { + (*vars)["calls_method"] = "io.grpc.stub.ClientCalls.blockingUnaryCall"; + } + + p->Print( + *vars, + "return $calls_method$(\n" + " getChannel(), $method_method_name$(), getCallOptions(), $params$);\n"); } - p->Print( - *vars, - "return $calls_method$(\n" - " getChannel(), $method_method_name$(), getCallOptions(), $params$);\n"); break; case ASYNC_CALL: if (server_streaming) { @@ -804,7 +870,7 @@ static void PrintStub( "return $calls_method$(\n" " getChannel().newCall($method_method_name$(), getCallOptions()), request);\n"); break; - } + } } else { GRPC_CODEGEN_FAIL << "Do not create Stub interfaces"; } @@ -1173,6 +1239,21 @@ static void PrintService(const ServiceDescriptor* service, p->Outdent(); p->Print("}\n\n"); + // TODO(nmittler): Replace with WriteDocComment once included by protobuf distro. + GrpcWriteDocComment(p, " Creates a new blocking-style stub that supports all types of calls " + "on the service"); + p->Print( + *vars, + "public static $service_name$BlockingV2Stub newBlockingV2Stub(\n" + " $Channel$ channel) {\n"); + p->Indent(); + PrintStubFactory(service, vars, p, BLOCKING_V2_CLIENT_IMPL); + p->Print( + *vars, + "return $service_name$BlockingV2Stub.newStub(factory, channel);\n"); + p->Outdent(); + p->Print("}\n\n"); + // TODO(nmittler): Replace with WriteDocComment once included by protobuf distro. GrpcWriteDocComment(p, " Creates a new blocking-style stub that supports unary and streaming " "output calls on the service"); @@ -1206,6 +1287,7 @@ static void PrintService(const ServiceDescriptor* service, PrintStub(service, vars, p, ASYNC_INTERFACE); PrintAbstractClassStub(service, vars, p); PrintStub(service, vars, p, ASYNC_CLIENT_IMPL); + PrintStub(service, vars, p, BLOCKING_V2_CLIENT_IMPL); PrintStub(service, vars, p, BLOCKING_CLIENT_IMPL); PrintStub(service, vars, p, FUTURE_CLIENT_IMPL); @@ -1257,6 +1339,7 @@ void GenerateService(const ServiceDescriptor* service, vars["RpcMethod"] = "io.grpc.stub.annotations.RpcMethod"; vars["MethodDescriptor"] = "io.grpc.MethodDescriptor"; vars["StreamObserver"] = "io.grpc.stub.StreamObserver"; + vars["BlockingClientCall"] = "io.grpc.stub.BlockingClientCall"; vars["Iterator"] = "java.util.Iterator"; vars["GrpcGenerated"] = "io.grpc.stub.annotations.GrpcGenerated"; vars["ListenableFuture"] = diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index 36ff11a160a..c97bc9d44c9 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -64,6 +64,21 @@ public final class TestDeprecatedServiceGrpc { return TestDeprecatedServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static TestDeprecatedServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public TestDeprecatedServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestDeprecatedServiceBlockingV2Stub(channel, callOptions); + } + }; + return TestDeprecatedServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -169,6 +184,38 @@ public final class TestDeprecatedServiceGrpc { * */ @java.lang.Deprecated + public static final class TestDeprecatedServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private TestDeprecatedServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected TestDeprecatedServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestDeprecatedServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * An RPC method that has been deprecated and should generate with Java's @Deprecated annotation
+     * 
+ */ + @java.lang.Deprecated + public io.grpc.testing.compiler.Test.SimpleResponse deprecatedMethod(io.grpc.testing.compiler.Test.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getDeprecatedMethodMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service TestDeprecatedService. + *
+   * Test service that has been deprecated and should generate with Java's @Deprecated annotation
+   * 
+ */ + @java.lang.Deprecated public static final class TestDeprecatedServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private TestDeprecatedServiceBlockingStub( diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index 69a1b0947b5..229b7dcc252 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -282,6 +282,21 @@ public final class TestServiceGrpc { return TestServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static TestServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public TestServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestServiceBlockingV2Stub(channel, callOptions); + } + }; + return TestServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -546,6 +561,125 @@ public final class TestServiceGrpc { * Test service that supports all call types. * */ + public static final class TestServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private TestServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected TestServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * One request followed by one response.
+     * The server returns the client payload as-is.
+     * 
+ */ + public io.grpc.testing.compiler.Test.SimpleResponse unaryCall(io.grpc.testing.compiler.Test.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+     * One request followed by a sequence of responses (streamed download).
+     * The server returns the payload with client desired type and sizes.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingOutputCall(io.grpc.testing.compiler.Test.StreamingOutputCallRequest request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getStreamingOutputCallMethod(), getCallOptions(), request); + } + + /** + *
+     * A sequence of requests followed by one response (streamed upload).
+     * The server returns the aggregated size of client payload as the result.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingInputCall() { + return io.grpc.stub.ClientCalls.blockingClientStreamingCall( + getChannel(), getStreamingInputCallMethod(), getCallOptions()); + } + + /** + *
+     * A sequence of requests with each request served by the server immediately.
+     * As one request could lead to multiple responses, this interface
+     * demonstrates the idea of full bidirectionality.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + fullBidiCall() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getFullBidiCallMethod(), getCallOptions()); + } + + /** + *
+     * A sequence of requests followed by a sequence of responses.
+     * The server buffers all the client requests and then serves them in order. A
+     * stream of responses are returned to the client when the server starts with
+     * first request.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + halfBidiCall() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getHalfBidiCallMethod(), getCallOptions()); + } + + /** + *
+     * An RPC method whose Java name collides with a keyword, and whose generated
+     * method should have a '_' appended.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + import_() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getImportMethod(), getCallOptions()); + } + + /** + *
+     * A unary call that is Safe.
+     * 
+ */ + public io.grpc.testing.compiler.Test.SimpleResponse safeCall(io.grpc.testing.compiler.Test.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getSafeCallMethod(), getCallOptions(), request); + } + + /** + *
+     * A unary call that is Idempotent.
+     * 
+ */ + public io.grpc.testing.compiler.Test.SimpleResponse idempotentCall(io.grpc.testing.compiler.Test.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getIdempotentCallMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service TestService. + *
+   * Test service that supports all call types.
+   * 
+ */ public static final class TestServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private TestServiceBlockingStub( diff --git a/compiler/src/testLite/golden/TestDeprecatedService.java.txt b/compiler/src/testLite/golden/TestDeprecatedService.java.txt index 3a7dba9bbb5..fb0bfb8c38d 100644 --- a/compiler/src/testLite/golden/TestDeprecatedService.java.txt +++ b/compiler/src/testLite/golden/TestDeprecatedService.java.txt @@ -60,6 +60,21 @@ public final class TestDeprecatedServiceGrpc { return TestDeprecatedServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static TestDeprecatedServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public TestDeprecatedServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestDeprecatedServiceBlockingV2Stub(channel, callOptions); + } + }; + return TestDeprecatedServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -165,6 +180,38 @@ public final class TestDeprecatedServiceGrpc { * */ @java.lang.Deprecated + public static final class TestDeprecatedServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private TestDeprecatedServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected TestDeprecatedServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestDeprecatedServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * An RPC method that has been deprecated and should generate with Java's @Deprecated annotation
+     * 
+ */ + @java.lang.Deprecated + public io.grpc.testing.compiler.Test.SimpleResponse deprecatedMethod(io.grpc.testing.compiler.Test.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getDeprecatedMethodMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service TestDeprecatedService. + *
+   * Test service that has been deprecated and should generate with Java's @Deprecated annotation
+   * 
+ */ + @java.lang.Deprecated public static final class TestDeprecatedServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private TestDeprecatedServiceBlockingStub( diff --git a/compiler/src/testLite/golden/TestService.java.txt b/compiler/src/testLite/golden/TestService.java.txt index f86fb50d7dc..75101ddd7b3 100644 --- a/compiler/src/testLite/golden/TestService.java.txt +++ b/compiler/src/testLite/golden/TestService.java.txt @@ -271,6 +271,21 @@ public final class TestServiceGrpc { return TestServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static TestServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public TestServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestServiceBlockingV2Stub(channel, callOptions); + } + }; + return TestServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -535,6 +550,125 @@ public final class TestServiceGrpc { * Test service that supports all call types. * */ + public static final class TestServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private TestServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected TestServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * One request followed by one response.
+     * The server returns the client payload as-is.
+     * 
+ */ + public io.grpc.testing.compiler.Test.SimpleResponse unaryCall(io.grpc.testing.compiler.Test.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+     * One request followed by a sequence of responses (streamed download).
+     * The server returns the payload with client desired type and sizes.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingOutputCall(io.grpc.testing.compiler.Test.StreamingOutputCallRequest request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getStreamingOutputCallMethod(), getCallOptions(), request); + } + + /** + *
+     * A sequence of requests followed by one response (streamed upload).
+     * The server returns the aggregated size of client payload as the result.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingInputCall() { + return io.grpc.stub.ClientCalls.blockingClientStreamingCall( + getChannel(), getStreamingInputCallMethod(), getCallOptions()); + } + + /** + *
+     * A sequence of requests with each request served by the server immediately.
+     * As one request could lead to multiple responses, this interface
+     * demonstrates the idea of full bidirectionality.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + fullBidiCall() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getFullBidiCallMethod(), getCallOptions()); + } + + /** + *
+     * A sequence of requests followed by a sequence of responses.
+     * The server buffers all the client requests and then serves them in order. A
+     * stream of responses are returned to the client when the server starts with
+     * first request.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + halfBidiCall() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getHalfBidiCallMethod(), getCallOptions()); + } + + /** + *
+     * An RPC method whose Java name collides with a keyword, and whose generated
+     * method should have a '_' appended.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + import_() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getImportMethod(), getCallOptions()); + } + + /** + *
+     * A unary call that is Safe.
+     * 
+ */ + public io.grpc.testing.compiler.Test.SimpleResponse safeCall(io.grpc.testing.compiler.Test.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getSafeCallMethod(), getCallOptions(), request); + } + + /** + *
+     * A unary call that is Idempotent.
+     * 
+ */ + public io.grpc.testing.compiler.Test.SimpleResponse idempotentCall(io.grpc.testing.compiler.Test.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getIdempotentCallMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service TestService. + *
+   * Test service that supports all call types.
+   * 
+ */ public static final class TestServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private TestServiceBlockingStub( diff --git a/examples/build.gradle b/examples/build.gradle index 756a9c47bb1..c50a5ab8eb7 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -109,6 +109,7 @@ createStartScripts('io.grpc.examples.keepalive.KeepAliveClient') createStartScripts('io.grpc.examples.keepalive.KeepAliveServer') createStartScripts('io.grpc.examples.loadbalance.LoadBalanceClient') createStartScripts('io.grpc.examples.loadbalance.LoadBalanceServer') +createStartScripts('io.grpc.examples.manualflowcontrol.BidiBlockingClient') createStartScripts('io.grpc.examples.manualflowcontrol.ManualFlowControlClient') createStartScripts('io.grpc.examples.manualflowcontrol.ManualFlowControlServer') createStartScripts('io.grpc.examples.multiplex.MultiplexingServer') diff --git a/examples/src/main/java/io/grpc/examples/manualflowcontrol/BidiBlockingClient.java b/examples/src/main/java/io/grpc/examples/manualflowcontrol/BidiBlockingClient.java new file mode 100644 index 00000000000..902d46c8cc6 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/manualflowcontrol/BidiBlockingClient.java @@ -0,0 +1,286 @@ +/* + * Copyright 2023 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.examples.manualflowcontrol; + +import com.google.protobuf.ByteString; +import io.grpc.Grpc; +import io.grpc.InsecureChannelCredentials; +import io.grpc.ManagedChannel; +import io.grpc.StatusException; +import io.grpc.examples.manualflowcontrol.StreamingGreeterGrpc.StreamingGreeterBlockingV2Stub; +import io.grpc.stub.BlockingClientCall; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + + +/** + * A class that tries multiple ways to do blocking bidi streaming + * communication with an echo server + */ +public class BidiBlockingClient { + + private static final Logger logger = Logger.getLogger(BidiBlockingClient.class.getName()); + + /** + * Greet server. If provided, the first element of {@code args} is the name to use in the + * greeting. The second argument is the target server. You can see the multiplexing in the server + * logs. + */ + public static void main(String[] args) throws Exception { + System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tH:%1$tM:%1$tS %5$s%6$s%n"); + + // Access a service running on the local machine on port 50051 + String target = "localhost:50051"; + // Allow passing in the user and target strings as command line arguments + if (args.length > 0) { + if ("--help".equals(args[0])) { + System.err.println("Usage: [target]\n"); + System.err.println(" target The server to connect to. Defaults to " + target); + System.exit(1); + } + target = args[0]; + } + + // Create a communication channel to the server, known as a Channel. Channels are thread-safe + // and reusable. It is common to create channels at the beginning of your application and reuse + // them until the application shuts down. + // + // For the example we use plaintext insecure credentials to avoid needing TLS certificates. To + // use TLS, use TlsChannelCredentials instead. + ManagedChannel channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()) + .build(); + StreamingGreeterBlockingV2Stub blockingStub = StreamingGreeterGrpc.newBlockingV2Stub(channel); + List echoInput = names(); + try { + long start = System.currentTimeMillis(); + List twoThreadResult = useTwoThreads(blockingStub, echoInput); + long finish = System.currentTimeMillis(); + + System.out.println("The echo requests and results were:"); + printResultMessage("Input", echoInput, 0L); + printResultMessage("2 threads", twoThreadResult, finish - start); + } finally { + // ManagedChannels use resources like threads and TCP connections. To prevent leaking these + // resources the channel should be shut down when it will no longer be used. If it may be used + // again leave it running. + channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); + } + } + + private static void printResultMessage(String type, List result, long millis) { + String msg = String.format("%-32s: %2d, %.3f sec", type, result.size(), millis/1000.0); + logger.info(msg); + } + + private static void logMethodStart(String method) { + logger.info("--------------------- Starting to process using method: " + method); + } + + /** + * Create 2 threads, one that writes all values, and one that reads until the stream closes. + */ + private static List useTwoThreads(StreamingGreeterBlockingV2Stub blockingStub, + List valuesToWrite) throws InterruptedException { + logMethodStart("Two Threads"); + + List readValues = new ArrayList<>(); + final BlockingClientCall stream = blockingStub.sayHelloStreaming(); + + Thread reader = new Thread(null, + new Runnable() { + @Override + public void run() { + int count = 0; + try { + while (stream.hasNext()) { + readValues.add(stream.read().getMessage()); + if (++count % 10 == 0) { + logger.info("Finished " + count + " reads"); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + stream.cancel("Interrupted", e); + } catch (StatusException e) { + logger.warning("Encountered error while reading: " + e); + } + } + },"reader"); + + Thread writer = new Thread(null, + new Runnable() { + @Override + public void run() { + ByteString padding = createPadding(); + int count = 0; + Iterator iterator = valuesToWrite.iterator(); + boolean hadProblem = false; + try { + while (iterator.hasNext()) { + if (!stream.write(HelloRequest.newBuilder().setName(iterator.next()).setPadding(padding) + .build())) { + logger.warning("Stream closed before writes completed"); + hadProblem = true; + break; + } + if (++count % 10 == 0) { + logger.info("Finished " + count + " writes"); + } + } + if (!hadProblem) { + logger.info("Completed writes"); + stream.halfClose(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + stream.cancel("Interrupted", e); + } catch (StatusException e) { + logger.warning("Encountered error while writing: " + e); + } + } + }, "writer"); + + writer.start(); + reader.start(); + writer.join(); + reader.join(); + + return readValues; + } + + private static ByteString createPadding() { + int multiple = 50; + ByteBuffer data = ByteBuffer.allocate(1024 * multiple); + + for (int i = 0; i < multiple * 1024 / 4; i++) { + data.putInt(4 * i, 1111); + } + + return ByteString.copyFrom(data); + } + + + private static List names() { + return Arrays.asList( + "Sophia", + "Jackson", + "Emma", + "Aiden", + "Olivia", + "Lucas", + "Ava", + "Liam", + "Mia", + "Noah", + "Isabella", + "Ethan", + "Riley", + "Mason", + "Aria", + "Caden", + "Zoe", + "Oliver", + "Charlotte", + "Elijah", + "Lily", + "Grayson", + "Layla", + "Jacob", + "Amelia", + "Michael", + "Emily", + "Benjamin", + "Madelyn", + "Carter", + "Aubrey", + "James", + "Adalyn", + "Jayden", + "Madison", + "Logan", + "Chloe", + "Alexander", + "Harper", + "Caleb", + "Abigail", + "Ryan", + "Aaliyah", + "Luke", + "Avery", + "Daniel", + "Evelyn", + "Jack", + "Kaylee", + "William", + "Ella", + "Owen", + "Ellie", + "Gabriel", + "Scarlett", + "Matthew", + "Arianna", + "Connor", + "Hailey", + "Jayce", + "Nora", + "Isaac", + "Addison", + "Sebastian", + "Brooklyn", + "Henry", + "Hannah", + "Muhammad", + "Mila", + "Cameron", + "Leah", + "Wyatt", + "Elizabeth", + "Dylan", + "Sarah", + "Nathan", + "Eliana", + "Nicholas", + "Mackenzie", + "Julian", + "Peyton", + "Eli", + "Maria", + "Levi", + "Grace", + "Isaiah", + "Adeline", + "Landon", + "Elena", + "David", + "Anna", + "Christian", + "Victoria", + "Andrew", + "Camilla", + "Brayden", + "Lillian", + "John", + "Natalie", + "Lincoln" + ); + } +} diff --git a/examples/src/main/java/io/grpc/examples/manualflowcontrol/ManualFlowControlServer.java b/examples/src/main/java/io/grpc/examples/manualflowcontrol/ManualFlowControlServer.java index de8142596ea..3b7f980e08c 100644 --- a/examples/src/main/java/io/grpc/examples/manualflowcontrol/ManualFlowControlServer.java +++ b/examples/src/main/java/io/grpc/examples/manualflowcontrol/ManualFlowControlServer.java @@ -72,6 +72,7 @@ public void run() { // Give gRPC a StreamObserver that can observe and process incoming requests. return new StreamObserver() { + int cnt = 0; @Override public void onNext(HelloRequest request) { // Process the request and send a response or an error. @@ -81,7 +82,8 @@ public void onNext(HelloRequest request) { logger.info("--> " + name); // Simulate server "work" - Thread.sleep(100); + int sleepMillis = ++cnt % 20 == 0 ? 2000 : 100; + Thread.sleep(sleepMillis); // Send a response. String message = "Hello " + name; diff --git a/examples/src/main/proto/hello_streaming.proto b/examples/src/main/proto/hello_streaming.proto index 325b9093b0c..b4f0f5287dd 100644 --- a/examples/src/main/proto/hello_streaming.proto +++ b/examples/src/main/proto/hello_streaming.proto @@ -29,6 +29,7 @@ service StreamingGreeter { // The request message containing the user's name. message HelloRequest { string name = 1; + bytes padding = 2; } // The response message containing the greetings diff --git a/grpclb/src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java b/grpclb/src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java index c96c5400aac..80acb5d7bf3 100644 --- a/grpclb/src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java +++ b/grpclb/src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java @@ -60,6 +60,21 @@ public LoadBalancerStub newStub(io.grpc.Channel channel, io.grpc.CallOptions cal return LoadBalancerStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static LoadBalancerBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public LoadBalancerBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new LoadBalancerBlockingV2Stub(channel, callOptions); + } + }; + return LoadBalancerBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -147,6 +162,35 @@ public io.grpc.stub.StreamObserver balanceLoad /** * A stub to allow clients to do synchronous rpc calls to service LoadBalancer. */ + public static final class LoadBalancerBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private LoadBalancerBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected LoadBalancerBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new LoadBalancerBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Bidirectional rpc to get a list of servers.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + balanceLoad() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getBalanceLoadMethod(), getCallOptions()); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service LoadBalancer. + */ public static final class LoadBalancerBlockingStub extends io.grpc.stub.AbstractBlockingStub { private LoadBalancerBlockingStub( diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java index 2f4dc69c0c6..fcdd4491938 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java @@ -94,6 +94,21 @@ public LoadBalancerStatsServiceStub newStub(io.grpc.Channel channel, io.grpc.Cal return LoadBalancerStatsServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static LoadBalancerStatsServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public LoadBalancerStatsServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new LoadBalancerStatsServiceBlockingV2Stub(channel, callOptions); + } + }; + return LoadBalancerStatsServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -214,6 +229,46 @@ public void getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadB * A service used to obtain stats for verifying LB behavior. * */ + public static final class LoadBalancerStatsServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private LoadBalancerStatsServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected LoadBalancerStatsServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new LoadBalancerStatsServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Gets the backend distribution for RPCs sent by a test client.
+     * 
+ */ + public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetClientStatsMethod(), getCallOptions(), request); + } + + /** + *
+     * Gets the accumulated stats for RPCs sent by a test client.
+     * 
+ */ + public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetClientAccumulatedStatsMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service LoadBalancerStatsService. + *
+   * A service used to obtain stats for verifying LB behavior.
+   * 
+ */ public static final class LoadBalancerStatsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private LoadBalancerStatsServiceBlockingStub( diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java index 1650365bd52..7084ee53781 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java @@ -91,6 +91,21 @@ public MetricsServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions c return MetricsServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static MetricsServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public MetricsServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new MetricsServiceBlockingV2Stub(channel, callOptions); + } + }; + return MetricsServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -201,6 +216,46 @@ public void getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request, /** * A stub to allow clients to do synchronous rpc calls to service MetricsService. */ + public static final class MetricsServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private MetricsServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected MetricsServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new MetricsServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Returns the values of all the gauges that are currently being maintained by
+     * the service
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + getAllGauges(io.grpc.testing.integration.Metrics.EmptyMessage request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getGetAllGaugesMethod(), getCallOptions(), request); + } + + /** + *
+     * Returns the value of one gauge
+     * 
+ */ + public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetGaugeMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service MetricsService. + */ public static final class MetricsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private MetricsServiceBlockingStub( diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java index d1887ee83c4..68a0faa35c7 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java @@ -94,6 +94,21 @@ public ReconnectServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions return ReconnectServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static ReconnectServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public ReconnectServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ReconnectServiceBlockingV2Stub(channel, callOptions); + } + }; + return ReconnectServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -202,6 +217,40 @@ public void stop(io.grpc.testing.integration.EmptyProtos.Empty request, * A service used to control reconnect server. * */ + public static final class ReconnectServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private ReconnectServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected ReconnectServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ReconnectServiceBlockingV2Stub(channel, callOptions); + } + + /** + */ + public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getStartMethod(), getCallOptions(), request); + } + + /** + */ + public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getStopMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service ReconnectService. + *
+   * A service used to control reconnect server.
+   * 
+ */ public static final class ReconnectServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private ReconnectServiceBlockingStub( diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java index 08071a3b653..6855b61c63a 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java @@ -281,6 +281,21 @@ public TestServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions call return TestServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static TestServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public TestServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestServiceBlockingV2Stub(channel, callOptions); + } + }; + return TestServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -551,6 +566,125 @@ public void unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty requ * performance with various types of payload. * */ + public static final class TestServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private TestServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected TestServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new TestServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * One empty request followed by one empty response.
+     * 
+ */ + public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getEmptyCallMethod(), getCallOptions(), request); + } + + /** + *
+     * One request followed by one response.
+     * 
+ */ + public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+     * One request followed by one response. Response has cache control
+     * headers set such that a caching HTTP proxy (such as GFE) can
+     * satisfy subsequent requests.
+     * 
+ */ + public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getCacheableUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+     * One request followed by a sequence of responses (streamed download).
+     * The server returns the payload with client desired type and sizes.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingOutputCall(io.grpc.testing.integration.Messages.StreamingOutputCallRequest request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getStreamingOutputCallMethod(), getCallOptions(), request); + } + + /** + *
+     * A sequence of requests followed by one response (streamed upload).
+     * The server returns the aggregated size of client payload as the result.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamingInputCall() { + return io.grpc.stub.ClientCalls.blockingClientStreamingCall( + getChannel(), getStreamingInputCallMethod(), getCallOptions()); + } + + /** + *
+     * A sequence of requests with each request served by the server immediately.
+     * As one request could lead to multiple responses, this interface
+     * demonstrates the idea of full duplexing.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + fullDuplexCall() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getFullDuplexCallMethod(), getCallOptions()); + } + + /** + *
+     * A sequence of requests followed by a sequence of responses.
+     * The server buffers all the client requests and then serves them in order. A
+     * stream of responses are returned to the client when the server starts with
+     * first request.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + halfDuplexCall() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getHalfDuplexCallMethod(), getCallOptions()); + } + + /** + *
+     * The test server will not implement this method. It will be used
+     * to test the behavior when clients call unimplemented methods.
+     * 
+ */ + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service TestService. + *
+   * A simple service to test the various types of RPCs and experiment with
+   * performance with various types of payload.
+   * 
+ */ public static final class TestServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private TestServiceBlockingStub( diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java index 9711386185e..cd77c1cfa5c 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java @@ -64,6 +64,21 @@ public UnimplementedServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOpt return UnimplementedServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static UnimplementedServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public UnimplementedServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new UnimplementedServiceBlockingV2Stub(channel, callOptions); + } + }; + return UnimplementedServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -167,6 +182,37 @@ public void unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty requ * that case. * */ + public static final class UnimplementedServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private UnimplementedServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected UnimplementedServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new UnimplementedServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * A call that no server should implement
+     * 
+ */ + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service UnimplementedService. + *
+   * A simple service NOT implemented at servers so clients can test for
+   * that case.
+   * 
+ */ public static final class UnimplementedServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private UnimplementedServiceBlockingStub( diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java index 164119a29e7..545b69243f1 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java @@ -63,6 +63,21 @@ public XdsUpdateClientConfigureServiceStub newStub(io.grpc.Channel channel, io.g return XdsUpdateClientConfigureServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static XdsUpdateClientConfigureServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public XdsUpdateClientConfigureServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new XdsUpdateClientConfigureServiceBlockingV2Stub(channel, callOptions); + } + }; + return XdsUpdateClientConfigureServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -162,6 +177,36 @@ public void configure(io.grpc.testing.integration.Messages.ClientConfigureReques * A service to dynamically update the configuration of an xDS test client. * */ + public static final class XdsUpdateClientConfigureServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private XdsUpdateClientConfigureServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected XdsUpdateClientConfigureServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new XdsUpdateClientConfigureServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Update the tes client's configuration.
+     * 
+ */ + public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getConfigureMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service XdsUpdateClientConfigureService. + *
+   * A service to dynamically update the configuration of an xDS test client.
+   * 
+ */ public static final class XdsUpdateClientConfigureServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private XdsUpdateClientConfigureServiceBlockingStub( diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java index dccd23ccbee..7120a9d1c85 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java @@ -94,6 +94,21 @@ public XdsUpdateHealthServiceStub newStub(io.grpc.Channel channel, io.grpc.CallO return XdsUpdateHealthServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static XdsUpdateHealthServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public XdsUpdateHealthServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new XdsUpdateHealthServiceBlockingV2Stub(channel, callOptions); + } + }; + return XdsUpdateHealthServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -202,6 +217,40 @@ public void setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request, * A service to remotely control health status of an xDS test server. * */ + public static final class XdsUpdateHealthServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private XdsUpdateHealthServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected XdsUpdateHealthServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new XdsUpdateHealthServiceBlockingV2Stub(channel, callOptions); + } + + /** + */ + public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getSetServingMethod(), getCallOptions(), request); + } + + /** + */ + public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getSetNotServingMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service XdsUpdateHealthService. + *
+   * A service to remotely control health status of an xDS test server.
+   * 
+ */ public static final class XdsUpdateHealthServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private XdsUpdateHealthServiceBlockingStub( diff --git a/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java b/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java index 1f48c16aed3..5c01afd15c6 100644 --- a/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java +++ b/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java @@ -91,6 +91,21 @@ public EchoTestServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions return EchoTestServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static EchoTestServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public EchoTestServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new EchoTestServiceBlockingV2Stub(channel, callOptions); + } + }; + return EchoTestServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -187,6 +202,37 @@ public void forwardEcho(io.istio.test.Echo.ForwardEchoRequest request, /** * A stub to allow clients to do synchronous rpc calls to service EchoTestService. */ + public static final class EchoTestServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private EchoTestServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected EchoTestServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new EchoTestServiceBlockingV2Stub(channel, callOptions); + } + + /** + */ + public io.istio.test.Echo.EchoResponse echo(io.istio.test.Echo.EchoRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getEchoMethod(), getCallOptions(), request); + } + + /** + */ + public io.istio.test.Echo.ForwardEchoResponse forwardEcho(io.istio.test.Echo.ForwardEchoRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getForwardEchoMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service EchoTestService. + */ public static final class EchoTestServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private EchoTestServiceBlockingStub( diff --git a/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java b/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java index d7334b942ff..98768d37611 100644 --- a/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java +++ b/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java @@ -60,6 +60,21 @@ public RouteLookupServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptio return RouteLookupServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static RouteLookupServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public RouteLookupServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new RouteLookupServiceBlockingV2Stub(channel, callOptions); + } + }; + return RouteLookupServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -147,6 +162,33 @@ public void routeLookup(io.grpc.lookup.v1.RouteLookupRequest request, /** * A stub to allow clients to do synchronous rpc calls to service RouteLookupService. */ + public static final class RouteLookupServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private RouteLookupServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected RouteLookupServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new RouteLookupServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Lookup returns a target for a single key.
+     * 
+ */ + public io.grpc.lookup.v1.RouteLookupResponse routeLookup(io.grpc.lookup.v1.RouteLookupRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getRouteLookupMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service RouteLookupService. + */ public static final class RouteLookupServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private RouteLookupServiceBlockingStub( diff --git a/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java b/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java index d759128a4c5..69f1d78f55e 100644 --- a/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java +++ b/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java @@ -60,6 +60,21 @@ public S2AServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callO return S2AServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static S2AServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public S2AServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new S2AServiceBlockingV2Stub(channel, callOptions); + } + }; + return S2AServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -149,6 +164,36 @@ public io.grpc.stub.StreamObserver s /** * A stub to allow clients to do synchronous rpc calls to service S2AService. */ + public static final class S2AServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private S2AServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected S2AServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new S2AServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * SetUpSession is a bidirectional stream used by applications to offload
+     * operations from the TLS handshake.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + setUpSession() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getSetUpSessionMethod(), getCallOptions()); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service S2AService. + */ public static final class S2AServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private S2AServiceBlockingStub( diff --git a/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java b/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java index b3c1c285c8f..8d8c0eb2971 100644 --- a/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java +++ b/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java @@ -250,6 +250,21 @@ public ChannelzStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOpt return ChannelzStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static ChannelzBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public ChannelzBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ChannelzBlockingV2Stub(channel, callOptions); + } + }; + return ChannelzBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -481,6 +496,98 @@ public void getSocket(io.grpc.channelz.v1.GetSocketRequest request, * information. * */ + public static final class ChannelzBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private ChannelzBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected ChannelzBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ChannelzBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Gets all root channels (i.e. channels the application has directly
+     * created). This does not include subchannels nor non-top level channels.
+     * 
+ */ + public io.grpc.channelz.v1.GetTopChannelsResponse getTopChannels(io.grpc.channelz.v1.GetTopChannelsRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetTopChannelsMethod(), getCallOptions(), request); + } + + /** + *
+     * Gets all servers that exist in the process.
+     * 
+ */ + public io.grpc.channelz.v1.GetServersResponse getServers(io.grpc.channelz.v1.GetServersRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetServersMethod(), getCallOptions(), request); + } + + /** + *
+     * Returns a single Server, or else a NOT_FOUND code.
+     * 
+ */ + public io.grpc.channelz.v1.GetServerResponse getServer(io.grpc.channelz.v1.GetServerRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetServerMethod(), getCallOptions(), request); + } + + /** + *
+     * Gets all server sockets that exist in the process.
+     * 
+ */ + public io.grpc.channelz.v1.GetServerSocketsResponse getServerSockets(io.grpc.channelz.v1.GetServerSocketsRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetServerSocketsMethod(), getCallOptions(), request); + } + + /** + *
+     * Returns a single Channel, or else a NOT_FOUND code.
+     * 
+ */ + public io.grpc.channelz.v1.GetChannelResponse getChannel(io.grpc.channelz.v1.GetChannelRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetChannelMethod(), getCallOptions(), request); + } + + /** + *
+     * Returns a single Subchannel, or else a NOT_FOUND code.
+     * 
+ */ + public io.grpc.channelz.v1.GetSubchannelResponse getSubchannel(io.grpc.channelz.v1.GetSubchannelRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetSubchannelMethod(), getCallOptions(), request); + } + + /** + *
+     * Returns a single Socket or else a NOT_FOUND code.
+     * 
+ */ + public io.grpc.channelz.v1.GetSocketResponse getSocket(io.grpc.channelz.v1.GetSocketRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getGetSocketMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service Channelz. + *
+   * Channelz is a service exposed by gRPC servers that provides detailed debug
+   * information.
+   * 
+ */ public static final class ChannelzBlockingStub extends io.grpc.stub.AbstractBlockingStub { private ChannelzBlockingStub( diff --git a/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java b/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java index 73ddd4e0d23..d7795a5fdbf 100644 --- a/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java +++ b/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java @@ -91,6 +91,21 @@ public HealthStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptio return HealthStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static HealthBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public HealthBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new HealthBlockingV2Stub(channel, callOptions); + } + }; + return HealthBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -225,6 +240,58 @@ public void watch(io.grpc.health.v1.HealthCheckRequest request, /** * A stub to allow clients to do synchronous rpc calls to service Health. */ + public static final class HealthBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private HealthBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected HealthBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new HealthBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * If the requested service is unknown, the call will fail with status
+     * NOT_FOUND.
+     * 
+ */ + public io.grpc.health.v1.HealthCheckResponse check(io.grpc.health.v1.HealthCheckRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getCheckMethod(), getCallOptions(), request); + } + + /** + *
+     * Performs a watch for the serving status of the requested service.
+     * The server will immediately send back a message indicating the current
+     * serving status.  It will then subsequently send a new message whenever
+     * the service's serving status changes.
+     * If the requested service is unknown when the call is received, the
+     * server will send a message setting the serving status to
+     * SERVICE_UNKNOWN but will *not* terminate the call.  If at some
+     * future point, the serving status of the service becomes known, the
+     * server will send a new message with the service's serving status.
+     * If the call terminates with status UNIMPLEMENTED, then clients
+     * should assume this method is not supported and should not retry the
+     * call.  If the call terminates with any other status (including OK),
+     * clients should retry the call with appropriate exponential backoff.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + watch(io.grpc.health.v1.HealthCheckRequest request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getWatchMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service Health. + */ public static final class HealthBlockingStub extends io.grpc.stub.AbstractBlockingStub { private HealthBlockingStub( diff --git a/services/src/generated/main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java b/services/src/generated/main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java index 4f2dce26486..3089b8302cc 100644 --- a/services/src/generated/main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java +++ b/services/src/generated/main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java @@ -60,6 +60,21 @@ public ServerReflectionStub newStub(io.grpc.Channel channel, io.grpc.CallOptions return ServerReflectionStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static ServerReflectionBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public ServerReflectionBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ServerReflectionBlockingV2Stub(channel, callOptions); + } + }; + return ServerReflectionBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -149,6 +164,36 @@ public io.grpc.stub.StreamObserver { + private ServerReflectionBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected ServerReflectionBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ServerReflectionBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * The reflection service is structured as a bidirectional stream, ensuring
+     * all related requests go to a single server.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + serverReflectionInfo() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getServerReflectionInfoMethod(), getCallOptions()); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service ServerReflection. + */ public static final class ServerReflectionBlockingStub extends io.grpc.stub.AbstractBlockingStub { private ServerReflectionBlockingStub( diff --git a/services/src/generated/main/grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java b/services/src/generated/main/grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java index 7119e96d1f3..a0fc1bcf2de 100644 --- a/services/src/generated/main/grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java +++ b/services/src/generated/main/grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java @@ -60,6 +60,21 @@ public ServerReflectionStub newStub(io.grpc.Channel channel, io.grpc.CallOptions return ServerReflectionStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static ServerReflectionBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public ServerReflectionBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ServerReflectionBlockingV2Stub(channel, callOptions); + } + }; + return ServerReflectionBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -149,6 +164,36 @@ public io.grpc.stub.StreamObserver { + private ServerReflectionBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected ServerReflectionBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ServerReflectionBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * The reflection service is structured as a bidirectional stream, ensuring
+     * all related requests go to a single server.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + serverReflectionInfo() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getServerReflectionInfoMethod(), getCallOptions()); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service ServerReflection. + */ public static final class ServerReflectionBlockingStub extends io.grpc.stub.AbstractBlockingStub { private ServerReflectionBlockingStub( diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java index 088d27b619c..843d1e30135 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java @@ -63,6 +63,21 @@ public AnotherDynamicServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOp return AnotherDynamicServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static AnotherDynamicServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public AnotherDynamicServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AnotherDynamicServiceBlockingV2Stub(channel, callOptions); + } + }; + return AnotherDynamicServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -162,6 +177,36 @@ public void method(io.grpc.reflection.testing.DynamicRequest request, * AnotherDynamicService * */ + public static final class AnotherDynamicServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private AnotherDynamicServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected AnotherDynamicServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AnotherDynamicServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * A method
+     * 
+ */ + public io.grpc.reflection.testing.DynamicReply method(io.grpc.reflection.testing.DynamicRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getMethodMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service AnotherDynamicService. + *
+   * AnotherDynamicService
+   * 
+ */ public static final class AnotherDynamicServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private AnotherDynamicServiceBlockingStub( diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java index a84b95b2126..41c7f5468da 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java @@ -60,6 +60,21 @@ public AnotherReflectableServiceStub newStub(io.grpc.Channel channel, io.grpc.Ca return AnotherReflectableServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static AnotherReflectableServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public AnotherReflectableServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AnotherReflectableServiceBlockingV2Stub(channel, callOptions); + } + }; + return AnotherReflectableServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -141,6 +156,30 @@ public void method(io.grpc.reflection.testing.Request request, /** * A stub to allow clients to do synchronous rpc calls to service AnotherReflectableService. */ + public static final class AnotherReflectableServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private AnotherReflectableServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected AnotherReflectableServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AnotherReflectableServiceBlockingV2Stub(channel, callOptions); + } + + /** + */ + public io.grpc.reflection.testing.Reply method(io.grpc.reflection.testing.Request request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getMethodMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service AnotherReflectableService. + */ public static final class AnotherReflectableServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private AnotherReflectableServiceBlockingStub( diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java index 338b67e684d..4878109dc81 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java @@ -63,6 +63,21 @@ public DynamicServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions c return DynamicServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static DynamicServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public DynamicServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new DynamicServiceBlockingV2Stub(channel, callOptions); + } + }; + return DynamicServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -162,6 +177,36 @@ public void method(io.grpc.reflection.testing.DynamicRequest request, * A DynamicService * */ + public static final class DynamicServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private DynamicServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected DynamicServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new DynamicServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * A method
+     * 
+ */ + public io.grpc.reflection.testing.DynamicReply method(io.grpc.reflection.testing.DynamicRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getMethodMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service DynamicService. + *
+   * A DynamicService
+   * 
+ */ public static final class DynamicServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private DynamicServiceBlockingStub( diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java index 0b8954b5eb9..3e6cf5b8030 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java @@ -60,6 +60,21 @@ public ReflectableServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptio return ReflectableServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static ReflectableServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public ReflectableServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ReflectableServiceBlockingV2Stub(channel, callOptions); + } + }; + return ReflectableServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -141,6 +156,30 @@ public void method(io.grpc.reflection.testing.Request request, /** * A stub to allow clients to do synchronous rpc calls to service ReflectableService. */ + public static final class ReflectableServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private ReflectableServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected ReflectableServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ReflectableServiceBlockingV2Stub(channel, callOptions); + } + + /** + */ + public io.grpc.reflection.testing.Reply method(io.grpc.reflection.testing.Request request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getMethodMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service ReflectableService. + */ public static final class ReflectableServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private ReflectableServiceBlockingStub( diff --git a/stub/src/main/java/io/grpc/stub/BlockingClientCall.java b/stub/src/main/java/io/grpc/stub/BlockingClientCall.java new file mode 100644 index 00000000000..58881ef0592 --- /dev/null +++ b/stub/src/main/java/io/grpc/stub/BlockingClientCall.java @@ -0,0 +1,352 @@ +/* + * Copyright 2023 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.stub; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import io.grpc.ClientCall; +import io.grpc.ExperimentalApi; +import io.grpc.Metadata; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.stub.ClientCalls.ThreadSafeThreadlessExecutor; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a bidirectional streaming call from a client. Allows in a blocking manner, sending + * over the stream and receiving from the stream. Also supports terminating the call. + * Wraps a ClientCall and converts from async communication to the sync paradigm used by the + * various blocking stream methods in {@link ClientCalls} which are used by the generated stubs. + * + *

Supports separate threads for reads and writes, but only 1 of each + * + *

Read methods consist of: + *

    + *
  • {@link #read()} + *
  • {@link #read(long timeout, TimeUnit unit)} + *
  • {@link #hasNext()} + *
  • {@link #cancel(String, Throwable)} + *
+ * + *

Write methods consist of: + *

    + *
  • {@link #write(Object)} + *
  • {@link #write(Object, long timeout, TimeUnit unit)} + *
  • {@link #halfClose()} + *
+ * + * @param Type of the Request Message + * @param Type of the Response Message + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") +public final class BlockingClientCall { + + private static final Logger logger = Logger.getLogger(BlockingClientCall.class.getName()); + + private final BlockingQueue buffer; + private final ClientCall call; + + private final ThreadSafeThreadlessExecutor executor; + + private boolean writeClosed; + private volatile Status closedStatus; // null if not closed + + BlockingClientCall(ClientCall call, ThreadSafeThreadlessExecutor executor) { + this.call = call; + this.executor = executor; + buffer = new ArrayBlockingQueue<>(1); + } + + /** + * Wait if necessary for a value to be available from the server. If there is an available value + * return it immediately, if the stream is closed return a null. Otherwise, wait for a value to be + * available or the stream to be closed + * + * @return value from server or null if stream has been closed + * @throws StatusException If the stream has closed in an error state + */ + public RespT read() throws InterruptedException, StatusException { + try { + return read(true, 0, TimeUnit.NANOSECONDS); + } catch (TimeoutException e) { + throw new AssertionError("should never happen", e); + } + } + + /** + * Wait with timeout, if necessary, for a value to be available from the server. If there is an + * available value, return it immediately. If the stream is closed return a null. Otherwise, wait + * for a value to be available, the stream to be closed or the timeout to expire. + * + * @param timeout how long to wait before giving up. Values <= 0 are no wait + * @param unit a TimeUnit determining how to interpret the timeout parameter + * @return value from server or null (if stream has been closed) + * @throws TimeoutException if no read becomes ready before the specified timeout expires + * @throws StatusException If the stream has closed in an error state + */ + public RespT read(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException, + StatusException { + return read(false, timeout, unit); + } + + private RespT read(boolean waitForever, long timeout, TimeUnit unit) + throws InterruptedException, TimeoutException, StatusException { + long start = System.nanoTime(); + long end = start + unit.toNanos(timeout); + + Predicate> predicate = BlockingClientCall::skipWaitingForRead; + executor.waitAndDrainWithTimeout(waitForever, end, predicate, this); + RespT bufferedValue = buffer.poll(); + + if (logger.isLoggable(Level.FINER)) { + logger.finer("Client Blocking read had value: " + bufferedValue); + } + + Status currentClosedStatus; + if (bufferedValue != null) { + call.request(1); + return bufferedValue; + } else if ((currentClosedStatus = closedStatus) == null) { + throw new IllegalStateException( + "The message disappeared... are you reading from multiple threads?"); + } else if (!currentClosedStatus.isOk()) { + throw currentClosedStatus.asException(); + } else { + return null; + } + } + + boolean skipWaitingForRead() { + return closedStatus != null || !buffer.isEmpty(); + } + + /** + * Wait for a value to be available from the server. If there is an + * available value, return true immediately. If the stream was closed with Status.OK, return + * false. If the stream was closed with an error status, throw a StatusException. Otherwise, wait + * for a value to be available or the stream to be closed. + * + * @return True when there is a value to read. Return false if stream closed cleanly. + * @throws StatusException If the stream was closed in an error state + */ + public boolean hasNext() throws InterruptedException, StatusException { + executor.waitAndDrain((x) -> !x.buffer.isEmpty() || x.closedStatus != null, this); + + Status currentClosedStatus = closedStatus; + if (currentClosedStatus != null && !currentClosedStatus.isOk()) { + throw currentClosedStatus.asException(); + } + + return !buffer.isEmpty(); + } + + /** + * Send a value to the stream for sending to server, wait if necessary for the grpc stream to be + * ready. + * + *

If write is not legal at the time of call, immediately returns false + * + *


NOTE: This method will return as soon as it passes the request to the grpc stream + * layer. It will not block while the message is being sent on the wire and returning true does + * not guarantee that the server gets the message. + * + *


WARNING: Doing only writes without reads can lead to deadlocks. This is because + * flow control, imposed by networks to protect intermediary routers and endpoints that are + * operating under resource constraints, requires reads to be done in order to progress writes. + * Furthermore, the server closing the stream will only be identified after + * the last sent value is read. + * + * @param request Message to send to the server + * @return true if the request is sent to stream, false if skipped + * @throws StatusException If the stream has closed in an error state + */ + public boolean write(ReqT request) throws InterruptedException, StatusException { + try { + return write(true, request, Integer.MAX_VALUE, TimeUnit.DAYS); + } catch (TimeoutException e) { + throw new RuntimeException(e); // should never happen + } + } + + /** + * Send a value to the stream for sending to server, wait if necessary for the grpc stream to be + * ready up to specified timeout. + * + *

If write is not legal at the time of call, immediately returns false + * + *


NOTE: This method will return as soon as it passes the request to the grpc stream + * layer. It will not block while the message is being sent on the wire and returning true does + * not guarantee that the server gets the message. + * + *


WARNING: Doing only writes without reads can lead to deadlocks as a result of + * flow control. Furthermore, the server closing the stream will only be identified after the + * last sent value is read. + * + * @param request Message to send to the server + * @param timeout How long to wait before giving up. Values <= 0 are no wait + * @param unit A TimeUnit determining how to interpret the timeout parameter + * @return true if the request is sent to stream, false if skipped + * @throws TimeoutException if write does not become ready before the specified timeout expires + * @throws StatusException If the stream has closed in an error state + */ + public boolean write(ReqT request, long timeout, TimeUnit unit) + throws InterruptedException, TimeoutException, StatusException { + return write(false, request, timeout, unit); + } + + private boolean write(boolean waitForever, ReqT request, long timeout, TimeUnit unit) + throws InterruptedException, TimeoutException, StatusException { + + if (writeClosed) { + throw new IllegalStateException("Writes cannot be done after calling halfClose or cancel"); + } + + long end = System.nanoTime() + unit.toNanos(timeout); + + Predicate> predicate = + (x) -> x.call.isReady() || x.closedStatus != null; + executor.waitAndDrainWithTimeout(waitForever, end, predicate, this); + Status savedClosedStatus = closedStatus; + if (savedClosedStatus == null) { + call.sendMessage(request); + return true; + } else if (savedClosedStatus.isOk()) { + return false; + } else { + // Propagate any errors returned from the server + throw savedClosedStatus.asException(); + } + } + + void sendSingleRequest(ReqT request) { + call.sendMessage(request); + } + + /** + * Cancel stream and stop any further writes. Note that some reads that are in flight may still + * happen after the cancel. + * + * @param message if not {@code null}, will appear as the description of the CANCELLED status + * @param cause if not {@code null}, will appear as the cause of the CANCELLED status + */ + public void cancel(String message, Throwable cause) { + writeClosed = true; + call.cancel(message, cause); + } + + /** + * Indicate that no more writes will be done and the stream will be closed from the client side. + * + * @see ClientCall#halfClose() + */ + public void halfClose() { + if (writeClosed) { + throw new IllegalStateException( + "halfClose cannot be called after already half closed or cancelled"); + } + + writeClosed = true; + call.halfClose(); + } + + /** + * Status that server sent when closing channel from its side. + * + * @return null if stream not closed by server, otherwise Status sent by server + */ + @VisibleForTesting + Status getClosedStatus() { + drainQuietly(); + return closedStatus; + } + + /** + * Check for whether some action is ready. + * + * @return True if legal to write and writeOrRead can run without blocking + */ + @VisibleForTesting + boolean isEitherReadOrWriteReady() { + return (isWriteLegal() && isWriteReady()) || isReadReady(); + } + + /** + * Check whether there are any values waiting to be read. + * + * @return true if read will not block + */ + @VisibleForTesting + boolean isReadReady() { + drainQuietly(); + + return !buffer.isEmpty(); + } + + /** + * Check that write hasn't been marked complete and stream is ready to receive a write (so will + * not block). + * + * @return true if legal to write and write will not block + */ + @VisibleForTesting + boolean isWriteReady() { + drainQuietly(); + + return isWriteLegal() && call.isReady(); + } + + /** + * Check whether we'll ever be able to do writes or should terminate. + * @return True if writes haven't been closed and the server hasn't closed the stream + */ + private boolean isWriteLegal() { + return !writeClosed && closedStatus == null; + } + + ClientCall.Listener getListener() { + return new QueuingListener(); + } + + private void drainQuietly() { + try { + executor.drain(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private final class QueuingListener extends ClientCall.Listener { + @Override + public void onMessage(RespT value) { + Preconditions.checkState(closedStatus == null, "ClientCall already closed"); + buffer.add(value); + } + + @Override + public void onClose(Status status, Metadata trailers) { + Preconditions.checkState(closedStatus == null, "ClientCall already closed"); + closedStatus = status; + } + } + +} diff --git a/stub/src/main/java/io/grpc/stub/ClientCalls.java b/stub/src/main/java/io/grpc/stub/ClientCalls.java index 13fb00d3b3e..f307c806489 100644 --- a/stub/src/main/java/io/grpc/stub/ClientCalls.java +++ b/stub/src/main/java/io/grpc/stub/ClientCalls.java @@ -22,12 +22,14 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; import com.google.common.base.Strings; import com.google.common.util.concurrent.AbstractFuture; import com.google.common.util.concurrent.ListenableFuture; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; +import io.grpc.ExperimentalApi; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.Status; @@ -42,9 +44,14 @@ import java.util.concurrent.Executor; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.LockSupport; +import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nonnull; import javax.annotation.Nullable; /** @@ -184,7 +191,6 @@ public static RespT blockingUnaryCall( * * @return an iterator over the response stream. */ - // TODO(louiscryan): Not clear if we want to use this idiom for 'simple' stubs. public static Iterator blockingServerStreamingCall( ClientCall call, ReqT req) { BlockingResponseStream result = new BlockingResponseStream<>(call); @@ -194,11 +200,12 @@ public static Iterator blockingServerStreamingCall( /** * Executes a server-streaming call returning a blocking {@link Iterator} over the - * response stream. The {@code call} should not be already started. After calling this method, - * {@code call} should no longer be used. + * response stream. * *

The returned iterator may throw {@link StatusRuntimeException} on error. * + *

Warning: the iterator can result in leaks if not completely consumed. + * * @return an iterator over the response stream. */ public static Iterator blockingServerStreamingCall( @@ -211,6 +218,82 @@ public static Iterator blockingServerStreamingCall( return result; } + /** + * Initiates a client streaming call over the specified channel. It returns an + * object which can be used in a blocking manner to retrieve responses.. + * + *

The methods {@link BlockingClientCall#hasNext()} and {@link + * BlockingClientCall#cancel(String, Throwable)} can be used for more extensive control. + * + * @return A {@link BlockingClientCall} that has had the request sent and halfClose called + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public static BlockingClientCall blockingV2ServerStreamingCall( + Channel channel, MethodDescriptor method, CallOptions callOptions, ReqT req) { + BlockingClientCall call = + blockingBidiStreamingCall(channel, method, callOptions); + + call.sendSingleRequest(req); + call.halfClose(); + return call; + } + + /** + * Initiates a server streaming call and sends the specified request to the server. It returns an + * object which can be used in a blocking manner to retrieve values from the server. After the + * last value has been read, the next read call will return null. + * + *

Call {@link BlockingClientCall#read()} for + * retrieving values. A {@code null} will be returned after the server has closed the stream. + * + *

The methods {@link BlockingClientCall#hasNext()} and {@link + * BlockingClientCall#cancel(String, Throwable)} can be used for more extensive control. + * + *


Example usage: + *

 {@code  while ((response = call.read()) != null) { ... } } 
+ * or + *
 {@code
+   *   while (call.hasNext()) {
+   *     response = call.read();
+   *     ...
+   *   }
+   * } 
+ * + *

Note that this paradigm is different from the original + * {@link #blockingServerStreamingCall(Channel, MethodDescriptor, CallOptions, Object)} + * which returns an iterator, which would leave the stream open if not completely consumed. + * + * @return A {@link BlockingClientCall} which can be used by the client to write and receive + * messages over the grpc channel. + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public static BlockingClientCall blockingClientStreamingCall( + Channel channel, MethodDescriptor method, CallOptions callOptions) { + return blockingBidiStreamingCall(channel, method, callOptions); + } + + /** + * Initiate a bidirectional-streaming {@link ClientCall} and returning a stream object + * ({@link BlockingClientCall}) which can be used by the client to send and receive messages over + * the grpc channel. + * + * @return an object representing the call which can be used to read, write and terminate it. + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public static BlockingClientCall blockingBidiStreamingCall( + Channel channel, MethodDescriptor method, CallOptions callOptions) { + ThreadSafeThreadlessExecutor executor = new ThreadSafeThreadlessExecutor(); + ClientCall call = channel.newCall(method, callOptions.withExecutor(executor)); + + BlockingClientCall blockingClientCall = new BlockingClientCall<>(call, executor); + + // Get the call started + call.start(blockingClientCall.getListener(), new Metadata()); + call.request(1); + + return blockingClientCall; + } + /** * Executes a unary call and returns a {@link ListenableFuture} to the response. The * {@code call} should not be already started. After calling this method, {@code call} should no @@ -414,7 +497,7 @@ public void disableAutoRequestWithInitial(int request) { public void request(int count) { if (!streamingResponse && count == 1) { // Initially ask for two responses from flow-control so that if a misbehaving server - // sends more than one responses, we can catch it and fail it in the listener. + // sends more than one response, we can catch it and fail it in the listener. call.request(2); } else { call.request(count); @@ -637,7 +720,7 @@ public boolean hasNext() { public T next() { // Eagerly call request(1) so it can be processing the next message while we wait for the // current one, which reduces latency for the next message. With MigratingThreadDeframer and - // if the data has already been recieved, every other message can be delivered instantly. This + // if the data has already been received, every other message can be delivered instantly. This // can be run after hasNext(), but just would be slower. if (!(last instanceof StatusRuntimeException) && last != this) { call.request(1); @@ -726,6 +809,12 @@ public void waitAndDrain() throws InterruptedException { } while ((runnable = poll()) != null); } + private static void throwIfInterrupted() throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + } + /** * Called after final call to {@link #waitAndDrain()}, from same thread. */ @@ -745,12 +834,6 @@ private static void runQuietly(Runnable runnable) { } } - private static void throwIfInterrupted() throws InterruptedException { - if (Thread.interrupted()) { - throw new InterruptedException(); - } - } - @Override public void execute(Runnable runnable) { add(runnable); @@ -763,6 +846,131 @@ public void execute(Runnable runnable) { } } + @SuppressWarnings("serial") + static final class ThreadSafeThreadlessExecutor extends ConcurrentLinkedQueue + implements Executor { + private static final Logger log = + Logger.getLogger(ThreadSafeThreadlessExecutor.class.getName()); + + private final Lock waiterLock = new ReentrantLock(); + private final Condition waiterCondition = waiterLock.newCondition(); + + // Non private to avoid synthetic class + ThreadSafeThreadlessExecutor() {} + + /** + * Waits until there is a Runnable, then executes it and all queued Runnables after it. + */ + public void waitAndDrain(Predicate predicate, T testTarget) throws InterruptedException { + try { + waitAndDrainWithTimeout(true, 0, predicate, testTarget); + } catch (TimeoutException e) { + throw new AssertionError(e); // Should never happen + } + } + + /** + * Waits for up to specified nanoseconds until there is a Runnable, then executes it and all + * queued Runnables after it. + * + *

his should always be called in a loop that checks whether the reason we are waiting has + * been satisfied.

T + * + * @param waitForever ignore the rest of the arguments and wait until there is a task to run + * @param end System.nanoTime() to stop waiting if haven't been woken up yet + * @param predicate non-null condition to test for skipping wake or waking up threads + * @param testTarget object to pass to predicate + */ + public void waitAndDrainWithTimeout(boolean waitForever, long end, + @Nonnull Predicate predicate, T testTarget) + throws InterruptedException, TimeoutException { + throwIfInterrupted(); + Runnable runnable; + + while (!predicate.apply(testTarget)) { + waiterLock.lock(); + try { + while ((runnable = poll()) == null) { + if (predicate.apply(testTarget)) { + return; // The condition for which we were waiting is now satisfied + } + + if (waitForever) { + waiterCondition.await(); + } else { + long waitNanos = end - System.nanoTime(); + if (waitNanos <= 0) { + throw new TimeoutException(); // Deadline is expired + } + waiterCondition.awaitNanos(waitNanos); + } + } + } finally { + waiterLock.unlock(); + } + + do { + runQuietly(runnable); + } while ((runnable = poll()) != null); + // Wake everything up now that we've done something and they can check in their outer loop + // if they can continue or need to wait again. + signallAll(); + } + } + + /** + * Executes all queued Runnables and if there were any wakes up any waiting threads. + */ + public void drain() throws InterruptedException { + throwIfInterrupted(); + Runnable runnable; + boolean didWork = false; + + while ((runnable = poll()) != null) { + runQuietly(runnable); + didWork = true; + } + + if (didWork) { + signallAll(); + } + } + + private void signallAll() { + waiterLock.lock(); + try { + waiterCondition.signalAll(); + } finally { + waiterLock.unlock(); + } + } + + private static void runQuietly(Runnable runnable) { + try { + runnable.run(); + } catch (Throwable t) { + log.log(Level.WARNING, "Runnable threw exception", t); + } + } + + private static void throwIfInterrupted() throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + } + + @Override + public void execute(Runnable runnable) { + waiterLock.lock(); + try { + add(runnable); + waiterCondition.signalAll(); // If anything is waiting let it wake up and process this task + } finally { + waiterLock.unlock(); + } + } + } + enum StubType { BLOCKING, FUTURE, ASYNC } diff --git a/stub/src/test/java/io/grpc/stub/BlockingClientCallTest.java b/stub/src/test/java/io/grpc/stub/BlockingClientCallTest.java new file mode 100644 index 00000000000..112b092eaed --- /dev/null +++ b/stub/src/test/java/io/grpc/stub/BlockingClientCallTest.java @@ -0,0 +1,520 @@ +/* + * Copyright 2023 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.stub; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import io.grpc.CallOptions; +import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor; +import io.grpc.MethodDescriptor.MethodType; +import io.grpc.Server; +import io.grpc.ServerServiceDefinition; +import io.grpc.ServiceDescriptor; +import io.grpc.Status; +import io.grpc.Status.Code; +import io.grpc.StatusException; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.ServerCalls.BidiStreamingMethod; +import io.grpc.stub.ServerCallsTest.IntegerMarshaller; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BlockingClientCallTest { + private static final Logger logger = Logger.getLogger(BlockingClientCallTest.class.getName()); + + public static final int DELAY_MILLIS = 2000; + public static final long DELAY_NANOS = TimeUnit.MILLISECONDS.toNanos(DELAY_MILLIS); + private static final MethodDescriptor BIDI_STREAMING_METHOD = + MethodDescriptor.newBuilder() + .setType(MethodType.BIDI_STREAMING) + .setFullMethodName("some/method") + .setRequestMarshaller(new IntegerMarshaller()) + .setResponseMarshaller(new IntegerMarshaller()) + .build(); + + private Server server; + + private ManagedChannel channel; + + private IntegerTestMethod testMethod; + private BlockingClientCall biDiStream; + + @Before + public void setUp() throws Exception { + testMethod = new IntegerTestMethod(); + + ServerServiceDefinition service = ServerServiceDefinition.builder( + new ServiceDescriptor("some", BIDI_STREAMING_METHOD)) + .addMethod(BIDI_STREAMING_METHOD, ServerCalls.asyncBidiStreamingCall(testMethod)) + .build(); + long tag = System.nanoTime(); + + server = InProcessServerBuilder.forName("go-with-the-flow" + tag).directExecutor() + .addService(service).build().start(); + + channel = InProcessChannelBuilder.forName("go-with-the-flow" + tag).directExecutor().build(); + } + + @After + public void tearDown() { + if (server != null) { + server.shutdownNow(); + } + if (channel != null) { + channel.shutdownNow(); + } + if (biDiStream != null) { + biDiStream.cancel("In teardown", null); + } + } + + @Test + public void sanityTest() throws Exception { + Integer req = 2; + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + + // verify activity ready + assertTrue(biDiStream.isEitherReadOrWriteReady()); + assertTrue(biDiStream.isWriteReady()); + + // Have server send a value + testMethod.sendValueToClient(10); + + // Do a writeOrRead + biDiStream.write(req, 3, TimeUnit.SECONDS); + assertEquals(Integer.valueOf(10), biDiStream.read(DELAY_MILLIS, TimeUnit.MILLISECONDS)); + + // mark complete + biDiStream.halfClose(); + assertNull(biDiStream.read(2, TimeUnit.SECONDS)); + + // verify activity !ready and !writeable + assertFalse(biDiStream.isEitherReadOrWriteReady()); + assertFalse(biDiStream.isWriteReady()); + + assertEquals(Code.OK, biDiStream.getClosedStatus().getCode()); + } + + @Test + public void testReadSuccess_withoutBlocking() throws Exception { + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + + // Have server push a value + testMethod.sendValueToClient(11); + + long start = System.nanoTime(); + Integer value = biDiStream.read(100, TimeUnit.SECONDS); + assertNotNull(value); + long timeTaken = System.nanoTime() - start; + assertThat(timeTaken).isLessThan(TimeUnit.MILLISECONDS.toNanos(100)); + } + + @Test + public void testReadSuccess_withBlocking() throws Exception { + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + + try { + biDiStream.read(1, TimeUnit.SECONDS); + fail("Expected timeout"); + } catch (TimeoutException t) { + // ignore + } + + long start = System.nanoTime(); + delayedAddValue(DELAY_MILLIS, 12); + assertNotNull(biDiStream.read(DELAY_MILLIS * 2, TimeUnit.MILLISECONDS)); + long timeTaken = System.nanoTime() - start; + assertThat(timeTaken).isGreaterThan(DELAY_NANOS); + assertThat(timeTaken).isLessThan(DELAY_NANOS * 2); + + start = System.nanoTime(); + Integer[] values = {13, 14, 15, 16}; + delayedAddValue(DELAY_MILLIS, values); + for (Integer value : values) { + Integer readValue = biDiStream.read(DELAY_MILLIS * 2, TimeUnit.MILLISECONDS); + assertEquals(value, readValue); + } + timeTaken = System.nanoTime() - start; + assertThat(timeTaken).isLessThan(DELAY_NANOS * 2); + assertThat(timeTaken).isAtLeast(DELAY_NANOS); + + start = System.nanoTime(); + delayedVoidMethod(100, testMethod::halfClose); + assertNull(biDiStream.read(DELAY_MILLIS * 2, TimeUnit.MILLISECONDS)); + timeTaken = System.nanoTime() - start; + assertThat(timeTaken).isLessThan(DELAY_NANOS); + } + + @Test + public void testCancel() throws Exception { + testMethod.disableAutoRequest(); + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + + // read terminated + long start = System.currentTimeMillis(); + delayedCancel(biDiStream, "cancel read"); + try { + assertNull(biDiStream.read(2 * DELAY_MILLIS, TimeUnit.MILLISECONDS)); + fail("No exception thrown by read after cancel"); + } catch (StatusException e) { + assertEquals(Status.CANCELLED.getCode(), e.getStatus().getCode()); + assertThat(System.currentTimeMillis() - start).isLessThan(2 * DELAY_MILLIS); + } + + // write terminated + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + start = System.currentTimeMillis(); + delayedCancel(biDiStream, "cancel write"); + + // Write interrupted by cancel + try { + assertFalse(biDiStream.write(30)); // this is interrupted by cancel + fail("No exception thrown when write was interrupted by cancel"); + } catch (StatusException e) { + assertEquals(Status.CANCELLED.getCode(), e.getStatus().getCode()); + } + + // Write after cancel + try { + start = System.currentTimeMillis(); + biDiStream.write(30); + fail("No exception doing write after cancel"); + } catch (IllegalStateException e) { + assertThat(System.currentTimeMillis() - start).isLessThan(200); + assertThat(e.getMessage()).contains("cancel"); + } + + // new read after cancel immediately throws an exception + try { + start = System.currentTimeMillis(); + assertNull(biDiStream.read(2, TimeUnit.SECONDS)); + } catch (StatusException e) { + assertEquals(Status.CANCELLED.getCode(), e.getStatus().getCode()); + assertThat(System.currentTimeMillis() - start).isLessThan(200); + } + + } + + @Test + public void testIsActivityReady() throws Exception { + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + + // write only ready + assertTrue(biDiStream.isEitherReadOrWriteReady()); + assertTrue(biDiStream.isWriteReady()); + assertFalse(biDiStream.isReadReady()); + + // both ready + testMethod.sendValueToClient(40); + assertTrue(biDiStream.isEitherReadOrWriteReady()); + assertTrue(biDiStream.isReadReady()); + assertTrue(biDiStream.isWriteReady()); + + // read only ready + biDiStream.halfClose(); + assertTrue(biDiStream.isEitherReadOrWriteReady()); + assertTrue(biDiStream.isReadReady()); + assertFalse(biDiStream.isWriteReady()); + + // Neither ready + assertNotNull(biDiStream.read(1, TimeUnit.MILLISECONDS)); + assertFalse(biDiStream.isEitherReadOrWriteReady()); + assertFalse(biDiStream.isReadReady()); + assertFalse(biDiStream.isWriteReady()); + } + + @Test + public void testWriteSuccess_withBlocking() throws Exception { + testMethod.disableAutoRequest(); + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + + assertFalse(biDiStream.isWriteReady()); + delayedWriteEnable(500); + assertTrue(biDiStream.write(40)); + + delayedWriteEnable(500); + assertTrue(biDiStream.write(41, 0, TimeUnit.NANOSECONDS)); + } + + + @Test + public void testReadNonblocking_whenWriteBlocked() throws Exception { + testMethod.disableAutoRequest(); + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + + // One value waiting + testMethod.sendValueToClient(50); + long start = System.currentTimeMillis(); + assertEquals(Integer.valueOf(50), biDiStream.read()); + assertThat(System.currentTimeMillis() - start).isLessThan(DELAY_MILLIS); + + // Two values waiting + start = System.currentTimeMillis(); + testMethod.sendValuesToClient(51, 52); + assertEquals(Integer.valueOf(51), biDiStream.read()); + assertEquals(Integer.valueOf(52), biDiStream.read()); + assertThat(System.currentTimeMillis() - start).isLessThan(DELAY_MILLIS); + } + + @Test + public void testReadsAndWritesInterleaved_withBlocking() throws Exception { + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + + Integer[] valuesOut = {1001, 10022, 1003}; + Integer[] valuesIn = new Integer[valuesOut.length]; + delayedAddValue(300, valuesOut); + int iteration = 0; + for (int i = 0; i < valuesOut.length && iteration++ < (20 + valuesOut.length); ) { + try { + if ((valuesIn[i] = biDiStream.read(50, TimeUnit.MILLISECONDS)) != null) { + i++; + } + } catch (TimeoutException e) { + logger.info("Read timed out for " + i); + } + } + assertArrayEquals(valuesOut, valuesIn); + } + + @Test + public void testReadsAndWritesInterleaved_BlockingWrites() throws Exception { + testMethod.disableAutoRequest(); + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + + testMethod.sendValuesToClient(10, 11, 12); + delayedWriteEnable(500); + long start = System.currentTimeMillis(); + boolean done = false; + int count = 0; + while (!done) { + count++; + if (!biDiStream.isWriteReady() && biDiStream.isReadReady()) { + biDiStream.read(100, TimeUnit.MILLISECONDS); + } else { + done = biDiStream.write(100, 1, TimeUnit.SECONDS); + } + } + assertEquals(4, count); + assertThat(System.currentTimeMillis() - start).isLessThan(700); + + testMethod.sendValuesToClient(20, 21, 22); + delayedWriteEnable(100); + while (!biDiStream.isWriteReady()) { + Thread.sleep(20); + } + + assertTrue(biDiStream.write(1000, 2 * DELAY_MILLIS, TimeUnit.MILLISECONDS)); + + assertEquals(Integer.valueOf(20), biDiStream.read(200, TimeUnit.MILLISECONDS)); + assertEquals(Integer.valueOf(21), biDiStream.read(200, TimeUnit.MILLISECONDS)); + assertEquals(Integer.valueOf(22), biDiStream.read(200, TimeUnit.MILLISECONDS)); + try { + Integer value = biDiStream.read(200, TimeUnit.MILLISECONDS); + fail("Unexpected read success instead of timeout. Value was: " + value); + } catch (TimeoutException ignore) { + // ignore since expected + } + } + + @Test + public void testWriteCompleted() throws Exception { + testMethod.disableAutoRequest(); + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + + // Verify pending write released + long start = System.currentTimeMillis(); + delayedVoidMethod(DELAY_MILLIS, biDiStream::halfClose); + assertFalse(biDiStream.write(1)); // should block until writeComplete is triggered + long end = System.currentTimeMillis(); + assertThat(end - start).isAtLeast(DELAY_MILLIS); + + // verify new writes throw an illegalStateException + try { + assertFalse(biDiStream.write(2)); + fail("write did not throw an exception when called after halfClose"); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).containsMatch("after.*halfClose.*cancel"); + } + + // verify pending write with timeout released + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + delayedVoidMethod(DELAY_MILLIS, biDiStream::halfClose); + assertFalse(biDiStream.write(3, 2 * DELAY_MILLIS, TimeUnit.MILLISECONDS)); + } + + @Test + public void testClose_withException() throws Exception { + biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, + CallOptions.DEFAULT); + + String descr = "too many small numbers"; + testMethod.sendError( + Status.FAILED_PRECONDITION.withDescription(descr).asRuntimeException()); + Status closedStatus = biDiStream.getClosedStatus(); + assertEquals(Code.FAILED_PRECONDITION, closedStatus.getCode()); + assertEquals(descr, closedStatus.getDescription()); + try { + assertFalse(biDiStream.write(1)); + } catch (StatusException e) { + assertThat(e.getMessage()).startsWith("FAILED_PRECONDITION"); + } + } + + private void delayedAddValue(int delayMillis, Integer... values) { + new Thread("delayedAddValue " + values.length) { + @Override + public void run() { + try { + Thread.sleep(delayMillis); + for (Integer cur : values) { + testMethod.sendValueToClient(cur); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }.start(); + } + + public interface Thunk { void apply(); } // supports passing void method w/out args + + private void delayedVoidMethod(int delayMillis, Thunk method) { + new Thread("delayedHalfClose") { + @Override + public void run() { + try { + Thread.sleep(delayMillis); + method.apply(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }.start(); + } + + private void delayedWriteEnable(int delayMillis) { + delayedVoidMethod(delayMillis, testMethod::readValueFromClient); + } + + private void delayedCancel(BlockingClientCall biDiStream, String message) { + new Thread("delayedCancel") { + @Override + public void run() { + try { + Thread.sleep(BlockingClientCallTest.DELAY_MILLIS); + biDiStream.cancel(message, new RuntimeException("Test requested close")); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }.start(); + } + + private static class IntegerTestMethod implements BidiStreamingMethod { + boolean autoRequest = true; + + void disableAutoRequest() { + assertNull("Can't disable auto request after invoke has been called", serverCallObserver); + autoRequest = false; + } + + ServerCallStreamObserver serverCallObserver; + + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + serverCallObserver = (ServerCallStreamObserver) responseObserver; + if (!autoRequest) { + serverCallObserver.disableAutoRequest(); + } + + return new StreamObserver() { + @Override + public void onNext(Integer value) { + if (!autoRequest) { + serverCallObserver.request(1); + } + + // For testing ReqResp actions + if (value > 1000) { + serverCallObserver.onNext(value); + } + } + + @Override + public void onError(Throwable t) { + // no-op + } + + @Override + public void onCompleted() { + serverCallObserver.onCompleted(); + } + }; + } + + void readValueFromClient() { + serverCallObserver.request(1); + } + + void sendValueToClient(int value) { + serverCallObserver.onNext(value); + } + + private void sendValuesToClient(int ...values) { + for (int cur : values) { + sendValueToClient(cur); + } + } + + void halfClose() { + serverCallObserver.onCompleted(); + } + + void sendError(Throwable t) { + serverCallObserver.onError(t); + } + } + +} diff --git a/stub/src/test/java/io/grpc/stub/ClientCallsTest.java b/stub/src/test/java/io/grpc/stub/ClientCallsTest.java index f3d101b862a..b711b2a23b5 100644 --- a/stub/src/test/java/io/grpc/stub/ClientCallsTest.java +++ b/stub/src/test/java/io/grpc/stub/ClientCallsTest.java @@ -971,8 +971,8 @@ public ClientCall interceptCall( } @Override public void halfClose() { - Thread.currentThread().interrupt(); super.halfClose(); + Thread.currentThread().interrupt(); } }; } diff --git a/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java b/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java index 8c58f2c5a2c..fac753a1b32 100644 --- a/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java +++ b/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java @@ -156,6 +156,21 @@ public SimpleServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions ca return SimpleServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static SimpleServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public SimpleServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new SimpleServiceBlockingV2Stub(channel, callOptions); + } + }; + return SimpleServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -318,6 +333,72 @@ public io.grpc.stub.StreamObserver bidiS * A simple service for test. * */ + public static final class SimpleServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private SimpleServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected SimpleServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new SimpleServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Simple unary RPC.
+     * 
+ */ + public io.grpc.testing.protobuf.SimpleResponse unaryRpc(io.grpc.testing.protobuf.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getUnaryRpcMethod(), getCallOptions(), request); + } + + /** + *
+     * Simple client-to-server streaming RPC.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + clientStreamingRpc() { + return io.grpc.stub.ClientCalls.blockingClientStreamingCall( + getChannel(), getClientStreamingRpcMethod(), getCallOptions()); + } + + /** + *
+     * Simple server-to-client streaming RPC.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + serverStreamingRpc(io.grpc.testing.protobuf.SimpleRequest request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getServerStreamingRpcMethod(), getCallOptions(), request); + } + + /** + *
+     * Simple bidirectional streaming RPC.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + bidiStreamingRpc() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getBidiStreamingRpcMethod(), getCallOptions()); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service SimpleService. + *
+   * A simple service for test.
+   * 
+ */ public static final class SimpleServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private SimpleServiceBlockingStub( diff --git a/xds/src/generated/thirdparty/grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java b/xds/src/generated/thirdparty/grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java index de2c7424fca..bb74f2e0cb2 100644 --- a/xds/src/generated/thirdparty/grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java @@ -70,6 +70,21 @@ public OpenRcaServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions c return OpenRcaServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static OpenRcaServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public OpenRcaServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new OpenRcaServiceBlockingV2Stub(channel, callOptions); + } + }; + return OpenRcaServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -191,6 +206,42 @@ public void streamCoreMetrics(com.github.xds.service.orca.v3.OrcaLoadReportReque * a new call to change backend reporting frequency. * */ + public static final class OpenRcaServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private OpenRcaServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected OpenRcaServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new OpenRcaServiceBlockingV2Stub(channel, callOptions); + } + + /** + */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamCoreMetrics(com.github.xds.service.orca.v3.OrcaLoadReportRequest request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getStreamCoreMetricsMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service OpenRcaService. + *
+   * Out-of-band (OOB) load reporting service for the additional load reporting
+   * agent that does not sit in the request path. Reports are periodically sampled
+   * with sufficient frequency to provide temporal association with requests.
+   * OOB reporting compensates the limitation of in-band reporting in revealing
+   * costs for backends that do not provide a steady stream of telemetry such as
+   * long running stream operations and zero QPS services. This is a server
+   * streaming service, client needs to terminate current RPC and initiate
+   * a new call to change backend reporting frequency.
+   * 
+ */ public static final class OpenRcaServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private OpenRcaServiceBlockingStub( diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java index e039c2193e8..192b23a3db1 100644 --- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java @@ -99,6 +99,21 @@ public AggregatedDiscoveryServiceStub newStub(io.grpc.Channel channel, io.grpc.C return AggregatedDiscoveryServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static AggregatedDiscoveryServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public AggregatedDiscoveryServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AggregatedDiscoveryServiceBlockingV2Stub(channel, callOptions); + } + }; + return AggregatedDiscoveryServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -233,6 +248,52 @@ public io.grpc.stub.StreamObserver */ + public static final class AggregatedDiscoveryServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private AggregatedDiscoveryServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected AggregatedDiscoveryServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AggregatedDiscoveryServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * This is a gRPC-only API.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamAggregatedResources() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getStreamAggregatedResourcesMethod(), getCallOptions()); + } + + /** + */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + deltaAggregatedResources() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getDeltaAggregatedResourcesMethod(), getCallOptions()); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service AggregatedDiscoveryService. + *
+   * See https://github.com/envoyproxy/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( diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/load_stats/v3/LoadReportingServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/load_stats/v3/LoadReportingServiceGrpc.java index 2adbf02e98a..fbe8dd5a5ec 100644 --- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/load_stats/v3/LoadReportingServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/load_stats/v3/LoadReportingServiceGrpc.java @@ -60,6 +60,21 @@ public LoadReportingServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOpt return LoadReportingServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static LoadReportingServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public LoadReportingServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new LoadReportingServiceBlockingV2Stub(channel, callOptions); + } + }; + return LoadReportingServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -199,6 +214,61 @@ public io.grpc.stub.StreamObserver { + private LoadReportingServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected LoadReportingServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new LoadReportingServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Advanced API to allow for multi-dimensional load balancing by remote
+     * server. For receiving LB assignments, the steps are:
+     * 1, The management server is configured with per cluster/zone/load metric
+     *    capacity configuration. The capacity configuration definition is
+     *    outside of the scope of this document.
+     * 2. Envoy issues a standard {Stream,Fetch}Endpoints request for the clusters
+     *    to balance.
+     * Independently, Envoy will initiate a StreamLoadStats bidi stream with a
+     * management server:
+     * 1. Once a connection establishes, the management server publishes a
+     *    LoadStatsResponse for all clusters it is interested in learning load
+     *    stats about.
+     * 2. For each cluster, Envoy load balances incoming traffic to upstream hosts
+     *    based on per-zone weights and/or per-instance weights (if specified)
+     *    based on intra-zone LbPolicy. This information comes from the above
+     *    {Stream,Fetch}Endpoints.
+     * 3. When upstream hosts reply, they optionally add header <define header
+     *    name> with ASCII representation of EndpointLoadMetricStats.
+     * 4. Envoy aggregates load reports over the period of time given to it in
+     *    LoadStatsResponse.load_reporting_interval. This includes aggregation
+     *    stats Envoy maintains by itself (total_requests, rpc_errors etc.) as
+     *    well as load metrics from upstream hosts.
+     * 5. When the timer of load_reporting_interval expires, Envoy sends new
+     *    LoadStatsRequest filled with load reports for each cluster.
+     * 6. The management server uses the load reports from all reported Envoys
+     *    from around the world, computes global assignment and prepares traffic
+     *    assignment destined for each zone Envoys are located in. Goto 2.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamLoadStats() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getStreamLoadStatsMethod(), getCallOptions()); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service LoadReportingService. + */ public static final class LoadReportingServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private LoadReportingServiceBlockingStub( diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java index 2cbb7536d4c..fa1d235f082 100644 --- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java @@ -63,6 +63,21 @@ public RateLimitQuotaServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOp return RateLimitQuotaServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static RateLimitQuotaServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public RateLimitQuotaServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new RateLimitQuotaServiceBlockingV2Stub(channel, callOptions); + } + }; + return RateLimitQuotaServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -164,6 +179,39 @@ public io.grpc.stub.StreamObserver */ + public static final class RateLimitQuotaServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private RateLimitQuotaServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected RateLimitQuotaServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new RateLimitQuotaServiceBlockingV2Stub(channel, callOptions); + } + + /** + *
+     * Main communication channel: the data plane sends usage reports to the RLQS server,
+     * and the server asynchronously responding with the assignments.
+     * 
+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamRateLimitQuotas() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getStreamRateLimitQuotasMethod(), getCallOptions()); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service RateLimitQuotaService. + *
+   * Defines the Rate Limit Quota Service (RLQS).
+   * 
+ */ public static final class RateLimitQuotaServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private RateLimitQuotaServiceBlockingStub( diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java index 3f8874248d0..75ce84b77f9 100644 --- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java @@ -96,6 +96,21 @@ public ClientStatusDiscoveryServiceStub newStub(io.grpc.Channel channel, io.grpc return ClientStatusDiscoveryServiceStub.newStub(factory, channel); } + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static ClientStatusDiscoveryServiceBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public ClientStatusDiscoveryServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ClientStatusDiscoveryServiceBlockingV2Stub(channel, callOptions); + } + }; + return ClientStatusDiscoveryServiceBlockingV2Stub.newStub(factory, channel); + } + /** * Creates a new blocking-style stub that supports unary and streaming output calls on the service */ @@ -212,6 +227,44 @@ public void fetchClientStatus(io.envoyproxy.envoy.service.status.v3.ClientStatus * also be used to get the current xDS states directly from the client. * */ + public static final class ClientStatusDiscoveryServiceBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private ClientStatusDiscoveryServiceBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected ClientStatusDiscoveryServiceBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new ClientStatusDiscoveryServiceBlockingV2Stub(channel, callOptions); + } + + /** + */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall + streamClientStatus() { + return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( + getChannel(), getStreamClientStatusMethod(), getCallOptions()); + } + + /** + */ + public io.envoyproxy.envoy.service.status.v3.ClientStatusResponse fetchClientStatus(io.envoyproxy.envoy.service.status.v3.ClientStatusRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getFetchClientStatusMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do llimited synchronous rpc calls to service ClientStatusDiscoveryService. + *
+   * CSDS is Client Status Discovery Service. It can be used to get the status of
+   * an xDS-compliant client from the management server's point of view. It can
+   * also be used to get the current xDS states directly from the client.
+   * 
+ */ public static final class ClientStatusDiscoveryServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private ClientStatusDiscoveryServiceBlockingStub( From 6516c7387e2cb944784115303f8a36416f53b4a1 Mon Sep 17 00:00:00 2001 From: Vindhya Ningegowda Date: Fri, 20 Dec 2024 19:50:09 -0800 Subject: [PATCH 121/591] xds: Remove xds authority label from metric registration (#11760) * Remove `grpc.xds.authority` label while registering `grpc.xds_client.resources` gauge, until the label value is available to record. --- .../io/grpc/xds/XdsClientMetricReporterImpl.java | 2 +- .../grpc/xds/XdsClientMetricReporterImplTest.java | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java b/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java index fa88237a7ea..0b592eb019e 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java +++ b/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java @@ -90,7 +90,7 @@ final class XdsClientMetricReporterImpl implements XdsClientMetricReporter { Arrays.asList("grpc.target", "grpc.xds.server"), Collections.emptyList(), false); RESOURCES_GAUGE = metricInstrumentRegistry.registerLongGauge("grpc.xds_client.resources", "EXPERIMENTAL. Number of xDS resources.", "{resource}", - Arrays.asList("grpc.target", "grpc.xds.authority", "grpc.xds.cache_state", + Arrays.asList("grpc.target", "grpc.xds.cache_state", "grpc.xds.resource_type"), Collections.emptyList(), false); } diff --git a/xds/src/test/java/io/grpc/xds/XdsClientMetricReporterImplTest.java b/xds/src/test/java/io/grpc/xds/XdsClientMetricReporterImplTest.java index 9ee3f88d921..df5ab87a1c0 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientMetricReporterImplTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientMetricReporterImplTest.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -82,14 +83,14 @@ public class XdsClientMetricReporterImplTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); - @Mock - private MetricRecorder mockMetricRecorder; @Mock private XdsClient mockXdsClient; - @Mock - private BatchRecorder mockBatchRecorder; @Captor private ArgumentCaptor gaugeBatchCallbackCaptor; + private MetricRecorder mockMetricRecorder = mock(MetricRecorder.class, + delegatesTo(new MetricRecorderImpl())); + private BatchRecorder mockBatchRecorder = mock(BatchRecorder.class, + delegatesTo(new BatchRecorderImpl())); private XdsClientMetricReporterImpl reporter; @@ -372,6 +373,12 @@ public boolean matches(T instrument) { }); } + static class MetricRecorderImpl implements MetricRecorder { + } + + static class BatchRecorderImpl implements BatchRecorder { + } + static class TestlogHandler extends Handler { List logs = new ArrayList<>(); From ebe2b486777f5aea8a063ca7d7903fdc867244d5 Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Sun, 22 Dec 2024 03:09:57 +0200 Subject: [PATCH 122/591] api: StatusRuntimeException without stacktrace - Android compatibility (#11072) This is an alternative to e36f099be9 that avoids the "fillInStaceTrace" constructor which is only available starting at Android API level 24. --- api/build.gradle | 12 +++--- api/src/main/java/io/grpc/InternalStatus.java | 9 ++--- .../grpc/InternalStatusRuntimeException.java | 39 +++++++++++++++++++ .../main/java/io/grpc/StatusException.java | 7 +--- .../java/io/grpc/StatusRuntimeException.java | 7 +--- .../java/io/grpc/StatusExceptionTest.java | 8 ---- .../io/grpc/StatusRuntimeExceptionTest.java | 2 +- .../java/io/grpc/internal/ServerCallImpl.java | 6 +-- .../java/io/grpc/internal/ServerImpl.java | 4 +- 9 files changed, 56 insertions(+), 38 deletions(-) create mode 100644 api/src/main/java/io/grpc/InternalStatusRuntimeException.java diff --git a/api/build.gradle b/api/build.gradle index 4edfba7c0d8..6a75597ed55 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -54,13 +54,11 @@ dependencies { extension = "signature" } } - // TODO: Temporarily disabled until StatusException is fixed. - // Context: https://github.com/grpc/grpc-java/pull/11066 - //signature (libraries.signature.android) { - // artifact { - // extension = "signature" - // } - //} + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } tasks.named("javadoc").configure { diff --git a/api/src/main/java/io/grpc/InternalStatus.java b/api/src/main/java/io/grpc/InternalStatus.java index b6549bb435f..56df1decf38 100644 --- a/api/src/main/java/io/grpc/InternalStatus.java +++ b/api/src/main/java/io/grpc/InternalStatus.java @@ -38,12 +38,11 @@ private InternalStatus() {} public static final Metadata.Key CODE_KEY = Status.CODE_KEY; /** - * Create a new {@link StatusRuntimeException} with the internal option of skipping the filling - * of the stack trace. + * Create a new {@link StatusRuntimeException} skipping the filling of the stack trace. */ @Internal - public static final StatusRuntimeException asRuntimeException(Status status, - @Nullable Metadata trailers, boolean fillInStackTrace) { - return new StatusRuntimeException(status, trailers, fillInStackTrace); + public static StatusRuntimeException asRuntimeExceptionWithoutStacktrace(Status status, + @Nullable Metadata trailers) { + return new InternalStatusRuntimeException(status, trailers); } } diff --git a/api/src/main/java/io/grpc/InternalStatusRuntimeException.java b/api/src/main/java/io/grpc/InternalStatusRuntimeException.java new file mode 100644 index 00000000000..6090b701f0b --- /dev/null +++ b/api/src/main/java/io/grpc/InternalStatusRuntimeException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import javax.annotation.Nullable; + +/** + * StatusRuntimeException without stack trace, implemented as a subclass, as the + * {@code String, Throwable, boolean, boolean} constructor is not available in the supported + * version of Android. + * + * @see StatusRuntimeException + */ +class InternalStatusRuntimeException extends StatusRuntimeException { + private static final long serialVersionUID = 0; + + public InternalStatusRuntimeException(Status status, @Nullable Metadata trailers) { + super(status, trailers); + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } +} diff --git a/api/src/main/java/io/grpc/StatusException.java b/api/src/main/java/io/grpc/StatusException.java index b719f881132..f9416bf72e3 100644 --- a/api/src/main/java/io/grpc/StatusException.java +++ b/api/src/main/java/io/grpc/StatusException.java @@ -44,12 +44,7 @@ public StatusException(Status status) { * @since 1.0.0 */ public StatusException(Status status, @Nullable Metadata trailers) { - this(status, trailers, /*fillInStackTrace=*/ true); - } - - StatusException(Status status, @Nullable Metadata trailers, boolean fillInStackTrace) { - super(Status.formatThrowableMessage(status), status.getCause(), - /* enableSuppression */ true, /* writableStackTrace */fillInStackTrace); + super(Status.formatThrowableMessage(status), status.getCause()); this.status = status; this.trailers = trailers; } diff --git a/api/src/main/java/io/grpc/StatusRuntimeException.java b/api/src/main/java/io/grpc/StatusRuntimeException.java index 9465e4c38cd..dd22d6b2486 100644 --- a/api/src/main/java/io/grpc/StatusRuntimeException.java +++ b/api/src/main/java/io/grpc/StatusRuntimeException.java @@ -45,12 +45,7 @@ public StatusRuntimeException(Status status) { * @since 1.0.0 */ public StatusRuntimeException(Status status, @Nullable Metadata trailers) { - this(status, trailers, /*fillInStackTrace=*/ true); - } - - StatusRuntimeException(Status status, @Nullable Metadata trailers, boolean fillInStackTrace) { - super(Status.formatThrowableMessage(status), status.getCause(), - /* enable suppressions */ true, /* writableStackTrace */ fillInStackTrace); + super(Status.formatThrowableMessage(status), status.getCause()); this.status = status; this.trailers = trailers; } diff --git a/api/src/test/java/io/grpc/StatusExceptionTest.java b/api/src/test/java/io/grpc/StatusExceptionTest.java index dd0d12dccda..410cfb2289a 100644 --- a/api/src/test/java/io/grpc/StatusExceptionTest.java +++ b/api/src/test/java/io/grpc/StatusExceptionTest.java @@ -28,14 +28,6 @@ @RunWith(JUnit4.class) public class StatusExceptionTest { - @Test - public void internalCtorRemovesStack() { - StackTraceElement[] trace = - new StatusException(Status.CANCELLED, null, false) {}.getStackTrace(); - - assertThat(trace).isEmpty(); - } - @Test public void normalCtorKeepsStack() { StackTraceElement[] trace = diff --git a/api/src/test/java/io/grpc/StatusRuntimeExceptionTest.java b/api/src/test/java/io/grpc/StatusRuntimeExceptionTest.java index ab20c111254..d965ed86253 100644 --- a/api/src/test/java/io/grpc/StatusRuntimeExceptionTest.java +++ b/api/src/test/java/io/grpc/StatusRuntimeExceptionTest.java @@ -31,7 +31,7 @@ public class StatusRuntimeExceptionTest { @Test public void internalCtorRemovesStack() { StackTraceElement[] trace = - new StatusRuntimeException(Status.CANCELLED, null, false) {}.getStackTrace(); + new InternalStatusRuntimeException(Status.CANCELLED, null) {}.getStackTrace(); assertThat(trace).isEmpty(); } diff --git a/core/src/main/java/io/grpc/internal/ServerCallImpl.java b/core/src/main/java/io/grpc/internal/ServerCallImpl.java index dda36258e7c..e224384ce8f 100644 --- a/core/src/main/java/io/grpc/internal/ServerCallImpl.java +++ b/core/src/main/java/io/grpc/internal/ServerCallImpl.java @@ -373,10 +373,10 @@ private void closedInternal(Status status) { } else { call.cancelled = true; listener.onCancel(); - // The status will not have a cause in all failure scenarios but we want to make sure + // The status will not have a cause in all failure scenarios, but we want to make sure // we always cancel the context with one to keep the context cancelled state consistent. - cancelCause = InternalStatus.asRuntimeException( - Status.CANCELLED.withDescription("RPC cancelled"), null, false); + cancelCause = InternalStatus.asRuntimeExceptionWithoutStacktrace( + Status.CANCELLED.withDescription("RPC cancelled"), null); } } finally { // Cancel context after delivering RPC closure notification to allow the application to diff --git a/core/src/main/java/io/grpc/internal/ServerImpl.java b/core/src/main/java/io/grpc/internal/ServerImpl.java index cec2a13a301..eceb7d7a738 100644 --- a/core/src/main/java/io/grpc/internal/ServerImpl.java +++ b/core/src/main/java/io/grpc/internal/ServerImpl.java @@ -887,8 +887,8 @@ private void closedInternal(final Status status) { // failed status has an exception we will create one here if needed. Throwable cancelCause = status.getCause(); if (cancelCause == null) { - cancelCause = InternalStatus.asRuntimeException( - Status.CANCELLED.withDescription("RPC cancelled"), null, false); + cancelCause = InternalStatus.asRuntimeExceptionWithoutStacktrace( + Status.CANCELLED.withDescription("RPC cancelled"), null); } // The callExecutor might be busy doing user work. To avoid waiting, use an executor that From aafab74087405fca3660d167c4dc40e4028c49f0 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 23 Dec 2024 09:38:55 -0800 Subject: [PATCH 123/591] api: Use package-private IgnoreJRERequirement This avoids the dependency on animalsniffer-annotations. grpc-api, and particularly grpc-context, are used many low-level places and it is beneficial for them to be very low dependency. This brings grpc-context back to zero-dependency. --- api/BUILD.bazel | 1 - api/build.gradle | 5 +++- .../java/io/grpc/IgnoreJRERequirement.java | 30 +++++++++++++++++++ api/src/main/java/io/grpc/TimeUtils.java | 1 - .../test/java/io/grpc/CallOptionsTest.java | 1 - .../io/grpc/SynchronizationContextTest.java | 1 - api/src/test/java/io/grpc/TimeUtilsTest.java | 1 - buildscripts/checkstyle.xml | 6 ++++ netty/build.gradle | 1 + stub/build.gradle | 1 + 10 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 api/src/main/java/io/grpc/IgnoreJRERequirement.java diff --git a/api/BUILD.bazel b/api/BUILD.bazel index 34e8de95335..6bf3375e9f0 100644 --- a/api/BUILD.bazel +++ b/api/BUILD.bazel @@ -13,6 +13,5 @@ java_library( artifact("com.google.errorprone:error_prone_annotations"), artifact("com.google.guava:failureaccess"), # future transitive dep of Guava. See #5214 artifact("com.google.guava:guava"), - artifact("org.codehaus.mojo:animal-sniffer-annotations"), ], ) diff --git a/api/build.gradle b/api/build.gradle index 6a75597ed55..dc3eaea3f4e 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -36,7 +36,6 @@ tasks.named("jar").configure { dependencies { compileOnly sourceSets.context.output api libraries.jsr305, - libraries.animalsniffer.annotations, libraries.errorprone.annotations implementation libraries.guava @@ -61,6 +60,10 @@ dependencies { } } +animalsniffer { + annotation = 'io.grpc.IgnoreJRERequirement' +} + tasks.named("javadoc").configure { source sourceSets.context.allSource // We want io.grpc.Internal, but not io.grpc.Internal* diff --git a/api/src/main/java/io/grpc/IgnoreJRERequirement.java b/api/src/main/java/io/grpc/IgnoreJRERequirement.java new file mode 100644 index 00000000000..2db406c5953 --- /dev/null +++ b/api/src/main/java/io/grpc/IgnoreJRERequirement.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Disables Animal Sniffer's signature checking. This is our own package-private version to avoid + * dependening on animalsniffer-annotations. + * + *

FIELD is purposefully not supported, as Android wouldn't be able to ignore a field. Instead, + * the entire class would need to be avoided on Android. + */ +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +@interface IgnoreJRERequirement {} diff --git a/api/src/main/java/io/grpc/TimeUtils.java b/api/src/main/java/io/grpc/TimeUtils.java index c3cdf843d79..01b8c158822 100644 --- a/api/src/main/java/io/grpc/TimeUtils.java +++ b/api/src/main/java/io/grpc/TimeUtils.java @@ -17,7 +17,6 @@ package io.grpc; import java.time.Duration; -import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; final class TimeUtils { private TimeUtils() {} diff --git a/api/src/test/java/io/grpc/CallOptionsTest.java b/api/src/test/java/io/grpc/CallOptionsTest.java index 051c1b2d851..65fb7ff3bf2 100644 --- a/api/src/test/java/io/grpc/CallOptionsTest.java +++ b/api/src/test/java/io/grpc/CallOptionsTest.java @@ -34,7 +34,6 @@ import io.grpc.internal.SerializingExecutor; import java.time.Duration; import java.util.concurrent.Executor; -import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; diff --git a/api/src/test/java/io/grpc/SynchronizationContextTest.java b/api/src/test/java/io/grpc/SynchronizationContextTest.java index d063c58a978..668f5ae4d6d 100644 --- a/api/src/test/java/io/grpc/SynchronizationContextTest.java +++ b/api/src/test/java/io/grpc/SynchronizationContextTest.java @@ -36,7 +36,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.junit.After; import org.junit.Rule; import org.junit.Test; diff --git a/api/src/test/java/io/grpc/TimeUtilsTest.java b/api/src/test/java/io/grpc/TimeUtilsTest.java index 75c0437ce26..728b8512cd7 100644 --- a/api/src/test/java/io/grpc/TimeUtilsTest.java +++ b/api/src/test/java/io/grpc/TimeUtilsTest.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertEquals; import java.time.Duration; -import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; diff --git a/buildscripts/checkstyle.xml b/buildscripts/checkstyle.xml index 035b1dfc900..0ec8ecc79ce 100644 --- a/buildscripts/checkstyle.xml +++ b/buildscripts/checkstyle.xml @@ -38,6 +38,12 @@ + + + + + + diff --git a/netty/build.gradle b/netty/build.gradle index 3662f8ec399..cb97ae10b55 100644 --- a/netty/build.gradle +++ b/netty/build.gradle @@ -17,6 +17,7 @@ tasks.named("jar").configure { dependencies { api project(':grpc-api'), + libraries.animalsniffer.annotations, libraries.netty.codec.http2 implementation project(':grpc-core'), libs.netty.handler.proxy, diff --git a/stub/build.gradle b/stub/build.gradle index a9a7cec5a0e..2dabd9e6202 100644 --- a/stub/build.gradle +++ b/stub/build.gradle @@ -16,6 +16,7 @@ tasks.named("jar").configure { dependencies { api project(':grpc-api'), + libraries.animalsniffer.annotations, libraries.guava implementation libraries.errorprone.annotations testImplementation libraries.truth, From 805cad3782b83bb8e368f0f6e13ff9c7d347e728 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 23 Dec 2024 09:42:23 -0800 Subject: [PATCH 124/591] bazel: Restore DoNotCall ErrorProne check In e08b9db20 we added `@DoNotCall` annotations to some call sites, but Bazel used an older version of ErrorProne that complained at times it shouldn't. The minimum version of Bazel we test/support is now Bazel 6, well past Bazel 3.4+. --- api/BUILD.bazel | 1 - core/BUILD.bazel | 1 - 2 files changed, 2 deletions(-) diff --git a/api/BUILD.bazel b/api/BUILD.bazel index 6bf3375e9f0..965d14a9eb0 100644 --- a/api/BUILD.bazel +++ b/api/BUILD.bazel @@ -6,7 +6,6 @@ java_library( "src/main/java/**/*.java", "src/context/java/**/*.java", ]), - javacopts = ["-Xep:DoNotCall:OFF"], # Remove once requiring Bazel 3.4.0+; allows non-final visibility = ["//visibility:public"], deps = [ artifact("com.google.code.findbugs:jsr305"), diff --git a/core/BUILD.bazel b/core/BUILD.bazel index 35c20628d0b..6dd5199b5c0 100644 --- a/core/BUILD.bazel +++ b/core/BUILD.bazel @@ -17,7 +17,6 @@ java_library( srcs = glob([ "src/main/java/io/grpc/internal/*.java", ]), - javacopts = ["-Xep:DoNotCall:OFF"], # Remove once requiring Bazel 3.4.0+; allows non-final resources = glob([ "src/bazel-internal/resources/**", ]), From 1126a8e30b884c66f831e88f89ebf2f10a0e7795 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Mon, 23 Dec 2024 13:29:40 -0800 Subject: [PATCH 125/591] binder: A standard API for pointing resolvers at a different Android User. (#11775) --- .../src/main/java/io/grpc/binder/ApiConstants.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/binder/src/main/java/io/grpc/binder/ApiConstants.java b/binder/src/main/java/io/grpc/binder/ApiConstants.java index 43e94338fdc..292c580c2b8 100644 --- a/binder/src/main/java/io/grpc/binder/ApiConstants.java +++ b/binder/src/main/java/io/grpc/binder/ApiConstants.java @@ -17,7 +17,9 @@ package io.grpc.binder; import android.content.Intent; +import android.os.UserHandle; import io.grpc.ExperimentalApi; +import io.grpc.NameResolver; /** Constant parts of the gRPC binder transport public API. */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022") @@ -29,4 +31,16 @@ private ApiConstants() {} * themselves in a {@link android.app.Service#onBind(Intent)} call. */ public static final String ACTION_BIND = "grpc.io.action.BIND"; + + /** + * Specifies the Android user in which target URIs should be resolved. + * + *

{@link UserHandle} can't reasonably be encoded in a target URI string. Instead, all + * {@link io.grpc.NameResolverProvider}s producing {@link AndroidComponentAddress}es should let + * clients address servers in another Android user using this argument. + * + *

See also {@link AndroidComponentAddress#getTargetUser()}. + */ + public static final NameResolver.Args.Key TARGET_ANDROID_USER = + NameResolver.Args.Key.create("target-android-user"); } From 5e8abc6774348ef93112a2bad2eddc5069013bf3 Mon Sep 17 00:00:00 2001 From: vinodhabib <47808007+vinodhabib@users.noreply.github.com> Date: Tue, 24 Dec 2024 06:48:03 +0000 Subject: [PATCH 126/591] examples: Updated the attachHeaders to newAttachHeadersInterceptor in HeaderClientInterceptor (#11759) --- .../java/io/grpc/examples/header/HeaderClientInterceptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/io/grpc/examples/header/HeaderClientInterceptor.java b/examples/src/main/java/io/grpc/examples/header/HeaderClientInterceptor.java index b9a73931299..2a60eeda6c4 100644 --- a/examples/src/main/java/io/grpc/examples/header/HeaderClientInterceptor.java +++ b/examples/src/main/java/io/grpc/examples/header/HeaderClientInterceptor.java @@ -52,7 +52,7 @@ public void start(Listener responseListener, Metadata headers) { public void onHeaders(Metadata headers) { /** * if you don't need receive header from server, - * you can use {@link io.grpc.stub.MetadataUtils#attachHeaders} + * you can use {@link io.grpc.stub.MetadataUtils#newAttachHeadersInterceptor} * directly to send header */ logger.info("header received from server:" + headers); From 8c261c3f28c7d83e1e976acfc34b62027078252b Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Thu, 26 Dec 2024 15:31:34 -0600 Subject: [PATCH 127/591] Fix typo in deprecated blocking stub javadoc. (#11772) --- .../main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java | 2 +- .../grpc/testing/integration/LoadBalancerStatsServiceGrpc.java | 2 +- .../grpc/io/grpc/testing/integration/MetricsServiceGrpc.java | 2 +- .../grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java | 2 +- .../debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java | 2 +- .../io/grpc/testing/integration/UnimplementedServiceGrpc.java | 2 +- .../integration/XdsUpdateClientConfigureServiceGrpc.java | 2 +- .../io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java | 2 +- .../grpc/testing/integration/LoadBalancerStatsServiceGrpc.java | 2 +- .../grpc/io/grpc/testing/integration/MetricsServiceGrpc.java | 2 +- .../grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java | 2 +- .../grpc/io/grpc/testing/integration/TestServiceGrpc.java | 2 +- .../io/grpc/testing/integration/UnimplementedServiceGrpc.java | 2 +- .../integration/XdsUpdateClientConfigureServiceGrpc.java | 2 +- .../io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java | 2 +- .../grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java | 2 +- .../io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java | 2 +- .../main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java | 2 +- compiler/src/java_plugin/cpp/java_generator.cpp | 2 +- compiler/src/test/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/test/golden/TestService.java.txt | 2 +- compiler/src/testLite/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/testLite/golden/TestService.java.txt | 2 +- .../src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java | 2 +- .../grpc/testing/integration/LoadBalancerStatsServiceGrpc.java | 2 +- .../grpc/io/grpc/testing/integration/MetricsServiceGrpc.java | 2 +- .../grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java | 2 +- .../main/grpc/io/grpc/testing/integration/TestServiceGrpc.java | 2 +- .../io/grpc/testing/integration/UnimplementedServiceGrpc.java | 2 +- .../integration/XdsUpdateClientConfigureServiceGrpc.java | 2 +- .../io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java | 2 +- .../generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java | 2 +- .../main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java | 2 +- .../grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java | 2 +- .../generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java | 2 +- .../src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java | 2 +- .../main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java | 2 +- .../grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java | 2 +- .../io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java | 2 +- .../grpc/reflection/testing/AnotherReflectableServiceGrpc.java | 2 +- .../grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java | 2 +- .../grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java | 2 +- .../main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java | 2 +- .../grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java | 2 +- .../service/discovery/v3/AggregatedDiscoveryServiceGrpc.java | 2 +- .../envoy/service/load_stats/v3/LoadReportingServiceGrpc.java | 2 +- .../service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java | 2 +- .../service/status/v3/ClientStatusDiscoveryServiceGrpc.java | 2 +- 48 files changed, 48 insertions(+), 48 deletions(-) diff --git a/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java b/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java index bfb98ea3e68..91e88f331d7 100644 --- a/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java +++ b/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java @@ -204,7 +204,7 @@ protected HandshakerServiceBlockingV2Stub build( } /** - * A stub to allow clients to do llimited synchronous rpc calls to service HandshakerService. + * A stub to allow clients to do limited synchronous rpc calls to service HandshakerService. */ public static final class HandshakerServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java index fe8c30ba3c4..120033a8051 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java @@ -262,7 +262,7 @@ public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse } /** - * A stub to allow clients to do llimited synchronous rpc calls to service LoadBalancerStatsService. + * A stub to allow clients to do limited synchronous rpc calls to service LoadBalancerStatsService. *

    * A service used to obtain stats for verifying LB behavior.
    * 
diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java index 57a7db1ec89..489838ddc6c 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java @@ -252,7 +252,7 @@ public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testin } /** - * A stub to allow clients to do llimited synchronous rpc calls to service MetricsService. + * A stub to allow clients to do limited synchronous rpc calls to service MetricsService. */ public static final class MetricsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java index 79f0bdf0fab..e0ea29e42e7 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java @@ -244,7 +244,7 @@ public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.i } /** - * A stub to allow clients to do llimited synchronous rpc calls to service ReconnectService. + * A stub to allow clients to do limited synchronous rpc calls to service ReconnectService. *
    * A service used to control reconnect server.
    * 
diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java index 1086974d823..a0f44f46473 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java @@ -671,7 +671,7 @@ public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.t } /** - * A stub to allow clients to do llimited synchronous rpc calls to service TestService. + * A stub to allow clients to do limited synchronous rpc calls to service TestService. *
    * A simple service to test the various types of RPCs and experiment with
    * performance with various types of payload.
diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java
index be3d1eab13a..f758c2d0840 100644
--- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java
+++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java
@@ -206,7 +206,7 @@ public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.t
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service UnimplementedService.
+   * A stub to allow clients to do limited synchronous rpc calls to service UnimplementedService.
    * 
    * A simple service NOT implemented at servers so clients can test for
    * that case.
diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java
index f72a09192f0..5fa43e4721a 100644
--- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java
+++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java
@@ -201,7 +201,7 @@ public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service XdsUpdateClientConfigureService.
+   * A stub to allow clients to do limited synchronous rpc calls to service XdsUpdateClientConfigureService.
    * 
    * A service to dynamically update the configuration of an xDS test client.
    * 
diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java index 3258354cc20..2492ec0f90b 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java @@ -244,7 +244,7 @@ public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testi } /** - * A stub to allow clients to do llimited synchronous rpc calls to service XdsUpdateHealthService. + * A stub to allow clients to do limited synchronous rpc calls to service XdsUpdateHealthService. *
    * A service to remotely control health status of an xDS test server.
    * 
diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java index fe8c30ba3c4..120033a8051 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java @@ -262,7 +262,7 @@ public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse } /** - * A stub to allow clients to do llimited synchronous rpc calls to service LoadBalancerStatsService. + * A stub to allow clients to do limited synchronous rpc calls to service LoadBalancerStatsService. *
    * A service used to obtain stats for verifying LB behavior.
    * 
diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java index 57a7db1ec89..489838ddc6c 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java @@ -252,7 +252,7 @@ public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testin } /** - * A stub to allow clients to do llimited synchronous rpc calls to service MetricsService. + * A stub to allow clients to do limited synchronous rpc calls to service MetricsService. */ public static final class MetricsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java index 79f0bdf0fab..e0ea29e42e7 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java @@ -244,7 +244,7 @@ public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.i } /** - * A stub to allow clients to do llimited synchronous rpc calls to service ReconnectService. + * A stub to allow clients to do limited synchronous rpc calls to service ReconnectService. *
    * A service used to control reconnect server.
    * 
diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java index 1086974d823..a0f44f46473 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java @@ -671,7 +671,7 @@ public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.t } /** - * A stub to allow clients to do llimited synchronous rpc calls to service TestService. + * A stub to allow clients to do limited synchronous rpc calls to service TestService. *
    * A simple service to test the various types of RPCs and experiment with
    * performance with various types of payload.
diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java
index be3d1eab13a..f758c2d0840 100644
--- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java
+++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java
@@ -206,7 +206,7 @@ public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.t
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service UnimplementedService.
+   * A stub to allow clients to do limited synchronous rpc calls to service UnimplementedService.
    * 
    * A simple service NOT implemented at servers so clients can test for
    * that case.
diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java
index f72a09192f0..5fa43e4721a 100644
--- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java
+++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java
@@ -201,7 +201,7 @@ public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service XdsUpdateClientConfigureService.
+   * A stub to allow clients to do limited synchronous rpc calls to service XdsUpdateClientConfigureService.
    * 
    * A service to dynamically update the configuration of an xDS test client.
    * 
diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java index 3258354cc20..2492ec0f90b 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java @@ -244,7 +244,7 @@ public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testi } /** - * A stub to allow clients to do llimited synchronous rpc calls to service XdsUpdateHealthService. + * A stub to allow clients to do limited synchronous rpc calls to service XdsUpdateHealthService. *
    * A service to remotely control health status of an xDS test server.
    * 
diff --git a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java index 9422ed7b1fa..242d4551d6e 100644 --- a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java +++ b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java @@ -461,7 +461,7 @@ public io.grpc.benchmarks.proto.Messages.SimpleResponse unaryCall(io.grpc.benchm } /** - * A stub to allow clients to do llimited synchronous rpc calls to service BenchmarkService. + * A stub to allow clients to do limited synchronous rpc calls to service BenchmarkService. */ public static final class BenchmarkServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java index a7f2fdd3b5e..8f466185ea0 100644 --- a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java +++ b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java @@ -187,7 +187,7 @@ public io.grpc.benchmarks.proto.Control.Void reportScenario(io.grpc.benchmarks.p } /** - * A stub to allow clients to do llimited synchronous rpc calls to service ReportQpsScenarioService. + * A stub to allow clients to do limited synchronous rpc calls to service ReportQpsScenarioService. */ public static final class ReportQpsScenarioServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java index bf6c115be35..11859482972 100644 --- a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java +++ b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java @@ -407,7 +407,7 @@ public io.grpc.benchmarks.proto.Control.Void quitWorker(io.grpc.benchmarks.proto } /** - * A stub to allow clients to do llimited synchronous rpc calls to service WorkerService. + * A stub to allow clients to do limited synchronous rpc calls to service WorkerService. */ public static final class WorkerServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/compiler/src/java_plugin/cpp/java_generator.cpp b/compiler/src/java_plugin/cpp/java_generator.cpp index 5d12beb87c6..df96bb1c1b2 100644 --- a/compiler/src/java_plugin/cpp/java_generator.cpp +++ b/compiler/src/java_plugin/cpp/java_generator.cpp @@ -412,7 +412,7 @@ static void GrpcWriteServiceDocComment(Printer* printer, printer->Print(vars, " * A stub to allow clients to do asynchronous rpc calls to service $service$.\n"); break; case BLOCKING_CLIENT_IMPL: - printer->Print(vars, " * A stub to allow clients to do llimited synchronous rpc calls to service $service$.\n"); + printer->Print(vars, " * A stub to allow clients to do limited synchronous rpc calls to service $service$.\n"); break; case BLOCKING_V2_CLIENT_IMPL: printer->Print(vars, " * A stub to allow clients to do synchronous rpc calls to service $service$.\n"); diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index c97bc9d44c9..01af7872648 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -210,7 +210,7 @@ public final class TestDeprecatedServiceGrpc { } /** - * A stub to allow clients to do llimited synchronous rpc calls to service TestDeprecatedService. + * A stub to allow clients to do limited synchronous rpc calls to service TestDeprecatedService. *
    * Test service that has been deprecated and should generate with Java's @Deprecated annotation
    * 
diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index 229b7dcc252..b82475642b0 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -675,7 +675,7 @@ public final class TestServiceGrpc { } /** - * A stub to allow clients to do llimited synchronous rpc calls to service TestService. + * A stub to allow clients to do limited synchronous rpc calls to service TestService. *
    * Test service that supports all call types.
    * 
diff --git a/compiler/src/testLite/golden/TestDeprecatedService.java.txt b/compiler/src/testLite/golden/TestDeprecatedService.java.txt index fb0bfb8c38d..22a57c71523 100644 --- a/compiler/src/testLite/golden/TestDeprecatedService.java.txt +++ b/compiler/src/testLite/golden/TestDeprecatedService.java.txt @@ -206,7 +206,7 @@ public final class TestDeprecatedServiceGrpc { } /** - * A stub to allow clients to do llimited synchronous rpc calls to service TestDeprecatedService. + * A stub to allow clients to do limited synchronous rpc calls to service TestDeprecatedService. *
    * Test service that has been deprecated and should generate with Java's @Deprecated annotation
    * 
diff --git a/compiler/src/testLite/golden/TestService.java.txt b/compiler/src/testLite/golden/TestService.java.txt index 75101ddd7b3..d13d9959a4a 100644 --- a/compiler/src/testLite/golden/TestService.java.txt +++ b/compiler/src/testLite/golden/TestService.java.txt @@ -664,7 +664,7 @@ public final class TestServiceGrpc { } /** - * A stub to allow clients to do llimited synchronous rpc calls to service TestService. + * A stub to allow clients to do limited synchronous rpc calls to service TestService. *
    * Test service that supports all call types.
    * 
diff --git a/grpclb/src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java b/grpclb/src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java index 80acb5d7bf3..2a81dfe4ee5 100644 --- a/grpclb/src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java +++ b/grpclb/src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java @@ -189,7 +189,7 @@ protected LoadBalancerBlockingV2Stub build( } /** - * A stub to allow clients to do llimited synchronous rpc calls to service LoadBalancer. + * A stub to allow clients to do limited synchronous rpc calls to service LoadBalancer. */ public static final class LoadBalancerBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java index fcdd4491938..f060a1308d8 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java @@ -264,7 +264,7 @@ public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse } /** - * A stub to allow clients to do llimited synchronous rpc calls to service LoadBalancerStatsService. + * A stub to allow clients to do limited synchronous rpc calls to service LoadBalancerStatsService. *
    * A service used to obtain stats for verifying LB behavior.
    * 
diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java index 7084ee53781..3104f7b263e 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java @@ -254,7 +254,7 @@ public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testin } /** - * A stub to allow clients to do llimited synchronous rpc calls to service MetricsService. + * A stub to allow clients to do limited synchronous rpc calls to service MetricsService. */ public static final class MetricsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java index 68a0faa35c7..39df95f4d90 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java @@ -246,7 +246,7 @@ public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.i } /** - * A stub to allow clients to do llimited synchronous rpc calls to service ReconnectService. + * A stub to allow clients to do limited synchronous rpc calls to service ReconnectService. *
    * A service used to control reconnect server.
    * 
diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java index 6855b61c63a..2b519686d85 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java @@ -679,7 +679,7 @@ public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.t } /** - * A stub to allow clients to do llimited synchronous rpc calls to service TestService. + * A stub to allow clients to do limited synchronous rpc calls to service TestService. *
    * A simple service to test the various types of RPCs and experiment with
    * performance with various types of payload.
diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java
index cd77c1cfa5c..400ac6dc4a3 100644
--- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java
+++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java
@@ -207,7 +207,7 @@ public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.t
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service UnimplementedService.
+   * A stub to allow clients to do limited synchronous rpc calls to service UnimplementedService.
    * 
    * A simple service NOT implemented at servers so clients can test for
    * that case.
diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java
index 545b69243f1..9cc13780723 100644
--- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java
+++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java
@@ -202,7 +202,7 @@ public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service XdsUpdateClientConfigureService.
+   * A stub to allow clients to do limited synchronous rpc calls to service XdsUpdateClientConfigureService.
    * 
    * A service to dynamically update the configuration of an xDS test client.
    * 
diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java index 7120a9d1c85..8243ba713fa 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java @@ -246,7 +246,7 @@ public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testi } /** - * A stub to allow clients to do llimited synchronous rpc calls to service XdsUpdateHealthService. + * A stub to allow clients to do limited synchronous rpc calls to service XdsUpdateHealthService. *
    * A service to remotely control health status of an xDS test server.
    * 
diff --git a/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java b/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java index 5c01afd15c6..3cb27ac1067 100644 --- a/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java +++ b/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java @@ -231,7 +231,7 @@ public io.istio.test.Echo.ForwardEchoResponse forwardEcho(io.istio.test.Echo.For } /** - * A stub to allow clients to do llimited synchronous rpc calls to service EchoTestService. + * A stub to allow clients to do limited synchronous rpc calls to service EchoTestService. */ public static final class EchoTestServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java b/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java index 98768d37611..558a223173c 100644 --- a/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java +++ b/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java @@ -187,7 +187,7 @@ public io.grpc.lookup.v1.RouteLookupResponse routeLookup(io.grpc.lookup.v1.Route } /** - * A stub to allow clients to do llimited synchronous rpc calls to service RouteLookupService. + * A stub to allow clients to do limited synchronous rpc calls to service RouteLookupService. */ public static final class RouteLookupServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java b/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java index 69f1d78f55e..95e217ac695 100644 --- a/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java +++ b/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java @@ -192,7 +192,7 @@ protected S2AServiceBlockingV2Stub build( } /** - * A stub to allow clients to do llimited synchronous rpc calls to service S2AService. + * A stub to allow clients to do limited synchronous rpc calls to service S2AService. */ public static final class S2AServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java b/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java index 8d8c0eb2971..7dd74034efe 100644 --- a/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java +++ b/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java @@ -582,7 +582,7 @@ public io.grpc.channelz.v1.GetSocketResponse getSocket(io.grpc.channelz.v1.GetSo } /** - * A stub to allow clients to do llimited synchronous rpc calls to service Channelz. + * A stub to allow clients to do limited synchronous rpc calls to service Channelz. *
    * Channelz is a service exposed by gRPC servers that provides detailed debug
    * information.
diff --git a/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java b/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java
index d7795a5fdbf..9786392cfe6 100644
--- a/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java
+++ b/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java
@@ -290,7 +290,7 @@ public io.grpc.health.v1.HealthCheckResponse check(io.grpc.health.v1.HealthCheck
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service Health.
+   * A stub to allow clients to do limited synchronous rpc calls to service Health.
    */
   public static final class HealthBlockingStub
       extends io.grpc.stub.AbstractBlockingStub {
diff --git a/services/src/generated/main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java b/services/src/generated/main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java
index 3089b8302cc..19bf6ed90b3 100644
--- a/services/src/generated/main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java
+++ b/services/src/generated/main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java
@@ -192,7 +192,7 @@ protected ServerReflectionBlockingV2Stub build(
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service ServerReflection.
+   * A stub to allow clients to do limited synchronous rpc calls to service ServerReflection.
    */
   public static final class ServerReflectionBlockingStub
       extends io.grpc.stub.AbstractBlockingStub {
diff --git a/services/src/generated/main/grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java b/services/src/generated/main/grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java
index a0fc1bcf2de..152a8f7c81d 100644
--- a/services/src/generated/main/grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java
+++ b/services/src/generated/main/grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java
@@ -192,7 +192,7 @@ protected ServerReflectionBlockingV2Stub build(
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service ServerReflection.
+   * A stub to allow clients to do limited synchronous rpc calls to service ServerReflection.
    */
   public static final class ServerReflectionBlockingStub
       extends io.grpc.stub.AbstractBlockingStub {
diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java
index 843d1e30135..a53d199ef52 100644
--- a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java
+++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java
@@ -202,7 +202,7 @@ public io.grpc.reflection.testing.DynamicReply method(io.grpc.reflection.testing
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service AnotherDynamicService.
+   * A stub to allow clients to do limited synchronous rpc calls to service AnotherDynamicService.
    * 
    * AnotherDynamicService
    * 
diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java index 41c7f5468da..a22138ecb03 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java @@ -178,7 +178,7 @@ public io.grpc.reflection.testing.Reply method(io.grpc.reflection.testing.Reques } /** - * A stub to allow clients to do llimited synchronous rpc calls to service AnotherReflectableService. + * A stub to allow clients to do limited synchronous rpc calls to service AnotherReflectableService. */ public static final class AnotherReflectableServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java index 4878109dc81..6e9dfac72db 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java @@ -202,7 +202,7 @@ public io.grpc.reflection.testing.DynamicReply method(io.grpc.reflection.testing } /** - * A stub to allow clients to do llimited synchronous rpc calls to service DynamicService. + * A stub to allow clients to do limited synchronous rpc calls to service DynamicService. *
    * A DynamicService
    * 
diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java index 3e6cf5b8030..aeefc77aff8 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java @@ -178,7 +178,7 @@ public io.grpc.reflection.testing.Reply method(io.grpc.reflection.testing.Reques } /** - * A stub to allow clients to do llimited synchronous rpc calls to service ReflectableService. + * A stub to allow clients to do limited synchronous rpc calls to service ReflectableService. */ public static final class ReflectableServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { diff --git a/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java b/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java index fac753a1b32..8b33691535d 100644 --- a/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java +++ b/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java @@ -394,7 +394,7 @@ public io.grpc.testing.protobuf.SimpleResponse unaryRpc(io.grpc.testing.protobuf } /** - * A stub to allow clients to do llimited synchronous rpc calls to service SimpleService. + * A stub to allow clients to do limited synchronous rpc calls to service SimpleService. *
    * A simple service for test.
    * 
diff --git a/xds/src/generated/thirdparty/grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java b/xds/src/generated/thirdparty/grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java index bb74f2e0cb2..cb129fd31ba 100644 --- a/xds/src/generated/thirdparty/grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java @@ -230,7 +230,7 @@ protected OpenRcaServiceBlockingV2Stub build( } /** - * A stub to allow clients to do llimited synchronous rpc calls to service OpenRcaService. + * A stub to allow clients to do limited synchronous rpc calls to service OpenRcaService. *
    * Out-of-band (OOB) load reporting service for the additional load reporting
    * agent that does not sit in the request path. Reports are periodically sampled
diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java
index 192b23a3db1..b93a5df2b71 100644
--- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java
+++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java
@@ -284,7 +284,7 @@ protected AggregatedDiscoveryServiceBlockingV2Stub build(
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service AggregatedDiscoveryService.
+   * A stub to allow clients to do limited synchronous rpc calls to service AggregatedDiscoveryService.
    * 
    * See https://github.com/envoyproxy/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
diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/load_stats/v3/LoadReportingServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/load_stats/v3/LoadReportingServiceGrpc.java
index fbe8dd5a5ec..3bce13a7cf5 100644
--- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/load_stats/v3/LoadReportingServiceGrpc.java
+++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/load_stats/v3/LoadReportingServiceGrpc.java
@@ -267,7 +267,7 @@ protected LoadReportingServiceBlockingV2Stub build(
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service LoadReportingService.
+   * A stub to allow clients to do limited synchronous rpc calls to service LoadReportingService.
    */
   public static final class LoadReportingServiceBlockingStub
       extends io.grpc.stub.AbstractBlockingStub {
diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java
index fa1d235f082..f05d38290dd 100644
--- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java
+++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java
@@ -207,7 +207,7 @@ protected RateLimitQuotaServiceBlockingV2Stub build(
   }
 
   /**
-   * A stub to allow clients to do llimited synchronous rpc calls to service RateLimitQuotaService.
+   * A stub to allow clients to do limited synchronous rpc calls to service RateLimitQuotaService.
    * 
    * Defines the Rate Limit Quota Service (RLQS).
    * 
diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java index 75ce84b77f9..1a3240875cc 100644 --- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java @@ -258,7 +258,7 @@ public io.envoyproxy.envoy.service.status.v3.ClientStatusResponse fetchClientSta } /** - * A stub to allow clients to do llimited synchronous rpc calls to service ClientStatusDiscoveryService. + * A stub to allow clients to do limited synchronous rpc calls to service ClientStatusDiscoveryService. *
    * CSDS is Client Status Discovery Service. It can be used to get the status of
    * an xDS-compliant client from the management server's point of view. It can

From c96e926e65357d50e338a3e29361bb44664b372b Mon Sep 17 00:00:00 2001
From: Eric Anderson 
Date: Fri, 27 Dec 2024 10:41:51 -0800
Subject: [PATCH 128/591] examples: Remove references to
 maven-central.storage-download.googleapis.com

As stated [on its main page][index], it isn't officially supported, so
we shouldn't include it in our examples.

[index]: https://maven-central.storage-download.googleapis.com/index.html
---
 examples/build.gradle                               | 2 --
 examples/example-alts/build.gradle                  | 3 ---
 examples/example-alts/settings.gradle               | 3 ---
 examples/example-debug/build.gradle                 | 2 --
 examples/example-dualstack/build.gradle             | 2 --
 examples/example-dualstack/settings.gradle          | 3 ---
 examples/example-gauth/build.gradle                 | 3 ---
 examples/example-gauth/settings.gradle              | 3 ---
 examples/example-gcp-csm-observability/build.gradle | 3 ---
 examples/example-gcp-observability/build.gradle     | 3 ---
 examples/example-hostname/build.gradle              | 2 --
 examples/example-jwt-auth/build.gradle              | 4 +---
 examples/example-jwt-auth/settings.gradle           | 3 ---
 examples/example-oauth/build.gradle                 | 4 +---
 examples/example-oauth/settings.gradle              | 3 ---
 examples/example-opentelemetry/build.gradle         | 3 ---
 examples/example-orca/build.gradle                  | 2 --
 examples/example-reflection/build.gradle            | 2 --
 examples/example-servlet/build.gradle               | 3 +--
 examples/example-servlet/settings.gradle            | 3 ---
 examples/example-tls/build.gradle                   | 3 ---
 examples/example-tls/settings.gradle                | 3 ---
 examples/example-xds/build.gradle                   | 2 --
 examples/settings.gradle                            | 3 ---
 24 files changed, 3 insertions(+), 64 deletions(-)

diff --git a/examples/build.gradle b/examples/build.gradle
index c50a5ab8eb7..62428d7f81d 100644
--- a/examples/build.gradle
+++ b/examples/build.gradle
@@ -7,8 +7,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/" }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle
index f0dc0bd8850..2a119c5b80d 100644
--- a/examples/example-alts/build.gradle
+++ b/examples/example-alts/build.gradle
@@ -7,9 +7,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/"
-    }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/example-alts/settings.gradle b/examples/example-alts/settings.gradle
index 273558dd9cf..c665ff96674 100644
--- a/examples/example-alts/settings.gradle
+++ b/examples/example-alts/settings.gradle
@@ -1,8 +1,5 @@
 pluginManagement {
     repositories {
-        maven { // The google mirror is less flaky than mavenCentral()
-            url "https://maven-central.storage-download.googleapis.com/maven2/"
-        }
         gradlePluginPortal()
     }
 }
diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle
index 7cd3b5de38e..eeabbef9473 100644
--- a/examples/example-debug/build.gradle
+++ b/examples/example-debug/build.gradle
@@ -9,8 +9,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/" }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle
index 875bdfda623..1e0879f38c4 100644
--- a/examples/example-dualstack/build.gradle
+++ b/examples/example-dualstack/build.gradle
@@ -9,8 +9,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/" }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/example-dualstack/settings.gradle b/examples/example-dualstack/settings.gradle
index 0aae8f7304e..49a762696f7 100644
--- a/examples/example-dualstack/settings.gradle
+++ b/examples/example-dualstack/settings.gradle
@@ -1,8 +1,5 @@
 pluginManagement {
     repositories {
-        maven { // The google mirror is less flaky than mavenCentral()
-            url "https://maven-central.storage-download.googleapis.com/maven2/"
-        }
         gradlePluginPortal()
     }
 }
diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle
index 3ff0284c5a1..844e86b71af 100644
--- a/examples/example-gauth/build.gradle
+++ b/examples/example-gauth/build.gradle
@@ -7,9 +7,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/"
-    }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/example-gauth/settings.gradle b/examples/example-gauth/settings.gradle
index 273558dd9cf..c665ff96674 100644
--- a/examples/example-gauth/settings.gradle
+++ b/examples/example-gauth/settings.gradle
@@ -1,8 +1,5 @@
 pluginManagement {
     repositories {
-        maven { // The google mirror is less flaky than mavenCentral()
-            url "https://maven-central.storage-download.googleapis.com/maven2/"
-        }
         gradlePluginPortal()
     }
 }
diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle
index c72f1ba9753..40dd86e4ba1 100644
--- a/examples/example-gcp-csm-observability/build.gradle
+++ b/examples/example-gcp-csm-observability/build.gradle
@@ -8,9 +8,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/"
-    }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle
index 5199105baae..8fe78c8b6ee 100644
--- a/examples/example-gcp-observability/build.gradle
+++ b/examples/example-gcp-observability/build.gradle
@@ -8,9 +8,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/"
-    }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle
index 784b0c71a47..a8d1d994a40 100644
--- a/examples/example-hostname/build.gradle
+++ b/examples/example-hostname/build.gradle
@@ -7,8 +7,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/" }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle
index ba7aeb05b19..831486ca872 100644
--- a/examples/example-jwt-auth/build.gradle
+++ b/examples/example-jwt-auth/build.gradle
@@ -7,9 +7,7 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/"
-    }
+    mavenCentral()
     mavenLocal()
 }
 
diff --git a/examples/example-jwt-auth/settings.gradle b/examples/example-jwt-auth/settings.gradle
index 273558dd9cf..c665ff96674 100644
--- a/examples/example-jwt-auth/settings.gradle
+++ b/examples/example-jwt-auth/settings.gradle
@@ -1,8 +1,5 @@
 pluginManagement {
     repositories {
-        maven { // The google mirror is less flaky than mavenCentral()
-            url "https://maven-central.storage-download.googleapis.com/maven2/"
-        }
         gradlePluginPortal()
     }
 }
diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle
index 6e07cbc7447..08d53a247c5 100644
--- a/examples/example-oauth/build.gradle
+++ b/examples/example-oauth/build.gradle
@@ -7,9 +7,7 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/"
-    }
+    mavenCentral()
     mavenLocal()
 }
 
diff --git a/examples/example-oauth/settings.gradle b/examples/example-oauth/settings.gradle
index 273558dd9cf..c665ff96674 100644
--- a/examples/example-oauth/settings.gradle
+++ b/examples/example-oauth/settings.gradle
@@ -1,8 +1,5 @@
 pluginManagement {
     repositories {
-        maven { // The google mirror is less flaky than mavenCentral()
-            url "https://maven-central.storage-download.googleapis.com/maven2/"
-        }
         gradlePluginPortal()
     }
 }
diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle
index 5c1e419c82e..4c6f4803014 100644
--- a/examples/example-opentelemetry/build.gradle
+++ b/examples/example-opentelemetry/build.gradle
@@ -7,9 +7,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/"
-    }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle
index babd0c7d18e..a9fb615446c 100644
--- a/examples/example-orca/build.gradle
+++ b/examples/example-orca/build.gradle
@@ -7,8 +7,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/" }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle
index d82f5ecda7e..2549f5eecc1 100644
--- a/examples/example-reflection/build.gradle
+++ b/examples/example-reflection/build.gradle
@@ -7,8 +7,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/" }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle
index 50b9d47632c..df846ed42be 100644
--- a/examples/example-servlet/build.gradle
+++ b/examples/example-servlet/build.gradle
@@ -6,8 +6,7 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/" }
+    mavenCentral()
     mavenLocal()
 }
 
diff --git a/examples/example-servlet/settings.gradle b/examples/example-servlet/settings.gradle
index 273558dd9cf..c665ff96674 100644
--- a/examples/example-servlet/settings.gradle
+++ b/examples/example-servlet/settings.gradle
@@ -1,8 +1,5 @@
 pluginManagement {
     repositories {
-        maven { // The google mirror is less flaky than mavenCentral()
-            url "https://maven-central.storage-download.googleapis.com/maven2/"
-        }
         gradlePluginPortal()
     }
 }
diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle
index 2d61862700b..61a9706ebf6 100644
--- a/examples/example-tls/build.gradle
+++ b/examples/example-tls/build.gradle
@@ -7,9 +7,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/"
-    }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/example-tls/settings.gradle b/examples/example-tls/settings.gradle
index 273558dd9cf..c665ff96674 100644
--- a/examples/example-tls/settings.gradle
+++ b/examples/example-tls/settings.gradle
@@ -1,8 +1,5 @@
 pluginManagement {
     repositories {
-        maven { // The google mirror is less flaky than mavenCentral()
-            url "https://maven-central.storage-download.googleapis.com/maven2/"
-        }
         gradlePluginPortal()
     }
 }
diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle
index bb404bdb07c..a1e2501a7b6 100644
--- a/examples/example-xds/build.gradle
+++ b/examples/example-xds/build.gradle
@@ -7,8 +7,6 @@ plugins {
 }
 
 repositories {
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/" }
     mavenCentral()
     mavenLocal()
 }
diff --git a/examples/settings.gradle b/examples/settings.gradle
index 0473750a54f..dadd24b1c0f 100644
--- a/examples/settings.gradle
+++ b/examples/settings.gradle
@@ -1,8 +1,5 @@
 pluginManagement {
     repositories {
-        maven { // The google mirror is less flaky than mavenCentral()
-            url "https://maven-central.storage-download.googleapis.com/maven2/"
-        }
         gradlePluginPortal()
     }
 }

From fdb9a5a94fc07109a5b187d3e2adad6c508c8349 Mon Sep 17 00:00:00 2001
From: Eric Anderson 
Date: Fri, 27 Dec 2024 10:45:42 -0800
Subject: [PATCH 129/591] gae-interop-testing: Remove duplicate repositories

These repositories are already included from the main build.gradle, so
they don't do anything. Much less do they need to be defined twice in
the same file.
---
 gae-interop-testing/gae-jdk8/build.gradle | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/gae-interop-testing/gae-jdk8/build.gradle b/gae-interop-testing/gae-jdk8/build.gradle
index 3ed395198c2..14abbc05a9b 100644
--- a/gae-interop-testing/gae-jdk8/build.gradle
+++ b/gae-interop-testing/gae-jdk8/build.gradle
@@ -14,10 +14,6 @@
 
 buildscript {
     // Configuration for building
-    repositories {
-        maven { // The google mirror is less flaky than mavenCentral()
-            url "https://maven-central.storage-download.googleapis.com/maven2/" }
-    }
     dependencies {
         classpath 'com.squareup.okhttp:okhttp:2.7.4'
     }
@@ -33,13 +29,6 @@ plugins {
 
 description = 'gRPC: gae interop testing (jdk8)'
 
-repositories {
-    // repositories for Jar's you access in your code
-    mavenLocal()
-    maven { // The google mirror is less flaky than mavenCentral()
-        url "https://maven-central.storage-download.googleapis.com/maven2/" }
-}
-
 dependencies {
     providedCompile group: 'javax.servlet', name: 'servlet-api', version:'2.5'
     runtimeOnly 'com.google.appengine:appengine-api-1.0-sdk:1.9.59'

From b272f634c10ad9415649c0499b2ddc736e3f41d8 Mon Sep 17 00:00:00 2001
From: Eric Anderson 
Date: Fri, 27 Dec 2024 12:04:58 -0800
Subject: [PATCH 130/591] Disable Gradle Module Metadata resolution

The module metadata in Guava causes the -jre version to be selected even
when you choose the -android version. Gradle did not give any clues that
this was happening, and while
`println(configurations.compileClasspath.resolve())` shows the different
jar in use, most other diagonstics don't. dependencyInsight can show you
this is happening, but only if you know which dependency has a problem
and read Guava's module metadata first to understand the significance of
the results.

You could argue this is a Guava-specific problem. I was able to get
parts of our build working with attributes and resolutionStrategy
configurations mentioned at
https://github.com/google/guava/releases/tag/v32.1.0 , so that only
Guava would be changed. But it was fickle giving poor error messages or
silently swapping back to the -jre version.

Given the weak debuggability, the added complexity, and the lack of
value module metadata is providing us, disabling module metadata for our
entire build seems prudent.

See https://github.com/google/guava/issues/7575
---
 android-interop-testing/build.gradle |  1 -
 android/build.gradle                 |  1 -
 binder/build.gradle                  |  1 -
 build.gradle                         | 21 ++++++++++++++++++---
 cronet/build.gradle                  |  1 -
 gradle/libs.versions.toml            |  2 +-
 6 files changed, 19 insertions(+), 8 deletions(-)

diff --git a/android-interop-testing/build.gradle b/android-interop-testing/build.gradle
index 9b3b021afce..1d39aee1750 100644
--- a/android-interop-testing/build.gradle
+++ b/android-interop-testing/build.gradle
@@ -7,7 +7,6 @@ description = 'gRPC: Android Integration Testing'
 
 repositories {
     google()
-    mavenCentral()
 }
 
 android {
diff --git a/android/build.gradle b/android/build.gradle
index 3b3bfa59b96..3c717da18a3 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -31,7 +31,6 @@ android {
 
 repositories {
     google()
-    mavenCentral()
 }
 
 dependencies {
diff --git a/binder/build.gradle b/binder/build.gradle
index e18361e08b3..3390e02fce7 100644
--- a/binder/build.gradle
+++ b/binder/build.gradle
@@ -32,7 +32,6 @@ android {
 
 repositories {
     google()
-    mavenCentral()
 }
 
 dependencies {
diff --git a/build.gradle b/build.gradle
index 52ca19ab4ba..39d2b6ad289 100644
--- a/build.gradle
+++ b/build.gradle
@@ -25,9 +25,24 @@ subprojects {
 
     repositories {
         maven { // The google mirror is less flaky than mavenCentral()
-            url "https://maven-central.storage-download.googleapis.com/maven2/" }
-        mavenCentral()
-        mavenLocal()
+            url "https://maven-central.storage-download.googleapis.com/maven2/"
+            metadataSources {
+                mavenPom()
+                ignoreGradleMetadataRedirection()
+            }
+        }
+        mavenCentral() {
+            metadataSources {
+                mavenPom()
+                ignoreGradleMetadataRedirection()
+            }
+        }
+        mavenLocal() {
+            metadataSources {
+                mavenPom()
+                ignoreGradleMetadataRedirection()
+            }
+        }
     }
 
     tasks.withType(JavaCompile).configureEach {
diff --git a/cronet/build.gradle b/cronet/build.gradle
index 3252a9d249b..1a327f9f966 100644
--- a/cronet/build.gradle
+++ b/cronet/build.gradle
@@ -8,7 +8,6 @@ description = "gRPC: Cronet Android"
 
 repositories {
     google()
-    mavenCentral()
 }
 
 android {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4d871239cc2..43ec3368b76 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -62,7 +62,7 @@ jetty-servlet10 = "org.eclipse.jetty:jetty-servlet:10.0.20"
 jsr305 = "com.google.code.findbugs:jsr305:3.0.2"
 junit = "junit:junit:4.13.2"
 # 2.17+ require Java 11+ (not mentioned in release notes)
-lincheck = "org.jetbrains.kotlinx:lincheck:2.16"
+lincheck = "org.jetbrains.kotlinx:lincheck-jvm:2.16"
 # Update notes / 2023-07-19 sergiitk:
 #    Couldn't update to 5.4.0, updated to the last in 4.x line. Version 5.x breaks some tests.
 #    Error log: https://github.com/grpc/grpc-java/pull/10359#issuecomment-1632834435

From bac8b320435f7e78931c7a6d208cb5caab9026ca Mon Sep 17 00:00:00 2001
From: Benjamin Peterson 
Date: Fri, 3 Jan 2025 10:42:42 -0800
Subject: [PATCH 131/591] Fix equality and hashcode of
 CancelServerStreamCommand. (#11785)

In e036b1b198bfa2eb5fbdd27fc02a5df95ecd939b, CancelServerStreamCommand got another field. But, its hashCode and equals methods were not updated.
---
 .../java/io/grpc/netty/CancelServerStreamCommand.java     | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/netty/src/main/java/io/grpc/netty/CancelServerStreamCommand.java b/netty/src/main/java/io/grpc/netty/CancelServerStreamCommand.java
index d49e3bd672b..a3b29457670 100644
--- a/netty/src/main/java/io/grpc/netty/CancelServerStreamCommand.java
+++ b/netty/src/main/java/io/grpc/netty/CancelServerStreamCommand.java
@@ -69,13 +69,14 @@ public boolean equals(Object o) {
 
     CancelServerStreamCommand that = (CancelServerStreamCommand) o;
 
-    return Objects.equal(this.stream, that.stream)
-        && Objects.equal(this.reason, that.reason);
+    return this.stream.equals(that.stream)
+        && this.reason.equals(that.reason)
+        && this.peerNotify.equals(that.peerNotify);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hashCode(stream, reason);
+    return Objects.hashCode(stream, reason, peerNotify);
   }
 
   @Override
@@ -83,6 +84,7 @@ public String toString() {
     return MoreObjects.toStringHelper(this)
         .add("stream", stream)
         .add("reason", reason)
+        .add("peerNotify", peerNotify)
         .toString();
   }
 

From 9a712c3f77d0cd07d2b933ab4bd261ac29ba7298 Mon Sep 17 00:00:00 2001
From: Eric Anderson 
Date: Fri, 27 Dec 2024 22:56:48 -0800
Subject: [PATCH 132/591] xds: Make XdsClient.ResourceStore package-private

There's no reason to use the interface outside of
XdsClientImpl/ControlPlaneClient. Since XdsClientImpl implements the
interface directly, its methods are still public. That can be a future
cleanup.
---
 .../java/io/grpc/xds/client/XdsClient.java    |  2 +-
 .../java/io/grpc/xds/CsdsServiceTest.java     | 74 ++++---------------
 2 files changed, 16 insertions(+), 60 deletions(-)

diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClient.java b/xds/src/main/java/io/grpc/xds/client/XdsClient.java
index 36f8bd591c7..1b53f6778c7 100644
--- a/xds/src/main/java/io/grpc/xds/client/XdsClient.java
+++ b/xds/src/main/java/io/grpc/xds/client/XdsClient.java
@@ -428,7 +428,7 @@ void handleResourceResponse(
     void handleStreamClosed(Status error, boolean shouldTryFallback);
   }
 
-  public interface ResourceStore {
+  interface ResourceStore {
 
     /**
      * Returns the collection of resources currently subscribed to which have an authority matching
diff --git a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java
index df0687f6706..2c0aae24e52 100644
--- a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java
+++ b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java
@@ -299,7 +299,7 @@ private void verifyResponse(ClientStatusResponse response) {
       assertThat(response.getConfigCount()).isEqualTo(1);
       ClientConfig clientConfig = response.getConfig(0);
       verifyClientConfigNode(clientConfig);
-      verifyClientConfigNoResources(XDS_CLIENT_NO_RESOURCES, clientConfig);
+      assertThat(clientConfig.getGenericXdsConfigsList()).isEmpty();
       assertThat(clientConfig.getClientScope()).isEmpty();
     }
 
@@ -310,7 +310,7 @@ private Collection verifyMultiResponse(ClientStatusResponse response, in
       for (int i = 0; i < numExpected; i++) {
         ClientConfig clientConfig = response.getConfig(i);
         verifyClientConfigNode(clientConfig);
-        verifyClientConfigNoResources(XDS_CLIENT_NO_RESOURCES, clientConfig);
+        assertThat(clientConfig.getGenericXdsConfigsList()).isEmpty();
         clientScopes.add(clientConfig.getClientScope());
       }
 
@@ -382,16 +382,6 @@ public void getClientConfigForXdsClient_subscribedResourcesToGenericXdsConfig()
               .put(EDS, ImmutableMap.of("subscribedResourceName.EDS", METADATA_ACKED_EDS))
               .buildOrThrow();
         }
-
-        @Override
-        public Map> getSubscribedResourceTypesWithTypeUrl() {
-          return ImmutableMap.of(
-              LDS.typeUrl(), LDS,
-              RDS.typeUrl(), RDS,
-              CDS.typeUrl(), CDS,
-              EDS.typeUrl(), EDS
-          );
-        }
       };
       ClientConfig clientConfig = CsdsService.getClientConfigForXdsClient(fakeXdsClient,
           FAKE_CLIENT_SCOPE);
@@ -403,31 +393,31 @@ public Map> getSubscribedResourceTypesWithTypeUrl() {
       // is propagated to the correct resource types.
       int xdsConfigCount = clientConfig.getGenericXdsConfigsCount();
       assertThat(xdsConfigCount).isEqualTo(4);
-      Map, GenericXdsConfig> configDumps = mapConfigDumps(fakeXdsClient,
-          clientConfig);
-      assertThat(configDumps.keySet()).containsExactly(LDS, RDS, CDS, EDS);
+      Map configDumps = mapConfigDumps(clientConfig);
+      assertThat(configDumps.keySet())
+          .containsExactly(LDS.typeUrl(), RDS.typeUrl(), CDS.typeUrl(), EDS.typeUrl());
 
       // LDS.
-      GenericXdsConfig genericXdsConfigLds = configDumps.get(LDS);
+      GenericXdsConfig genericXdsConfigLds = configDumps.get(LDS.typeUrl());
       assertThat(genericXdsConfigLds.getName()).isEqualTo("subscribedResourceName.LDS");
       assertThat(genericXdsConfigLds.getClientStatus()).isEqualTo(ClientResourceStatus.ACKED);
       assertThat(genericXdsConfigLds.getVersionInfo()).isEqualTo(VERSION_ACK_LDS);
       assertThat(genericXdsConfigLds.getXdsConfig()).isEqualTo(RAW_LISTENER);
 
       // RDS.
-      GenericXdsConfig genericXdsConfigRds = configDumps.get(RDS);
+      GenericXdsConfig genericXdsConfigRds = configDumps.get(RDS.typeUrl());
       assertThat(genericXdsConfigRds.getClientStatus()).isEqualTo(ClientResourceStatus.ACKED);
       assertThat(genericXdsConfigRds.getVersionInfo()).isEqualTo(VERSION_ACK_RDS);
       assertThat(genericXdsConfigRds.getXdsConfig()).isEqualTo(RAW_ROUTE_CONFIGURATION);
 
       // CDS.
-      GenericXdsConfig genericXdsConfigCds = configDumps.get(CDS);
+      GenericXdsConfig genericXdsConfigCds = configDumps.get(CDS.typeUrl());
       assertThat(genericXdsConfigCds.getClientStatus()).isEqualTo(ClientResourceStatus.ACKED);
       assertThat(genericXdsConfigCds.getVersionInfo()).isEqualTo(VERSION_ACK_CDS);
       assertThat(genericXdsConfigCds.getXdsConfig()).isEqualTo(RAW_CLUSTER);
 
       // RDS.
-      GenericXdsConfig genericXdsConfigEds = configDumps.get(EDS);
+      GenericXdsConfig genericXdsConfigEds = configDumps.get(EDS.typeUrl());
       assertThat(genericXdsConfigEds.getClientStatus()).isEqualTo(ClientResourceStatus.ACKED);
       assertThat(genericXdsConfigEds.getVersionInfo()).isEqualTo(VERSION_ACK_EDS);
       assertThat(genericXdsConfigEds.getXdsConfig()).isEqualTo(RAW_CLUSTER_LOAD_ASSIGNMENT);
@@ -438,23 +428,11 @@ public void getClientConfigForXdsClient_noSubscribedResources() throws Interrupt
       ClientConfig clientConfig =
           CsdsService.getClientConfigForXdsClient(XDS_CLIENT_NO_RESOURCES, FAKE_CLIENT_SCOPE);
       verifyClientConfigNode(clientConfig);
-      verifyClientConfigNoResources(XDS_CLIENT_NO_RESOURCES, clientConfig);
+      assertThat(clientConfig.getGenericXdsConfigsList()).isEmpty();
       assertThat(clientConfig.getClientScope()).isEqualTo(FAKE_CLIENT_SCOPE);
     }
   }
 
-  /**
-   * Assuming {@link MetadataToProtoTests} passes, and metadata converted to corresponding
-   * config dumps correctly, perform a minimal verification of the general shape of ClientConfig.
-   */
-  private static void verifyClientConfigNoResources(FakeXdsClient xdsClient,
-                                                    ClientConfig clientConfig) {
-    int xdsConfigCount = clientConfig.getGenericXdsConfigsCount();
-    assertThat(xdsConfigCount).isEqualTo(0);
-    Map, GenericXdsConfig> configDumps = mapConfigDumps(xdsClient, clientConfig);
-    assertThat(configDumps).isEmpty();
-  }
-
   /**
    * Assuming {@link EnvoyProtoDataTest#convertNode} passes, perform a minimal check,
    * just verify the node itself is the one we expect.
@@ -465,21 +443,17 @@ private static void verifyClientConfigNode(ClientConfig clientConfig) {
     assertThat(node).isEqualTo(BOOTSTRAP_NODE.toEnvoyProtoNode());
   }
 
-  private static Map, GenericXdsConfig> mapConfigDumps(FakeXdsClient client,
-                                                                          ClientConfig config) {
-    Map, GenericXdsConfig> xdsConfigMap = new HashMap<>();
+  private static Map mapConfigDumps(ClientConfig config) {
+    Map xdsConfigMap = new HashMap<>();
     List xdsConfigList = config.getGenericXdsConfigsList();
     for (GenericXdsConfig genericXdsConfig : xdsConfigList) {
-      XdsResourceType type = client.getSubscribedResourceTypesWithTypeUrl()
-          .get(genericXdsConfig.getTypeUrl());
-      assertThat(type).isNotNull();
-      assertThat(xdsConfigMap).doesNotContainKey(type);
-      xdsConfigMap.put(type, genericXdsConfig);
+      assertThat(xdsConfigMap).doesNotContainKey(genericXdsConfig.getTypeUrl());
+      xdsConfigMap.put(genericXdsConfig.getTypeUrl(), genericXdsConfig);
     }
     return xdsConfigMap;
   }
 
-  private static class FakeXdsClient extends XdsClient implements XdsClient.ResourceStore {
+  private static class FakeXdsClient extends XdsClient {
     protected Map, Map>
         getSubscribedResourcesMetadata() {
       return ImmutableMap.of();
@@ -495,24 +469,6 @@ private static class FakeXdsClient extends XdsClient implements XdsClient.Resour
     public BootstrapInfo getBootstrapInfo() {
       return BOOTSTRAP_INFO;
     }
-
-    @Nullable
-    @Override
-    public Collection getSubscribedResources(
-        ServerInfo serverInfo, XdsResourceType type) {
-      return null;
-    }
-
-    @Override
-    public void startMissingResourceTimers(Collection resourceNames,
-                                           XdsResourceType resourceType) {
-      // do nothing
-    }
-
-    @Override
-    public Map> getSubscribedResourceTypesWithTypeUrl() {
-      return ImmutableMap.of();
-    }
   }
 
   private static class FakeXdsClientPoolFactory implements XdsClientPoolFactory {

From 1cf1927d1a7024e353f239fe5b5d50cbb49a8bae Mon Sep 17 00:00:00 2001
From: Eric Anderson 
Date: Fri, 3 Jan 2025 12:34:47 -0800
Subject: [PATCH 133/591] xds: Preserve nonce when unsubscribing type

This fixes a regression introduced in 19c9b998.

b/374697875
---
 .../java/io/grpc/xds/client/ControlPlaneClient.java    | 10 +++++++---
 .../java/io/grpc/xds/GrpcXdsClientImplTestBase.java    |  5 ++---
 2 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java
index 9c7d744816a..878f1686a89 100644
--- a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java
+++ b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java
@@ -167,9 +167,10 @@ void adjustResourceSubscription(XdsResourceType resourceType) {
     resourceStore.startMissingResourceTimers(resources, resourceType);
 
     if (resources.isEmpty()) {
-      // The resource type no longer has subscribing resources; clean up references to it
+      // The resource type no longer has subscribing resources; clean up references to it, except
+      // for nonces. If the resource type becomes used again the control plane can ignore requests
+      // for old/missing nonces. Old type's nonces are dropped when the ADS stream is restarted.
       versions.remove(resourceType);
-      adsStream.respNonces.remove(resourceType);
     }
   }
 
@@ -313,7 +314,10 @@ private class AdsStream implements XdsTransportFactory.EventHandler, String> respNonces = new HashMap<>();
     private final StreamingCall call;
     private final MethodDescriptor methodDescriptor =
diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java
index 1b0c363d94f..9a80e3c36b6 100644
--- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java
+++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java
@@ -2899,10 +2899,9 @@ public void edsCleanupNonceAfterUnsubscription() {
     verifySubscribedResourcesMetadataSizes(0, 0, 0, 0);
     call.verifyRequest(EDS, Arrays.asList(), VERSION_1, "0000", NODE);
 
-    // When re-subscribing, the version and nonce were properly forgotten, so the request is the
-    // same as the initial request
+    // When re-subscribing, the version was forgotten but not the nonce
     xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), "A.1", edsResourceWatcher);
-    call.verifyRequest(EDS, "A.1", "", "", NODE, Mockito.timeout(2000).times(2));
+    call.verifyRequest(EDS, "A.1", "", "0000", NODE, Mockito.timeout(2000));
   }
 
   @Test

From 4a0f707331fc3b23403e1549b8165e4188dddb20 Mon Sep 17 00:00:00 2001
From: Eric Anderson 
Date: Fri, 3 Jan 2025 14:52:37 -0800
Subject: [PATCH 134/591] xds: Avoid depending on io.grpc.xds.Internal* classes

Internal* classes should generally be accessors that are used outside of
the package/project. Only one attribute was used outside of xds, so
leave only that one attribute in InternalXdsAttributes. One attribute
was used by the internal.security package, so move the definition to the
same package to reduce the circular dependencies.
---
 .../java/io/grpc/xds/CdsLoadBalancer2.java    |   2 +-
 .../io/grpc/xds/ClusterImplLoadBalancer.java  |  19 ++--
 .../grpc/xds/ClusterResolverLoadBalancer.java |  18 ++--
 ...ilterChainMatchingProtocolNegotiators.java |   4 +-
 .../io/grpc/xds/GcpAuthenticationFilter.java  |   2 +-
 .../io/grpc/xds/InternalXdsAttributes.java    |  81 +-------------
 .../io/grpc/xds/RingHashLoadBalancer.java     |   4 +-
 .../io/grpc/xds/WrrLocalityLoadBalancer.java  |   4 +-
 .../main/java/io/grpc/xds/XdsAttributes.java  | 101 ++++++++++++++++++
 .../java/io/grpc/xds/XdsNameResolver.java     |   4 +-
 .../java/io/grpc/xds/XdsServerBuilder.java    |   4 +-
 .../security/SecurityProtocolNegotiators.java |  11 +-
 .../io/grpc/xds/CdsLoadBalancer2Test.java     |   2 +-
 .../grpc/xds/ClusterImplLoadBalancerTest.java |  36 ++++---
 .../xds/ClusterResolverLoadBalancerTest.java  |  30 +++---
 .../io/grpc/xds/RingHashLoadBalancerTest.java |   2 +-
 .../grpc/xds/WrrLocalityLoadBalancerTest.java |   6 +-
 .../java/io/grpc/xds/XdsNameResolverTest.java |  12 +--
 .../grpc/xds/XdsSecurityClientServerTest.java |   3 +-
 .../SecurityProtocolNegotiatorsTest.java      |   3 +-
 20 files changed, 190 insertions(+), 158 deletions(-)
 create mode 100644 xds/src/main/java/io/grpc/xds/XdsAttributes.java

diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java
index 3f1eb3e7e4f..04b7663fd35 100644
--- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java
+++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java
@@ -88,7 +88,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
     }
     logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses);
     this.resolvedAddresses = resolvedAddresses;
-    xdsClientPool = resolvedAddresses.getAttributes().get(InternalXdsAttributes.XDS_CLIENT_POOL);
+    xdsClientPool = resolvedAddresses.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL);
     xdsClient = xdsClientPool.getObject();
     CdsConfig config = (CdsConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
     logger.log(XdsLogLevel.INFO, "Config: {0}", config);
diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java
index e4bb28a1b6a..23f89d161e2 100644
--- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java
+++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java
@@ -52,6 +52,7 @@
 import io.grpc.xds.client.XdsClient;
 import io.grpc.xds.client.XdsLogger;
 import io.grpc.xds.client.XdsLogger.XdsLogLevel;
+import io.grpc.xds.internal.security.SecurityProtocolNegotiators;
 import io.grpc.xds.internal.security.SslContextProviderSupplier;
 import io.grpc.xds.orca.OrcaPerRequestUtil;
 import io.grpc.xds.orca.OrcaPerRequestUtil.OrcaPerRequestReportListener;
@@ -114,12 +115,12 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
     logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses);
     Attributes attributes = resolvedAddresses.getAttributes();
     if (xdsClientPool == null) {
-      xdsClientPool = attributes.get(InternalXdsAttributes.XDS_CLIENT_POOL);
+      xdsClientPool = attributes.get(XdsAttributes.XDS_CLIENT_POOL);
       assert xdsClientPool != null;
       xdsClient = xdsClientPool.getObject();
     }
     if (callCounterProvider == null) {
-      callCounterProvider = attributes.get(InternalXdsAttributes.CALL_COUNTER_PROVIDER);
+      callCounterProvider = attributes.get(XdsAttributes.CALL_COUNTER_PROVIDER);
     }
 
     ClusterImplConfig config =
@@ -236,9 +237,9 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) {
           .set(ATTR_CLUSTER_LOCALITY, localityAtomicReference);
       if (GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE", false)) {
         String hostname = args.getAddresses().get(0).getAttributes()
-            .get(InternalXdsAttributes.ATTR_ADDRESS_NAME);
+            .get(XdsAttributes.ATTR_ADDRESS_NAME);
         if (hostname != null) {
-          attrsBuilder.set(InternalXdsAttributes.ATTR_ADDRESS_NAME, hostname);
+          attrsBuilder.set(XdsAttributes.ATTR_ADDRESS_NAME, hostname);
         }
       }
       args = args.toBuilder().setAddresses(addresses).setAttributes(attrsBuilder.build()).build();
@@ -287,10 +288,10 @@ private List withAdditionalAttributes(
       List newAddresses = new ArrayList<>();
       for (EquivalentAddressGroup eag : addresses) {
         Attributes.Builder attrBuilder = eag.getAttributes().toBuilder().set(
-            InternalXdsAttributes.ATTR_CLUSTER_NAME, cluster);
+            XdsAttributes.ATTR_CLUSTER_NAME, cluster);
         if (sslContextProviderSupplier != null) {
           attrBuilder.set(
-              InternalXdsAttributes.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER,
+              SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER,
               sslContextProviderSupplier);
         }
         newAddresses.add(new EquivalentAddressGroup(eag.getAddresses(), attrBuilder.build()));
@@ -299,8 +300,8 @@ private List withAdditionalAttributes(
     }
 
     private ClusterLocality createClusterLocalityFromAttributes(Attributes addressAttributes) {
-      Locality locality = addressAttributes.get(InternalXdsAttributes.ATTR_LOCALITY);
-      String localityName = addressAttributes.get(InternalXdsAttributes.ATTR_LOCALITY_NAME);
+      Locality locality = addressAttributes.get(XdsAttributes.ATTR_LOCALITY);
+      String localityName = addressAttributes.get(XdsAttributes.ATTR_LOCALITY_NAME);
 
       // Endpoint addresses resolved by ClusterResolverLoadBalancer should always contain
       // attributes with its locality, including endpoints in LOGICAL_DNS clusters.
@@ -431,7 +432,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) {
             result = PickResult.withSubchannel(result.getSubchannel(),
                 result.getStreamTracerFactory(),
                 result.getSubchannel().getAttributes().get(
-                    InternalXdsAttributes.ATTR_ADDRESS_NAME));
+                    XdsAttributes.ATTR_ADDRESS_NAME));
           }
         }
         return result;
diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java
index 3ef79699b10..4e08ddc5973 100644
--- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java
+++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java
@@ -119,7 +119,7 @@ final class ClusterResolverLoadBalancer extends LoadBalancer {
   public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
     logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses);
     if (xdsClientPool == null) {
-      xdsClientPool = resolvedAddresses.getAttributes().get(InternalXdsAttributes.XDS_CLIENT_POOL);
+      xdsClientPool = resolvedAddresses.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL);
       xdsClient = xdsClientPool.getObject();
     }
     ClusterResolverConfig config =
@@ -423,12 +423,12 @@ public void run() {
                   String localityName = localityName(locality);
                   Attributes attr =
                       endpoint.eag().getAttributes().toBuilder()
-                          .set(InternalXdsAttributes.ATTR_LOCALITY, locality)
-                          .set(InternalXdsAttributes.ATTR_LOCALITY_NAME, localityName)
-                          .set(InternalXdsAttributes.ATTR_LOCALITY_WEIGHT,
+                          .set(XdsAttributes.ATTR_LOCALITY, locality)
+                          .set(XdsAttributes.ATTR_LOCALITY_NAME, localityName)
+                          .set(XdsAttributes.ATTR_LOCALITY_WEIGHT,
                               localityLbInfo.localityWeight())
-                          .set(InternalXdsAttributes.ATTR_SERVER_WEIGHT, weight)
-                          .set(InternalXdsAttributes.ATTR_ADDRESS_NAME, endpoint.hostname())
+                          .set(XdsAttributes.ATTR_SERVER_WEIGHT, weight)
+                          .set(XdsAttributes.ATTR_ADDRESS_NAME, endpoint.hostname())
                           .build();
                   EquivalentAddressGroup eag = new EquivalentAddressGroup(
                       endpoint.eag().getAddresses(), attr);
@@ -630,9 +630,9 @@ public void run() {
                 // to handle such it.
                 String localityName = localityName(LOGICAL_DNS_CLUSTER_LOCALITY);
                 Attributes attr = eag.getAttributes().toBuilder()
-                    .set(InternalXdsAttributes.ATTR_LOCALITY, LOGICAL_DNS_CLUSTER_LOCALITY)
-                    .set(InternalXdsAttributes.ATTR_LOCALITY_NAME, localityName)
-                    .set(InternalXdsAttributes.ATTR_ADDRESS_NAME, dnsHostName)
+                    .set(XdsAttributes.ATTR_LOCALITY, LOGICAL_DNS_CLUSTER_LOCALITY)
+                    .set(XdsAttributes.ATTR_LOCALITY_NAME, localityName)
+                    .set(XdsAttributes.ATTR_ADDRESS_NAME, dnsHostName)
                     .build();
                 eag = new EquivalentAddressGroup(eag.getAddresses(), attr);
                 eag = AddressFilter.setPathFilter(eag, Arrays.asList(priorityName, localityName));
diff --git a/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java b/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java
index 37a0e6a8ae0..aaf8d69d2c1 100644
--- a/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java
+++ b/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java
@@ -17,8 +17,8 @@
 package io.grpc.xds;
 
 import static com.google.common.base.Preconditions.checkNotNull;
-import static io.grpc.xds.InternalXdsAttributes.ATTR_DRAIN_GRACE_NANOS;
-import static io.grpc.xds.InternalXdsAttributes.ATTR_FILTER_CHAIN_SELECTOR_MANAGER;
+import static io.grpc.xds.XdsAttributes.ATTR_DRAIN_GRACE_NANOS;
+import static io.grpc.xds.XdsAttributes.ATTR_FILTER_CHAIN_SELECTOR_MANAGER;
 import static io.grpc.xds.XdsServerWrapper.ATTR_SERVER_ROUTING_CONFIG;
 import static io.grpc.xds.internal.security.SecurityProtocolNegotiators.ATTR_SERVER_SSL_CONTEXT_PROVIDER_SUPPLIER;
 
diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java
index edd8a09e4ca..4e242708e1f 100644
--- a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java
+++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java
@@ -106,7 +106,7 @@ public ClientInterceptor buildClientInterceptor(FilterConfig config,
       public  ClientCall interceptCall(
           MethodDescriptor method, CallOptions callOptions, Channel next) {
 
-        /*String clusterName = callOptions.getOption(InternalXdsAttributes.ATTR_CLUSTER_NAME);
+        /*String clusterName = callOptions.getOption(XdsAttributes.ATTR_CLUSTER_NAME);
         if (clusterName == null) {
           return next.newCall(method, callOptions);
         }*/
diff --git a/xds/src/main/java/io/grpc/xds/InternalXdsAttributes.java b/xds/src/main/java/io/grpc/xds/InternalXdsAttributes.java
index 575bda73f0c..ed70e6f5e78 100644
--- a/xds/src/main/java/io/grpc/xds/InternalXdsAttributes.java
+++ b/xds/src/main/java/io/grpc/xds/InternalXdsAttributes.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The gRPC Authors
+ * Copyright 2024 The gRPC Authors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,96 +18,19 @@
 
 import io.grpc.Attributes;
 import io.grpc.EquivalentAddressGroup;
-import io.grpc.Grpc;
 import io.grpc.Internal;
-import io.grpc.NameResolver;
-import io.grpc.internal.ObjectPool;
-import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider;
-import io.grpc.xds.client.Locality;
-import io.grpc.xds.client.XdsClient;
-import io.grpc.xds.internal.security.SslContextProviderSupplier;
 
 /**
  * Internal attributes used for xDS implementation. Do not use.
  */
 @Internal
 public final class InternalXdsAttributes {
-
-  // TODO(sanjaypujare): move to xds internal package.
-  /** Attribute key for SslContextProviderSupplier (used from client) for a subchannel. */
-  @Grpc.TransportAttr
-  public static final Attributes.Key
-      ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER =
-          Attributes.Key.create("io.grpc.xds.internal.security.SslContextProviderSupplier");
-
-  /**
-   * Attribute key for passing around the XdsClient object pool across NameResolver/LoadBalancers.
-   */
-  @NameResolver.ResolutionResultAttr
-  static final Attributes.Key> XDS_CLIENT_POOL =
-      Attributes.Key.create("io.grpc.xds.InternalXdsAttributes.xdsClientPool");
-
-  /**
-   * Attribute key for obtaining the global provider that provides atomics for aggregating
-   * outstanding RPCs sent to each cluster.
-   */
-  @NameResolver.ResolutionResultAttr
-  static final Attributes.Key CALL_COUNTER_PROVIDER =
-      Attributes.Key.create("io.grpc.xds.InternalXdsAttributes.callCounterProvider");
-
-  /**
-   * Map from localities to their weights.
-   */
-  @NameResolver.ResolutionResultAttr
-  static final Attributes.Key ATTR_LOCALITY_WEIGHT =
-      Attributes.Key.create("io.grpc.xds.InternalXdsAttributes.localityWeight");
-
   /**
    * Name of the cluster that provides this EquivalentAddressGroup.
    */
-  @Internal
   @EquivalentAddressGroup.Attr
   public static final Attributes.Key ATTR_CLUSTER_NAME =
-      Attributes.Key.create("io.grpc.xds.InternalXdsAttributes.clusterName");
-
-  /**
-   * The locality that this EquivalentAddressGroup is in.
-   */
-  @EquivalentAddressGroup.Attr
-  static final Attributes.Key ATTR_LOCALITY =
-      Attributes.Key.create("io.grpc.xds.InternalXdsAttributes.locality");
-
-  /**
-   * The name of the locality that this EquivalentAddressGroup is in.
-   */
-  @EquivalentAddressGroup.Attr
-  static final Attributes.Key ATTR_LOCALITY_NAME =
-      Attributes.Key.create("io.grpc.xds.InternalXdsAttributes.localityName");
-
-  /**
-   * Endpoint weight for load balancing purposes.
-   */
-  @EquivalentAddressGroup.Attr
-  static final Attributes.Key ATTR_SERVER_WEIGHT =
-      Attributes.Key.create("io.grpc.xds.InternalXdsAttributes.serverWeight");
-
-  /** Name associated with individual address, if available (e.g., DNS name). */
-  @EquivalentAddressGroup.Attr
-  static final Attributes.Key ATTR_ADDRESS_NAME =
-      Attributes.Key.create("io.grpc.xds.InternalXdsAttributes.addressName");
-
-  /**
-   * Filter chain match for network filters.
-   */
-  @Grpc.TransportAttr
-  static final Attributes.Key
-          ATTR_FILTER_CHAIN_SELECTOR_MANAGER = Attributes.Key.create(
-          "io.grpc.xds.InternalXdsAttributes.filterChainSelectorManager");
-
-  /** Grace time to use when draining. Null for an infinite grace time. */
-  @Grpc.TransportAttr
-  static final Attributes.Key ATTR_DRAIN_GRACE_NANOS =
-      Attributes.Key.create("io.grpc.xds.InternalXdsAttributes.drainGraceTime");
+      XdsAttributes.ATTR_CLUSTER_NAME;
 
   private InternalXdsAttributes() {}
 }
diff --git a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java
index 4f93974b52c..0c4792cb924 100644
--- a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java
+++ b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java
@@ -102,7 +102,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
       Map serverWeights = new HashMap<>();
       long totalWeight = 0L;
       for (EquivalentAddressGroup eag : addrList) {
-        Long weight = eag.getAttributes().get(InternalXdsAttributes.ATTR_SERVER_WEIGHT);
+        Long weight = eag.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT);
         // Support two ways of server weighing: either multiple instances of the same address
         // or each address contains a per-address weight attribute. If a weight is not provided,
         // each occurrence of the address will be counted a weight value of one.
@@ -241,7 +241,7 @@ private Status validateAddrList(List addrList) {
 
     long totalWeight = 0;
     for (EquivalentAddressGroup eag : addrList) {
-      Long weight = eag.getAttributes().get(InternalXdsAttributes.ATTR_SERVER_WEIGHT);
+      Long weight = eag.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT);
 
       if (weight == null) {
         weight = 1L;
diff --git a/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java
index 46d2443d36a..3e58efa7c23 100644
--- a/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java
+++ b/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java
@@ -74,8 +74,8 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
     Map localityWeights = new HashMap<>();
     for (EquivalentAddressGroup eag : resolvedAddresses.getAddresses()) {
       Attributes eagAttrs = eag.getAttributes();
-      String locality = eagAttrs.get(InternalXdsAttributes.ATTR_LOCALITY_NAME);
-      Integer localityWeight = eagAttrs.get(InternalXdsAttributes.ATTR_LOCALITY_WEIGHT);
+      String locality = eagAttrs.get(XdsAttributes.ATTR_LOCALITY_NAME);
+      Integer localityWeight = eagAttrs.get(XdsAttributes.ATTR_LOCALITY_WEIGHT);
 
       if (locality == null) {
         Status unavailableStatus = Status.UNAVAILABLE.withDescription(
diff --git a/xds/src/main/java/io/grpc/xds/XdsAttributes.java b/xds/src/main/java/io/grpc/xds/XdsAttributes.java
new file mode 100644
index 00000000000..af8139d8ff4
--- /dev/null
+++ b/xds/src/main/java/io/grpc/xds/XdsAttributes.java
@@ -0,0 +1,101 @@
+/*
+ * 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.grpc.Attributes;
+import io.grpc.EquivalentAddressGroup;
+import io.grpc.Grpc;
+import io.grpc.NameResolver;
+import io.grpc.internal.ObjectPool;
+import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider;
+import io.grpc.xds.client.Locality;
+import io.grpc.xds.client.XdsClient;
+
+/**
+ * Attributes used for xDS implementation.
+ */
+final class XdsAttributes {
+  /**
+   * Attribute key for passing around the XdsClient object pool across NameResolver/LoadBalancers.
+   */
+  @NameResolver.ResolutionResultAttr
+  static final Attributes.Key> XDS_CLIENT_POOL =
+      Attributes.Key.create("io.grpc.xds.XdsAttributes.xdsClientPool");
+
+  /**
+   * Attribute key for obtaining the global provider that provides atomics for aggregating
+   * outstanding RPCs sent to each cluster.
+   */
+  @NameResolver.ResolutionResultAttr
+  static final Attributes.Key CALL_COUNTER_PROVIDER =
+      Attributes.Key.create("io.grpc.xds.XdsAttributes.callCounterProvider");
+
+  /**
+   * Map from localities to their weights.
+   */
+  @NameResolver.ResolutionResultAttr
+  static final Attributes.Key ATTR_LOCALITY_WEIGHT =
+      Attributes.Key.create("io.grpc.xds.XdsAttributes.localityWeight");
+
+  /**
+   * Name of the cluster that provides this EquivalentAddressGroup.
+   */
+  @EquivalentAddressGroup.Attr
+  public static final Attributes.Key ATTR_CLUSTER_NAME =
+      Attributes.Key.create("io.grpc.xds.XdsAttributes.clusterName");
+
+  /**
+   * The locality that this EquivalentAddressGroup is in.
+   */
+  @EquivalentAddressGroup.Attr
+  static final Attributes.Key ATTR_LOCALITY =
+      Attributes.Key.create("io.grpc.xds.XdsAttributes.locality");
+
+  /**
+   * The name of the locality that this EquivalentAddressGroup is in.
+   */
+  @EquivalentAddressGroup.Attr
+  static final Attributes.Key ATTR_LOCALITY_NAME =
+      Attributes.Key.create("io.grpc.xds.XdsAttributes.localityName");
+
+  /**
+   * Endpoint weight for load balancing purposes.
+   */
+  @EquivalentAddressGroup.Attr
+  static final Attributes.Key ATTR_SERVER_WEIGHT =
+      Attributes.Key.create("io.grpc.xds.XdsAttributes.serverWeight");
+
+  /** Name associated with individual address, if available (e.g., DNS name). */
+  @EquivalentAddressGroup.Attr
+  static final Attributes.Key ATTR_ADDRESS_NAME =
+      Attributes.Key.create("io.grpc.xds.XdsAttributes.addressName");
+
+  /**
+   * Filter chain match for network filters.
+   */
+  @Grpc.TransportAttr
+  static final Attributes.Key
+          ATTR_FILTER_CHAIN_SELECTOR_MANAGER = Attributes.Key.create(
+          "io.grpc.xds.XdsAttributes.filterChainSelectorManager");
+
+  /** Grace time to use when draining. Null for an infinite grace time. */
+  @Grpc.TransportAttr
+  static final Attributes.Key ATTR_DRAIN_GRACE_NANOS =
+      Attributes.Key.create("io.grpc.xds.XdsAttributes.drainGraceTime");
+
+  private XdsAttributes() {}
+}
diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java
index c51709c174c..3c7f4455fde 100644
--- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java
+++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java
@@ -307,8 +307,8 @@ private void updateResolutionResult() {
     ConfigOrError parsedServiceConfig = serviceConfigParser.parseServiceConfig(rawServiceConfig);
     Attributes attrs =
         Attributes.newBuilder()
-            .set(InternalXdsAttributes.XDS_CLIENT_POOL, xdsClientPool)
-            .set(InternalXdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider)
+            .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool)
+            .set(XdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider)
             .set(InternalConfigSelector.KEY, configSelector)
             .build();
     ResolutionResult result =
diff --git a/xds/src/main/java/io/grpc/xds/XdsServerBuilder.java b/xds/src/main/java/io/grpc/xds/XdsServerBuilder.java
index b75d5755f6e..928f22c4d5e 100644
--- a/xds/src/main/java/io/grpc/xds/XdsServerBuilder.java
+++ b/xds/src/main/java/io/grpc/xds/XdsServerBuilder.java
@@ -19,8 +19,8 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
-import static io.grpc.xds.InternalXdsAttributes.ATTR_DRAIN_GRACE_NANOS;
-import static io.grpc.xds.InternalXdsAttributes.ATTR_FILTER_CHAIN_SELECTOR_MANAGER;
+import static io.grpc.xds.XdsAttributes.ATTR_DRAIN_GRACE_NANOS;
+import static io.grpc.xds.XdsAttributes.ATTR_FILTER_CHAIN_SELECTOR_MANAGER;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.errorprone.annotations.DoNotCall;
diff --git a/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java b/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java
index 00659e53de1..c34fab74032 100644
--- a/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java
+++ b/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java
@@ -20,6 +20,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import io.grpc.Attributes;
+import io.grpc.Grpc;
 import io.grpc.internal.GrpcUtil;
 import io.grpc.internal.ObjectPool;
 import io.grpc.netty.GrpcHttp2ConnectionHandler;
@@ -28,7 +29,6 @@
 import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator;
 import io.grpc.netty.InternalProtocolNegotiators;
 import io.grpc.netty.ProtocolNegotiationEvent;
-import io.grpc.xds.InternalXdsAttributes;
 import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerAdapter;
 import io.netty.channel.ChannelHandlerContext;
@@ -63,6 +63,12 @@ private SecurityProtocolNegotiators() {
           ATTR_SERVER_SSL_CONTEXT_PROVIDER_SUPPLIER =
           Attributes.Key.create("io.grpc.xds.internal.security.server.sslContextProviderSupplier");
 
+  /** Attribute key for SslContextProviderSupplier (used from client) for a subchannel. */
+  @Grpc.TransportAttr
+  public static final Attributes.Key
+      ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER =
+          Attributes.Key.create("io.grpc.xds.internal.security.SslContextProviderSupplier");
+
   /**
    * Returns a {@link InternalProtocolNegotiator.ClientFactory}.
    *
@@ -130,8 +136,7 @@ public AsciiString scheme() {
     public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) {
       // check if SslContextProviderSupplier was passed via attributes
       SslContextProviderSupplier localSslContextProviderSupplier =
-          grpcHandler.getEagAttributes().get(
-              InternalXdsAttributes.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER);
+          grpcHandler.getEagAttributes().get(ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER);
       if (localSslContextProviderSupplier == null) {
         checkNotNull(
             fallbackProtocolNegotiator, "No TLS config and no fallbackProtocolNegotiator!");
diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java
index da32332a2a5..82a61e79abf 100644
--- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java
+++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java
@@ -160,7 +160,7 @@ public void setUp() {
             .setAttributes(
                 // Other attributes not used by cluster_resolver LB are omitted.
                 Attributes.newBuilder()
-                    .set(InternalXdsAttributes.XDS_CLIENT_POOL, xdsClientPool)
+                    .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool)
                     .build())
             .setLoadBalancingPolicyConfig(new CdsConfig(CLUSTER))
             .build());
diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java
index 0d18af0b04a..1918ea5724c 100644
--- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java
+++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java
@@ -77,6 +77,7 @@
 import io.grpc.xds.client.Stats.UpstreamLocalityStats;
 import io.grpc.xds.client.XdsClient;
 import io.grpc.xds.internal.security.CommonTlsContextTestsUtil;
+import io.grpc.xds.internal.security.SecurityProtocolNegotiators;
 import io.grpc.xds.internal.security.SslContextProvider;
 import io.grpc.xds.internal.security.SslContextProviderSupplier;
 import java.net.SocketAddress;
@@ -195,7 +196,7 @@ public void handleResolvedAddresses_propagateToChildPolicy() {
     FakeLoadBalancer childBalancer = Iterables.getOnlyElement(downstreamBalancers);
     assertThat(Iterables.getOnlyElement(childBalancer.addresses)).isEqualTo(endpoint);
     assertThat(childBalancer.config).isSameInstanceAs(weightedTargetConfig);
-    assertThat(childBalancer.attributes.get(InternalXdsAttributes.XDS_CLIENT_POOL))
+    assertThat(childBalancer.attributes.get(XdsAttributes.XDS_CLIENT_POOL))
         .isSameInstanceAs(xdsClientPool);
   }
 
@@ -533,7 +534,7 @@ public void dropRpcsWithRespectToLbConfigDropCategories() {
             .setAddresses(Collections.singletonList(endpoint))
             .setAttributes(
                 Attributes.newBuilder()
-                    .set(InternalXdsAttributes.XDS_CLIENT_POOL, xdsClientPool)
+                    .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool)
                     .build())
             .setLoadBalancingPolicyConfig(config)
             .build());
@@ -738,14 +739,14 @@ public void endpointAddressesAttachedWithClusterName() {
             .build();
     Subchannel subchannel = leafBalancer.helper.createSubchannel(args);
     for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) {
-      assertThat(eag.getAttributes().get(InternalXdsAttributes.ATTR_CLUSTER_NAME))
+      assertThat(eag.getAttributes().get(XdsAttributes.ATTR_CLUSTER_NAME))
           .isEqualTo(CLUSTER);
     }
 
     // An address update should also retain the cluster attribute.
     subchannel.updateAddresses(leafBalancer.addresses);
     for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) {
-      assertThat(eag.getAttributes().get(InternalXdsAttributes.ATTR_CLUSTER_NAME))
+      assertThat(eag.getAttributes().get(XdsAttributes.ATTR_CLUSTER_NAME))
           .isEqualTo(CLUSTER);
     }
   }
@@ -783,10 +784,10 @@ public void endpointAddressesAttachedWithClusterName() {
               new FixedResultPicker(PickResult.withSubchannel(subchannel)));
         }
       });
-      assertThat(subchannel.getAttributes().get(InternalXdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(
+      assertThat(subchannel.getAttributes().get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(
           "authority-host-name");
       for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) {
-        assertThat(eag.getAttributes().get(InternalXdsAttributes.ATTR_ADDRESS_NAME))
+        assertThat(eag.getAttributes().get(XdsAttributes.ATTR_ADDRESS_NAME))
             .isEqualTo("authority-host-name");
       }
 
@@ -835,9 +836,9 @@ public void endpointAddressesAttachedWithClusterName() {
       }
     });
     // Sub Channel wrapper args won't have the address name although addresses will.
-    assertThat(subchannel.getAttributes().get(InternalXdsAttributes.ATTR_ADDRESS_NAME)).isNull();
+    assertThat(subchannel.getAttributes().get(XdsAttributes.ATTR_ADDRESS_NAME)).isNull();
     for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) {
-      assertThat(eag.getAttributes().get(InternalXdsAttributes.ATTR_ADDRESS_NAME))
+      assertThat(eag.getAttributes().get(XdsAttributes.ATTR_ADDRESS_NAME))
           .isEqualTo("authority-host-name");
     }
 
@@ -877,7 +878,7 @@ public void endpointAddressesAttachedWithTlsConfig_securityEnabledByDefault() {
     Subchannel subchannel = leafBalancer.helper.createSubchannel(args);
     for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) {
       SslContextProviderSupplier supplier =
-          eag.getAttributes().get(InternalXdsAttributes.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER);
+          eag.getAttributes().get(SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER);
       assertThat(supplier.getTlsContext()).isEqualTo(upstreamTlsContext);
     }
 
@@ -891,7 +892,8 @@ public void endpointAddressesAttachedWithTlsConfig_securityEnabledByDefault() {
     assertThat(Iterables.getOnlyElement(downstreamBalancers)).isSameInstanceAs(leafBalancer);
     subchannel = leafBalancer.helper.createSubchannel(args);  // creates new connections
     for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) {
-      assertThat(eag.getAttributes().get(InternalXdsAttributes.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER))
+      assertThat(
+            eag.getAttributes().get(SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER))
           .isNull();
     }
 
@@ -908,14 +910,14 @@ public void endpointAddressesAttachedWithTlsConfig_securityEnabledByDefault() {
     subchannel = leafBalancer.helper.createSubchannel(args);  // creates new connections
     for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) {
       SslContextProviderSupplier supplier =
-          eag.getAttributes().get(InternalXdsAttributes.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER);
+          eag.getAttributes().get(SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER);
       assertThat(supplier.isShutdown()).isFalse();
       assertThat(supplier.getTlsContext()).isEqualTo(upstreamTlsContext);
     }
     loadBalancer.shutdown();
     for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) {
       SslContextProviderSupplier supplier =
-              eag.getAttributes().get(InternalXdsAttributes.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER);
+          eag.getAttributes().get(SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER);
       assertThat(supplier.isShutdown()).isTrue();
     }
     loadBalancer = null;
@@ -928,8 +930,8 @@ private void deliverAddressesAndConfig(List addresses,
             .setAddresses(addresses)
             .setAttributes(
                 Attributes.newBuilder()
-                    .set(InternalXdsAttributes.XDS_CLIENT_POOL, xdsClientPool)
-                    .set(InternalXdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider)
+                    .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool)
+                    .set(XdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider)
                     .build())
             .setLoadBalancingPolicyConfig(config)
             .build());
@@ -986,11 +988,11 @@ public String toString() {
     }
 
     Attributes.Builder attributes = Attributes.newBuilder()
-        .set(InternalXdsAttributes.ATTR_LOCALITY, locality)
+        .set(XdsAttributes.ATTR_LOCALITY, locality)
         // Unique but arbitrary string
-        .set(InternalXdsAttributes.ATTR_LOCALITY_NAME, locality.toString());
+        .set(XdsAttributes.ATTR_LOCALITY_NAME, locality.toString());
     if (authorityHostname != null) {
-      attributes.set(InternalXdsAttributes.ATTR_ADDRESS_NAME, authorityHostname);
+      attributes.set(XdsAttributes.ATTR_ADDRESS_NAME, authorityHostname);
     }
     EquivalentAddressGroup eag = new EquivalentAddressGroup(new FakeSocketAddress(name),
         attributes.build());
diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java
index 29c46963da3..9243abba6d3 100644
--- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java
+++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java
@@ -272,13 +272,13 @@ public void edsClustersWithRingHashEndpointLbPolicy() {
     // Endpoints in locality1 have no endpoint-level weight specified, so all endpoints within
     // locality1 are equally weighted.
     assertThat(addr1.getAddresses()).isEqualTo(endpoint1.getAddresses());
-    assertThat(addr1.getAttributes().get(InternalXdsAttributes.ATTR_SERVER_WEIGHT))
+    assertThat(addr1.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT))
         .isEqualTo(10);
     assertThat(addr2.getAddresses()).isEqualTo(endpoint2.getAddresses());
-    assertThat(addr2.getAttributes().get(InternalXdsAttributes.ATTR_SERVER_WEIGHT))
+    assertThat(addr2.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT))
         .isEqualTo(10);
     assertThat(addr3.getAddresses()).isEqualTo(endpoint3.getAddresses());
-    assertThat(addr3.getAttributes().get(InternalXdsAttributes.ATTR_SERVER_WEIGHT))
+    assertThat(addr3.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT))
         .isEqualTo(50 * 60);
     assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME);
     PriorityLbConfig priorityLbConfig = (PriorityLbConfig) childBalancer.config;
@@ -342,7 +342,7 @@ public void edsClustersWithLeastRequestEndpointLbPolicy() {
 
     assertThat(
         childBalancer.addresses.get(0).getAttributes()
-            .get(InternalXdsAttributes.ATTR_LOCALITY_WEIGHT)).isEqualTo(100);
+            .get(XdsAttributes.ATTR_LOCALITY_WEIGHT)).isEqualTo(100);
   }
 
   @Test
@@ -368,7 +368,7 @@ public void edsClustersEndpointHostname_addedToAddressAttribute() {
 
     assertThat(
         childBalancer.addresses.get(0).getAttributes()
-            .get(InternalXdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo("hostname1");
+            .get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo("hostname1");
   }
 
 
@@ -468,16 +468,16 @@ public void onlyEdsClusters_receivedEndpoints() {
     assertThat(childProvider3.getPolicyName()).isEqualTo("round_robin");
 
     for (EquivalentAddressGroup eag : childBalancer.addresses) {
-      if (eag.getAttributes().get(InternalXdsAttributes.ATTR_LOCALITY) == locality1) {
-        assertThat(eag.getAttributes().get(InternalXdsAttributes.ATTR_LOCALITY_WEIGHT))
+      if (eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY) == locality1) {
+        assertThat(eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY_WEIGHT))
             .isEqualTo(70);
       }
-      if (eag.getAttributes().get(InternalXdsAttributes.ATTR_LOCALITY) == locality2) {
-        assertThat(eag.getAttributes().get(InternalXdsAttributes.ATTR_LOCALITY_WEIGHT))
+      if (eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY) == locality2) {
+        assertThat(eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY_WEIGHT))
             .isEqualTo(10);
       }
-      if (eag.getAttributes().get(InternalXdsAttributes.ATTR_LOCALITY) == locality3) {
-        assertThat(eag.getAttributes().get(InternalXdsAttributes.ATTR_LOCALITY_WEIGHT))
+      if (eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY) == locality3) {
+        assertThat(eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY_WEIGHT))
             .isEqualTo(20);
       }
     }
@@ -659,7 +659,7 @@ public void handleEdsResource_ignoreLocalitiesWithNoHealthyEndpoints() {
 
     FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers);
     for (EquivalentAddressGroup eag : childBalancer.addresses) {
-      assertThat(eag.getAttributes().get(InternalXdsAttributes.ATTR_LOCALITY)).isEqualTo(locality2);
+      assertThat(eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY)).isEqualTo(locality2);
     }
   }
 
@@ -740,9 +740,9 @@ public void onlyLogicalDnsCluster_endpointsResolved() {
         Collections.emptyList(), "pick_first");
     assertAddressesEqual(Arrays.asList(endpoint1, endpoint2), childBalancer.addresses);
     assertThat(childBalancer.addresses.get(0).getAttributes()
-        .get(InternalXdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME);
+        .get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME);
     assertThat(childBalancer.addresses.get(1).getAttributes()
-        .get(InternalXdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME);
+        .get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME);
 
   }
 
@@ -1068,7 +1068,7 @@ private void deliverLbConfig(ClusterResolverConfig config) {
             .setAttributes(
                 // Other attributes not used by cluster_resolver LB are omitted.
                 Attributes.newBuilder()
-                    .set(InternalXdsAttributes.XDS_CLIENT_POOL, xdsClientPool)
+                    .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool)
                     .build())
             .setLoadBalancingPolicyConfig(config)
             .build());
diff --git a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java
index 50c2ef1d549..65fc1527b0c 100644
--- a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java
+++ b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java
@@ -1084,7 +1084,7 @@ private static List createWeightedServerAddrs(long... we
     for (int i = 0; i < weights.length; i++) {
       SocketAddress addr = new FakeSocketAddress("server" + i);
       Attributes attr = Attributes.newBuilder().set(
-          InternalXdsAttributes.ATTR_SERVER_WEIGHT, weights[i]).build();
+          XdsAttributes.ATTR_SERVER_WEIGHT, weights[i]).build();
       EquivalentAddressGroup eag = new EquivalentAddressGroup(addr, attr);
       addrs.add(eag);
     }
diff --git a/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java
index fcf8c826d86..a87d881563c 100644
--- a/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java
+++ b/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java
@@ -185,7 +185,7 @@ public void localityWeightAttributeNotPropagated() {
     verify(mockWeightedTargetLb).handleResolvedAddresses(resolvedAddressesCaptor.capture());
 
     //assertThat(resolvedAddressesCaptor.getValue().getAttributes()
-    //    .get(InternalXdsAttributes.ATTR_LOCALITY_WEIGHTS)).isNull();
+    //    .get(XdsAttributes.ATTR_LOCALITY_WEIGHTS)).isNull();
   }
 
   @Test
@@ -254,9 +254,9 @@ public String toString() {
     }
 
     Attributes.Builder attrBuilder = Attributes.newBuilder()
-        .set(InternalXdsAttributes.ATTR_LOCALITY_NAME, locality);
+        .set(XdsAttributes.ATTR_LOCALITY_NAME, locality);
     if (localityWeight != null) {
-      attrBuilder.set(InternalXdsAttributes.ATTR_LOCALITY_WEIGHT, localityWeight);
+      attrBuilder.set(XdsAttributes.ATTR_LOCALITY_WEIGHT, localityWeight);
     }
 
     EquivalentAddressGroup eag = new EquivalentAddressGroup(new FakeSocketAddress(name),
diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java
index 6f4c1503cee..d895cecdb10 100644
--- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java
+++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java
@@ -755,8 +755,8 @@ public void resolved_simpleCallFailedToRoute_routeWithNonForwardingAction() {
     assertServiceConfigForLoadBalancingConfig(
         Collections.singletonList(cluster2),
         (Map) result.getServiceConfig().getConfig());
-    assertThat(result.getAttributes().get(InternalXdsAttributes.XDS_CLIENT_POOL)).isNotNull();
-    assertThat(result.getAttributes().get(InternalXdsAttributes.CALL_COUNTER_PROVIDER)).isNotNull();
+    assertThat(result.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL)).isNotNull();
+    assertThat(result.getAttributes().get(XdsAttributes.CALL_COUNTER_PROVIDER)).isNotNull();
     InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY);
     // Simulates making a call1 RPC.
     Result selectResult = configSelector.selectConfig(
@@ -1156,7 +1156,7 @@ public void resolved_simpleCallSucceeds_routeToWeightedCluster() {
     assertThat(result.getAddressesOrError().getValue()).isEmpty();
     assertServiceConfigForLoadBalancingConfig(
         Arrays.asList(cluster1, cluster2), (Map) result.getServiceConfig().getConfig());
-    assertThat(result.getAttributes().get(InternalXdsAttributes.XDS_CLIENT_POOL)).isNotNull();
+    assertThat(result.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL)).isNotNull();
     InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY);
     assertCallSelectClusterResult(call1, configSelector, cluster2, 20.0);
     assertCallSelectClusterResult(call1, configSelector, cluster1, 20.0);
@@ -1207,7 +1207,7 @@ public void resolved_simpleCallSucceeds_routeToRls() {
                 ImmutableList.of(ImmutableMap.of("rls_experimental", expectedRlsLbConfig)))));
     assertThat(clusterManagerLbConfig).isEqualTo(expectedClusterManagerLbConfig);
 
-    assertThat(result.getAttributes().get(InternalXdsAttributes.XDS_CLIENT_POOL)).isNotNull();
+    assertThat(result.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL)).isNotNull();
     InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY);
     assertCallSelectRlsPluginResult(
         call1, configSelector, "rls-plugin-foo", 20.0);
@@ -1345,8 +1345,8 @@ private InternalConfigSelector resolveToClusters() {
     assertThat(result.getAddressesOrError().getValue()).isEmpty();
     assertServiceConfigForLoadBalancingConfig(
         Arrays.asList(cluster1, cluster2), (Map) result.getServiceConfig().getConfig());
-    assertThat(result.getAttributes().get(InternalXdsAttributes.XDS_CLIENT_POOL)).isNotNull();
-    assertThat(result.getAttributes().get(InternalXdsAttributes.CALL_COUNTER_PROVIDER)).isNotNull();
+    assertThat(result.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL)).isNotNull();
+    assertThat(result.getAttributes().get(XdsAttributes.CALL_COUNTER_PROVIDER)).isNotNull();
     return result.getAttributes().get(InternalConfigSelector.KEY);
   }
 
diff --git a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java
index 6915ac6c13e..cd3ef293369 100644
--- a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java
+++ b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java
@@ -70,6 +70,7 @@
 import io.grpc.xds.client.CommonBootstrapperTestUtils;
 import io.grpc.xds.internal.Matchers.HeaderMatcher;
 import io.grpc.xds.internal.security.CommonTlsContextTestsUtil;
+import io.grpc.xds.internal.security.SecurityProtocolNegotiators;
 import io.grpc.xds.internal.security.SslContextProviderSupplier;
 import io.grpc.xds.internal.security.TlsContextManagerImpl;
 import io.grpc.xds.internal.security.certprovider.FileWatcherCertificateProviderProvider;
@@ -653,7 +654,7 @@ private SimpleServiceGrpc.SimpleServiceBlockingStub getBlockingStub(
     Attributes attrs =
         (upstreamTlsContext != null)
             ? Attributes.newBuilder()
-                .set(InternalXdsAttributes.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER,
+                .set(SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER,
                     new SslContextProviderSupplier(
                         upstreamTlsContext, tlsContextManagerForClient))
                 .build()
diff --git a/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java b/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java
index 955c812233a..a0139618f9f 100644
--- a/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java
+++ b/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java
@@ -47,7 +47,6 @@
 import io.grpc.netty.ProtocolNegotiationEvent;
 import io.grpc.xds.EnvoyServerProtoData.DownstreamTlsContext;
 import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext;
-import io.grpc.xds.InternalXdsAttributes;
 import io.grpc.xds.TlsContextManager;
 import io.grpc.xds.client.Bootstrapper;
 import io.grpc.xds.client.CommonBootstrapperTestUtils;
@@ -134,7 +133,7 @@ public void clientSecurityProtocolNegotiatorNewHandler_withTlsContextAttribute()
     when(mockHandler.getEagAttributes())
         .thenReturn(
             Attributes.newBuilder()
-                .set(InternalXdsAttributes.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER,
+                .set(SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER,
                     new SslContextProviderSupplier(upstreamTlsContext, mockTlsContextManager))
                 .build());
     ChannelHandler newHandler = pn.newHandler(mockHandler);

From 6c12c2bd2438551dc19bd941d158c065eed9e37c Mon Sep 17 00:00:00 2001
From: Eric Anderson 
Date: Mon, 6 Jan 2025 10:24:42 -0800
Subject: [PATCH 135/591] xds: Remember nonces for unknown types

If the control plane sends a resource type the client doesn't understand
at-the-moment, the control plane will still expect the client to include
the nonce if the client subscribes to the type in the future.

This most easily happens when unsubscribing the last resource of a type.
Which meant 1cf1927d1 was insufficient.
---
 .../main/java/io/grpc/xds/client/ControlPlaneClient.java  | 8 ++++----
 .../test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java  | 5 ++++-
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java
index 878f1686a89..278bf8b80e8 100644
--- a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java
+++ b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java
@@ -309,7 +309,7 @@ private class AdsStream implements XdsTransportFactory.EventHandler, String> respNonces = new HashMap<>();
+    private final Map respNonces = new HashMap<>();
     private final StreamingCall call;
     private final MethodDescriptor methodDescriptor =
         AggregatedDiscoveryServiceGrpc.getStreamAggregatedResourcesMethod();
@@ -369,7 +369,7 @@ void sendDiscoveryRequest(XdsResourceType type, String versionInfo,
     final void sendDiscoveryRequest(XdsResourceType type, Collection resources) {
       logger.log(XdsLogLevel.INFO, "Sending {0} request for resources: {1}", type, resources);
       sendDiscoveryRequest(type, versions.getOrDefault(type, ""), resources,
-          respNonces.getOrDefault(type, ""), null);
+          respNonces.getOrDefault(type.typeUrl(), ""), null);
     }
 
     @Override
@@ -400,6 +400,7 @@ public void run() {
           boolean isFirstResponse = !responseReceived;
           responseReceived = true;
           inError = false;
+          respNonces.put(response.getTypeUrl(), response.getNonce());
 
           XdsResourceType type = fromTypeUrl(response.getTypeUrl());
           if (logger.isLoggable(XdsLogLevel.DEBUG)) {
@@ -433,7 +434,6 @@ final void handleRpcResponse(XdsResourceType type, String versionInfo, List call.startRecvMessage(), syncContext);
       xdsResponseHandler.handleResourceResponse(type, serverInfo, versionInfo, resources, nonce,
diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java
index 9a80e3c36b6..00fbfe669af 100644
--- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java
+++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java
@@ -2898,10 +2898,13 @@ public void edsCleanupNonceAfterUnsubscription() {
     xdsClient.cancelXdsResourceWatch(XdsEndpointResource.getInstance(), "A.1", edsResourceWatcher);
     verifySubscribedResourcesMetadataSizes(0, 0, 0, 0);
     call.verifyRequest(EDS, Arrays.asList(), VERSION_1, "0000", NODE);
+    // The control plane can send an updated response for the empty subscription list, with a new
+    // nonce.
+    call.sendResponse(EDS, Arrays.asList(), VERSION_1, "0001");
 
     // When re-subscribing, the version was forgotten but not the nonce
     xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), "A.1", edsResourceWatcher);
-    call.verifyRequest(EDS, "A.1", "", "0000", NODE, Mockito.timeout(2000));
+    call.verifyRequest(EDS, "A.1", "", "0001", NODE, Mockito.timeout(2000));
   }
 
   @Test

From 4222f77587a0f740d5d135197cd3209c07d53c3e Mon Sep 17 00:00:00 2001
From: Larry Safran 
Date: Mon, 6 Jan 2025 13:09:42 -0800
Subject: [PATCH 136/591] xds:Move creating the retry timer in
 handleRpcStreamClosed to as late as possible and call close() (#11776)

* Move creating the retry timer in handleRpcStreamClosed to as late as possible and call `close` so that the `call` is cancelled.
Also add some debug logging.
---
 .../java/io/grpc/internal/AbstractStream.java | 10 +++++++++
 .../grpc/xds/client/ControlPlaneClient.java   | 22 +++++++++----------
 .../io/grpc/xds/client/XdsClientImpl.java     |  5 +++++
 3 files changed, 25 insertions(+), 12 deletions(-)

diff --git a/core/src/main/java/io/grpc/internal/AbstractStream.java b/core/src/main/java/io/grpc/internal/AbstractStream.java
index 9efc488657b..56f540d623f 100644
--- a/core/src/main/java/io/grpc/internal/AbstractStream.java
+++ b/core/src/main/java/io/grpc/internal/AbstractStream.java
@@ -27,6 +27,8 @@
 import io.perfmark.PerfMark;
 import io.perfmark.TaskCloseable;
 import java.io.InputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import javax.annotation.concurrent.GuardedBy;
 
 /**
@@ -34,6 +36,8 @@
  * application thread.
  */
 public abstract class AbstractStream implements Stream {
+  private static final Logger log = Logger.getLogger(AbstractStream.class.getName());
+
   /** The framer to use for sending messages. */
   protected abstract Framer framer();
 
@@ -371,6 +375,12 @@ private void notifyIfReady() {
       boolean doNotify;
       synchronized (onReadyLock) {
         doNotify = isReady();
+        if (!doNotify && log.isLoggable(Level.FINEST)) {
+          log.log(Level.FINEST,
+              "Stream not ready so skip notifying listener.\n"
+                  + "details: allocated/deallocated:{0}/{3}, sent queued: {1}, ready thresh: {2}",
+              new Object[] {allocated, numSentBytesQueued, onReadyThreshold, deallocated});
+        }
       }
       if (doNotify) {
         listener().onReady();
diff --git a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java
index 278bf8b80e8..d05942914dc 100644
--- a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java
+++ b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java
@@ -453,16 +453,6 @@ private void handleRpcStreamClosed(Status status) {
         stopwatch.reset();
       }
 
-      // FakeClock in tests isn't thread-safe. Schedule the retry timer before notifying callbacks
-      // to avoid TSAN races, since tests may wait until callbacks are called but then would run
-      // concurrently with the stopwatch and schedule.
-
-      long elapsed = stopwatch.elapsed(TimeUnit.NANOSECONDS);
-      long delayNanos = Math.max(0, retryBackoffPolicy.nextBackoffNanos() - elapsed);
-
-      rpcRetryTimer =
-          syncContext.schedule(new RpcRetryTask(), delayNanos, TimeUnit.NANOSECONDS, timeService);
-
       Status newStatus = status;
       if (responseReceived) {
         // A closed ADS stream after a successful response is not considered an error. Servers may
@@ -490,9 +480,17 @@ private void handleRpcStreamClosed(Status status) {
             newStatus.getCode(), newStatus.getDescription(), newStatus.getCause());
       }
 
-      closed = true;
+      close(newStatus.asException());
+
+      // FakeClock in tests isn't thread-safe. Schedule the retry timer before notifying callbacks
+      // to avoid TSAN races, since tests may wait until callbacks are called but then would run
+      // concurrently with the stopwatch and schedule.
+      long elapsed = stopwatch.elapsed(TimeUnit.NANOSECONDS);
+      long delayNanos = Math.max(0, retryBackoffPolicy.nextBackoffNanos() - elapsed);
+      rpcRetryTimer =
+          syncContext.schedule(new RpcRetryTask(), delayNanos, TimeUnit.NANOSECONDS, timeService);
+
       xdsResponseHandler.handleStreamClosed(newStatus, !responseReceived);
-      cleanUp();
     }
 
     private void close(Exception error) {
diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java
index 791ba3cc62d..4304d1d9e6f 100644
--- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java
+++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java
@@ -444,6 +444,9 @@ private Set getResourceKeys(XdsResourceType xdsResourceType) {
   // cpcForThisStream is null when doing shutdown
   private void cleanUpResourceTimers(ControlPlaneClient cpcForThisStream) {
     Collection authoritiesForCpc = getActiveAuthorities(cpcForThisStream);
+    String target = cpcForThisStream == null ? "null" : cpcForThisStream.getServerInfo().target();
+    logger.log(XdsLogLevel.DEBUG, "Cleaning up resource timers for CPC {0}, authorities {1}",
+        target, authoritiesForCpc);
 
     for (Map> subscriberMap : resourceSubscribers.values()) {
       for (ResourceSubscriber subscriber : subscriberMap.values()) {
@@ -957,6 +960,8 @@ public void handleStreamClosed(Status status, boolean shouldTryFallback) {
 
       ControlPlaneClient cpcClosed = serverCpClientMap.get(serverInfo);
       if (cpcClosed == null) {
+        logger.log(XdsLogLevel.DEBUG,
+            "Couldn't find closing CPC for {0}, so skipping cleanup and reporting", serverInfo);
         return;
       }
 

From 1edc4d84d44ad08c0db589c5631890375a42eb45 Mon Sep 17 00:00:00 2001
From: MV Shiva 
Date: Tue, 7 Jan 2025 10:03:13 +0530
Subject: [PATCH 137/591] xds: Parsing xDS Cluster Metadata (#11741)

---
 .../io/grpc/xds/GcpAuthenticationFilter.java  |  21 +++
 .../java/io/grpc/xds/MetadataRegistry.java    |  71 ++++++++
 .../java/io/grpc/xds/XdsClusterResource.java  |  65 ++++++-
 .../xds/internal/ProtobufJsonConverter.java   |  61 +++++++
 .../grpc/xds/GrpcXdsClientImplDataTest.java   | 170 ++++++++++++++++++
 .../internal/ProtobufJsonConverterTest.java   |  83 +++++++++
 6 files changed, 470 insertions(+), 1 deletion(-)
 create mode 100644 xds/src/main/java/io/grpc/xds/MetadataRegistry.java
 create mode 100644 xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java
 create mode 100644 xds/src/test/java/io/grpc/xds/internal/ProtobufJsonConverterTest.java

diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java
index 4e242708e1f..6d05e8ffa95 100644
--- a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java
+++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java
@@ -22,6 +22,7 @@
 import com.google.protobuf.Any;
 import com.google.protobuf.InvalidProtocolBufferException;
 import com.google.protobuf.Message;
+import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.Audience;
 import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig;
 import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfig;
 import io.grpc.CallCredentials;
@@ -36,6 +37,7 @@
 import io.grpc.Status;
 import io.grpc.auth.MoreCallCredentials;
 import io.grpc.xds.Filter.ClientInterceptorBuilder;
+import io.grpc.xds.MetadataRegistry.MetadataValueParser;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.concurrent.ScheduledExecutorService;
@@ -219,4 +221,23 @@ V getOrInsert(K key, Function create) {
       return cache.computeIfAbsent(key, create);
     }
   }
+
+  static class AudienceMetadataParser implements MetadataValueParser {
+
+    @Override
+    public String getTypeUrl() {
+      return "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience";
+    }
+
+    @Override
+    public String parse(Any any) throws InvalidProtocolBufferException {
+      Audience audience = any.unpack(Audience.class);
+      String url = audience.getUrl();
+      if (url.isEmpty()) {
+        throw new InvalidProtocolBufferException(
+            "Audience URL is empty. Metadata value must contain a valid URL.");
+      }
+      return url;
+    }
+  }
 }
diff --git a/xds/src/main/java/io/grpc/xds/MetadataRegistry.java b/xds/src/main/java/io/grpc/xds/MetadataRegistry.java
new file mode 100644
index 00000000000..8243b6a6f0f
--- /dev/null
+++ b/xds/src/main/java/io/grpc/xds/MetadataRegistry.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 The gRPC Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.grpc.xds;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.protobuf.Any;
+import com.google.protobuf.InvalidProtocolBufferException;
+import io.grpc.xds.GcpAuthenticationFilter.AudienceMetadataParser;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Registry for parsing cluster metadata values.
+ *
+ * 

This class maintains a mapping of type URLs to {@link MetadataValueParser} instances, + * allowing for the parsing of different metadata types. + */ +final class MetadataRegistry { + private static final MetadataRegistry INSTANCE = new MetadataRegistry(); + + private final Map supportedParsers = new HashMap<>(); + + private MetadataRegistry() { + registerParser(new AudienceMetadataParser()); + } + + static MetadataRegistry getInstance() { + return INSTANCE; + } + + MetadataValueParser findParser(String typeUrl) { + return supportedParsers.get(typeUrl); + } + + @VisibleForTesting + void registerParser(MetadataValueParser parser) { + supportedParsers.put(parser.getTypeUrl(), parser); + } + + void removeParser(MetadataValueParser parser) { + supportedParsers.remove(parser.getTypeUrl()); + } + + interface MetadataValueParser { + + String getTypeUrl(); + + /** + * Parses the given {@link Any} object into a specific metadata value. + * + * @param any the {@link Any} object to parse. + * @return the parsed metadata value. + * @throws InvalidProtocolBufferException if the parsing fails. + */ + Object parse(Any any) throws InvalidProtocolBufferException; + } +} diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index 1afe865d10a..626d61c1f55 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -25,6 +25,7 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Any; import com.google.protobuf.Duration; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; @@ -32,6 +33,7 @@ import com.google.protobuf.util.Durations; import io.envoyproxy.envoy.config.cluster.v3.CircuitBreakers.Thresholds; import io.envoyproxy.envoy.config.cluster.v3.Cluster; +import io.envoyproxy.envoy.config.core.v3.Metadata; import io.envoyproxy.envoy.config.core.v3.RoutingPriority; import io.envoyproxy.envoy.config.core.v3.SocketAddress; import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; @@ -44,12 +46,15 @@ import io.grpc.internal.ServiceConfigUtil.LbConfig; import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; +import io.grpc.xds.MetadataRegistry.MetadataValueParser; import io.grpc.xds.XdsClusterResource.CdsUpdate; import io.grpc.xds.client.XdsClient.ResourceUpdate; import io.grpc.xds.client.XdsResourceType; +import io.grpc.xds.internal.ProtobufJsonConverter; import io.grpc.xds.internal.security.CommonTlsContextUtil; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import javax.annotation.Nullable; @@ -171,9 +176,62 @@ static CdsUpdate processCluster(Cluster cluster, updateBuilder.filterMetadata( ImmutableMap.copyOf(cluster.getMetadata().getFilterMetadataMap())); + try { + ImmutableMap parsedFilterMetadata = + parseClusterMetadata(cluster.getMetadata()); + updateBuilder.parsedMetadata(parsedFilterMetadata); + } catch (InvalidProtocolBufferException e) { + throw new ResourceInvalidException( + "Failed to parse xDS filter metadata for cluster '" + cluster.getName() + "': " + + e.getMessage(), e); + } + return updateBuilder.build(); } + /** + * Parses cluster metadata into a structured map. + * + *

Values in {@code typed_filter_metadata} take precedence over + * {@code filter_metadata} when keys overlap, following Envoy API behavior. See + * + * Envoy metadata documentation for details. + * + * @param metadata the {@link Metadata} containing the fields to parse. + * @return an immutable map of parsed metadata. + * @throws InvalidProtocolBufferException if parsing {@code typed_filter_metadata} fails. + */ + private static ImmutableMap parseClusterMetadata(Metadata metadata) + throws InvalidProtocolBufferException { + ImmutableMap.Builder parsedMetadata = ImmutableMap.builder(); + + MetadataRegistry registry = MetadataRegistry.getInstance(); + // Process typed_filter_metadata + for (Map.Entry entry : metadata.getTypedFilterMetadataMap().entrySet()) { + String key = entry.getKey(); + Any value = entry.getValue(); + MetadataValueParser parser = registry.findParser(value.getTypeUrl()); + if (parser != null) { + Object parsedValue = parser.parse(value); + parsedMetadata.put(key, parsedValue); + } + } + // building once to reuse in the next loop + ImmutableMap intermediateParsedMetadata = parsedMetadata.build(); + + // Process filter_metadata for remaining keys + for (Map.Entry entry : metadata.getFilterMetadataMap().entrySet()) { + String key = entry.getKey(); + if (!intermediateParsedMetadata.containsKey(key)) { + Struct structValue = entry.getValue(); + Object jsonValue = ProtobufJsonConverter.convertToJson(structValue); + parsedMetadata.put(key, jsonValue); + } + } + + return parsedMetadata.build(); + } + private static StructOrError parseAggregateCluster(Cluster cluster) { String clusterName = cluster.getName(); Cluster.CustomClusterType customType = cluster.getClusterType(); @@ -573,13 +631,16 @@ abstract static class CdsUpdate implements ResourceUpdate { abstract ImmutableMap filterMetadata(); + abstract ImmutableMap parsedMetadata(); + private static Builder newBuilder(String clusterName) { return new AutoValue_XdsClusterResource_CdsUpdate.Builder() .clusterName(clusterName) .minRingSize(0) .maxRingSize(0) .choiceCount(0) - .filterMetadata(ImmutableMap.of()); + .filterMetadata(ImmutableMap.of()) + .parsedMetadata(ImmutableMap.of()); } static Builder forAggregate(String clusterName, List prioritizedClusterNames) { @@ -698,6 +759,8 @@ Builder leastRequestLbPolicy(Integer choiceCount) { protected abstract Builder filterMetadata(ImmutableMap filterMetadata); + protected abstract Builder parsedMetadata(ImmutableMap parsedMetadata); + abstract CdsUpdate build(); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java b/xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java new file mode 100644 index 00000000000..964c28c57e0 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/ProtobufJsonConverter.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal; + +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import io.grpc.Internal; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Converter for Protobuf {@link Struct} to JSON-like {@link Map}. + */ +@Internal +public final class ProtobufJsonConverter { + private ProtobufJsonConverter() {} + + public static Map convertToJson(Struct struct) { + Map result = new HashMap<>(); + for (Map.Entry entry : struct.getFieldsMap().entrySet()) { + result.put(entry.getKey(), convertValue(entry.getValue())); + } + return result; + } + + private static Object convertValue(Value value) { + switch (value.getKindCase()) { + case STRUCT_VALUE: + return convertToJson(value.getStructValue()); + case LIST_VALUE: + return value.getListValue().getValuesList().stream() + .map(ProtobufJsonConverter::convertValue) + .collect(Collectors.toList()); + case NUMBER_VALUE: + return value.getNumberValue(); + case STRING_VALUE: + return value.getStringValue(); + case BOOL_VALUE: + return value.getBoolValue(); + case NULL_VALUE: + return null; + default: + throw new IllegalArgumentException("Unknown Value type: " + value.getKindCase()); + } + } +} diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 6b905c4e2ba..654e85143b8 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -50,6 +50,7 @@ import io.envoyproxy.envoy.config.core.v3.DataSource; import io.envoyproxy.envoy.config.core.v3.HttpProtocolOptions; import io.envoyproxy.envoy.config.core.v3.Locality; +import io.envoyproxy.envoy.config.core.v3.Metadata; import io.envoyproxy.envoy.config.core.v3.PathConfigSource; import io.envoyproxy.envoy.config.core.v3.RuntimeFractionalPercent; import io.envoyproxy.envoy.config.core.v3.SelfConfigSource; @@ -84,6 +85,7 @@ import io.envoyproxy.envoy.extensions.filters.common.fault.v3.FaultDelay; import io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort; import io.envoyproxy.envoy.extensions.filters.http.fault.v3.HTTPFault; +import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.Audience; import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBACPerRoute; import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager; @@ -127,6 +129,7 @@ import io.grpc.xds.Endpoints.LbEndpoint; import io.grpc.xds.Endpoints.LocalityLbEndpoints; import io.grpc.xds.Filter.FilterConfig; +import io.grpc.xds.MetadataRegistry.MetadataValueParser; import io.grpc.xds.RouteLookupServiceClusterSpecifierPlugin.RlsPluginConfig; import io.grpc.xds.VirtualHost.Route; import io.grpc.xds.VirtualHost.Route.RouteAction; @@ -2341,6 +2344,173 @@ public void parseCluster_validateEdsSourceConfig() throws ResourceInvalidExcepti LoadBalancerRegistry.getDefaultRegistry()); } + @Test + public void processCluster_parsesMetadata() + throws ResourceInvalidException, InvalidProtocolBufferException { + MetadataRegistry metadataRegistry = MetadataRegistry.getInstance(); + + MetadataValueParser testParser = + new MetadataValueParser() { + @Override + public String getTypeUrl() { + return "type.googleapis.com/test.Type"; + } + + @Override + public Object parse(Any value) { + assertThat(value.getValue().toStringUtf8()).isEqualTo("test"); + return value.getValue().toStringUtf8() + "_processed"; + } + }; + metadataRegistry.registerParser(testParser); + + Any typedFilterMetadata = Any.newBuilder() + .setTypeUrl("type.googleapis.com/test.Type") + .setValue(ByteString.copyFromUtf8("test")) + .build(); + + Struct filterMetadata = Struct.newBuilder() + .putFields("key1", Value.newBuilder().setStringValue("value1").build()) + .putFields("key2", Value.newBuilder().setNumberValue(42).build()) + .build(); + + Metadata metadata = Metadata.newBuilder() + .putTypedFilterMetadata("TYPED_FILTER_METADATA", typedFilterMetadata) + .putFilterMetadata("FILTER_METADATA", filterMetadata) + .build(); + + Cluster cluster = Cluster.newBuilder() + .setName("cluster-foo.googleapis.com") + .setType(DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.getDefaultInstance())) + .setServiceName("service-foo.googleapis.com")) + .setLbPolicy(LbPolicy.ROUND_ROBIN) + .setMetadata(metadata) + .build(); + + CdsUpdate update = XdsClusterResource.processCluster( + cluster, null, LRS_SERVER_INFO, + LoadBalancerRegistry.getDefaultRegistry()); + + ImmutableMap expectedParsedMetadata = ImmutableMap.of( + "TYPED_FILTER_METADATA", "test_processed", + "FILTER_METADATA", ImmutableMap.of( + "key1", "value1", + "key2", 42.0)); + assertThat(update.parsedMetadata()).isEqualTo(expectedParsedMetadata); + metadataRegistry.removeParser(testParser); + } + + @Test + public void processCluster_parsesAudienceMetadata() + throws ResourceInvalidException, InvalidProtocolBufferException { + MetadataRegistry.getInstance(); + + Audience audience = Audience.newBuilder() + .setUrl("https://example.com") + .build(); + + Any audienceMetadata = Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience") + .setValue(audience.toByteString()) + .build(); + + Struct filterMetadata = Struct.newBuilder() + .putFields("key1", Value.newBuilder().setStringValue("value1").build()) + .putFields("key2", Value.newBuilder().setNumberValue(42).build()) + .build(); + + Metadata metadata = Metadata.newBuilder() + .putTypedFilterMetadata("AUDIENCE_METADATA", audienceMetadata) + .putFilterMetadata("FILTER_METADATA", filterMetadata) + .build(); + + Cluster cluster = Cluster.newBuilder() + .setName("cluster-foo.googleapis.com") + .setType(DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.getDefaultInstance())) + .setServiceName("service-foo.googleapis.com")) + .setLbPolicy(LbPolicy.ROUND_ROBIN) + .setMetadata(metadata) + .build(); + + CdsUpdate update = XdsClusterResource.processCluster( + cluster, null, LRS_SERVER_INFO, + LoadBalancerRegistry.getDefaultRegistry()); + + ImmutableMap expectedParsedMetadata = ImmutableMap.of( + "AUDIENCE_METADATA", "https://example.com", + "FILTER_METADATA", ImmutableMap.of( + "key1", "value1", + "key2", 42.0)); + assertThat(update.parsedMetadata()).isEqualTo(expectedParsedMetadata); + } + + @Test + public void processCluster_metadataKeyCollision_resolvesToTypedMetadata() + throws ResourceInvalidException, InvalidProtocolBufferException { + MetadataRegistry metadataRegistry = MetadataRegistry.getInstance(); + + MetadataValueParser testParser = + new MetadataValueParser() { + @Override + public String getTypeUrl() { + return "type.googleapis.com/test.Type"; + } + + @Override + public Object parse(Any value) { + return "typedMetadataValue"; + } + }; + metadataRegistry.registerParser(testParser); + + Any typedFilterMetadata = Any.newBuilder() + .setTypeUrl("type.googleapis.com/test.Type") + .setValue(ByteString.copyFromUtf8("test")) + .build(); + + Struct filterMetadata = Struct.newBuilder() + .putFields("key1", Value.newBuilder().setStringValue("filterMetadataValue").build()) + .build(); + + Metadata metadata = Metadata.newBuilder() + .putTypedFilterMetadata("key1", typedFilterMetadata) + .putFilterMetadata("key1", filterMetadata) + .build(); + + Cluster cluster = Cluster.newBuilder() + .setName("cluster-foo.googleapis.com") + .setType(DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.getDefaultInstance())) + .setServiceName("service-foo.googleapis.com")) + .setLbPolicy(LbPolicy.ROUND_ROBIN) + .setMetadata(metadata) + .build(); + + CdsUpdate update = XdsClusterResource.processCluster( + cluster, null, LRS_SERVER_INFO, + LoadBalancerRegistry.getDefaultRegistry()); + + ImmutableMap expectedParsedMetadata = ImmutableMap.of( + "key1", "typedMetadataValue"); + assertThat(update.parsedMetadata()).isEqualTo(expectedParsedMetadata); + metadataRegistry.removeParser(testParser); + } + + @Test public void parseServerSideListener_invalidTrafficDirection() throws ResourceInvalidException { Listener listener = diff --git a/xds/src/test/java/io/grpc/xds/internal/ProtobufJsonConverterTest.java b/xds/src/test/java/io/grpc/xds/internal/ProtobufJsonConverterTest.java new file mode 100644 index 00000000000..86f9be4dda8 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/ProtobufJsonConverterTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; + +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.ListValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ProtobufJsonConverterTest { + + @Test + public void testEmptyStruct() { + Struct emptyStruct = Struct.newBuilder().build(); + Map result = ProtobufJsonConverter.convertToJson(emptyStruct); + assertThat(result).isEmpty(); + } + + @Test + public void testStructWithValues() { + Struct struct = Struct.newBuilder() + .putFields("stringKey", Value.newBuilder().setStringValue("stringValue").build()) + .putFields("numberKey", Value.newBuilder().setNumberValue(123.45).build()) + .putFields("boolKey", Value.newBuilder().setBoolValue(true).build()) + .putFields("nullKey", Value.newBuilder().setNullValueValue(0).build()) + .putFields("structKey", Value.newBuilder() + .setStructValue(Struct.newBuilder() + .putFields("nestedKey", Value.newBuilder().setStringValue("nestedValue").build()) + .build()) + .build()) + .putFields("listKey", Value.newBuilder() + .setListValue(ListValue.newBuilder() + .addValues(Value.newBuilder().setNumberValue(1).build()) + .addValues(Value.newBuilder().setStringValue("two").build()) + .addValues(Value.newBuilder().setBoolValue(false).build()) + .build()) + .build()) + .build(); + + Map result = ProtobufJsonConverter.convertToJson(struct); + + Map goldenResult = new HashMap<>(); + goldenResult.put("stringKey", "stringValue"); + goldenResult.put("numberKey", 123.45); + goldenResult.put("boolKey", true); + goldenResult.put("nullKey", null); + goldenResult.put("structKey", ImmutableMap.of("nestedKey", "nestedValue")); + goldenResult.put("listKey", Arrays.asList(1.0, "two", false)); + + assertEquals(goldenResult, result); + } + + @Test(expected = IllegalArgumentException.class) + public void testUnknownValueType() { + Value unknownValue = Value.newBuilder().build(); // Default instance with no kind case set. + ProtobufJsonConverter.convertToJson( + Struct.newBuilder().putFields("unknownKey", unknownValue).build()); + } +} From ae109727d37e210292a87a0fccb9fb3b829d8b72 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 7 Jan 2025 14:33:09 +0530 Subject: [PATCH 138/591] Start 1.71.0 development cycle (#11801) --- MODULE.bazel | 2 +- build.gradle | 2 +- .../src/test/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/test/golden/TestService.java.txt | 2 +- core/src/main/java/io/grpc/internal/GrpcUtil.java | 2 +- examples/android/clientcache/app/build.gradle | 10 +++++----- examples/android/helloworld/app/build.gradle | 8 ++++---- examples/android/routeguide/app/build.gradle | 8 ++++---- examples/android/strictmode/app/build.gradle | 8 ++++---- examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 4 ++-- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 4 ++-- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 4 ++-- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 4 ++-- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 4 ++-- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- 33 files changed, 54 insertions(+), 54 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index edb5284a23d..4c7e1b3dca5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "grpc-java", compatibility_level = 0, repo_name = "io_grpc_grpc_java", - version = "1.70.0-SNAPSHOT", # CURRENT_GRPC_VERSION + version = "1.71.0-SNAPSHOT", # CURRENT_GRPC_VERSION ) # GRPC_DEPS_START diff --git a/build.gradle b/build.gradle index 39d2b6ad289..09c78735776 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.70.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.71.0-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index 01af7872648..e696def7b99 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; *

*/ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.70.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.71.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index b82475642b0..c052093cbbc 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; *
*/ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.70.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.71.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index 8a65c7d6c2b..fc420195667 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -219,7 +219,7 @@ public byte[] parseAsciiString(byte[] serialized) { public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); - public static final String IMPLEMENTATION_VERSION = "1.70.0-SNAPSHOT"; // CURRENT_GRPC_VERSION + public static final String IMPLEMENTATION_VERSION = "1.71.0-SNAPSHOT"; // CURRENT_GRPC_VERSION /** * The default timeout in nanos for a keepalive ping request. diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index d279155b64e..7700b2248eb 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -34,7 +34,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -54,12 +54,12 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.5' - testImplementation 'io.grpc:grpc-testing:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + testImplementation 'io.grpc:grpc-testing:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index d56a447377b..0fb396bbe39 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index 1dceb21b5c9..1a8209913a2 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index 2bb696027fd..02b17c189f9 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -33,7 +33,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -53,8 +53,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/build.gradle b/examples/build.gradle index 62428d7f81d..d4991f02f43 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index 2a119c5b80d..17b2568c7ea 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index eeabbef9473..7701465dee2 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index 2239bc03d5a..2976782a5d7 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -6,13 +6,13 @@ jar - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT example-debug https://github.com/grpc/grpc-java UTF-8 - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index 1e0879f38c4..a0f29660afc 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index 3c7f14cacde..99f98cc5b48 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -6,13 +6,13 @@ jar - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT example-dualstack https://github.com/grpc/grpc-java UTF-8 - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 844e86b71af..aea626a5193 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index 0aeedccbda3..00e7ee0e3ad 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,13 +6,13 @@ jar - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index 40dd86e4ba1..5ccb5bb0d3a 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 8fe78c8b6ee..3c7b0587e8f 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index a8d1d994a40..9eb5f38c364 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index b22318f4d53..d7ac43e79ae 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,13 +6,13 @@ jar - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index 831486ca872..64ea928456b 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index f90da62fcd8..6c1e172b2e0 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,13 +7,13 @@ jar - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 08d53a247c5..4de1183e0d7 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index 22d42abf642..f01b362347a 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -7,13 +7,13 @@ jar - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT example-oauth https://github.com/grpc/grpc-java UTF-8 - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index 4c6f4803014..2e0cbfbe0b6 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index a9fb615446c..ffa8295f849 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index 2549f5eecc1..8c885d9cb99 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index df846ed42be..163276aec10 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -15,7 +15,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index 61a9706ebf6..87e37c5a3b1 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index e16ce1018a8..ade33ee8769 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,13 +6,13 @@ jar - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT example-tls https://github.com/grpc/grpc-java UTF-8 - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index a1e2501a7b6..ef1bc185816 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.70.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/pom.xml b/examples/pom.xml index 04cd792cd3e..6370ce7d56a 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,13 +6,13 @@ jar - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT examples https://github.com/grpc/grpc-java UTF-8 - 1.70.0-SNAPSHOT + 1.71.0-SNAPSHOT 3.25.5 3.25.5 From e61b03cb9f3575dff82b5adf19181b61b3d62f5e Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 7 Jan 2025 08:31:15 -0800 Subject: [PATCH 139/591] netty: Fix getAttributes() data races in NettyClientTransportTest Since approximately the LBv2 API (the current API) was introduced, gRPC won't use a transport until it is ready. Long ago, transports could be used before they were ready and these old tests were not waiting for the negotiator to complete before starting. We need them to wait for the handshake to complete to avoid a test-only data race in getAttributes() noticed by TSAN. Throwing away data frames in the Noop handshaker is necessary to act like a normal handshaker; they don't allow data frames to pass until the handshake is complete. Without the handling, it goes through invalid code paths in NettyClientHandler where a terminated transport becomes ready, and a similar data race. ``` Write of size 4 at 0x00008db31e2c by thread T37: #0 io.grpc.netty.NettyClientHandler.handleProtocolNegotiationCompleted(Lio/grpc/Attributes;Lio/grpc/InternalChannelz$Security;)V NettyClientHandler.java:517 #1 io.grpc.netty.ProtocolNegotiators$GrpcNegotiationHandler.userEventTriggered(Lio/netty/channel/ChannelHandlerContext;Ljava/lang/Object;)V ProtocolNegotiators.java:937 #2 io.netty.channel.AbstractChannelHandlerContext.invokeUserEventTriggered(Ljava/lang/Object;)V AbstractChannelHandlerContext.java:398 #3 io.netty.channel.AbstractChannelHandlerContext.invokeUserEventTriggered(Lio/netty/channel/AbstractChannelHandlerContext;Ljava/lang/Object;)V AbstractChannelHandlerContext.java:376 #4 io.netty.channel.AbstractChannelHandlerContext.fireUserEventTriggered(Ljava/lang/Object;)Lio/netty/channel/ChannelHandlerContext; AbstractChannelHandlerContext.java:368 #5 io.grpc.netty.ProtocolNegotiators$ProtocolNegotiationHandler.fireProtocolNegotiationEvent(Lio/netty/channel/ChannelHandlerContext;)V ProtocolNegotiators.java:1107 #6 io.grpc.netty.ProtocolNegotiators$WaitUntilActiveHandler.channelActive(Lio/netty/channel/ChannelHandlerContext;)V ProtocolNegotiators.java:1011 ... Previous read of size 4 at 0x00008db31e2c by thread T4 (mutexes: write M0, write M1, write M2, write M3): #0 io.grpc.netty.NettyClientHandler.getAttributes()Lio/grpc/Attributes; NettyClientHandler.java:345 #1 io.grpc.netty.NettyClientTransport.getAttributes()Lio/grpc/Attributes; NettyClientTransport.java:387 #2 io.grpc.netty.NettyClientTransport.newStream(Lio/grpc/MethodDescriptor;Lio/grpc/Metadata;Lio/grpc/CallOptions;[Lio/grpc/ClientStreamTracer;)Lio/grpc/internal/ClientStream; NettyClientTransport.java:198 #3 io.grpc.netty.NettyClientTransportTest$Rpc.(Lio/grpc/netty/NettyClientTransport;Lio/grpc/Metadata;)V NettyClientTransportTest.java:953 #4 io.grpc.netty.NettyClientTransportTest.huffmanCodingShouldNotBePerformed()V NettyClientTransportTest.java:631 ... ``` ``` Read of size 4 at 0x00008f983a3c by thread T4 (mutexes: write M0, write M1): #0 io.grpc.netty.NettyClientHandler.getAttributes()Lio/grpc/Attributes; NettyClientHandler.java:345 #1 io.grpc.netty.NettyClientTransport.getAttributes()Lio/grpc/Attributes; NettyClientTransport.java:387 #2 io.grpc.netty.NettyClientTransport.newStream(Lio/grpc/MethodDescriptor;Lio/grpc/Metadata;Lio/grpc/CallOptions;[Lio/grpc/ClientStreamTracer;)Lio/grpc/internal/ClientStream; NettyClientTransport.java:198 #3 io.grpc.netty.NettyClientTransportTest$Rpc.(Lio/grpc/netty/NettyClientTransport;Lio/grpc/Metadata;)V NettyClientTransportTest.java:973 #4 io.grpc.netty.NettyClientTransportTest$Rpc.(Lio/grpc/netty/NettyClientTransport;)V NettyClientTransportTest.java:969 #5 io.grpc.netty.NettyClientTransportTest.handlerExceptionDuringNegotiatonPropagatesToStatus()V NettyClientTransportTest.java:425 ... Previous write of size 4 at 0x00008f983a3c by thread T56: #0 io.grpc.netty.NettyClientHandler$FrameListener.onSettingsRead(Lio/netty/channel/ChannelHandlerContext;Lio/netty/handler/codec/http2/Http2Settings;)V NettyClientHandler.java:960 ... ``` --- .../grpc/netty/NettyClientTransportTest.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index 8d0d656859f..d0a6456c430 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -37,6 +37,8 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.common.base.Optional; @@ -95,6 +97,7 @@ import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.SslContext; import io.netty.util.AsciiString; +import io.netty.util.ReferenceCountUtil; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -187,6 +190,7 @@ public void addDefaultUserAgent() throws Exception { startServer(); NettyClientTransport transport = newTransport(newNegotiator()); callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); // Send a single RPC and wait for the response. new Rpc(transport).halfClose().waitForResponse(); @@ -244,6 +248,7 @@ public void overrideDefaultUserAgent() throws Exception { NettyClientTransport transport = newTransport(newNegotiator(), DEFAULT_MAX_MESSAGE_SIZE, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, "testUserAgent", true); callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); new Rpc(transport, new Metadata()).halfClose().waitForResponse(); @@ -261,6 +266,7 @@ public void maxMessageSizeShouldBeEnforced() throws Throwable { NettyClientTransport transport = newTransport(newNegotiator(), 1, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, null, true); callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); try { // Send a single RPC and wait for the response. @@ -287,6 +293,7 @@ public void creatingMultipleTlsTransportsShouldSucceed() throws Exception { NettyClientTransport transport = newTransport(negotiator); callMeMaybe(transport.start(clientTransportListener)); } + verify(clientTransportListener, timeout(5000).times(2)).transportReady(); // Send a single RPC on each transport. final List rpcs = new ArrayList<>(transports.size()); @@ -316,6 +323,7 @@ public void run() { failureStatus.asRuntimeException()); } }); + verify(clientTransportListener, timeout(5000)).transportTerminated(); Rpc rpc = new Rpc(transport).halfClose(); try { @@ -349,6 +357,7 @@ public void tlsNegotiationFailurePropagatesToStatus() throws Exception { ProtocolNegotiator negotiator = ProtocolNegotiators.tls(clientContext); final NettyClientTransport transport = newTransport(negotiator); callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportTerminated(); Rpc rpc = new Rpc(transport).halfClose(); try { @@ -378,6 +387,7 @@ public void channelExceptionDuringNegotiatonPropagatesToStatus() throws Exceptio callMeMaybe(transport.start(clientTransportListener)); final Status failureStatus = Status.UNAVAILABLE.withDescription("oh noes!"); transport.channel().pipeline().fireExceptionCaught(failureStatus.asRuntimeException()); + verify(clientTransportListener, timeout(5000)).transportTerminated(); Rpc rpc = new Rpc(transport).halfClose(); try { @@ -409,6 +419,7 @@ public void run() { } } }); + verify(clientTransportListener, timeout(5000)).transportTerminated(); Rpc rpc = new Rpc(transport).halfClose(); try { @@ -428,6 +439,7 @@ public void bufferedStreamsShouldBeClosedWhenConnectionTerminates() throws Excep NettyClientTransport transport = newTransport(newNegotiator()); callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); // Send a dummy RPC in order to ensure that the updated SETTINGS_MAX_CONCURRENT_STREAMS // has been received by the remote endpoint. @@ -579,6 +591,7 @@ public void maxHeaderListSizeShouldBeEnforcedOnClient() throws Exception { NettyClientTransport transport = newTransport(newNegotiator(), DEFAULT_MAX_MESSAGE_SIZE, 1, null, true); callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); try { // Send a single RPC and wait for the response. @@ -612,6 +625,7 @@ public void huffmanCodingShouldNotBePerformed() throws Exception { longStringOfA); callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); AtomicBoolean foundExpectedHeaderBytes = new AtomicBoolean(false); @@ -641,6 +655,7 @@ public void maxHeaderListSizeShouldBeEnforcedOnServer() throws Exception { NettyClientTransport transport = newTransport(newNegotiator()); callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); try { // Send a single RPC and wait for the response. @@ -685,6 +700,7 @@ public void clientStreamGetsAttributes() throws Exception { startServer(); NettyClientTransport transport = newTransport(newNegotiator()); callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); Rpc rpc = new Rpc(transport).halfClose(); rpc.waitForResponse(); @@ -703,6 +719,7 @@ public void keepAliveEnabled() throws Exception { NettyClientTransport transport = newTransport(newNegotiator(), DEFAULT_MAX_MESSAGE_SIZE, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, null /* user agent */, true /* keep alive */); callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); Rpc rpc = new Rpc(transport).halfClose(); rpc.waitForResponse(); @@ -715,6 +732,7 @@ public void keepAliveDisabled() throws Exception { NettyClientTransport transport = newTransport(newNegotiator(), DEFAULT_MAX_MESSAGE_SIZE, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, null /* user agent */, false /* keep alive */); callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); Rpc rpc = new Rpc(transport).halfClose(); rpc.waitForResponse(); @@ -808,6 +826,7 @@ public void tlsNegotiationServerExecutorShouldSucceed() throws Exception { assertEquals(true, clientExecutorPool.isInUse()); final NettyClientTransport transport = newTransport(negotiator); callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); Rpc rpc = new Rpc(transport).halfClose(); rpc.waitForResponse(); // closing the negotiators should return the executors back to pool, and release the resource @@ -1098,9 +1117,15 @@ public NoopHandler(GrpcHttp2ConnectionHandler grpcHandler) { this.grpcHandler = grpcHandler; } + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // Prevent any data being passed to NettyClientHandler + ReferenceCountUtil.release(msg); + } + @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { - ctx.pipeline().addBefore(ctx.name(), null, grpcHandler); + ctx.pipeline().addAfter(ctx.name(), null, grpcHandler); } public void fail(ChannelHandlerContext ctx, Throwable cause) { From 73721acc0d341acd2a45e6edc040460ddf40ae5f Mon Sep 17 00:00:00 2001 From: Albumen Kevin Date: Fri, 10 Jan 2025 03:53:36 +0800 Subject: [PATCH 140/591] Add UnitTest to verify updateTrustCredentials rotate (#11798) * Add lastUpdateTime to avoid read --- .../util/AdvancedTlsX509TrustManager.java | 6 +-- .../util/AdvancedTlsX509TrustManagerTest.java | 43 ++++++++++++++++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java b/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java index 088f4caa000..b4b9b25d1de 100644 --- a/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java +++ b/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java @@ -265,7 +265,7 @@ public Closeable updateTrustCredentials(File trustCertFile, long period, TimeUni } final ScheduledFuture future = checkNotNull(executor, "executor").scheduleWithFixedDelay( - new LoadFilePathExecution(trustCertFile), period, period, unit); + new LoadFilePathExecution(trustCertFile, updatedTime), period, period, unit); return () -> future.cancel(false); } @@ -312,9 +312,9 @@ private class LoadFilePathExecution implements Runnable { File file; long currentTime; - public LoadFilePathExecution(File file) { + public LoadFilePathExecution(File file, long currentTime) { this.file = file; - this.currentTime = 0; + this.currentTime = currentTime; } @Override diff --git a/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java b/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java index 91159d121ad..5ad0e85fa36 100644 --- a/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java +++ b/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.when; import com.google.common.collect.Iterables; +import com.google.common.io.Files; import io.grpc.internal.FakeClock; import io.grpc.internal.testing.TestUtils; import io.grpc.testing.TlsTesting; @@ -57,21 +58,28 @@ public class AdvancedTlsX509TrustManagerTest { private static final String CA_PEM_FILE = "ca.pem"; private static final String SERVER_0_PEM_FILE = "server0.pem"; + private static final String SERVER_1_PEM_FILE = "server1.pem"; private File caCertFile; private File serverCert0File; + private File serverCert1File; private X509Certificate[] caCert; private X509Certificate[] serverCert0; + private X509Certificate[] serverCert1; + private FakeClock fakeClock; private ScheduledExecutorService executor; @Before public void setUp() throws IOException, GeneralSecurityException { - executor = new FakeClock().getScheduledExecutorService(); + fakeClock = new FakeClock(); + executor = fakeClock.getScheduledExecutorService(); caCertFile = TestUtils.loadCert(CA_PEM_FILE); caCert = CertificateUtils.getX509Certificates(TlsTesting.loadCert(CA_PEM_FILE)); serverCert0File = TestUtils.loadCert(SERVER_0_PEM_FILE); serverCert0 = CertificateUtils.getX509Certificates(TlsTesting.loadCert(SERVER_0_PEM_FILE)); + serverCert1File = TestUtils.loadCert(SERVER_1_PEM_FILE); + serverCert1 = CertificateUtils.getX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); } @Test @@ -147,6 +155,39 @@ public void clientTrustedWithSocketTest() throws Exception { assertEquals("No handshake session", ce.getMessage()); } + @Test + public void updateTrustCredentials_rotate() throws GeneralSecurityException, IOException { + AdvancedTlsX509TrustManager trustManager = AdvancedTlsX509TrustManager.newBuilder().build(); + trustManager.updateTrustCredentials(serverCert0File); + assertArrayEquals(serverCert0, trustManager.getAcceptedIssuers()); + + trustManager.updateTrustCredentials(serverCert0File, 1, TimeUnit.MINUTES, + executor); + assertArrayEquals(serverCert0, trustManager.getAcceptedIssuers()); + + fakeClock.forwardTime(1, TimeUnit.MINUTES); + assertArrayEquals(serverCert0, trustManager.getAcceptedIssuers()); + + serverCert0File.setLastModified(serverCert0File.lastModified() - 10); + + fakeClock.forwardTime(1, TimeUnit.MINUTES); + assertArrayEquals(serverCert0, trustManager.getAcceptedIssuers()); + + long beforeModify = serverCert0File.lastModified(); + Files.copy(serverCert1File, serverCert0File); + serverCert0File.setLastModified(beforeModify); + + // although file content changed, file modification time is not changed + fakeClock.forwardTime(1, TimeUnit.MINUTES); + assertArrayEquals(serverCert0, trustManager.getAcceptedIssuers()); + + serverCert0File.setLastModified(beforeModify + 10); + + // file modification time changed + fakeClock.forwardTime(1, TimeUnit.MINUTES); + assertArrayEquals(serverCert1, trustManager.getAcceptedIssuers()); + } + private static class TestHandler extends Handler { private final List records = new ArrayList<>(); From 7b5d0692cc70c02ae644deb648410c7e82e7ea45 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 9 Jan 2025 13:45:35 -0800 Subject: [PATCH 141/591] Replace jsr305's CheckReturnValue with Error Prone's (#11811) We should avoid jsr305 and error prone's has the same semantics. Fixes #8687 --- api/src/main/java/io/grpc/CallOptions.java | 2 +- api/src/main/java/io/grpc/MethodDescriptor.java | 2 +- api/src/main/java/io/grpc/Status.java | 2 +- api/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java | 2 +- binder/src/main/java/io/grpc/binder/AsyncSecurityPolicy.java | 2 +- binder/src/main/java/io/grpc/binder/SecurityPolicy.java | 2 +- binder/src/main/java/io/grpc/binder/ServerSecurityPolicy.java | 2 +- .../src/main/java/io/grpc/binder/UntrustedSecurityPolicies.java | 2 +- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 2 +- .../java/io/grpc/binder/internal/BinderTransportSecurity.java | 2 +- core/src/main/java/io/grpc/internal/ClientTransportFactory.java | 2 +- core/src/main/java/io/grpc/internal/DelayedStream.java | 2 +- core/src/main/java/io/grpc/internal/KeepAliveEnforcer.java | 2 +- core/src/main/java/io/grpc/internal/ManagedClientTransport.java | 2 +- core/src/main/java/io/grpc/internal/RetriableStream.java | 2 +- core/src/main/java/io/grpc/internal/TransportFrameUtil.java | 2 +- .../src/main/java/io/grpc/inprocess/InProcessTransport.java | 2 +- netty/src/main/java/io/grpc/netty/ProtocolNegotiationEvent.java | 2 +- netty/src/main/java/io/grpc/netty/Utils.java | 2 +- netty/src/main/java/io/grpc/netty/package-info.java | 2 +- okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java | 2 +- okhttp/src/main/java/io/grpc/okhttp/Utils.java | 2 +- protobuf/BUILD.bazel | 1 + .../java/io/grpc/protobuf/ProtoMethodDescriptorSupplier.java | 2 +- rls/BUILD.bazel | 1 + rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java | 2 +- rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java | 2 +- rls/src/main/java/io/grpc/rls/LruCache.java | 2 +- rls/src/main/java/io/grpc/rls/RlsRequestFactory.java | 2 +- .../java/io/grpc/servlet/AsyncServletOutputStreamWriter.java | 2 +- stub/src/main/java/io/grpc/stub/AbstractAsyncStub.java | 2 +- stub/src/main/java/io/grpc/stub/AbstractBlockingStub.java | 2 +- stub/src/main/java/io/grpc/stub/AbstractFutureStub.java | 2 +- stub/src/main/java/io/grpc/stub/AbstractStub.java | 2 +- .../io/grpc/xds/internal/security/ReferenceCountingMap.java | 2 +- 35 files changed, 35 insertions(+), 33 deletions(-) diff --git a/api/src/main/java/io/grpc/CallOptions.java b/api/src/main/java/io/grpc/CallOptions.java index a1b8984c48b..800bdfb6c90 100644 --- a/api/src/main/java/io/grpc/CallOptions.java +++ b/api/src/main/java/io/grpc/CallOptions.java @@ -21,6 +21,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.CheckReturnValue; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -28,7 +29,6 @@ import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; diff --git a/api/src/main/java/io/grpc/MethodDescriptor.java b/api/src/main/java/io/grpc/MethodDescriptor.java index 1bfaccb4201..a02eb840deb 100644 --- a/api/src/main/java/io/grpc/MethodDescriptor.java +++ b/api/src/main/java/io/grpc/MethodDescriptor.java @@ -20,9 +20,9 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.CheckReturnValue; import java.io.InputStream; import java.util.concurrent.atomic.AtomicReferenceArray; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; diff --git a/api/src/main/java/io/grpc/Status.java b/api/src/main/java/io/grpc/Status.java index 5d7dd30df01..38cd9581f8e 100644 --- a/api/src/main/java/io/grpc/Status.java +++ b/api/src/main/java/io/grpc/Status.java @@ -23,6 +23,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Metadata.TrustedAsciiMarshaller; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -30,7 +31,6 @@ import java.util.Collections; import java.util.List; import java.util.TreeMap; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; diff --git a/api/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java b/api/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java index 94ab8fb9b18..c2b4d8412a7 100644 --- a/api/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java +++ b/api/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java @@ -24,9 +24,9 @@ import com.google.common.truth.ComparableSubject; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Deadline; import java.util.concurrent.TimeUnit; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; /** Propositions for {@link Deadline} subjects. */ diff --git a/binder/src/main/java/io/grpc/binder/AsyncSecurityPolicy.java b/binder/src/main/java/io/grpc/binder/AsyncSecurityPolicy.java index 2a37e6fd517..5b17ad35977 100644 --- a/binder/src/main/java/io/grpc/binder/AsyncSecurityPolicy.java +++ b/binder/src/main/java/io/grpc/binder/AsyncSecurityPolicy.java @@ -17,11 +17,11 @@ package io.grpc.binder; import com.google.common.util.concurrent.ListenableFuture; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.ExperimentalApi; import io.grpc.Status; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; -import javax.annotation.CheckReturnValue; /** * Decides whether a given Android UID is authorized to access some resource. diff --git a/binder/src/main/java/io/grpc/binder/SecurityPolicy.java b/binder/src/main/java/io/grpc/binder/SecurityPolicy.java index e539f17e394..261e5223a0f 100644 --- a/binder/src/main/java/io/grpc/binder/SecurityPolicy.java +++ b/binder/src/main/java/io/grpc/binder/SecurityPolicy.java @@ -16,8 +16,8 @@ package io.grpc.binder; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Status; -import javax.annotation.CheckReturnValue; /** * Decides whether a given Android UID is authorized to access some resource. diff --git a/binder/src/main/java/io/grpc/binder/ServerSecurityPolicy.java b/binder/src/main/java/io/grpc/binder/ServerSecurityPolicy.java index 6a9361c0eaf..4786a5e6cc4 100644 --- a/binder/src/main/java/io/grpc/binder/ServerSecurityPolicy.java +++ b/binder/src/main/java/io/grpc/binder/ServerSecurityPolicy.java @@ -19,10 +19,10 @@ import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Status; import java.util.HashMap; import java.util.Map; -import javax.annotation.CheckReturnValue; /** * A security policy for a gRPC server. diff --git a/binder/src/main/java/io/grpc/binder/UntrustedSecurityPolicies.java b/binder/src/main/java/io/grpc/binder/UntrustedSecurityPolicies.java index 64d8ac1426a..44612a82109 100644 --- a/binder/src/main/java/io/grpc/binder/UntrustedSecurityPolicies.java +++ b/binder/src/main/java/io/grpc/binder/UntrustedSecurityPolicies.java @@ -16,9 +16,9 @@ package io.grpc.binder; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.ExperimentalApi; import io.grpc.Status; -import javax.annotation.CheckReturnValue; /** Static factory methods for creating untrusted security policies. */ @CheckReturnValue diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 254ad5bb407..0c886a9829c 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -35,6 +35,7 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.ClientStreamTracer; @@ -78,7 +79,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransportSecurity.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransportSecurity.java index 430eee3e041..6f95ef8a83c 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransportSecurity.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransportSecurity.java @@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Attributes; import io.grpc.Internal; import io.grpc.Metadata; @@ -35,7 +36,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; /** diff --git a/core/src/main/java/io/grpc/internal/ClientTransportFactory.java b/core/src/main/java/io/grpc/internal/ClientTransportFactory.java index d987f9d5068..6c10ced4652 100644 --- a/core/src/main/java/io/grpc/internal/ClientTransportFactory.java +++ b/core/src/main/java/io/grpc/internal/ClientTransportFactory.java @@ -18,6 +18,7 @@ import com.google.common.base.Objects; import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Attributes; import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; @@ -27,7 +28,6 @@ import java.net.SocketAddress; import java.util.Collection; import java.util.concurrent.ScheduledExecutorService; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; /** Pre-configured factory for creating {@link ConnectionClientTransport} instances. */ diff --git a/core/src/main/java/io/grpc/internal/DelayedStream.java b/core/src/main/java/io/grpc/internal/DelayedStream.java index c94986a3458..5f14f24cfe5 100644 --- a/core/src/main/java/io/grpc/internal/DelayedStream.java +++ b/core/src/main/java/io/grpc/internal/DelayedStream.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.annotations.VisibleForTesting; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Attributes; import io.grpc.Compressor; import io.grpc.Deadline; @@ -30,7 +31,6 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; -import javax.annotation.CheckReturnValue; import javax.annotation.concurrent.GuardedBy; /** diff --git a/core/src/main/java/io/grpc/internal/KeepAliveEnforcer.java b/core/src/main/java/io/grpc/internal/KeepAliveEnforcer.java index dd539e75a18..6480336470c 100644 --- a/core/src/main/java/io/grpc/internal/KeepAliveEnforcer.java +++ b/core/src/main/java/io/grpc/internal/KeepAliveEnforcer.java @@ -18,8 +18,8 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.CheckReturnValue; import java.util.concurrent.TimeUnit; -import javax.annotation.CheckReturnValue; /** Monitors the client's PING usage to make sure the rate is permitted. */ public final class KeepAliveEnforcer { diff --git a/core/src/main/java/io/grpc/internal/ManagedClientTransport.java b/core/src/main/java/io/grpc/internal/ManagedClientTransport.java index 5f8fe52ef6b..184a4d98955 100644 --- a/core/src/main/java/io/grpc/internal/ManagedClientTransport.java +++ b/core/src/main/java/io/grpc/internal/ManagedClientTransport.java @@ -16,9 +16,9 @@ package io.grpc.internal; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Attributes; import io.grpc.Status; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; diff --git a/core/src/main/java/io/grpc/internal/RetriableStream.java b/core/src/main/java/io/grpc/internal/RetriableStream.java index ba9424ea25c..7fed77625d3 100644 --- a/core/src/main/java/io/grpc/internal/RetriableStream.java +++ b/core/src/main/java/io/grpc/internal/RetriableStream.java @@ -22,6 +22,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Objects; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Attributes; import io.grpc.ClientStreamTracer; import io.grpc.Compressor; @@ -47,7 +48,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.CheckForNull; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; diff --git a/core/src/main/java/io/grpc/internal/TransportFrameUtil.java b/core/src/main/java/io/grpc/internal/TransportFrameUtil.java index f3c32416426..3bd7ee72239 100644 --- a/core/src/main/java/io/grpc/internal/TransportFrameUtil.java +++ b/core/src/main/java/io/grpc/internal/TransportFrameUtil.java @@ -19,13 +19,13 @@ import static java.nio.charset.StandardCharsets.US_ASCII; import com.google.common.io.BaseEncoding; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.InternalMetadata; import io.grpc.Metadata; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.logging.Logger; -import javax.annotation.CheckReturnValue; /** * Utility functions for transport layer framing. diff --git a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java index eacf46ca4a2..b5bbbe563df 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java @@ -24,6 +24,7 @@ import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.ClientStreamTracer; @@ -74,7 +75,6 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; diff --git a/netty/src/main/java/io/grpc/netty/ProtocolNegotiationEvent.java b/netty/src/main/java/io/grpc/netty/ProtocolNegotiationEvent.java index 16da79e1af8..8103a2dc79f 100644 --- a/netty/src/main/java/io/grpc/netty/ProtocolNegotiationEvent.java +++ b/netty/src/main/java/io/grpc/netty/ProtocolNegotiationEvent.java @@ -20,10 +20,10 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Attributes; import io.grpc.Internal; import io.grpc.InternalChannelz.Security; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; /** diff --git a/netty/src/main/java/io/grpc/netty/Utils.java b/netty/src/main/java/io/grpc/netty/Utils.java index ba405637af5..c0981f5b219 100644 --- a/netty/src/main/java/io/grpc/netty/Utils.java +++ b/netty/src/main/java/io/grpc/netty/Utils.java @@ -27,6 +27,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.InternalChannelz; import io.grpc.InternalMetadata; import io.grpc.Metadata; @@ -68,7 +69,6 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.net.ssl.SSLException; diff --git a/netty/src/main/java/io/grpc/netty/package-info.java b/netty/src/main/java/io/grpc/netty/package-info.java index 54595b38573..d1d7b87cf51 100644 --- a/netty/src/main/java/io/grpc/netty/package-info.java +++ b/netty/src/main/java/io/grpc/netty/package-info.java @@ -18,5 +18,5 @@ * The main transport implementation based on Netty, * for both the client and the server. */ -@javax.annotation.CheckReturnValue +@com.google.errorprone.annotations.CheckReturnValue package io.grpc.netty; diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java index 15508110344..f42cb9fb16d 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java @@ -22,6 +22,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; import io.grpc.ChannelLogger; @@ -72,7 +73,6 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.net.SocketFactory; import javax.net.ssl.HostnameVerifier; diff --git a/okhttp/src/main/java/io/grpc/okhttp/Utils.java b/okhttp/src/main/java/io/grpc/okhttp/Utils.java index 2dc5f1e1ec9..4546143cf3b 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/Utils.java +++ b/okhttp/src/main/java/io/grpc/okhttp/Utils.java @@ -17,6 +17,7 @@ package io.grpc.okhttp; import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.InternalChannelz; import io.grpc.InternalMetadata; import io.grpc.Metadata; @@ -29,7 +30,6 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.CheckReturnValue; /** * Common utility methods for OkHttp transport. diff --git a/protobuf/BUILD.bazel b/protobuf/BUILD.bazel index 724c78ca6ee..02a136554d2 100644 --- a/protobuf/BUILD.bazel +++ b/protobuf/BUILD.bazel @@ -12,6 +12,7 @@ java_library( "@com_google_protobuf//:protobuf_java", artifact("com.google.api.grpc:proto-google-common-protos"), artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.errorprone:error_prone_annotations"), artifact("com.google.guava:guava"), ], ) diff --git a/protobuf/src/main/java/io/grpc/protobuf/ProtoMethodDescriptorSupplier.java b/protobuf/src/main/java/io/grpc/protobuf/ProtoMethodDescriptorSupplier.java index e5b2f38e3c0..e7cd3ed336f 100644 --- a/protobuf/src/main/java/io/grpc/protobuf/ProtoMethodDescriptorSupplier.java +++ b/protobuf/src/main/java/io/grpc/protobuf/ProtoMethodDescriptorSupplier.java @@ -16,8 +16,8 @@ package io.grpc.protobuf; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.protobuf.Descriptors.MethodDescriptor; -import javax.annotation.CheckReturnValue; /** * Provides access to the underlying proto service method descriptor. diff --git a/rls/BUILD.bazel b/rls/BUILD.bazel index 10a5e22524a..bfa0c7eee97 100644 --- a/rls/BUILD.bazel +++ b/rls/BUILD.bazel @@ -19,6 +19,7 @@ java_library( "@io_grpc_grpc_proto//:rls_java_proto", artifact("com.google.auto.value:auto-value-annotations"), artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.errorprone:error_prone_annotations"), artifact("com.google.guava:guava"), ], ) diff --git a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java index 3d52187a158..9775753ab50 100644 --- a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java +++ b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java @@ -28,6 +28,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.ChannelLogger; import io.grpc.ChannelLogger.ChannelLogLevel; import io.grpc.ConnectivityState; @@ -73,7 +74,6 @@ import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; diff --git a/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java b/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java index ba0575efa57..b39b463c762 100644 --- a/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java +++ b/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java @@ -22,6 +22,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Ticker; +import com.google.errorprone.annotations.CheckReturnValue; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -29,7 +30,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; /** diff --git a/rls/src/main/java/io/grpc/rls/LruCache.java b/rls/src/main/java/io/grpc/rls/LruCache.java index 1ad5a958289..8fc4ae98472 100644 --- a/rls/src/main/java/io/grpc/rls/LruCache.java +++ b/rls/src/main/java/io/grpc/rls/LruCache.java @@ -16,7 +16,7 @@ package io.grpc.rls; -import javax.annotation.CheckReturnValue; +import com.google.errorprone.annotations.CheckReturnValue; import javax.annotation.Nullable; /** An LruCache is a cache with least recently used eviction. */ diff --git a/rls/src/main/java/io/grpc/rls/RlsRequestFactory.java b/rls/src/main/java/io/grpc/rls/RlsRequestFactory.java index a6ca0137ff1..e26e49979e1 100644 --- a/rls/src/main/java/io/grpc/rls/RlsRequestFactory.java +++ b/rls/src/main/java/io/grpc/rls/RlsRequestFactory.java @@ -20,6 +20,7 @@ import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.Metadata; import io.grpc.rls.RlsProtoData.ExtraKeys; import io.grpc.rls.RlsProtoData.GrpcKeyBuilder; @@ -30,7 +31,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import javax.annotation.CheckReturnValue; /** * A RlsRequestFactory creates {@link RouteLookupRequest} using key builder map from {@link diff --git a/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java b/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java index cfd29b1a2fd..8c0c6ec6512 100644 --- a/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java +++ b/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java @@ -22,6 +22,7 @@ import static java.util.logging.Level.FINEST; import com.google.common.annotations.VisibleForTesting; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.InternalLogId; import io.grpc.servlet.ServletServerStream.ServletTransportState; import java.io.IOException; @@ -34,7 +35,6 @@ import java.util.function.BooleanSupplier; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.servlet.AsyncContext; import javax.servlet.ServletOutputStream; diff --git a/stub/src/main/java/io/grpc/stub/AbstractAsyncStub.java b/stub/src/main/java/io/grpc/stub/AbstractAsyncStub.java index c6f912cb3a7..f369eeaf87f 100644 --- a/stub/src/main/java/io/grpc/stub/AbstractAsyncStub.java +++ b/stub/src/main/java/io/grpc/stub/AbstractAsyncStub.java @@ -16,10 +16,10 @@ package io.grpc.stub; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.stub.ClientCalls.StubType; -import javax.annotation.CheckReturnValue; import javax.annotation.concurrent.ThreadSafe; /** diff --git a/stub/src/main/java/io/grpc/stub/AbstractBlockingStub.java b/stub/src/main/java/io/grpc/stub/AbstractBlockingStub.java index 1cb919e67b0..4bdb3c0bb94 100644 --- a/stub/src/main/java/io/grpc/stub/AbstractBlockingStub.java +++ b/stub/src/main/java/io/grpc/stub/AbstractBlockingStub.java @@ -16,10 +16,10 @@ package io.grpc.stub; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.stub.ClientCalls.StubType; -import javax.annotation.CheckReturnValue; import javax.annotation.concurrent.ThreadSafe; /** diff --git a/stub/src/main/java/io/grpc/stub/AbstractFutureStub.java b/stub/src/main/java/io/grpc/stub/AbstractFutureStub.java index 66570bcd6ff..5e37b1e4915 100644 --- a/stub/src/main/java/io/grpc/stub/AbstractFutureStub.java +++ b/stub/src/main/java/io/grpc/stub/AbstractFutureStub.java @@ -16,10 +16,10 @@ package io.grpc.stub; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.stub.ClientCalls.StubType; -import javax.annotation.CheckReturnValue; import javax.annotation.concurrent.ThreadSafe; /** diff --git a/stub/src/main/java/io/grpc/stub/AbstractStub.java b/stub/src/main/java/io/grpc/stub/AbstractStub.java index 7b4bbed34a8..697107760db 100644 --- a/stub/src/main/java/io/grpc/stub/AbstractStub.java +++ b/stub/src/main/java/io/grpc/stub/AbstractStub.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.InternalTimeUtils.convert; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.CallCredentials; import io.grpc.CallOptions; import io.grpc.Channel; @@ -30,7 +31,6 @@ import java.time.Duration; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; diff --git a/xds/src/main/java/io/grpc/xds/internal/security/ReferenceCountingMap.java b/xds/src/main/java/io/grpc/xds/internal/security/ReferenceCountingMap.java index b7f56492fa5..08b8f6a325b 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/ReferenceCountingMap.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/ReferenceCountingMap.java @@ -20,9 +20,9 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.errorprone.annotations.CheckReturnValue; import java.util.HashMap; import java.util.Map; -import javax.annotation.CheckReturnValue; import javax.annotation.concurrent.ThreadSafe; /** From 70825adce6a3de06f1f93543a05a010b7c77c4aa Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 9 Jan 2025 13:09:57 -0800 Subject: [PATCH 142/591] Replace jsr305's GuardedBy with Error Prone's We should avoid jsr305 and error prone's has the same semantics. --- alts/BUILD.bazel | 1 + alts/src/main/java/io/grpc/alts/internal/AsyncSemaphore.java | 2 +- .../src/main/java/io/grpc/android/AndroidChannelBuilder.java | 2 +- api/src/main/java/io/grpc/ConfiguratorRegistry.java | 2 +- api/src/main/java/io/grpc/ManagedChannelRegistry.java | 2 +- api/src/main/java/io/grpc/MetricInstrumentRegistry.java | 2 +- api/src/main/java/io/grpc/NameResolverRegistry.java | 2 +- api/src/main/java/io/grpc/ServerRegistry.java | 2 +- binder/src/androidTest/java/io/grpc/binder/HostServices.java | 2 +- .../io/grpc/binder/internal/BinderClientTransportTest.java | 2 +- .../java/io/grpc/binder/internal/ActiveTransportTracker.java | 2 +- .../src/main/java/io/grpc/binder/internal/BinderServer.java | 2 +- .../main/java/io/grpc/binder/internal/BinderTransport.java | 2 +- .../src/main/java/io/grpc/binder/internal/FlowController.java | 2 +- binder/src/main/java/io/grpc/binder/internal/Inbound.java | 2 +- binder/src/main/java/io/grpc/binder/internal/Outbound.java | 2 +- binder/src/main/java/io/grpc/binder/internal/PingTracker.java | 2 +- .../src/main/java/io/grpc/binder/internal/ServiceBinding.java | 2 +- census/BUILD.bazel | 1 + census/src/main/java/io/grpc/census/CensusStatsModule.java | 2 +- core/src/main/java/io/grpc/internal/AbstractStream.java | 2 +- .../internal/CallCredentialsApplyingTransportFactory.java | 2 +- core/src/main/java/io/grpc/internal/ChannelTracer.java | 2 +- core/src/main/java/io/grpc/internal/DelayedClientCall.java | 2 +- .../main/java/io/grpc/internal/DelayedClientTransport.java | 2 +- core/src/main/java/io/grpc/internal/DelayedStream.java | 2 +- core/src/main/java/io/grpc/internal/Http2Ping.java | 2 +- core/src/main/java/io/grpc/internal/KeepAliveManager.java | 2 +- core/src/main/java/io/grpc/internal/ManagedChannelImpl.java | 2 +- core/src/main/java/io/grpc/internal/MetadataApplierImpl.java | 2 +- .../main/java/io/grpc/internal/MigratingThreadDeframer.java | 2 +- core/src/main/java/io/grpc/internal/RetriableStream.java | 2 +- core/src/main/java/io/grpc/internal/ServerImpl.java | 2 +- cronet/src/main/java/io/grpc/cronet/CronetClientStream.java | 2 +- .../src/main/java/io/grpc/cronet/CronetClientTransport.java | 2 +- grpclb/BUILD.bazel | 1 + .../main/java/io/grpc/grpclb/GrpclbClientLoadRecorder.java | 2 +- .../io/grpc/inprocess/AnonymousInProcessSocketAddress.java | 2 +- .../src/main/java/io/grpc/inprocess/InProcessTransport.java | 2 +- .../java/io/grpc/testing/integration/TestServiceImpl.java | 2 +- okhttp/src/main/java/io/grpc/okhttp/AsyncSink.java | 2 +- okhttp/src/main/java/io/grpc/okhttp/OkHttpClientStream.java | 2 +- .../src/main/java/io/grpc/okhttp/OkHttpClientTransport.java | 4 ++-- okhttp/src/main/java/io/grpc/okhttp/OkHttpServerStream.java | 2 +- .../src/main/java/io/grpc/okhttp/OkHttpServerTransport.java | 2 +- .../io/grpc/opentelemetry/OpenTelemetryMetricsModule.java | 2 +- rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java | 2 +- services/BUILD.bazel | 2 ++ .../java/io/grpc/protobuf/services/HealthServiceImpl.java | 2 +- .../io/grpc/protobuf/services/ProtoReflectionServiceV1.java | 2 +- xds/src/main/java/io/grpc/xds/FilterChainSelectorManager.java | 2 +- .../main/java/io/grpc/xds/SharedXdsClientPoolProvider.java | 2 +- xds/src/main/java/io/grpc/xds/XdsCredentialsRegistry.java | 2 +- 53 files changed, 55 insertions(+), 50 deletions(-) diff --git a/alts/BUILD.bazel b/alts/BUILD.bazel index 73420e11053..3595064ffa4 100644 --- a/alts/BUILD.bazel +++ b/alts/BUILD.bazel @@ -18,6 +18,7 @@ java_library( "@com_google_protobuf//:protobuf_java", "@com_google_protobuf//:protobuf_java_util", artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.errorprone:error_prone_annotations"), artifact("com.google.guava:guava"), artifact("io.netty:netty-buffer"), artifact("io.netty:netty-codec"), diff --git a/alts/src/main/java/io/grpc/alts/internal/AsyncSemaphore.java b/alts/src/main/java/io/grpc/alts/internal/AsyncSemaphore.java index 3ccdcfc763a..a8251c7fbd3 100644 --- a/alts/src/main/java/io/grpc/alts/internal/AsyncSemaphore.java +++ b/alts/src/main/java/io/grpc/alts/internal/AsyncSemaphore.java @@ -16,12 +16,12 @@ package io.grpc.alts.internal; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import java.util.LinkedList; import java.util.Queue; -import javax.annotation.concurrent.GuardedBy; /** Provides a semaphore primitive, without blocking waiting on permits. */ final class AsyncSemaphore { diff --git a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java index 317b7a50b74..e56ce5fc405 100644 --- a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java +++ b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java @@ -28,6 +28,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.errorprone.annotations.InlineMe; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.CallOptions; import io.grpc.ClientCall; import io.grpc.ConnectivityState; @@ -41,7 +42,6 @@ import io.grpc.internal.GrpcUtil; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * Builds a {@link ManagedChannel} that, when provided with a {@link Context}, will automatically diff --git a/api/src/main/java/io/grpc/ConfiguratorRegistry.java b/api/src/main/java/io/grpc/ConfiguratorRegistry.java index b2efcc1cff4..a0a91609dde 100644 --- a/api/src/main/java/io/grpc/ConfiguratorRegistry.java +++ b/api/src/main/java/io/grpc/ConfiguratorRegistry.java @@ -16,10 +16,10 @@ package io.grpc; +import com.google.errorprone.annotations.concurrent.GuardedBy; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import javax.annotation.concurrent.GuardedBy; /** * A registry for {@link Configurator} instances. diff --git a/api/src/main/java/io/grpc/ManagedChannelRegistry.java b/api/src/main/java/io/grpc/ManagedChannelRegistry.java index 31f874b8094..aed5eca9abf 100644 --- a/api/src/main/java/io/grpc/ManagedChannelRegistry.java +++ b/api/src/main/java/io/grpc/ManagedChannelRegistry.java @@ -18,6 +18,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.concurrent.GuardedBy; import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; @@ -30,7 +31,6 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** diff --git a/api/src/main/java/io/grpc/MetricInstrumentRegistry.java b/api/src/main/java/io/grpc/MetricInstrumentRegistry.java index a61ac058a61..1b33ed17a71 100644 --- a/api/src/main/java/io/grpc/MetricInstrumentRegistry.java +++ b/api/src/main/java/io/grpc/MetricInstrumentRegistry.java @@ -21,12 +21,12 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.google.errorprone.annotations.concurrent.GuardedBy; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import javax.annotation.concurrent.GuardedBy; /** * A registry for globally registered metric instruments. diff --git a/api/src/main/java/io/grpc/NameResolverRegistry.java b/api/src/main/java/io/grpc/NameResolverRegistry.java index 23eec23fd6a..2648f8de1aa 100644 --- a/api/src/main/java/io/grpc/NameResolverRegistry.java +++ b/api/src/main/java/io/grpc/NameResolverRegistry.java @@ -20,6 +20,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.concurrent.GuardedBy; import java.net.URI; import java.util.ArrayList; import java.util.Collections; @@ -31,7 +32,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** diff --git a/api/src/main/java/io/grpc/ServerRegistry.java b/api/src/main/java/io/grpc/ServerRegistry.java index a083e45a000..5b9c8c558e7 100644 --- a/api/src/main/java/io/grpc/ServerRegistry.java +++ b/api/src/main/java/io/grpc/ServerRegistry.java @@ -18,6 +18,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.concurrent.GuardedBy; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -25,7 +26,6 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** diff --git a/binder/src/androidTest/java/io/grpc/binder/HostServices.java b/binder/src/androidTest/java/io/grpc/binder/HostServices.java index 4aa46e8254a..5d4a06a27fe 100644 --- a/binder/src/androidTest/java/io/grpc/binder/HostServices.java +++ b/binder/src/androidTest/java/io/grpc/binder/HostServices.java @@ -29,6 +29,7 @@ import androidx.lifecycle.LifecycleService; import com.google.auto.value.AutoValue; import com.google.common.base.Supplier; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Server; import java.io.IOException; import java.util.HashMap; @@ -38,7 +39,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * A test helper class for creating android services to host gRPC servers. diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java index c84a1fc296f..33c127f97a7 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java @@ -27,6 +27,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import com.google.errorprone.annotations.concurrent.GuardedBy; import com.google.protobuf.Empty; import io.grpc.CallOptions; import io.grpc.ClientStreamTracer; @@ -64,7 +65,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import org.junit.After; import org.junit.Before; import org.junit.Test; diff --git a/binder/src/main/java/io/grpc/binder/internal/ActiveTransportTracker.java b/binder/src/main/java/io/grpc/binder/internal/ActiveTransportTracker.java index ad410186486..2bfa9fea4cb 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ActiveTransportTracker.java +++ b/binder/src/main/java/io/grpc/binder/internal/ActiveTransportTracker.java @@ -2,13 +2,13 @@ import static com.google.common.base.Preconditions.checkState; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.Metadata; import io.grpc.internal.ServerListener; import io.grpc.internal.ServerStream; import io.grpc.internal.ServerTransport; import io.grpc.internal.ServerTransportListener; -import javax.annotation.concurrent.GuardedBy; /** * Tracks which {@link BinderTransport.BinderServerTransport} are currently active and allows diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderServer.java b/binder/src/main/java/io/grpc/binder/internal/BinderServer.java index 0ad54fb74d1..6b8347390b9 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderServer.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderServer.java @@ -25,6 +25,7 @@ import android.os.Parcel; import android.os.RemoteException; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.Grpc; import io.grpc.InternalChannelz.SocketStats; @@ -48,7 +49,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 0c886a9829c..f61c455edd5 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -36,6 +36,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.ClientStreamTracer; @@ -80,7 +81,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** diff --git a/binder/src/main/java/io/grpc/binder/internal/FlowController.java b/binder/src/main/java/io/grpc/binder/internal/FlowController.java index 1972ea00e6c..135f363a01e 100644 --- a/binder/src/main/java/io/grpc/binder/internal/FlowController.java +++ b/binder/src/main/java/io/grpc/binder/internal/FlowController.java @@ -15,7 +15,7 @@ */ package io.grpc.binder.internal; -import javax.annotation.concurrent.GuardedBy; +import com.google.errorprone.annotations.concurrent.GuardedBy; /** Keeps track of the number of bytes on the wire in a single direction. */ final class FlowController { diff --git a/binder/src/main/java/io/grpc/binder/internal/Inbound.java b/binder/src/main/java/io/grpc/binder/internal/Inbound.java index 19c0e4a0f08..50654297c74 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Inbound.java +++ b/binder/src/main/java/io/grpc/binder/internal/Inbound.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState; import android.os.Parcel; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.Metadata; import io.grpc.Status; @@ -34,7 +35,6 @@ import java.io.InputStream; import java.util.ArrayList; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * Handles incoming binder transactions for a single stream, turning those transactions into calls diff --git a/binder/src/main/java/io/grpc/binder/internal/Outbound.java b/binder/src/main/java/io/grpc/binder/internal/Outbound.java index e2896be02a1..f395fe1701f 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Outbound.java +++ b/binder/src/main/java/io/grpc/binder/internal/Outbound.java @@ -22,6 +22,7 @@ import static java.lang.Math.max; import android.os.Parcel; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Deadline; import io.grpc.Metadata; import io.grpc.MethodDescriptor; @@ -34,7 +35,6 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * Sends the set of outbound transactions for a single BinderStream (rpc). diff --git a/binder/src/main/java/io/grpc/binder/internal/PingTracker.java b/binder/src/main/java/io/grpc/binder/internal/PingTracker.java index 33fcb43918f..ab20af4d6ef 100644 --- a/binder/src/main/java/io/grpc/binder/internal/PingTracker.java +++ b/binder/src/main/java/io/grpc/binder/internal/PingTracker.java @@ -17,12 +17,12 @@ package io.grpc.binder.internal; import com.google.common.base.Ticker; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Status; import io.grpc.StatusException; import io.grpc.internal.ClientTransport.PingCallback; import java.util.concurrent.Executor; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * Tracks an ongoing ping request for a client-side binder transport. We only handle a single active diff --git a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java index 76f1d7aa9f7..ee171140045 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -28,13 +28,13 @@ import androidx.annotation.AnyThread; import androidx.annotation.MainThread; import com.google.common.annotations.VisibleForTesting; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Status; import io.grpc.binder.BinderChannelCredentials; import java.util.concurrent.Executor; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** diff --git a/census/BUILD.bazel b/census/BUILD.bazel index aec16c46af0..36be88fc3d8 100644 --- a/census/BUILD.bazel +++ b/census/BUILD.bazel @@ -10,6 +10,7 @@ java_library( "//api", "//context", artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.errorprone:error_prone_annotations"), artifact("com.google.guava:guava"), artifact("io.opencensus:opencensus-api"), artifact("io.opencensus:opencensus-contrib-grpc-metrics"), diff --git a/census/src/main/java/io/grpc/census/CensusStatsModule.java b/census/src/main/java/io/grpc/census/CensusStatsModule.java index ad16bef9604..8f571ceb627 100644 --- a/census/src/main/java/io/grpc/census/CensusStatsModule.java +++ b/census/src/main/java/io/grpc/census/CensusStatsModule.java @@ -22,6 +22,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import com.google.common.base.Supplier; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.Channel; @@ -62,7 +63,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * Provides factories for {@link StreamTracer} that records stats to Census. diff --git a/core/src/main/java/io/grpc/internal/AbstractStream.java b/core/src/main/java/io/grpc/internal/AbstractStream.java index 56f540d623f..46cdab7ef28 100644 --- a/core/src/main/java/io/grpc/internal/AbstractStream.java +++ b/core/src/main/java/io/grpc/internal/AbstractStream.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.annotations.VisibleForTesting; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Codec; import io.grpc.Compressor; import io.grpc.Decompressor; @@ -29,7 +30,6 @@ import java.io.InputStream; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.concurrent.GuardedBy; /** * The stream and stream state as used by the application. Must only be called from the sending diff --git a/core/src/main/java/io/grpc/internal/CallCredentialsApplyingTransportFactory.java b/core/src/main/java/io/grpc/internal/CallCredentialsApplyingTransportFactory.java index 42631851974..97a74bda97e 100644 --- a/core/src/main/java/io/grpc/internal/CallCredentialsApplyingTransportFactory.java +++ b/core/src/main/java/io/grpc/internal/CallCredentialsApplyingTransportFactory.java @@ -19,6 +19,7 @@ import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkNotNull; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.CallCredentials; import io.grpc.CallCredentials.RequestInfo; @@ -38,7 +39,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.concurrent.GuardedBy; final class CallCredentialsApplyingTransportFactory implements ClientTransportFactory { private final ClientTransportFactory delegate; diff --git a/core/src/main/java/io/grpc/internal/ChannelTracer.java b/core/src/main/java/io/grpc/internal/ChannelTracer.java index 8c8243c9021..a9730a365cc 100644 --- a/core/src/main/java/io/grpc/internal/ChannelTracer.java +++ b/core/src/main/java/io/grpc/internal/ChannelTracer.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.ChannelLogger; import io.grpc.InternalChannelz.ChannelStats; import io.grpc.InternalChannelz.ChannelTrace; @@ -31,7 +32,6 @@ import java.util.logging.LogRecord; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * Tracks a collections of channel tracing events for a channel/subchannel. diff --git a/core/src/main/java/io/grpc/internal/DelayedClientCall.java b/core/src/main/java/io/grpc/internal/DelayedClientCall.java index 92034e83f45..f2b0c9a3f06 100644 --- a/core/src/main/java/io/grpc/internal/DelayedClientCall.java +++ b/core/src/main/java/io/grpc/internal/DelayedClientCall.java @@ -22,6 +22,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.ClientCall; import io.grpc.Context; @@ -38,7 +39,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * A call that queues requests before a real call is ready to be delegated to. diff --git a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java index ae173f4ac26..2ff94b7804a 100644 --- a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java +++ b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java @@ -19,6 +19,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.CallOptions; import io.grpc.ClientStreamTracer; import io.grpc.Context; @@ -39,7 +40,6 @@ import java.util.concurrent.Executor; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * A client transport that queues requests before a real transport is available. When {@link diff --git a/core/src/main/java/io/grpc/internal/DelayedStream.java b/core/src/main/java/io/grpc/internal/DelayedStream.java index 5f14f24cfe5..2ca4630d6a1 100644 --- a/core/src/main/java/io/grpc/internal/DelayedStream.java +++ b/core/src/main/java/io/grpc/internal/DelayedStream.java @@ -21,6 +21,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.Compressor; import io.grpc.Deadline; @@ -31,7 +32,6 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; -import javax.annotation.concurrent.GuardedBy; /** * A stream that queues requests before the transport is available, and delegates to a real stream diff --git a/core/src/main/java/io/grpc/internal/Http2Ping.java b/core/src/main/java/io/grpc/internal/Http2Ping.java index 6104d876373..d96ac3ef214 100644 --- a/core/src/main/java/io/grpc/internal/Http2Ping.java +++ b/core/src/main/java/io/grpc/internal/Http2Ping.java @@ -17,6 +17,7 @@ package io.grpc.internal; import com.google.common.base.Stopwatch; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.internal.ClientTransport.PingCallback; import java.util.LinkedHashMap; import java.util.Map; @@ -24,7 +25,6 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.concurrent.GuardedBy; /** * Represents an outstanding PING operation on an HTTP/2 channel. This can be used by HTTP/2-based diff --git a/core/src/main/java/io/grpc/internal/KeepAliveManager.java b/core/src/main/java/io/grpc/internal/KeepAliveManager.java index 28e2a87276b..aed590c3051 100644 --- a/core/src/main/java/io/grpc/internal/KeepAliveManager.java +++ b/core/src/main/java/io/grpc/internal/KeepAliveManager.java @@ -22,11 +22,11 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import com.google.common.util.concurrent.MoreExecutors; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Status; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import javax.annotation.concurrent.GuardedBy; /** * Manages keepalive pings. diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 5c2871a1373..45819954473 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -32,6 +32,7 @@ import com.google.common.base.Supplier; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.CallCredentials; import io.grpc.CallOptions; @@ -117,7 +118,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** A communication channel for making outgoing RPCs. */ diff --git a/core/src/main/java/io/grpc/internal/MetadataApplierImpl.java b/core/src/main/java/io/grpc/internal/MetadataApplierImpl.java index 12cab15053f..09a1018c11c 100644 --- a/core/src/main/java/io/grpc/internal/MetadataApplierImpl.java +++ b/core/src/main/java/io/grpc/internal/MetadataApplierImpl.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.CallCredentials.MetadataApplier; import io.grpc.CallOptions; import io.grpc.ClientStreamTracer; @@ -28,7 +29,6 @@ import io.grpc.MethodDescriptor; import io.grpc.Status; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; final class MetadataApplierImpl extends MetadataApplier { private final ClientTransport transport; diff --git a/core/src/main/java/io/grpc/internal/MigratingThreadDeframer.java b/core/src/main/java/io/grpc/internal/MigratingThreadDeframer.java index c3342556c9f..e4f499ab483 100644 --- a/core/src/main/java/io/grpc/internal/MigratingThreadDeframer.java +++ b/core/src/main/java/io/grpc/internal/MigratingThreadDeframer.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Decompressor; import io.perfmark.Link; import io.perfmark.PerfMark; @@ -26,7 +27,6 @@ import java.io.InputStream; import java.util.ArrayDeque; import java.util.Queue; -import javax.annotation.concurrent.GuardedBy; /** * A deframer that moves decoding between the transport and app threads based on which is more diff --git a/core/src/main/java/io/grpc/internal/RetriableStream.java b/core/src/main/java/io/grpc/internal/RetriableStream.java index 7fed77625d3..85d7bc86584 100644 --- a/core/src/main/java/io/grpc/internal/RetriableStream.java +++ b/core/src/main/java/io/grpc/internal/RetriableStream.java @@ -23,6 +23,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Objects; import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.ClientStreamTracer; import io.grpc.Compressor; @@ -49,7 +50,6 @@ import java.util.concurrent.atomic.AtomicLong; import javax.annotation.CheckForNull; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** A logical {@link ClientStream} that is retriable. */ abstract class RetriableStream implements ClientStream { diff --git a/core/src/main/java/io/grpc/internal/ServerImpl.java b/core/src/main/java/io/grpc/internal/ServerImpl.java index eceb7d7a738..dc0709e1fb8 100644 --- a/core/src/main/java/io/grpc/internal/ServerImpl.java +++ b/core/src/main/java/io/grpc/internal/ServerImpl.java @@ -31,6 +31,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.BinaryLog; import io.grpc.CompressorRegistry; @@ -75,7 +76,6 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.concurrent.GuardedBy; /** * Default implementation of {@link io.grpc.Server}, for creation by transports. diff --git a/cronet/src/main/java/io/grpc/cronet/CronetClientStream.java b/cronet/src/main/java/io/grpc/cronet/CronetClientStream.java index 9ae97652316..95adb65ec40 100644 --- a/cronet/src/main/java/io/grpc/cronet/CronetClientStream.java +++ b/cronet/src/main/java/io/grpc/cronet/CronetClientStream.java @@ -25,6 +25,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.io.BaseEncoding; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.InternalMetadata; @@ -50,7 +51,6 @@ import java.util.Map; import java.util.concurrent.Executor; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import org.chromium.net.BidirectionalStream; import org.chromium.net.CronetException; import org.chromium.net.UrlResponseInfo; diff --git a/cronet/src/main/java/io/grpc/cronet/CronetClientTransport.java b/cronet/src/main/java/io/grpc/cronet/CronetClientTransport.java index b0b18620d0c..465df8b2cc9 100644 --- a/cronet/src/main/java/io/grpc/cronet/CronetClientTransport.java +++ b/cronet/src/main/java/io/grpc/cronet/CronetClientTransport.java @@ -19,6 +19,7 @@ import com.google.common.base.Preconditions; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.ClientStreamTracer; @@ -42,7 +43,6 @@ import java.util.Set; import java.util.concurrent.Executor; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * A cronet-based {@link ConnectionClientTransport} implementation. diff --git a/grpclb/BUILD.bazel b/grpclb/BUILD.bazel index 2dd24bb52a2..902ae7f47d4 100644 --- a/grpclb/BUILD.bazel +++ b/grpclb/BUILD.bazel @@ -20,6 +20,7 @@ java_library( "@com_google_protobuf//:protobuf_java_util", "@io_grpc_grpc_proto//:grpclb_load_balancer_java_proto", artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.errorprone:error_prone_annotations"), artifact("com.google.guava:guava"), ], ) diff --git a/grpclb/src/main/java/io/grpc/grpclb/GrpclbClientLoadRecorder.java b/grpclb/src/main/java/io/grpc/grpclb/GrpclbClientLoadRecorder.java index d27c485dc13..fe928263ef9 100644 --- a/grpclb/src/main/java/io/grpc/grpclb/GrpclbClientLoadRecorder.java +++ b/grpclb/src/main/java/io/grpc/grpclb/GrpclbClientLoadRecorder.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.errorprone.annotations.concurrent.GuardedBy; import com.google.protobuf.util.Timestamps; import io.grpc.ClientStreamTracer; import io.grpc.Metadata; @@ -29,7 +30,6 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicLongFieldUpdater; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** diff --git a/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java b/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java index 5f6486e335d..089a9f12b02 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java +++ b/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java @@ -18,11 +18,11 @@ import static com.google.common.base.Preconditions.checkState; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.ExperimentalApi; import java.io.IOException; import java.net.SocketAddress; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * Custom SocketAddress class for {@link InProcessTransport}, for diff --git a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java index b5bbbe563df..39ebe6e0ab7 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java @@ -25,6 +25,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.ClientStreamTracer; @@ -76,7 +77,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; @ThreadSafe diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceImpl.java b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceImpl.java index 8fa272122d0..a9ee9382495 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceImpl.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceImpl.java @@ -18,6 +18,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.Queues; +import com.google.errorprone.annotations.concurrent.GuardedBy; import com.google.protobuf.ByteString; import io.grpc.ForwardingServerCall.SimpleForwardingServerCall; import io.grpc.Metadata; @@ -54,7 +55,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -import javax.annotation.concurrent.GuardedBy; /** * Implementation of the business logic for the TestService. Uses an executor to schedule chunks diff --git a/okhttp/src/main/java/io/grpc/okhttp/AsyncSink.java b/okhttp/src/main/java/io/grpc/okhttp/AsyncSink.java index 1ac64d7ebb5..01ee23b905c 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/AsyncSink.java +++ b/okhttp/src/main/java/io/grpc/okhttp/AsyncSink.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.internal.SerializingExecutor; import io.grpc.okhttp.ExceptionHandlingFrameWriter.TransportExceptionHandler; import io.grpc.okhttp.internal.framed.ErrorCode; @@ -30,7 +31,6 @@ import java.io.IOException; import java.net.Socket; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import okio.Buffer; import okio.Sink; import okio.Timeout; diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientStream.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientStream.java index 9d9fe160715..e33617886cf 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientStream.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientStream.java @@ -21,6 +21,7 @@ import static io.grpc.internal.ClientStreamListener.RpcProgress.PROCESSED; import com.google.common.io.BaseEncoding; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.Metadata; @@ -37,7 +38,6 @@ import io.perfmark.Tag; import io.perfmark.TaskCloseable; import java.util.List; -import javax.annotation.concurrent.GuardedBy; import okio.Buffer; /** diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java index 59f824b1a3c..055d6e08161 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java @@ -27,6 +27,7 @@ import com.google.common.base.Supplier; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.ClientStreamTracer; @@ -93,7 +94,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import javax.net.SocketFactory; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; @@ -1460,4 +1460,4 @@ public void alternateService(int streamId, String origin, ByteString protocol, S // TODO(madongfly): Deal with alternateService propagation } } -} \ No newline at end of file +} diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerStream.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerStream.java index bcf8837b7eb..d1f1a3f4fe0 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerStream.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerStream.java @@ -17,6 +17,7 @@ package io.grpc.okhttp; import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.Metadata; import io.grpc.Status; @@ -30,7 +31,6 @@ import io.perfmark.Tag; import io.perfmark.TaskCloseable; import java.util.List; -import javax.annotation.concurrent.GuardedBy; import okio.Buffer; /** diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java index 2da041f571e..cc52bee85eb 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java @@ -22,6 +22,7 @@ import com.google.common.base.Preconditions; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.InternalChannelz; import io.grpc.InternalLogId; @@ -62,7 +63,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import okio.Buffer; import okio.BufferedSource; import okio.ByteString; diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java index f631da59d01..b6cf09d9db6 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java @@ -27,6 +27,7 @@ import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; @@ -54,7 +55,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * Provides factories for {@link StreamTracer} that records metrics to OpenTelemetry. diff --git a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java index 9775753ab50..70833416d5d 100644 --- a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java +++ b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java @@ -29,6 +29,7 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.ChannelLogger; import io.grpc.ChannelLogger.ChannelLogLevel; import io.grpc.ConnectivityState; @@ -75,7 +76,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** diff --git a/services/BUILD.bazel b/services/BUILD.bazel index d20e956ed49..9ac894ffaee 100644 --- a/services/BUILD.bazel +++ b/services/BUILD.bazel @@ -121,6 +121,7 @@ java_library( "@io_grpc_grpc_proto//:reflection_java_proto", "@io_grpc_grpc_proto//:reflection_java_proto_deprecated", artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.errorprone:error_prone_annotations"), artifact("com.google.guava:guava"), ], ) @@ -139,6 +140,7 @@ java_library( "//stub", "@io_grpc_grpc_proto//:health_java_proto", artifact("com.google.code.findbugs:jsr305"), + artifact("com.google.errorprone:error_prone_annotations"), artifact("com.google.guava:guava"), ], ) diff --git a/services/src/main/java/io/grpc/protobuf/services/HealthServiceImpl.java b/services/src/main/java/io/grpc/protobuf/services/HealthServiceImpl.java index 6ce602b9295..2efe4b3951a 100644 --- a/services/src/main/java/io/grpc/protobuf/services/HealthServiceImpl.java +++ b/services/src/main/java/io/grpc/protobuf/services/HealthServiceImpl.java @@ -18,6 +18,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.MoreExecutors; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Context; import io.grpc.Context.CancellationListener; import io.grpc.Status; @@ -34,7 +35,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; final class HealthServiceImpl extends HealthGrpc.HealthImplBase { diff --git a/services/src/main/java/io/grpc/protobuf/services/ProtoReflectionServiceV1.java b/services/src/main/java/io/grpc/protobuf/services/ProtoReflectionServiceV1.java index 578e9bbd409..59e9c33d279 100644 --- a/services/src/main/java/io/grpc/protobuf/services/ProtoReflectionServiceV1.java +++ b/services/src/main/java/io/grpc/protobuf/services/ProtoReflectionServiceV1.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.errorprone.annotations.concurrent.GuardedBy; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FileDescriptor; @@ -52,7 +53,6 @@ import java.util.Set; import java.util.WeakHashMap; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; /** * Provides a reflection service for Protobuf services (including the reflection service itself). diff --git a/xds/src/main/java/io/grpc/xds/FilterChainSelectorManager.java b/xds/src/main/java/io/grpc/xds/FilterChainSelectorManager.java index 4295d75f59b..b3cc14c6484 100644 --- a/xds/src/main/java/io/grpc/xds/FilterChainSelectorManager.java +++ b/xds/src/main/java/io/grpc/xds/FilterChainSelectorManager.java @@ -18,11 +18,11 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler.FilterChainSelector; import java.util.Comparator; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicLong; -import javax.annotation.concurrent.GuardedBy; /** * Maintains the current xDS selector and any resources using that selector. When the selector diff --git a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java index 779349744ff..2bc7be4a014 100644 --- a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java +++ b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java @@ -21,6 +21,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.MetricRecorder; import io.grpc.internal.ExponentialBackoffPolicy; import io.grpc.internal.GrpcUtil; @@ -40,7 +41,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** diff --git a/xds/src/main/java/io/grpc/xds/XdsCredentialsRegistry.java b/xds/src/main/java/io/grpc/xds/XdsCredentialsRegistry.java index c33b3cd2f85..9dfefaf1a65 100644 --- a/xds/src/main/java/io/grpc/xds/XdsCredentialsRegistry.java +++ b/xds/src/main/java/io/grpc/xds/XdsCredentialsRegistry.java @@ -21,6 +21,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.InternalServiceProviders; import java.util.ArrayList; import java.util.Collections; @@ -31,7 +32,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** From 82403b9bfa5577e5086135d793669218a5413d1c Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 9 Jan 2025 15:04:25 -0800 Subject: [PATCH 143/591] util: Increase modtime in AdvancedTlsX509TrustManagerTest I noticed an old JDK 8u275 failed on the test because the modification time's resolution was one second. A newer JDK 8u432 worked fine, so it's not really a problem for me, but increasing the time difference is cheap. I used two seconds as that's the resolution available on FAT (which is unlikely to be TMPDIR, even on Windows). --- .../java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java b/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java index 5ad0e85fa36..228dbf5ea5b 100644 --- a/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java +++ b/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java @@ -168,7 +168,7 @@ public void updateTrustCredentials_rotate() throws GeneralSecurityException, IOE fakeClock.forwardTime(1, TimeUnit.MINUTES); assertArrayEquals(serverCert0, trustManager.getAcceptedIssuers()); - serverCert0File.setLastModified(serverCert0File.lastModified() - 10); + serverCert0File.setLastModified(serverCert0File.lastModified() - 2000); fakeClock.forwardTime(1, TimeUnit.MINUTES); assertArrayEquals(serverCert0, trustManager.getAcceptedIssuers()); @@ -181,7 +181,7 @@ public void updateTrustCredentials_rotate() throws GeneralSecurityException, IOE fakeClock.forwardTime(1, TimeUnit.MINUTES); assertArrayEquals(serverCert0, trustManager.getAcceptedIssuers()); - serverCert0File.setLastModified(beforeModify + 10); + serverCert0File.setLastModified(beforeModify + 2000); // file modification time changed fakeClock.forwardTime(1, TimeUnit.MINUTES); From d65d3942e6078c75500b034155a58b3fa6d516e8 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 7 Jan 2025 09:57:18 -0800 Subject: [PATCH 144/591] xds: Increase speed of fallback test These changes reduce connect_then_mainServerDown_fallbackServerUp test time from 20 seconds to 5 s by faking time for the the does-no-exist timer. XdsClientImpl only uses the TimeProvider for CSDS cache details, so any implementation should be fine. FakeXdsClient provides an implementation, so might as well use it as it is one less clock to think about. --- .../io/grpc/xds/XdsClientFallbackTest.java | 21 ++++++++--- .../client/CommonBootstrapperTestUtils.java | 35 +++++++++---------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java index 39379d43aba..94b49bd94b2 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java @@ -335,10 +335,13 @@ private static void verifyNoSubscribers(ControlPlaneRule rule) { // This test takes a long time because of the 16 sec timeout for non-existent resource @Test - public void connect_then_mainServerDown_fallbackServerUp() throws InterruptedException { + public void connect_then_mainServerDown_fallbackServerUp() throws Exception { mainXdsServer.restartXdsServer(); fallbackServer.restartXdsServer(); - xdsClient = xdsClientPool.getObject(); + XdsClientImpl xdsClient = CommonBootstrapperTestUtils.createXdsClient( + new GrpcBootstrapperImpl().bootstrap(defaultBootstrapOverride()), + DEFAULT_XDS_TRANSPORT_FACTORY, fakeClock, new ExponentialBackoffPolicy.Provider(), + MessagePrinter.INSTANCE, xdsClientMetricReporter); xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); @@ -349,7 +352,12 @@ public void connect_then_mainServerDown_fallbackServerUp() throws InterruptedExc verify(rdsWatcher, timeout(5000)).onChanged(any()); mainXdsServer.getServer().shutdownNow(); - TimeUnit.SECONDS.sleep(5); // TODO(lsafran) Use FakeClock so test runs faster + // Sleep for the ADS stream disconnect to be processed and for the retry to fail. Between those + // two sleeps we need the fakeClock to progress by 1 second to restart the ADS stream. + for (int i = 0; i < 5; i++) { + fakeClock.forwardTime(1000, TimeUnit.MILLISECONDS); + TimeUnit.SECONDS.sleep(1); + } // Shouldn't do fallback since all watchers are loaded verify(ldsWatcher, never()).onChanged( @@ -372,7 +380,7 @@ public void connect_then_mainServerDown_fallbackServerUp() throws InterruptedExc XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER)); verify(ldsWatcher2, timeout(5000)).onChanged( XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER)); - verify(cdsWatcher, timeout(16000)).onChanged(any()); + verify(cdsWatcher, timeout(5000)).onChanged(any()); xdsClient.watchXdsResource( XdsRouteConfigureResource.getInstance(), FALLBACK_RDS_NAME, rdsWatcher3); @@ -381,7 +389,10 @@ public void connect_then_mainServerDown_fallbackServerUp() throws InterruptedExc // Test that resource defined in main but not fallback is handled correctly xdsClient.watchXdsResource( XdsClusterResource.getInstance(), CLUSTER_NAME, cdsWatcher2); - verify(cdsWatcher2, timeout(16000)).onResourceDoesNotExist(eq(CLUSTER_NAME)); + verify(cdsWatcher2, never()).onResourceDoesNotExist(eq(CLUSTER_NAME)); + fakeClock.forwardTime(15000, TimeUnit.MILLISECONDS); // Does not exist timer + verify(cdsWatcher2, timeout(5000)).onResourceDoesNotExist(eq(CLUSTER_NAME)); + xdsClient.shutdown(); } @Test diff --git a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java index 27a0d4ba1d9..f3de4549ba9 100644 --- a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java @@ -23,7 +23,6 @@ import io.grpc.internal.BackoffPolicy; import io.grpc.internal.FakeClock; import io.grpc.internal.JsonParser; -import io.grpc.internal.TimeProvider; import io.grpc.xds.client.Bootstrapper.ServerInfo; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import io.grpc.xds.internal.security.TlsContextManagerImpl; @@ -32,28 +31,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; public class CommonBootstrapperTestUtils { private static final ChannelCredentials CHANNEL_CREDENTIALS = InsecureChannelCredentials.create(); private static final String SERVER_URI_CUSTOM_AUTHORITY = "trafficdirector2.googleapis.com"; private static final String SERVER_URI_EMPTY_AUTHORITY = "trafficdirector3.googleapis.com"; - - private static final long TIME_INCREMENT = TimeUnit.SECONDS.toNanos(1); - - /** Fake time provider increments time TIME_INCREMENT each call. */ - private static TimeProvider newTimeProvider() { - return new TimeProvider() { - private long count; - - @Override - public long currentTimeNanos() { - return ++count * TIME_INCREMENT; - } - }; - } - private static final String FILE_WATCHER_CONFIG = "{\"path\": \"/etc/secret/certs\"}"; private static final String MESHCA_CONFIG = "{\n" @@ -183,14 +166,28 @@ public static XdsClientImpl createXdsClient(List serverUris, BackoffPolicy.Provider backoffPolicyProvider, MessagePrettyPrinter messagePrinter, XdsClientMetricReporter xdsClientMetricReporter) { - Bootstrapper.BootstrapInfo bootstrapInfo = buildBootStrap(serverUris); + return createXdsClient( + buildBootStrap(serverUris), + xdsTransportFactory, + fakeClock, + backoffPolicyProvider, + messagePrinter, + xdsClientMetricReporter); + } + + public static XdsClientImpl createXdsClient(Bootstrapper.BootstrapInfo bootstrapInfo, + XdsTransportFactory xdsTransportFactory, + FakeClock fakeClock, + BackoffPolicy.Provider backoffPolicyProvider, + MessagePrettyPrinter messagePrinter, + XdsClientMetricReporter xdsClientMetricReporter) { return new XdsClientImpl( xdsTransportFactory, bootstrapInfo, fakeClock.getScheduledExecutorService(), backoffPolicyProvider, fakeClock.getStopwatchSupplier(), - newTimeProvider(), + fakeClock.getTimeProvider(), messagePrinter, new TlsContextManagerImpl(bootstrapInfo), xdsClientMetricReporter); From 176f3eed12922bfa460b4604ccb0b8c50f31194f Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Fri, 10 Jan 2025 15:25:15 -0800 Subject: [PATCH 145/591] xds: Enable Xds Client Fallback by default (#11817) --- xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java index dffe19f9256..c00685f1781 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -56,7 +56,7 @@ public abstract class BootstrapperImpl extends Bootstrapper { private static final String SERVER_FEATURE_TRUSTED_XDS_SERVER = "trusted_xds_server"; @VisibleForTesting - static boolean enableXdsFallback = GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_FALLBACK, false); + static boolean enableXdsFallback = GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_FALLBACK, true); protected final XdsLogger logger; From 7162d2d6615eb36814e151bf8e926b68409dbed9 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 10 Jan 2025 15:35:43 -0800 Subject: [PATCH 146/591] xds: Pass grpc.xds.cluster label to tracer This is in service to gRFC A89. Since the gRFC isn't finalized this purposefully doesn't really do anything yet. The grpc-opentelemetry change to use this optional label will be done after the gRFC is merged. grpc-opentelemetry currently has a hard-coded list (one entry) of labels that it looks for, and this label will need to be added. b/356167676 --- .../io/grpc/xds/ClusterImplLoadBalancer.java | 1 + .../grpc/xds/ClusterImplLoadBalancerTest.java | 33 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index 23f89d161e2..200d3cba0ea 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -385,6 +385,7 @@ private RequestLimitingSubchannelPicker(SubchannelPicker delegate, public PickResult pickSubchannel(PickSubchannelArgs args) { args.getCallOptions().getOption(ClusterImplLoadBalancerProvider.FILTER_METADATA_CONSUMER) .accept(filterMetadata); + args.getPickDetailsConsumer().addOptionalLabel("grpc.xds.cluster", cluster); for (DropOverload dropOverload : dropPolicies) { int rand = random.nextInt(1_000_000); if (rand < dropOverload.dropsPerMillion()) { diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index 1918ea5724c..9503442e383 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -271,7 +271,7 @@ public void nameResolutionError_afterChildPolicyInstantiated_propagateToDownstre } @Test - public void pick_addsLocalityLabel() { + public void pick_addsOptionalLabels() { LoadBalancerProvider weightedTargetProvider = new WeightedTargetLoadBalancerProvider(); WeightedTargetConfig weightedTargetConfig = buildWeightedTargetConfig(ImmutableMap.of(locality, 10)); @@ -298,6 +298,31 @@ public void pick_addsLocalityLabel() { // The value will be determined by the parent policy, so can be different than the value used in // makeAddress() for the test. verify(detailsConsumer).addOptionalLabel("grpc.lb.locality", locality.toString()); + verify(detailsConsumer).addOptionalLabel("grpc.xds.cluster", CLUSTER); + } + + @Test + public void pick_noResult_addsClusterLabel() { + LoadBalancerProvider weightedTargetProvider = new WeightedTargetLoadBalancerProvider(); + WeightedTargetConfig weightedTargetConfig = + buildWeightedTargetConfig(ImmutableMap.of(locality, 10)); + ClusterImplConfig config = new ClusterImplConfig(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, + null, Collections.emptyList(), + GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + weightedTargetProvider, weightedTargetConfig), + null, Collections.emptyMap()); + EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); + deliverAddressesAndConfig(Collections.singletonList(endpoint), config); + FakeLoadBalancer leafBalancer = Iterables.getOnlyElement(downstreamBalancers); + leafBalancer.deliverSubchannelState(PickResult.withNoResult(), ConnectivityState.CONNECTING); + assertThat(currentState).isEqualTo(ConnectivityState.CONNECTING); + + PickDetailsConsumer detailsConsumer = mock(PickDetailsConsumer.class); + pickSubchannelArgs = new PickSubchannelArgsImpl( + TestMethodDescriptors.voidMethod(), new Metadata(), CallOptions.DEFAULT, detailsConsumer); + PickResult result = currentPicker.pickSubchannel(pickSubchannelArgs); + assertThat(result.getStatus().isOk()).isTrue(); + verify(detailsConsumer).addOptionalLabel("grpc.xds.cluster", CLUSTER); } @Test @@ -1061,10 +1086,14 @@ public void shutdown() { } void deliverSubchannelState(final Subchannel subchannel, ConnectivityState state) { + deliverSubchannelState(PickResult.withSubchannel(subchannel), state); + } + + void deliverSubchannelState(final PickResult result, ConnectivityState state) { SubchannelPicker picker = new SubchannelPicker() { @Override public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withSubchannel(subchannel); + return result; } }; helper.updateBalancingState(state, picker); From 228dcf7a011e69cd40b83dc80ab1e6cf507ab512 Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Mon, 13 Jan 2025 16:38:16 -0800 Subject: [PATCH 147/591] core: Alternate ipV4 and ipV6 addresses for Happy Eyeballs in PickFirstLeafLoadBalancer (#11624) * Interweave ipV4 and ipV6 addresses as per gRFC. --- .../internal/PickFirstLeafLoadBalancer.java | 159 +++++++++++++----- .../PickFirstLeafLoadBalancerTest.java | 87 +++++++++- 2 files changed, 199 insertions(+), 47 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java index 6f4794fdd46..042f9e63630 100644 --- a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java +++ b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java @@ -34,6 +34,8 @@ import io.grpc.LoadBalancer; import io.grpc.Status; import io.grpc.SynchronizationContext.ScheduledHandle; +import java.net.Inet4Address; +import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Collections; @@ -58,17 +60,17 @@ final class PickFirstLeafLoadBalancer extends LoadBalancer { private static final Logger log = Logger.getLogger(PickFirstLeafLoadBalancer.class.getName()); @VisibleForTesting static final int CONNECTION_DELAY_INTERVAL_MS = 250; + private final boolean enableHappyEyeballs = !isSerializingRetries() + && PickFirstLoadBalancerProvider.isEnabledHappyEyeballs(); private final Helper helper; private final Map subchannels = new HashMap<>(); - private final Index addressIndex = new Index(ImmutableList.of()); + private final Index addressIndex = new Index(ImmutableList.of(), this.enableHappyEyeballs); private int numTf = 0; private boolean firstPass = true; @Nullable private ScheduledHandle scheduleConnectionTask = null; private ConnectivityState rawConnectivityState = IDLE; private ConnectivityState concludedState = IDLE; - private final boolean enableHappyEyeballs = !isSerializingRetries() - && PickFirstLoadBalancerProvider.isEnabledHappyEyeballs(); private boolean notAPetiolePolicy = true; // means not under a petiole policy private final BackoffPolicy.Provider bkoffPolProvider = new ExponentialBackoffPolicy.Provider(); private BackoffPolicy reconnectPolicy; @@ -610,27 +612,26 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { } /** - * Index as in 'i', the pointer to an entry. Not a "search index." + * This contains both an ordered list of addresses and a pointer(i.e. index) to the current entry. * All updates should be done in a synchronization context. */ @VisibleForTesting static final class Index { - private List addressGroups; - private int size; - private int groupIndex; - private int addressIndex; + private List orderedAddresses; + private int activeElement = 0; + private boolean enableHappyEyeballs; - public Index(List groups) { + Index(List groups, boolean enableHappyEyeballs) { + this.enableHappyEyeballs = enableHappyEyeballs; updateGroups(groups); } public boolean isValid() { - // Is invalid if empty or has incremented off the end - return groupIndex < addressGroups.size(); + return activeElement < orderedAddresses.size(); } public boolean isAtBeginning() { - return groupIndex == 0 && addressIndex == 0; + return activeElement == 0; } /** @@ -642,79 +643,150 @@ public boolean increment() { return false; } - EquivalentAddressGroup group = addressGroups.get(groupIndex); - addressIndex++; - if (addressIndex >= group.getAddresses().size()) { - groupIndex++; - addressIndex = 0; - return groupIndex < addressGroups.size(); - } + activeElement++; - return true; + return isValid(); } public void reset() { - groupIndex = 0; - addressIndex = 0; + activeElement = 0; } public SocketAddress getCurrentAddress() { if (!isValid()) { throw new IllegalStateException("Index is past the end of the address group list"); } - return addressGroups.get(groupIndex).getAddresses().get(addressIndex); + return orderedAddresses.get(activeElement).address; } public Attributes getCurrentEagAttributes() { if (!isValid()) { throw new IllegalStateException("Index is off the end of the address group list"); } - return addressGroups.get(groupIndex).getAttributes(); + return orderedAddresses.get(activeElement).attributes; } public List getCurrentEagAsList() { - return Collections.singletonList( - new EquivalentAddressGroup(getCurrentAddress(), getCurrentEagAttributes())); + return Collections.singletonList(getCurrentEag()); + } + + private EquivalentAddressGroup getCurrentEag() { + if (!isValid()) { + throw new IllegalStateException("Index is past the end of the address group list"); + } + return orderedAddresses.get(activeElement).asEag(); } /** * Update to new groups, resetting the current index. */ public void updateGroups(List newGroups) { - addressGroups = checkNotNull(newGroups, "newGroups"); + checkNotNull(newGroups, "newGroups"); + orderedAddresses = enableHappyEyeballs + ? updateGroupsHE(newGroups) + : updateGroupsNonHE(newGroups); reset(); - int size = 0; - for (EquivalentAddressGroup eag : newGroups) { - size += eag.getAddresses().size(); - } - this.size = size; } /** * Returns false if the needle was not found and the current index was left unchanged. */ public boolean seekTo(SocketAddress needle) { - for (int i = 0; i < addressGroups.size(); i++) { - EquivalentAddressGroup group = addressGroups.get(i); - int j = group.getAddresses().indexOf(needle); - if (j == -1) { - continue; + checkNotNull(needle, "needle"); + for (int i = 0; i < orderedAddresses.size(); i++) { + if (orderedAddresses.get(i).address.equals(needle)) { + this.activeElement = i; + return true; } - this.groupIndex = i; - this.addressIndex = j; - return true; } return false; } public int size() { - return size; + return orderedAddresses.size(); + } + + private List updateGroupsNonHE(List newGroups) { + List entries = new ArrayList<>(); + for (int g = 0; g < newGroups.size(); g++) { + EquivalentAddressGroup eag = newGroups.get(g); + for (int a = 0; a < eag.getAddresses().size(); a++) { + SocketAddress addr = eag.getAddresses().get(a); + entries.add(new UnwrappedEag(eag.getAttributes(), addr)); + } + } + + return entries; + } + + private List updateGroupsHE(List newGroups) { + Boolean firstIsV6 = null; + List v4Entries = new ArrayList<>(); + List v6Entries = new ArrayList<>(); + for (int g = 0; g < newGroups.size(); g++) { + EquivalentAddressGroup eag = newGroups.get(g); + for (int a = 0; a < eag.getAddresses().size(); a++) { + SocketAddress addr = eag.getAddresses().get(a); + boolean isIpV4 = addr instanceof InetSocketAddress + && ((InetSocketAddress) addr).getAddress() instanceof Inet4Address; + if (isIpV4) { + if (firstIsV6 == null) { + firstIsV6 = false; + } + v4Entries.add(new UnwrappedEag(eag.getAttributes(), addr)); + } else { + if (firstIsV6 == null) { + firstIsV6 = true; + } + v6Entries.add(new UnwrappedEag(eag.getAttributes(), addr)); + } + } + } + + return firstIsV6 != null && firstIsV6 + ? interleave(v6Entries, v4Entries) + : interleave(v4Entries, v6Entries); + } + + private List interleave(List firstFamily, + List secondFamily) { + if (firstFamily.isEmpty()) { + return secondFamily; + } + if (secondFamily.isEmpty()) { + return firstFamily; + } + + List result = new ArrayList<>(firstFamily.size() + secondFamily.size()); + for (int i = 0; i < Math.max(firstFamily.size(), secondFamily.size()); i++) { + if (i < firstFamily.size()) { + result.add(firstFamily.get(i)); + } + if (i < secondFamily.size()) { + result.add(secondFamily.get(i)); + } + } + return result; + } + + private static final class UnwrappedEag { + private final Attributes attributes; + private final SocketAddress address; + + public UnwrappedEag(Attributes attributes, SocketAddress address) { + this.attributes = attributes; + this.address = address; + } + + private EquivalentAddressGroup asEag() { + return new EquivalentAddressGroup(address, attributes); + } } } @VisibleForTesting - int getGroupIndex() { - return addressIndex.groupIndex; + int getIndexLocation() { + return addressIndex.activeElement; } @VisibleForTesting @@ -778,4 +850,5 @@ public PickFirstLeafLoadBalancerConfig(@Nullable Boolean shuffleAddressList) { this.randomSeed = randomSeed; } } + } diff --git a/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java b/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java index f0031a6ae62..61bcb5c05ab 100644 --- a/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java @@ -32,6 +32,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assume.assumeTrue; import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.any; @@ -67,6 +68,7 @@ import io.grpc.Status.Code; import io.grpc.SynchronizationContext; import io.grpc.internal.PickFirstLeafLoadBalancer.PickFirstLeafLoadBalancerConfig; +import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; @@ -2618,7 +2620,7 @@ public void serialized_retries_two_passes() { forwardTimeByBackoffDelay(); // should trigger retry again for (int i = 0; i < subchannels.length; i++) { inOrder.verify(subchannels[i]).requestConnection(); - assertEquals(i, loadBalancer.getGroupIndex()); + assertEquals(i, loadBalancer.getIndexLocation()); listeners[i].onSubchannelState(ConnectivityStateInfo.forTransientFailure(error)); // cascade } } @@ -2637,7 +2639,7 @@ public void index_looping() { PickFirstLeafLoadBalancer.Index index = new PickFirstLeafLoadBalancer.Index(Arrays.asList( new EquivalentAddressGroup(Arrays.asList(addr1, addr2), attr1), new EquivalentAddressGroup(Arrays.asList(addr3), attr2), - new EquivalentAddressGroup(Arrays.asList(addr4, addr5), attr3))); + new EquivalentAddressGroup(Arrays.asList(addr4, addr5), attr3)), enableHappyEyeballs); assertThat(index.getCurrentAddress()).isSameInstanceAs(addr1); assertThat(index.getCurrentEagAttributes()).isSameInstanceAs(attr1); assertThat(index.isAtBeginning()).isTrue(); @@ -2696,7 +2698,7 @@ public void index_updateGroups_resets() { SocketAddress addr3 = new FakeSocketAddress("addr3"); PickFirstLeafLoadBalancer.Index index = new PickFirstLeafLoadBalancer.Index(Arrays.asList( new EquivalentAddressGroup(Arrays.asList(addr1)), - new EquivalentAddressGroup(Arrays.asList(addr2, addr3)))); + new EquivalentAddressGroup(Arrays.asList(addr2, addr3))), enableHappyEyeballs); index.increment(); index.increment(); // We want to make sure both groupIndex and addressIndex are reset @@ -2713,7 +2715,7 @@ public void index_seekTo() { SocketAddress addr3 = new FakeSocketAddress("addr3"); PickFirstLeafLoadBalancer.Index index = new PickFirstLeafLoadBalancer.Index(Arrays.asList( new EquivalentAddressGroup(Arrays.asList(addr1, addr2)), - new EquivalentAddressGroup(Arrays.asList(addr3)))); + new EquivalentAddressGroup(Arrays.asList(addr3))), enableHappyEyeballs); assertThat(index.seekTo(addr3)).isTrue(); assertThat(index.getCurrentAddress()).isSameInstanceAs(addr3); assertThat(index.seekTo(addr1)).isTrue(); @@ -2725,6 +2727,83 @@ public void index_seekTo() { assertThat(index.getCurrentAddress()).isSameInstanceAs(addr2); } + @Test + public void index_interleaving() { + InetSocketAddress addr1_6 = new InetSocketAddress("f38:1:1", 1234); + InetSocketAddress addr1_4 = new InetSocketAddress("10.1.1.1", 1234); + InetSocketAddress addr2_4 = new InetSocketAddress("10.1.1.2", 1234); + InetSocketAddress addr3_4 = new InetSocketAddress("10.1.1.3", 1234); + InetSocketAddress addr4_4 = new InetSocketAddress("10.1.1.4", 1234); + InetSocketAddress addr4_6 = new InetSocketAddress("f38:1:4", 1234); + + Attributes attrs1 = Attributes.newBuilder().build(); + Attributes attrs2 = Attributes.newBuilder().build(); + Attributes attrs3 = Attributes.newBuilder().build(); + Attributes attrs4 = Attributes.newBuilder().build(); + + PickFirstLeafLoadBalancer.Index index = new PickFirstLeafLoadBalancer.Index(Arrays.asList( + new EquivalentAddressGroup(Arrays.asList(addr1_4, addr1_6), attrs1), + new EquivalentAddressGroup(Arrays.asList(addr2_4), attrs2), + new EquivalentAddressGroup(Arrays.asList(addr3_4), attrs3), + new EquivalentAddressGroup(Arrays.asList(addr4_4, addr4_6), attrs4)), enableHappyEyeballs); + + assertThat(index.getCurrentAddress()).isSameInstanceAs(addr1_4); + assertThat(index.getCurrentEagAttributes()).isSameInstanceAs(attrs1); + assertThat(index.isAtBeginning()).isTrue(); + + index.increment(); + assertThat(index.isValid()).isTrue(); + assertThat(index.getCurrentAddress()).isSameInstanceAs(addr1_6); + assertThat(index.getCurrentEagAttributes()).isSameInstanceAs(attrs1); + assertThat(index.isAtBeginning()).isFalse(); + + index.increment(); + assertThat(index.getCurrentAddress()).isSameInstanceAs(addr2_4); + assertThat(index.getCurrentEagAttributes()).isSameInstanceAs(attrs2); + + index.increment(); + if (enableHappyEyeballs) { + assertThat(index.getCurrentAddress()).isSameInstanceAs(addr4_6); + assertThat(index.getCurrentEagAttributes()).isSameInstanceAs(attrs4); + } else { + assertThat(index.getCurrentAddress()).isSameInstanceAs(addr3_4); + assertThat(index.getCurrentEagAttributes()).isSameInstanceAs(attrs3); + } + + index.increment(); + if (enableHappyEyeballs) { + assertThat(index.getCurrentAddress()).isSameInstanceAs(addr3_4); + assertThat(index.getCurrentEagAttributes()).isSameInstanceAs(attrs3); + } else { + assertThat(index.getCurrentAddress()).isSameInstanceAs(addr4_4); + assertThat(index.getCurrentEagAttributes()).isSameInstanceAs(attrs4); + } + + // Move to last entry + assertThat(index.increment()).isTrue(); + assertThat(index.isValid()).isTrue(); + if (enableHappyEyeballs) { + assertThat(index.getCurrentAddress()).isSameInstanceAs(addr4_4); + } else { + assertThat(index.getCurrentAddress()).isSameInstanceAs(addr4_6); + } + + // Move off of the end + assertThat(index.increment()).isFalse(); + assertThat(index.isValid()).isFalse(); + assertThrows(IllegalStateException.class, index::getCurrentAddress); + + // Reset + index.reset(); + assertThat(index.getCurrentAddress()).isSameInstanceAs(addr1_4); + assertThat(index.isAtBeginning()).isTrue(); + assertThat(index.isValid()).isTrue(); + + // Seek to an address + assertThat(index.seekTo(addr4_4)).isTrue(); + assertThat(index.getCurrentAddress()).isSameInstanceAs(addr4_4); + } + private static class FakeSocketAddress extends SocketAddress { final String name; From 87b27b154538eaa3114c7280f02da2c4fdb0c91f Mon Sep 17 00:00:00 2001 From: zbilun Date: Mon, 13 Jan 2025 17:57:39 -0800 Subject: [PATCH 148/591] interop-testing: fix peer extraction issue in soak test iterations This PR resolves an issue with peer address extraction in the soak test. In current `TestServiceClient` implementation, the same `clientCallCapture` atomic is shared across threads, leading to incorrect peer extraction. This fix ensures that each thread uses a local variable for capturing the client call. --- .../io/grpc/testing/integration/AbstractInteropTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java index 11fe9832fd9..3f2aa048dec 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java @@ -1874,14 +1874,15 @@ private void executeSoakTestInThread( } long earliestNextStartNs = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(minTimeMsBetweenRpcs); - + // recordClientCallInterceptor takes an AtomicReference. + AtomicReference> soakThreadClientCallCapture = new AtomicReference<>(); currentChannel = maybeCreateChannel.apply(currentChannel); TestServiceGrpc.TestServiceBlockingStub currentStub = TestServiceGrpc .newBlockingStub(currentChannel) - .withInterceptors(recordClientCallInterceptor(clientCallCapture)); + .withInterceptors(recordClientCallInterceptor(soakThreadClientCallCapture)); SoakIterationResult result = performOneSoakIteration(currentStub, soakRequestSize, soakResponseSize); - SocketAddress peer = clientCallCapture + SocketAddress peer = soakThreadClientCallCapture .get().getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); StringBuilder logStr = new StringBuilder( String.format( From 87c7b7a375ac9addbffa581e6d4c28b7203b6dcb Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 14 Jan 2025 07:15:29 -0800 Subject: [PATCH 149/591] interop-testing: Move soak out of AbstractInteropTest The soak code grew considerably in 6a92a2a22e. Since it isn't a JUnit test and doesn't resemble the other tests, it doesn't belong in AbstractInteropTest. AbstractInteropTest has lots of users, including it being re-compiled for use on Android, so moving it out makes the remaining code more clear for the more common cases. --- android-interop-testing/build.gradle | 1 - .../integration/AbstractInteropTest.java | 231 -------------- .../grpc/testing/integration/SoakClient.java | 295 ++++++++++++++++++ .../integration/TestServiceClient.java | 16 +- .../integration/XdsFederationTestClient.java | 56 ++-- 5 files changed, 330 insertions(+), 269 deletions(-) create mode 100644 interop-testing/src/main/java/io/grpc/testing/integration/SoakClient.java diff --git a/android-interop-testing/build.gradle b/android-interop-testing/build.gradle index 1d39aee1750..4f775d734e9 100644 --- a/android-interop-testing/build.gradle +++ b/android-interop-testing/build.gradle @@ -73,7 +73,6 @@ dependencies { project(':grpc-protobuf-lite'), project(':grpc-stub'), project(':grpc-testing'), - libraries.hdrhistogram, libraries.junit, libraries.truth, libraries.androidx.test.rules, diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java index 3f2aa048dec..88d570e7134 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java @@ -28,7 +28,6 @@ import static org.junit.Assert.fail; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Function; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; @@ -120,7 +119,6 @@ import javax.annotation.Nullable; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; -import org.HdrHistogram.Histogram; import org.junit.After; import org.junit.Assert; import org.junit.Assume; @@ -1681,235 +1679,6 @@ public void getServerAddressAndLocalAddressFromClient() { assertNotNull(obtainLocalClientAddr()); } - private static class SoakIterationResult { - public SoakIterationResult(long latencyMs, Status status) { - this.latencyMs = latencyMs; - this.status = status; - } - - public long getLatencyMs() { - return latencyMs; - } - - public Status getStatus() { - return status; - } - - private long latencyMs = -1; - private Status status = Status.OK; - } - - - private static class ThreadResults { - private int threadFailures = 0; - private int iterationsDone = 0; - private Histogram latencies = new Histogram(4); - - public int getThreadFailures() { - return threadFailures; - } - - public int getIterationsDone() { - return iterationsDone; - } - - public Histogram getLatencies() { - return latencies; - } - } - - private SoakIterationResult performOneSoakIteration( - TestServiceGrpc.TestServiceBlockingStub soakStub, int soakRequestSize, int soakResponseSize) - throws InterruptedException { - long startNs = System.nanoTime(); - Status status = Status.OK; - try { - final SimpleRequest request = - SimpleRequest.newBuilder() - .setResponseSize(soakResponseSize) - .setPayload( - Payload.newBuilder().setBody(ByteString.copyFrom(new byte[soakRequestSize]))) - .build(); - final SimpleResponse goldenResponse = - SimpleResponse.newBuilder() - .setPayload( - Payload.newBuilder().setBody(ByteString.copyFrom(new byte[soakResponseSize]))) - .build(); - assertResponse(goldenResponse, soakStub.unaryCall(request)); - } catch (StatusRuntimeException e) { - status = e.getStatus(); - } - long elapsedNs = System.nanoTime() - startNs; - return new SoakIterationResult(TimeUnit.NANOSECONDS.toMillis(elapsedNs), status); - } - - /** - * Runs large unary RPCs in a loop with configurable failure thresholds - * and channel creation behavior. - */ - public void performSoakTest( - String serverUri, - int soakIterations, - int maxFailures, - int maxAcceptablePerIterationLatencyMs, - int minTimeMsBetweenRpcs, - int overallTimeoutSeconds, - int soakRequestSize, - int soakResponseSize, - int numThreads, - Function createNewChannel) - throws InterruptedException { - if (soakIterations % numThreads != 0) { - throw new IllegalArgumentException("soakIterations must be evenly divisible by numThreads."); - } - ManagedChannel sharedChannel = createChannel(); - long startNs = System.nanoTime(); - Thread[] threads = new Thread[numThreads]; - int soakIterationsPerThread = soakIterations / numThreads; - List threadResultsList = new ArrayList<>(numThreads); - for (int i = 0; i < numThreads; i++) { - threadResultsList.add(new ThreadResults()); - } - for (int threadInd = 0; threadInd < numThreads; threadInd++) { - final int currentThreadInd = threadInd; - threads[threadInd] = new Thread(() -> { - try { - executeSoakTestInThread( - soakIterationsPerThread, - startNs, - minTimeMsBetweenRpcs, - soakRequestSize, - soakResponseSize, - maxAcceptablePerIterationLatencyMs, - overallTimeoutSeconds, - serverUri, - threadResultsList.get(currentThreadInd), - sharedChannel, - createNewChannel); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Thread interrupted: " + e.getMessage(), e); - } - }); - threads[threadInd].start(); - } - for (Thread thread : threads) { - thread.join(); - } - - int totalFailures = 0; - int iterationsDone = 0; - Histogram latencies = new Histogram(4); - for (ThreadResults threadResult :threadResultsList) { - totalFailures += threadResult.getThreadFailures(); - iterationsDone += threadResult.getIterationsDone(); - latencies.add(threadResult.getLatencies()); - } - System.err.println( - String.format( - Locale.US, - "(server_uri: %s) soak test ran: %d / %d iterations. total failures: %d. " - + "p50: %d ms, p90: %d ms, p100: %d ms", - serverUri, - iterationsDone, - soakIterations, - totalFailures, - latencies.getValueAtPercentile(50), - latencies.getValueAtPercentile(90), - latencies.getValueAtPercentile(100))); - // check if we timed out - String timeoutErrorMessage = - String.format( - Locale.US, - "(server_uri: %s) soak test consumed all %d seconds of time and quit early, " - + "only having ran %d out of desired %d iterations.", - serverUri, - overallTimeoutSeconds, - iterationsDone, - soakIterations); - assertEquals(timeoutErrorMessage, iterationsDone, soakIterations); - // check if we had too many failures - String tooManyFailuresErrorMessage = - String.format( - Locale.US, - "(server_uri: %s) soak test total failures: %d exceeds max failures " - + "threshold: %d.", - serverUri, totalFailures, maxFailures); - assertTrue(tooManyFailuresErrorMessage, totalFailures <= maxFailures); - shutdownChannel(sharedChannel); - } - - private void shutdownChannel(ManagedChannel channel) throws InterruptedException { - if (channel != null) { - channel.shutdownNow(); - channel.awaitTermination(10, TimeUnit.SECONDS); - } - } - - protected ManagedChannel createNewChannel(ManagedChannel currentChannel) { - try { - shutdownChannel(currentChannel); - return createChannel(); - } catch (InterruptedException e) { - throw new RuntimeException("Interrupted while creating a new channel", e); - } - } - - private void executeSoakTestInThread( - int soakIterationsPerThread, - long startNs, - int minTimeMsBetweenRpcs, - int soakRequestSize, - int soakResponseSize, - int maxAcceptablePerIterationLatencyMs, - int overallTimeoutSeconds, - String serverUri, - ThreadResults threadResults, - ManagedChannel sharedChannel, - Function maybeCreateChannel) throws InterruptedException { - ManagedChannel currentChannel = sharedChannel; - for (int i = 0; i < soakIterationsPerThread; i++) { - if (System.nanoTime() - startNs >= TimeUnit.SECONDS.toNanos(overallTimeoutSeconds)) { - break; - } - long earliestNextStartNs = System.nanoTime() - + TimeUnit.MILLISECONDS.toNanos(minTimeMsBetweenRpcs); - // recordClientCallInterceptor takes an AtomicReference. - AtomicReference> soakThreadClientCallCapture = new AtomicReference<>(); - currentChannel = maybeCreateChannel.apply(currentChannel); - TestServiceGrpc.TestServiceBlockingStub currentStub = TestServiceGrpc - .newBlockingStub(currentChannel) - .withInterceptors(recordClientCallInterceptor(soakThreadClientCallCapture)); - SoakIterationResult result = performOneSoakIteration(currentStub, - soakRequestSize, soakResponseSize); - SocketAddress peer = soakThreadClientCallCapture - .get().getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); - StringBuilder logStr = new StringBuilder( - String.format( - Locale.US, - "thread id: %d soak iteration: %d elapsed_ms: %d peer: %s server_uri: %s", - Thread.currentThread().getId(), - i, result.getLatencyMs(), peer != null ? peer.toString() : "null", serverUri)); - if (!result.getStatus().equals(Status.OK)) { - threadResults.threadFailures++; - logStr.append(String.format(" failed: %s", result.getStatus())); - } else if (result.getLatencyMs() > maxAcceptablePerIterationLatencyMs) { - threadResults.threadFailures++; - logStr.append( - " exceeds max acceptable latency: " + maxAcceptablePerIterationLatencyMs); - } else { - logStr.append(" succeeded"); - } - System.err.println(logStr.toString()); - threadResults.iterationsDone++; - threadResults.getLatencies().recordValue(result.getLatencyMs()); - long remainingNs = earliestNextStartNs - System.nanoTime(); - if (remainingNs > 0) { - TimeUnit.NANOSECONDS.sleep(remainingNs); - } - } - } - private static void assertSuccess(StreamRecorder recorder) { if (recorder.getError() != null) { throw new AssertionError(recorder.getError()); diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/SoakClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/SoakClient.java new file mode 100644 index 00000000000..935586cfbdd --- /dev/null +++ b/interop-testing/src/main/java/io/grpc/testing/integration/SoakClient.java @@ -0,0 +1,295 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.testing.integration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.common.base.Function; +import com.google.protobuf.ByteString; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.Grpc; +import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.testing.integration.Messages.Payload; +import io.grpc.testing.integration.Messages.SimpleRequest; +import io.grpc.testing.integration.Messages.SimpleResponse; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.HdrHistogram.Histogram; + +/** + * Shared implementation for rpc_soak and channel_soak. Unlike the tests in AbstractInteropTest, + * these "test cases" are only intended to be run from the command line. They don't fit the regular + * test patterns of AbstractInteropTest. + * https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md#rpc_soak + */ +final class SoakClient { + private static class SoakIterationResult { + public SoakIterationResult(long latencyMs, Status status) { + this.latencyMs = latencyMs; + this.status = status; + } + + public long getLatencyMs() { + return latencyMs; + } + + public Status getStatus() { + return status; + } + + private long latencyMs = -1; + private Status status = Status.OK; + } + + private static class ThreadResults { + private int threadFailures = 0; + private int iterationsDone = 0; + private Histogram latencies = new Histogram(4); + + public int getThreadFailures() { + return threadFailures; + } + + public int getIterationsDone() { + return iterationsDone; + } + + public Histogram getLatencies() { + return latencies; + } + } + + private static SoakIterationResult performOneSoakIteration( + TestServiceGrpc.TestServiceBlockingStub soakStub, int soakRequestSize, int soakResponseSize) + throws InterruptedException { + long startNs = System.nanoTime(); + Status status = Status.OK; + try { + final SimpleRequest request = + SimpleRequest.newBuilder() + .setResponseSize(soakResponseSize) + .setPayload( + Payload.newBuilder().setBody(ByteString.copyFrom(new byte[soakRequestSize]))) + .build(); + final SimpleResponse goldenResponse = + SimpleResponse.newBuilder() + .setPayload( + Payload.newBuilder().setBody(ByteString.copyFrom(new byte[soakResponseSize]))) + .build(); + assertResponse(goldenResponse, soakStub.unaryCall(request)); + } catch (StatusRuntimeException e) { + status = e.getStatus(); + } + long elapsedNs = System.nanoTime() - startNs; + return new SoakIterationResult(TimeUnit.NANOSECONDS.toMillis(elapsedNs), status); + } + + /** + * Runs large unary RPCs in a loop with configurable failure thresholds + * and channel creation behavior. + */ + public static void performSoakTest( + String serverUri, + int soakIterations, + int maxFailures, + int maxAcceptablePerIterationLatencyMs, + int minTimeMsBetweenRpcs, + int overallTimeoutSeconds, + int soakRequestSize, + int soakResponseSize, + int numThreads, + ManagedChannel sharedChannel, + Function maybeCreateChannel) + throws InterruptedException { + if (soakIterations % numThreads != 0) { + throw new IllegalArgumentException("soakIterations must be evenly divisible by numThreads."); + } + long startNs = System.nanoTime(); + Thread[] threads = new Thread[numThreads]; + int soakIterationsPerThread = soakIterations / numThreads; + List threadResultsList = new ArrayList<>(numThreads); + for (int i = 0; i < numThreads; i++) { + threadResultsList.add(new ThreadResults()); + } + for (int threadInd = 0; threadInd < numThreads; threadInd++) { + final int currentThreadInd = threadInd; + threads[threadInd] = new Thread(() -> { + try { + executeSoakTestInThread( + soakIterationsPerThread, + startNs, + minTimeMsBetweenRpcs, + soakRequestSize, + soakResponseSize, + maxAcceptablePerIterationLatencyMs, + overallTimeoutSeconds, + serverUri, + threadResultsList.get(currentThreadInd), + sharedChannel, + maybeCreateChannel); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread interrupted: " + e.getMessage(), e); + } + }); + threads[threadInd].start(); + } + for (Thread thread : threads) { + thread.join(); + } + + int totalFailures = 0; + int iterationsDone = 0; + Histogram latencies = new Histogram(4); + for (ThreadResults threadResult :threadResultsList) { + totalFailures += threadResult.getThreadFailures(); + iterationsDone += threadResult.getIterationsDone(); + latencies.add(threadResult.getLatencies()); + } + System.err.println( + String.format( + Locale.US, + "(server_uri: %s) soak test ran: %d / %d iterations. total failures: %d. " + + "p50: %d ms, p90: %d ms, p100: %d ms", + serverUri, + iterationsDone, + soakIterations, + totalFailures, + latencies.getValueAtPercentile(50), + latencies.getValueAtPercentile(90), + latencies.getValueAtPercentile(100))); + // check if we timed out + String timeoutErrorMessage = + String.format( + Locale.US, + "(server_uri: %s) soak test consumed all %d seconds of time and quit early, " + + "only having ran %d out of desired %d iterations.", + serverUri, + overallTimeoutSeconds, + iterationsDone, + soakIterations); + assertEquals(timeoutErrorMessage, iterationsDone, soakIterations); + // check if we had too many failures + String tooManyFailuresErrorMessage = + String.format( + Locale.US, + "(server_uri: %s) soak test total failures: %d exceeds max failures " + + "threshold: %d.", + serverUri, totalFailures, maxFailures); + assertTrue(tooManyFailuresErrorMessage, totalFailures <= maxFailures); + sharedChannel.shutdownNow(); + sharedChannel.awaitTermination(10, TimeUnit.SECONDS); + } + + private static void executeSoakTestInThread( + int soakIterationsPerThread, + long startNs, + int minTimeMsBetweenRpcs, + int soakRequestSize, + int soakResponseSize, + int maxAcceptablePerIterationLatencyMs, + int overallTimeoutSeconds, + String serverUri, + ThreadResults threadResults, + ManagedChannel sharedChannel, + Function maybeCreateChannel) throws InterruptedException { + ManagedChannel currentChannel = sharedChannel; + for (int i = 0; i < soakIterationsPerThread; i++) { + if (System.nanoTime() - startNs >= TimeUnit.SECONDS.toNanos(overallTimeoutSeconds)) { + break; + } + long earliestNextStartNs = System.nanoTime() + + TimeUnit.MILLISECONDS.toNanos(minTimeMsBetweenRpcs); + // recordClientCallInterceptor takes an AtomicReference. + AtomicReference> soakThreadClientCallCapture = new AtomicReference<>(); + currentChannel = maybeCreateChannel.apply(currentChannel); + TestServiceGrpc.TestServiceBlockingStub currentStub = TestServiceGrpc + .newBlockingStub(currentChannel) + .withInterceptors(recordClientCallInterceptor(soakThreadClientCallCapture)); + SoakIterationResult result = performOneSoakIteration(currentStub, + soakRequestSize, soakResponseSize); + SocketAddress peer = soakThreadClientCallCapture + .get().getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + StringBuilder logStr = new StringBuilder( + String.format( + Locale.US, + "thread id: %d soak iteration: %d elapsed_ms: %d peer: %s server_uri: %s", + Thread.currentThread().getId(), + i, result.getLatencyMs(), peer != null ? peer.toString() : "null", serverUri)); + if (!result.getStatus().equals(Status.OK)) { + threadResults.threadFailures++; + logStr.append(String.format(" failed: %s", result.getStatus())); + } else if (result.getLatencyMs() > maxAcceptablePerIterationLatencyMs) { + threadResults.threadFailures++; + logStr.append( + " exceeds max acceptable latency: " + maxAcceptablePerIterationLatencyMs); + } else { + logStr.append(" succeeded"); + } + System.err.println(logStr.toString()); + threadResults.iterationsDone++; + threadResults.getLatencies().recordValue(result.getLatencyMs()); + long remainingNs = earliestNextStartNs - System.nanoTime(); + if (remainingNs > 0) { + TimeUnit.NANOSECONDS.sleep(remainingNs); + } + } + } + + private static void assertResponse(SimpleResponse expected, SimpleResponse actual) { + assertPayload(expected.getPayload(), actual.getPayload()); + assertEquals(expected.getUsername(), actual.getUsername()); + assertEquals(expected.getOauthScope(), actual.getOauthScope()); + } + + private static void assertPayload(Payload expected, Payload actual) { + // Compare non deprecated fields in Payload, to make this test forward compatible. + if (expected == null || actual == null) { + assertEquals(expected, actual); + } else { + assertEquals(expected.getBody(), actual.getBody()); + } + } + + /** + * Captures the ClientCall. Useful for testing {@link ClientCall#getAttributes()} + */ + private static ClientInterceptor recordClientCallInterceptor( + final AtomicReference> clientCallCapture) { + return new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + ClientCall clientCall = next.newCall(method,callOptions); + clientCallCapture.set(clientCall); + return clientCall; + } + }; + } + +} diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java index 8ade38cb024..125d876b705 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java @@ -523,7 +523,7 @@ private void runTest(TestCases testCase) throws Exception { } case RPC_SOAK: { - tester.performSoakTest( + SoakClient.performSoakTest( serverHost, soakIterations, soakMaxFailures, @@ -533,12 +533,13 @@ private void runTest(TestCases testCase) throws Exception { soakRequestSize, soakResponseSize, numThreads, + tester.createChannelBuilder().build(), (currentChannel) -> currentChannel); break; } case CHANNEL_SOAK: { - tester.performSoakTest( + SoakClient.performSoakTest( serverHost, soakIterations, soakMaxFailures, @@ -548,6 +549,7 @@ private void runTest(TestCases testCase) throws Exception { soakRequestSize, soakResponseSize, numThreads, + tester.createChannelBuilder().build(), (currentChannel) -> tester.createNewChannel(currentChannel)); break; } @@ -711,6 +713,16 @@ protected ManagedChannelBuilder createChannelBuilder() { return okBuilder.intercept(createCensusStatsClientInterceptor()); } + ManagedChannel createNewChannel(ManagedChannel currentChannel) { + currentChannel.shutdownNow(); + try { + currentChannel.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted while creating a new channel", e); + } + return createChannel(); + } + /** * Assuming "pick_first" policy is used, tests that all requests are sent to the same server. */ diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/XdsFederationTestClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/XdsFederationTestClient.java index 08d845422a5..bba282b7b6f 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/XdsFederationTestClient.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/XdsFederationTestClient.java @@ -22,9 +22,10 @@ import io.grpc.ChannelCredentials; import io.grpc.Grpc; import io.grpc.InsecureChannelCredentials; -import io.grpc.ManagedChannelBuilder; +import io.grpc.ManagedChannel; import io.grpc.alts.ComputeEngineChannelCredentials; import java.util.ArrayList; +import java.util.concurrent.TimeUnit; import java.util.logging.Logger; /** @@ -44,26 +45,8 @@ public final class XdsFederationTestClient { public static void main(String[] args) throws Exception { final XdsFederationTestClient client = new XdsFederationTestClient(); client.parseArgs(args); - Runtime.getRuntime() - .addShutdownHook( - new Thread() { - @Override - @SuppressWarnings("CatchAndPrintStackTrace") - public void run() { - System.out.println("Shutting down"); - try { - client.tearDown(); - } catch (RuntimeException e) { - e.printStackTrace(); - } - } - }); client.setUp(); - try { - client.run(); - } finally { - client.tearDown(); - } + client.run(); System.exit(0); } @@ -209,22 +192,13 @@ void setUp() { for (int i = 0; i < uris.length; i++) { clients.add(new InnerClient(creds[i], uris[i])); } - for (InnerClient c : clients) { - c.setUp(); - } - } - - private synchronized void tearDown() { - for (InnerClient c : clients) { - c.tearDown(); - } } /** * Wraps a single client stub configuration and executes a * soak test case with that configuration. */ - class InnerClient extends AbstractInteropTest { + class InnerClient { private final String credentialsType; private final String serverUri; private boolean runSucceeded = false; @@ -249,7 +223,7 @@ public void run() throws InterruptedException { try { switch (testCase) { case "rpc_soak": { - performSoakTest( + SoakClient.performSoakTest( serverUri, soakIterations, soakMaxFailures, @@ -259,11 +233,12 @@ public void run() throws InterruptedException { soakRequestSize, soakResponseSize, 1, + createChannel(), (currentChannel) -> currentChannel); } break; case "channel_soak": { - performSoakTest( + SoakClient.performSoakTest( serverUri, soakIterations, soakMaxFailures, @@ -273,6 +248,7 @@ public void run() throws InterruptedException { soakRequestSize, soakResponseSize, 1, + createChannel(), (currentChannel) -> createNewChannel(currentChannel)); } break; @@ -288,8 +264,7 @@ public void run() throws InterruptedException { } } - @Override - protected ManagedChannelBuilder createChannelBuilder() { + ManagedChannel createChannel() { ChannelCredentials channelCredentials; switch (credentialsType) { case "compute_engine_channel_creds": @@ -303,7 +278,18 @@ protected ManagedChannelBuilder createChannelBuilder() { } return Grpc.newChannelBuilder(serverUri, channelCredentials) .keepAliveTime(3600, SECONDS) - .keepAliveTimeout(20, SECONDS); + .keepAliveTimeout(20, SECONDS) + .build(); + } + + ManagedChannel createNewChannel(ManagedChannel currentChannel) { + currentChannel.shutdownNow(); + try { + currentChannel.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted while creating a new channel", e); + } + return createChannel(); } } From 4d8aff72dcd7d72b6e316136d4390e86501a125d Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Wed, 15 Jan 2025 10:43:48 -0800 Subject: [PATCH 150/591] stub: Eliminate invalid test cases where different threads were calling close from the thread writing. (#11822) * Eliminate invalid test cases where different threads were calling close from the thread writing. * Remove multi-thread cancel/write test --- .../io/grpc/stub/BlockingClientCallTest.java | 31 +++---------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/stub/src/test/java/io/grpc/stub/BlockingClientCallTest.java b/stub/src/test/java/io/grpc/stub/BlockingClientCallTest.java index 112b092eaed..e3a4f90e2c2 100644 --- a/stub/src/test/java/io/grpc/stub/BlockingClientCallTest.java +++ b/stub/src/test/java/io/grpc/stub/BlockingClientCallTest.java @@ -195,21 +195,12 @@ public void testCancel() throws Exception { assertThat(System.currentTimeMillis() - start).isLessThan(2 * DELAY_MILLIS); } - // write terminated + // after cancel tests biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, CallOptions.DEFAULT); - start = System.currentTimeMillis(); - delayedCancel(biDiStream, "cancel write"); - - // Write interrupted by cancel - try { - assertFalse(biDiStream.write(30)); // this is interrupted by cancel - fail("No exception thrown when write was interrupted by cancel"); - } catch (StatusException e) { - assertEquals(Status.CANCELLED.getCode(), e.getStatus().getCode()); - } + biDiStream.cancel("cancel write", new RuntimeException("Test requested close")); - // Write after cancel + // Write after cancel should throw an exception try { start = System.currentTimeMillis(); biDiStream.write(30); @@ -357,31 +348,19 @@ public void testReadsAndWritesInterleaved_BlockingWrites() throws Exception { } @Test - public void testWriteCompleted() throws Exception { + public void testWriteAfterCloseThrows() throws Exception { testMethod.disableAutoRequest(); biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, CallOptions.DEFAULT); - // Verify pending write released - long start = System.currentTimeMillis(); - delayedVoidMethod(DELAY_MILLIS, biDiStream::halfClose); - assertFalse(biDiStream.write(1)); // should block until writeComplete is triggered - long end = System.currentTimeMillis(); - assertThat(end - start).isAtLeast(DELAY_MILLIS); - // verify new writes throw an illegalStateException + biDiStream.halfClose(); try { assertFalse(biDiStream.write(2)); fail("write did not throw an exception when called after halfClose"); } catch (IllegalStateException e) { assertThat(e.getMessage()).containsMatch("after.*halfClose.*cancel"); } - - // verify pending write with timeout released - biDiStream = ClientCalls.blockingBidiStreamingCall(channel, BIDI_STREAMING_METHOD, - CallOptions.DEFAULT); - delayedVoidMethod(DELAY_MILLIS, biDiStream::halfClose); - assertFalse(biDiStream.write(3, 2 * DELAY_MILLIS, TimeUnit.MILLISECONDS)); } @Test From b44ebce45dd357e8b2ccb28264477a81ea5d9874 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Fri, 17 Jan 2025 14:58:52 +0530 Subject: [PATCH 151/591] xds: Envoy proto sync to 2024-11-11 (#11816) --- repositories.bzl | 6 +- .../test/java/io/grpc/xds/RbacFilterTest.java | 11 ++-- xds/third_party/envoy/import.sh | 2 +- .../proto/envoy/config/core/v3/base.proto | 1 + .../proto/envoy/config/core/v3/protocol.proto | 6 ++ .../listener/v3/listener_components.proto | 24 +------- .../envoy/config/overload/v3/overload.proto | 6 ++ .../proto/envoy/config/rbac/v3/rbac.proto | 51 +++++++++++++++-- .../filters/http/gcp_authn/v3/gcp_authn.proto | 28 +++++++++- .../v3/http_connection_manager.proto | 2 +- .../transport_sockets/tls/v3/tls.proto | 22 +++++++- .../proto/envoy/type/v3/http_status.proto | 56 +++++++++++++++++++ 12 files changed, 174 insertions(+), 41 deletions(-) diff --git a/repositories.bzl b/repositories.bzl index 7d01675e9ad..3f4cd11c1a6 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -141,10 +141,10 @@ def grpc_java_repositories(bzlmod = False): if not native.existing_rule("envoy_api"): http_archive( name = "envoy_api", - sha256 = "f439add0cc01f718d53d6feb4d0972ac0d48b3e145c18b53439a3b5148a0cb6e", - strip_prefix = "data-plane-api-55f8b2351962d84c84a6534da67da1dd9f671c50", + sha256 = "ecf71817233eba19cc8b4ee14e126ffd5838065d5b5a92b2506258a42ac55199", + strip_prefix = "data-plane-api-0bc95493c5e88b7b07e62758d23b39341813a827", urls = [ - "https://github.com/envoyproxy/data-plane-api/archive/55f8b2351962d84c84a6534da67da1dd9f671c50.tar.gz", + "https://github.com/envoyproxy/data-plane-api/archive/0bc95493c5e88b7b07e62758d23b39341813a827.tar.gz", ], ) diff --git a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java index 29af01b222f..013b21e3f45 100644 --- a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java @@ -219,14 +219,15 @@ public void headerParser_headerName() { @SuppressWarnings("unchecked") public void compositeRules() { MetadataMatcher metadataMatcher = MetadataMatcher.newBuilder().build(); + @SuppressWarnings("deprecation") + Permission permissionMetadata = Permission.newBuilder().setMetadata(metadataMatcher).build(); List permissionList = Arrays.asList( Permission.newBuilder().setOrRules(Permission.Set.newBuilder().addRules( - Permission.newBuilder().setMetadata(metadataMatcher).build() - ).build()).build()); + permissionMetadata).build()).build()); + @SuppressWarnings("deprecation") + Principal principalMetadata = Principal.newBuilder().setMetadata(metadataMatcher).build(); List principalList = Arrays.asList( - Principal.newBuilder().setNotId( - Principal.newBuilder().setMetadata(metadataMatcher).build() - ).build()); + Principal.newBuilder().setNotId(principalMetadata).build()); ConfigOrError result = parse(permissionList, principalList); assertThat(result.errorDetail).isNull(); assertThat(result.config).isInstanceOf(RbacConfig.class); diff --git a/xds/third_party/envoy/import.sh b/xds/third_party/envoy/import.sh index 254abbe271f..dbe6f81b1a8 100755 --- a/xds/third_party/envoy/import.sh +++ b/xds/third_party/envoy/import.sh @@ -17,7 +17,7 @@ set -e # import VERSION from the google internal copybara_version.txt for Envoy -VERSION=742a3b02e3b2a9dfb877a7e378607c6ed0c2aa53 +VERSION=0b90f64539c88dc3d2a6792dc714e8207bce0c08 DOWNLOAD_URL="https://github.com/envoyproxy/envoy/archive/${VERSION}.tar.gz" DOWNLOAD_BASE_DIR="envoy-${VERSION}" SOURCE_PROTO_BASE_DIR="${DOWNLOAD_BASE_DIR}/api" diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/base.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/base.proto index df91565d0a7..57f59373395 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/base.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/base.proto @@ -453,6 +453,7 @@ message HeaderValueOption { message HeaderMap { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.HeaderMap"; + // A list of header names and their values. repeated HeaderValue headers = 1; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto index d8ce3cd817c..7160cfb641a 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto @@ -123,6 +123,9 @@ message UpstreamHttpProtocolOptions { // header when :ref:`override_auto_sni_header ` // is set, as seen by the :ref:`router filter `. // Does nothing if a filter before the http router filter sets the corresponding metadata. + // + // See :ref:`SNI configuration ` for details on how this + // interacts with other validation options. bool auto_sni = 1; // Automatic validate upstream presented certificate for new upstream connections based on the @@ -130,6 +133,9 @@ message UpstreamHttpProtocolOptions { // is set, as seen by the :ref:`router filter `. // This field is intended to be set with ``auto_sni`` field. // Does nothing if a filter before the http router filter sets the corresponding metadata. + // + // See :ref:`validation configuration ` for how this interacts with + // other validation options. bool auto_san_validation = 2; // An optional alternative to the host/authority header to be used for setting the SNI value. diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/listener_components.proto b/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/listener_components.proto index 2adb8bc2c80..33eb349fd06 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/listener_components.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/listener_components.proto @@ -201,24 +201,9 @@ message FilterChainMatch { message FilterChain { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.listener.FilterChain"; - // The configuration for on-demand filter chain. If this field is not empty in FilterChain message, - // a filter chain will be built on-demand. - // On-demand filter chains help speedup the warming up of listeners since the building and initialization of - // an on-demand filter chain will be postponed to the arrival of new connection requests that require this filter chain. - // Filter chains that are not often used can be set as on-demand. - message OnDemandConfiguration { - // The timeout to wait for filter chain placeholders to complete rebuilding. - // 1. If this field is set to 0, timeout is disabled. - // 2. If not specified, a default timeout of 15s is used. - // Rebuilding will wait until dependencies are ready, have failed, or this timeout is reached. - // Upon failure or timeout, all connections related to this filter chain will be closed. - // Rebuilding will start again on the next new connection. - google.protobuf.Duration rebuild_timeout = 1; - } - - reserved 2; + reserved 2, 8; - reserved "tls_context"; + reserved "tls_context", "on_demand_configuration"; // The criteria to use when matching a connection to this filter chain. FilterChainMatch filter_chain_match = 1; @@ -269,11 +254,6 @@ message FilterChain { // ` // requires that filter chains are uniquely named within a listener. string name = 7; - - // [#not-implemented-hide:] The configuration to specify whether the filter chain will be built on-demand. - // If this field is not empty, the filter chain will be built on-demand. - // Otherwise, the filter chain will be built normally and block listener warming. - OnDemandConfiguration on_demand_configuration = 8; } // Listener filter chain match configuration. This is a recursive structure which allows complex diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/overload/v3/overload.proto b/xds/third_party/envoy/src/main/proto/envoy/config/overload/v3/overload.proto index d3b8b01a173..1f267c1863d 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/overload/v3/overload.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/overload/v3/overload.proto @@ -103,6 +103,12 @@ message ScaleTimersOverloadActionConfig { // This affects the value of // :ref:`FilterChain.transport_socket_connect_timeout `. TRANSPORT_SOCKET_CONNECT = 3; + + // Adjusts the max connection duration timer for downstream HTTP connections. + // This affects the value of + // :ref:`HttpConnectionManager.common_http_protocol_options.max_connection_duration + // `. + HTTP_DOWNSTREAM_CONNECTION_MAX = 4; } message ScaleTimer { diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/rbac/v3/rbac.proto b/xds/third_party/envoy/src/main/proto/envoy/config/rbac/v3/rbac.proto index 8d98fd7155d..e33a533e25a 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/rbac/v3/rbac.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/rbac/v3/rbac.proto @@ -28,6 +28,14 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Role Based Access Control (RBAC)] +enum MetadataSource { + // Query :ref:`dynamic metadata ` + DYNAMIC = 0; + + // Query :ref:`route metadata ` + ROUTE = 1; +} + // Role Based Access Control (RBAC) provides service-level and method-level access control for a // service. Requests are allowed or denied based on the ``action`` and whether a matching policy is // found. For instance, if the action is ALLOW and a matching policy is found the request should be @@ -193,8 +201,27 @@ message Policy { [(udpa.annotations.field_migrate).oneof_promotion = "expression_specifier"]; } +// SourcedMetadata enables matching against metadata from different sources in the request processing +// pipeline. It extends the base MetadataMatcher functionality by allowing specification of where the +// metadata should be sourced from, rather than only matching against dynamic metadata. +// +// The matcher can be configured to look up metadata from: +// * Dynamic metadata: Runtime metadata added by filters during request processing +// * Route metadata: Static metadata configured on the route entry +message SourcedMetadata { + // Metadata matcher configuration that defines what metadata to match against. This includes the filter name, + // metadata key path, and expected value. + type.matcher.v3.MetadataMatcher metadata_matcher = 1 + [(validate.rules).message = {required: true}]; + + // Specifies which metadata source should be used for matching. If not set, + // defaults to DYNAMIC (dynamic metadata). Set to ROUTE to match against + // static metadata configured on the route entry. + MetadataSource metadata_source = 2 [(validate.rules).enum = {defined_only: true}]; +} + // Permission defines an action (or actions) that a principal can take. -// [#next-free-field: 14] +// [#next-free-field: 15] message Permission { option (udpa.annotations.versioning).previous_message_type = "envoy.config.rbac.v2.Permission"; @@ -237,8 +264,10 @@ message Permission { // A port number range that describes a range of destination ports connecting to. type.v3.Int32Range destination_port_range = 11; - // Metadata that describes additional information about the action. - type.matcher.v3.MetadataMatcher metadata = 7; + // Metadata that describes additional information about the action. This field is deprecated; please use + // :ref:`sourced_metadata` instead. + type.matcher.v3.MetadataMatcher metadata = 7 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; // Negates matching the provided permission. For instance, if the value of // ``not_rule`` would match, this permission would not match. Conversely, if @@ -274,12 +303,16 @@ message Permission { // URI template path matching. // [#extension-category: envoy.path.match] core.v3.TypedExtensionConfig uri_template = 13; + + // Matches against metadata from either dynamic state or route configuration. Preferred over the + // ``metadata`` field as it provides more flexibility in metadata source selection. + SourcedMetadata sourced_metadata = 14; } } // Principal defines an identity or a group of identities for a downstream // subject. -// [#next-free-field: 13] +// [#next-free-field: 14] message Principal { option (udpa.annotations.versioning).previous_message_type = "envoy.config.rbac.v2.Principal"; @@ -356,8 +389,10 @@ message Principal { // A URL path on the incoming HTTP request. Only available for HTTP. type.matcher.v3.PathMatcher url_path = 9; - // Metadata that describes additional information about the principal. - type.matcher.v3.MetadataMatcher metadata = 7; + // Metadata that describes additional information about the principal. This field is deprecated; please use + // :ref:`sourced_metadata` instead. + type.matcher.v3.MetadataMatcher metadata = 7 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; // Identifies the principal using a filter state object. type.matcher.v3.FilterStateMatcher filter_state = 12; @@ -366,6 +401,10 @@ message Principal { // ``not_id`` would match, this principal would not match. Conversely, if the // value of ``not_id`` would not match, this principal would match. Principal not_id = 8; + + // Matches against metadata from either dynamic state or route configuration. Preferred over the + // ``metadata`` field as it provides more flexibility in metadata source selection. + SourcedMetadata sourced_metadata = 13; } } diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto index 05757c23e59..f4646389f7e 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto @@ -5,8 +5,10 @@ package envoy.extensions.filters.http.gcp_authn.v3; import "envoy/config/core/v3/base.proto"; import "envoy/config/core/v3/http_uri.proto"; +import "google/protobuf/duration.proto"; import "google/protobuf/wrappers.proto"; +import "envoy/annotations/deprecation.proto"; import "udpa/annotations/status.proto"; import "validate/validate.proto"; @@ -21,12 +23,21 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#extension: envoy.filters.http.gcp_authn] // Filter configuration. +// [#next-free-field: 7] message GcpAuthnFilterConfig { // The HTTP URI to fetch tokens from GCE Metadata Server(https://cloud.google.com/compute/docs/metadata/overview). // The URL format is "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=[AUDIENCE]" - config.core.v3.HttpUri http_uri = 1 [(validate.rules).message = {required: true}]; + // + // This field is deprecated because it does not match the API surface provided by the google auth libraries. + // Control planes should not attempt to override the metadata server URI. + // The cluster and timeout can be configured using the ``cluster`` and ``timeout`` fields instead. + // For backward compatibility, the cluster and timeout configured in this field will be used + // if the new ``cluster`` and ``timeout`` fields are not set. + config.core.v3.HttpUri http_uri = 1 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; - // Retry policy for fetching tokens. This field is optional. + // Retry policy for fetching tokens. + // Not supported by all data planes. config.core.v3.RetryPolicy retry_policy = 2; // Token cache configuration. This field is optional. @@ -34,7 +45,20 @@ message GcpAuthnFilterConfig { // Request header location to extract the token. By default (i.e. if this field is not specified), the token // is extracted to the Authorization HTTP header, in the format "Authorization: Bearer ". + // Not supported by all data planes. TokenHeader token_header = 4; + + // Cluster to send traffic to the GCE metadata server. Not supported + // by all data planes; a data plane may instead have its own mechanism + // for contacting the metadata server. + string cluster = 5; + + // Timeout for fetching the tokens from the GCE metadata server. + // Not supported by all data planes. + google.protobuf.Duration timeout = 6 [(validate.rules).duration = { + lt {seconds: 4294967296} + gte {} + }]; } // Audience is the URL of the receiving service that performs token authentication. diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index 3b49f132956..5fb9f24cc7c 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -691,7 +691,7 @@ message HttpConnectionManager { // information about internal/external addresses. // // .. warning:: - // In the next release, no IP addresses will be considered trusted. If you have tooling such as probes + // As of Envoy 1.33.0 no IP addresses will be considered trusted. If you have tooling such as probes // on your private network which need to be treated as trusted (e.g. changing arbitrary x-envoy headers) // you will have to manually include those addresses or CIDR ranges like: // diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto index c305ff74f42..44b8d269324 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto @@ -25,7 +25,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#extension: envoy.transport_sockets.tls] // The TLS contexts below provide the transport socket configuration for upstream/downstream TLS. -// [#next-free-field: 6] +// [#next-free-field: 8] message UpstreamTlsContext { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.auth.UpstreamTlsContext"; @@ -42,6 +42,26 @@ message UpstreamTlsContext { // SNI string to use when creating TLS backend connections. string sni = 2 [(validate.rules).string = {max_bytes: 255}]; + // If true, replaces the SNI for the connection with the hostname of the upstream host, if + // the hostname is known due to either a DNS cluster type or the + // :ref:`hostname ` is set on + // the host. + // + // See :ref:`SNI configuration ` for details on how this + // interacts with other validation options. + bool auto_host_sni = 6; + + // If true, replace any Subject Alternative Name validations with a validation for a DNS SAN matching + // the SNI value sent. Note that the validation will be against the actual requested SNI, regardless of how it + // is configured. + // + // For the common case where an SNI value is sent and it is expected that the server certificate contains a SAN + // matching that SNI value, this option will do the correct SAN validation. + // + // See :ref:`validation configuration ` for how this interacts with + // other validation options. + bool auto_sni_san_validation = 7; + // If true, server-initiated TLS renegotiation will be allowed. // // .. attention:: diff --git a/xds/third_party/envoy/src/main/proto/envoy/type/v3/http_status.proto b/xds/third_party/envoy/src/main/proto/envoy/type/v3/http_status.proto index ab03e1b2b72..40d697beefc 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/type/v3/http_status.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/type/v3/http_status.proto @@ -21,116 +21,172 @@ enum StatusCode { // `enum` type. Empty = 0; + // Continue - ``100`` status code. Continue = 100; + // OK - ``200`` status code. OK = 200; + // Created - ``201`` status code. Created = 201; + // Accepted - ``202`` status code. Accepted = 202; + // NonAuthoritativeInformation - ``203`` status code. NonAuthoritativeInformation = 203; + // NoContent - ``204`` status code. NoContent = 204; + // ResetContent - ``205`` status code. ResetContent = 205; + // PartialContent - ``206`` status code. PartialContent = 206; + // MultiStatus - ``207`` status code. MultiStatus = 207; + // AlreadyReported - ``208`` status code. AlreadyReported = 208; + // IMUsed - ``226`` status code. IMUsed = 226; + // MultipleChoices - ``300`` status code. MultipleChoices = 300; + // MovedPermanently - ``301`` status code. MovedPermanently = 301; + // Found - ``302`` status code. Found = 302; + // SeeOther - ``303`` status code. SeeOther = 303; + // NotModified - ``304`` status code. NotModified = 304; + // UseProxy - ``305`` status code. UseProxy = 305; + // TemporaryRedirect - ``307`` status code. TemporaryRedirect = 307; + // PermanentRedirect - ``308`` status code. PermanentRedirect = 308; + // BadRequest - ``400`` status code. BadRequest = 400; + // Unauthorized - ``401`` status code. Unauthorized = 401; + // PaymentRequired - ``402`` status code. PaymentRequired = 402; + // Forbidden - ``403`` status code. Forbidden = 403; + // NotFound - ``404`` status code. NotFound = 404; + // MethodNotAllowed - ``405`` status code. MethodNotAllowed = 405; + // NotAcceptable - ``406`` status code. NotAcceptable = 406; + // ProxyAuthenticationRequired - ``407`` status code. ProxyAuthenticationRequired = 407; + // RequestTimeout - ``408`` status code. RequestTimeout = 408; + // Conflict - ``409`` status code. Conflict = 409; + // Gone - ``410`` status code. Gone = 410; + // LengthRequired - ``411`` status code. LengthRequired = 411; + // PreconditionFailed - ``412`` status code. PreconditionFailed = 412; + // PayloadTooLarge - ``413`` status code. PayloadTooLarge = 413; + // URITooLong - ``414`` status code. URITooLong = 414; + // UnsupportedMediaType - ``415`` status code. UnsupportedMediaType = 415; + // RangeNotSatisfiable - ``416`` status code. RangeNotSatisfiable = 416; + // ExpectationFailed - ``417`` status code. ExpectationFailed = 417; + // MisdirectedRequest - ``421`` status code. MisdirectedRequest = 421; + // UnprocessableEntity - ``422`` status code. UnprocessableEntity = 422; + // Locked - ``423`` status code. Locked = 423; + // FailedDependency - ``424`` status code. FailedDependency = 424; + // UpgradeRequired - ``426`` status code. UpgradeRequired = 426; + // PreconditionRequired - ``428`` status code. PreconditionRequired = 428; + // TooManyRequests - ``429`` status code. TooManyRequests = 429; + // RequestHeaderFieldsTooLarge - ``431`` status code. RequestHeaderFieldsTooLarge = 431; + // InternalServerError - ``500`` status code. InternalServerError = 500; + // NotImplemented - ``501`` status code. NotImplemented = 501; + // BadGateway - ``502`` status code. BadGateway = 502; + // ServiceUnavailable - ``503`` status code. ServiceUnavailable = 503; + // GatewayTimeout - ``504`` status code. GatewayTimeout = 504; + // HTTPVersionNotSupported - ``505`` status code. HTTPVersionNotSupported = 505; + // VariantAlsoNegotiates - ``506`` status code. VariantAlsoNegotiates = 506; + // InsufficientStorage - ``507`` status code. InsufficientStorage = 507; + // LoopDetected - ``508`` status code. LoopDetected = 508; + // NotExtended - ``510`` status code. NotExtended = 510; + // NetworkAuthenticationRequired - ``511`` status code. NetworkAuthenticationRequired = 511; } From f299ef5e5832898fa29ede31dee1a6085b90c051 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 15 Jan 2025 14:51:45 -0800 Subject: [PATCH 152/591] compiler: Prepare for C++ protobuf using string_view Protobuf is interested in using absl::string_view instead of const std::string&. Just copy to std::string as the C++17 build isn't yet operational and that level of performance doesn't matter. cl/711732759 b/353571051 --- .../src/java_plugin/cpp/java_generator.cpp | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/compiler/src/java_plugin/cpp/java_generator.cpp b/compiler/src/java_plugin/cpp/java_generator.cpp index df96bb1c1b2..8421a4a9207 100644 --- a/compiler/src/java_plugin/cpp/java_generator.cpp +++ b/compiler/src/java_plugin/cpp/java_generator.cpp @@ -147,7 +147,7 @@ static std::set java_keywords = { // - decapitalize the first letter // - remove embedded underscores & capitalize the following letter // Finally, if the result is a reserved java keyword, append an underscore. -static std::string MixedLower(const std::string& word) { +static std::string MixedLower(std::string word) { std::string w; w += tolower(word[0]); bool after_underscore = false; @@ -169,7 +169,7 @@ static std::string MixedLower(const std::string& word) { // - An underscore is inserted where a lower case letter is followed by an // upper case letter. // - All letters are converted to upper case -static std::string ToAllUpperCase(const std::string& word) { +static std::string ToAllUpperCase(std::string word) { std::string w; for (size_t i = 0; i < word.length(); ++i) { w += toupper(word[i]); @@ -181,19 +181,19 @@ static std::string ToAllUpperCase(const std::string& word) { } static inline std::string LowerMethodName(const MethodDescriptor* method) { - return MixedLower(method->name()); + return MixedLower(std::string(method->name())); } static inline std::string MethodPropertiesFieldName(const MethodDescriptor* method) { - return "METHOD_" + ToAllUpperCase(method->name()); + return "METHOD_" + ToAllUpperCase(std::string(method->name())); } static inline std::string MethodPropertiesGetterName(const MethodDescriptor* method) { - return MixedLower("get_" + method->name() + "_method"); + return MixedLower("get_" + std::string(method->name()) + "_method"); } static inline std::string MethodIdFieldName(const MethodDescriptor* method) { - return "METHODID_" + ToAllUpperCase(method->name()); + return "METHODID_" + ToAllUpperCase(std::string(method->name())); } static inline std::string MessageFullJavaName(const Descriptor* desc) { @@ -406,7 +406,7 @@ static void GrpcWriteServiceDocComment(Printer* printer, StubType type) { printer->Print("/**\n"); - std::map vars = {{"service", service->name()}}; + std::map vars = {{"service", std::string(service->name())}}; switch (type) { case ASYNC_CLIENT_IMPL: printer->Print(vars, " * A stub to allow clients to do asynchronous rpc calls to service $service$.\n"); @@ -520,7 +520,8 @@ static void PrintMethodFields( " .setResponseMarshaller($ProtoUtils$.marshaller(\n" " $output_type$.getDefaultInstance()))\n"); - (*vars)["proto_method_descriptor_supplier"] = service->name() + "MethodDescriptorSupplier"; + (*vars)["proto_method_descriptor_supplier"] + = std::string(service->name()) + "MethodDescriptorSupplier"; if (flavor == ProtoFlavor::NORMAL) { p->Print( *vars, @@ -583,7 +584,7 @@ static void PrintStub( const ServiceDescriptor* service, std::map* vars, Printer* p, StubType type) { - const std::string service_name = service->name(); + std::string service_name = std::string(service->name()); (*vars)["service_name"] = service_name; std::string stub_name = service_name; std::string stub_base_class_name = "AbstractStub"; @@ -887,8 +888,7 @@ static void PrintAbstractClassStub( const ServiceDescriptor* service, std::map* vars, Printer* p) { - const std::string service_name = service->name(); - (*vars)["service_name"] = service_name; + (*vars)["service_name"] = service->name(); GrpcWriteServiceDocComment(p, service, ABSTRACT_CLASS); if (service->options().deprecated()) { @@ -1022,13 +1022,14 @@ static void PrintGetServiceDescriptorMethod(const ServiceDescriptor* service, std::map* vars, Printer* p, ProtoFlavor flavor) { - (*vars)["service_name"] = service->name(); + std::string service_name = std::string(service->name()); + (*vars)["service_name"] = service_name; if (flavor == ProtoFlavor::NORMAL) { - (*vars)["proto_base_descriptor_supplier"] = service->name() + "BaseDescriptorSupplier"; - (*vars)["proto_file_descriptor_supplier"] = service->name() + "FileDescriptorSupplier"; - (*vars)["proto_method_descriptor_supplier"] = service->name() + "MethodDescriptorSupplier"; + (*vars)["proto_base_descriptor_supplier"] = service_name + "BaseDescriptorSupplier"; + (*vars)["proto_file_descriptor_supplier"] = service_name + "FileDescriptorSupplier"; + (*vars)["proto_method_descriptor_supplier"] = service_name + "MethodDescriptorSupplier"; (*vars)["proto_class_name"] = protobuf::compiler::java::ClassName(service->file()); p->Print( *vars, @@ -1374,7 +1375,7 @@ std::string ServiceJavaPackage(const FileDescriptor* file) { } std::string ServiceClassName(const ServiceDescriptor* service) { - return service->name() + "Grpc"; + return std::string(service->name()) + "Grpc"; } } // namespace java_grpc_generator From a0a42fc8e6f3da144052f6a89936ab6e9248a624 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 16 Jan 2025 07:22:23 -0800 Subject: [PATCH 153/591] Update README etc to reference 1.69.1 --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c6a8f3bdd8a..11605e78335 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.69.0/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.69.0/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.69.1/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.69.1/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.69.0 + 1.69.1 runtime io.grpc grpc-protobuf - 1.69.0 + 1.69.1 io.grpc grpc-stub - 1.69.0 + 1.69.1 org.apache.tomcat @@ -79,18 +79,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.69.0' -implementation 'io.grpc:grpc-protobuf:1.69.0' -implementation 'io.grpc:grpc-stub:1.69.0' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.69.1' +implementation 'io.grpc:grpc-protobuf:1.69.1' +implementation 'io.grpc:grpc-stub:1.69.1' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.69.0' -implementation 'io.grpc:grpc-protobuf-lite:1.69.0' -implementation 'io.grpc:grpc-stub:1.69.0' +implementation 'io.grpc:grpc-okhttp:1.69.1' +implementation 'io.grpc:grpc-protobuf-lite:1.69.1' +implementation 'io.grpc:grpc-stub:1.69.1' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` @@ -99,7 +99,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.69.0 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.69.1 Development snapshots are available in [Sonatypes's snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/). @@ -131,7 +131,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.69.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.69.1:exe:${os.detected.classifier} @@ -161,7 +161,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.69.1' } } generateProtoTasks { @@ -194,7 +194,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.69.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.69.1' } } generateProtoTasks { From fc86084df5c8c2ea3080554021aa72cea81960b4 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 17 Jan 2025 16:06:36 -0800 Subject: [PATCH 154/591] xds: Rename grpc.xds.cluster to grpc.lb.backend_service The name is being changed to allow the value to be used in more metrics where xds-specifics are awkward. --- xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java | 2 +- .../test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index 200d3cba0ea..35afb2bfc21 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -385,7 +385,7 @@ private RequestLimitingSubchannelPicker(SubchannelPicker delegate, public PickResult pickSubchannel(PickSubchannelArgs args) { args.getCallOptions().getOption(ClusterImplLoadBalancerProvider.FILTER_METADATA_CONSUMER) .accept(filterMetadata); - args.getPickDetailsConsumer().addOptionalLabel("grpc.xds.cluster", cluster); + args.getPickDetailsConsumer().addOptionalLabel("grpc.lb.backend_service", cluster); for (DropOverload dropOverload : dropPolicies) { int rand = random.nextInt(1_000_000); if (rand < dropOverload.dropsPerMillion()) { diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index 9503442e383..b4507523510 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -298,7 +298,7 @@ public void pick_addsOptionalLabels() { // The value will be determined by the parent policy, so can be different than the value used in // makeAddress() for the test. verify(detailsConsumer).addOptionalLabel("grpc.lb.locality", locality.toString()); - verify(detailsConsumer).addOptionalLabel("grpc.xds.cluster", CLUSTER); + verify(detailsConsumer).addOptionalLabel("grpc.lb.backend_service", CLUSTER); } @Test @@ -322,7 +322,7 @@ public void pick_noResult_addsClusterLabel() { TestMethodDescriptors.voidMethod(), new Metadata(), CallOptions.DEFAULT, detailsConsumer); PickResult result = currentPicker.pickSubchannel(pickSubchannelArgs); assertThat(result.getStatus().isOk()).isTrue(); - verify(detailsConsumer).addOptionalLabel("grpc.xds.cluster", CLUSTER); + verify(detailsConsumer).addOptionalLabel("grpc.lb.backend_service", CLUSTER); } @Test From 495a8906b297650fe823b5711993a0e73077c335 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 22 Jan 2025 12:10:56 -0800 Subject: [PATCH 155/591] xds: Fix fallback test FakeClock TSAN failure d65d3942e increased the test speed of connect_then_mainServerDown_fallbackServerUp by using FakeClock. However, it introduced a data race because FakeClock is not thread-safe. This change injects a single thread for gRPC callbacks such that syncContext is run on a thread under the test's control. A simpler approach would be to expose syncContext from XdsClientImpl for testing. However, this test is in a different package and I wanted to avoid adding a public method. ``` Read of size 8 at 0x00008dec9d50 by thread T25: #0 io.grpc.internal.FakeClock$ScheduledExecutorImpl.schedule(Lio/grpc/internal/FakeClock$ScheduledTask;JLjava/util/concurrent/TimeUnit;)V FakeClock.java:140 #1 io.grpc.internal.FakeClock$ScheduledExecutorImpl.schedule(Ljava/lang/Runnable;JLjava/util/concurrent/TimeUnit;)Ljava/util/concurrent/ScheduledFuture; FakeClock.java:150 #2 io.grpc.SynchronizationContext.schedule(Ljava/lang/Runnable;JLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/ScheduledExecutorService;)Lio/grpc/SynchronizationContext$ScheduledHandle; SynchronizationContext.java:153 #3 io.grpc.xds.client.ControlPlaneClient$AdsStream.handleRpcStreamClosed(Lio/grpc/Status;)V ControlPlaneClient.java:491 #4 io.grpc.xds.client.ControlPlaneClient$AdsStream.lambda$onStatusReceived$0(Lio/grpc/Status;)V ControlPlaneClient.java:429 #5 io.grpc.xds.client.ControlPlaneClient$AdsStream$$Lambda+0x00000001004a95d0.run()V ?? #6 io.grpc.SynchronizationContext.drain()V SynchronizationContext.java:96 #7 io.grpc.SynchronizationContext.execute(Ljava/lang/Runnable;)V SynchronizationContext.java:128 #8 io.grpc.xds.client.ControlPlaneClient$AdsStream.onStatusReceived(Lio/grpc/Status;)V ControlPlaneClient.java:428 #9 io.grpc.xds.GrpcXdsTransportFactory$EventHandlerToCallListenerAdapter.onClose(Lio/grpc/Status;Lio/grpc/Metadata;)V GrpcXdsTransportFactory.java:149 #10 io.grpc.PartialForwardingClientCallListener.onClose(Lio/grpc/Status;Lio/grpc/Metadata;)V PartialForwardingClientCallListener.java:39 ... Previous write of size 8 at 0x00008dec9d50 by thread T4 (mutexes: write M0, write M1, write M2, write M3): #0 io.grpc.internal.FakeClock.forwardTime(JLjava/util/concurrent/TimeUnit;)I FakeClock.java:368 #1 io.grpc.xds.XdsClientFallbackTest.connect_then_mainServerDown_fallbackServerUp()V XdsClientFallbackTest.java:358 ... ``` --- .../io/grpc/xds/XdsClientFallbackTest.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java index 94b49bd94b2..7cf6280711f 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java @@ -31,6 +31,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import io.grpc.ChannelCredentials; +import io.grpc.Grpc; import io.grpc.MetricRecorder; import io.grpc.Status; import io.grpc.internal.ExponentialBackoffPolicy; @@ -43,11 +45,14 @@ import io.grpc.xds.client.XdsClientImpl; import io.grpc.xds.client.XdsClientMetricReporter; import io.grpc.xds.client.XdsInitializationException; +import io.grpc.xds.client.XdsTransportFactory; import java.net.InetSocketAddress; import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -338,9 +343,21 @@ private static void verifyNoSubscribers(ControlPlaneRule rule) { public void connect_then_mainServerDown_fallbackServerUp() throws Exception { mainXdsServer.restartXdsServer(); fallbackServer.restartXdsServer(); + ExecutorService executor = Executors.newFixedThreadPool(1); + XdsTransportFactory xdsTransportFactory = new XdsTransportFactory() { + @Override + public XdsTransport create(Bootstrapper.ServerInfo serverInfo) { + ChannelCredentials channelCredentials = + (ChannelCredentials) serverInfo.implSpecificConfig(); + return new GrpcXdsTransportFactory.GrpcXdsTransport( + Grpc.newChannelBuilder(serverInfo.target(), channelCredentials) + .executor(executor) + .build()); + } + }; XdsClientImpl xdsClient = CommonBootstrapperTestUtils.createXdsClient( new GrpcBootstrapperImpl().bootstrap(defaultBootstrapOverride()), - DEFAULT_XDS_TRANSPORT_FACTORY, fakeClock, new ExponentialBackoffPolicy.Provider(), + xdsTransportFactory, fakeClock, new ExponentialBackoffPolicy.Provider(), MessagePrinter.INSTANCE, xdsClientMetricReporter); xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); @@ -355,7 +372,8 @@ public void connect_then_mainServerDown_fallbackServerUp() throws Exception { // Sleep for the ADS stream disconnect to be processed and for the retry to fail. Between those // two sleeps we need the fakeClock to progress by 1 second to restart the ADS stream. for (int i = 0; i < 5; i++) { - fakeClock.forwardTime(1000, TimeUnit.MILLISECONDS); + // FakeClock is not thread-safe, and the retry scheduling is concurrent to this test thread + executor.submit(() -> fakeClock.forwardTime(1000, TimeUnit.MILLISECONDS)).get(); TimeUnit.SECONDS.sleep(1); } @@ -393,6 +411,7 @@ public void connect_then_mainServerDown_fallbackServerUp() throws Exception { fakeClock.forwardTime(15000, TimeUnit.MILLISECONDS); // Does not exist timer verify(cdsWatcher2, timeout(5000)).onResourceDoesNotExist(eq(CLUSTER_NAME)); xdsClient.shutdown(); + executor.shutdown(); } @Test From 0f5503ebb11ef36dae2d5c75c62c4c1f1b43415d Mon Sep 17 00:00:00 2001 From: Kannan J Date: Thu, 23 Jan 2025 16:10:21 +0000 Subject: [PATCH 156/591] =?UTF-8?q?xds:=20Include=20max=20concurrent=20req?= =?UTF-8?q?uest=20limit=20in=20the=20error=20status=20for=20concurre?= =?UTF-8?q?=E2=80=A6=20(#11845)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Include max concurrent request limit in the error status for concurrent connections limit exceeded --- xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java | 4 +++- .../test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index 35afb2bfc21..fd4f49fbb83 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -59,6 +59,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; @@ -406,7 +407,8 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { dropStats.recordDroppedRequest(); } return PickResult.withDrop(Status.UNAVAILABLE.withDescription( - "Cluster max concurrent requests limit exceeded")); + String.format(Locale.US, "Cluster max concurrent requests limit of %d exceeded", + maxConcurrentRequests))); } } final AtomicReference clusterLocality = diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index b4507523510..09a1abb36e0 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -636,7 +636,7 @@ private void subtest_maxConcurrentRequests_appliedByLbConfig(boolean enableCircu assertThat(result.getStatus().isOk()).isFalse(); assertThat(result.getStatus().getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(result.getStatus().getDescription()) - .isEqualTo("Cluster max concurrent requests limit exceeded"); + .isEqualTo("Cluster max concurrent requests limit of 100 exceeded"); assertThat(clusterStats.totalDroppedRequests()).isEqualTo(1L); } else { assertThat(result.getStatus().isOk()).isTrue(); @@ -667,7 +667,7 @@ private void subtest_maxConcurrentRequests_appliedByLbConfig(boolean enableCircu assertThat(result.getStatus().isOk()).isFalse(); assertThat(result.getStatus().getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(result.getStatus().getDescription()) - .isEqualTo("Cluster max concurrent requests limit exceeded"); + .isEqualTo("Cluster max concurrent requests limit of 101 exceeded"); assertThat(clusterStats.totalDroppedRequests()).isEqualTo(1L); } else { assertThat(result.getStatus().isOk()).isTrue(); @@ -731,7 +731,7 @@ private void subtest_maxConcurrentRequests_appliedWithDefaultValue( assertThat(result.getStatus().isOk()).isFalse(); assertThat(result.getStatus().getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(result.getStatus().getDescription()) - .isEqualTo("Cluster max concurrent requests limit exceeded"); + .isEqualTo("Cluster max concurrent requests limit of 1024 exceeded"); assertThat(clusterStats.totalDroppedRequests()).isEqualTo(1L); } else { assertThat(result.getStatus().isOk()).isTrue(); From bf8eb24a30e65f85fa090f0d9b52cc095f0d7644 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Fri, 24 Jan 2025 15:21:14 +0530 Subject: [PATCH 157/591] Update README etc to reference 1.70.0 (#11854) --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 11605e78335..c9f68d4ccb9 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.69.1/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.69.1/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.70.0/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.70.0/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.69.1 + 1.70.0 runtime io.grpc grpc-protobuf - 1.69.1 + 1.70.0 io.grpc grpc-stub - 1.69.1 + 1.70.0 org.apache.tomcat @@ -79,18 +79,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.69.1' -implementation 'io.grpc:grpc-protobuf:1.69.1' -implementation 'io.grpc:grpc-stub:1.69.1' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.70.0' +implementation 'io.grpc:grpc-protobuf:1.70.0' +implementation 'io.grpc:grpc-stub:1.70.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.69.1' -implementation 'io.grpc:grpc-protobuf-lite:1.69.1' -implementation 'io.grpc:grpc-stub:1.69.1' +implementation 'io.grpc:grpc-okhttp:1.70.0' +implementation 'io.grpc:grpc-protobuf-lite:1.70.0' +implementation 'io.grpc:grpc-stub:1.70.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` @@ -99,7 +99,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.69.1 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.70.0 Development snapshots are available in [Sonatypes's snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/). @@ -131,7 +131,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.69.1:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.70.0:exe:${os.detected.classifier} @@ -161,7 +161,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.69.1' + artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0' } } generateProtoTasks { @@ -194,7 +194,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.69.1' + artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0' } } generateProtoTasks { From 67351c0c53b1f361b7b15d2b49c9b7e876a85a95 Mon Sep 17 00:00:00 2001 From: Boddu Harshavardhan Date: Fri, 24 Jan 2025 16:50:41 +0530 Subject: [PATCH 158/591] gcp-observability: Optimize GcpObservabilityTest.enableObservability execution time (#11783) --- .../io/grpc/gcp/observability/GcpObservability.java | 12 ++++++++++-- .../grpc/gcp/observability/GcpObservabilityTest.java | 9 ++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/gcp-observability/src/main/java/io/grpc/gcp/observability/GcpObservability.java b/gcp-observability/src/main/java/io/grpc/gcp/observability/GcpObservability.java index 497a1eda30f..7fe4e3a8a3c 100644 --- a/gcp-observability/src/main/java/io/grpc/gcp/observability/GcpObservability.java +++ b/gcp-observability/src/main/java/io/grpc/gcp/observability/GcpObservability.java @@ -127,6 +127,15 @@ static GcpObservability grpcInit( /** Un-initialize/shutdown grpc-observability. */ @Override public void close() { + closeWithSleepTime(2 * METRICS_EXPORT_INTERVAL, TimeUnit.SECONDS); + } + + /** + * Method to close along with sleep time explicitly. + * + * @param sleepTime sleepTime + */ + void closeWithSleepTime(long sleepTime, TimeUnit timeUnit) { synchronized (GcpObservability.class) { if (instance == null) { throw new IllegalStateException("GcpObservability already closed!"); @@ -135,8 +144,7 @@ public void close() { if (config.isEnableCloudMonitoring() || config.isEnableCloudTracing()) { try { // Sleeping before shutdown to ensure all metrics and traces are flushed - Thread.sleep( - TimeUnit.MILLISECONDS.convert(2 * METRICS_EXPORT_INTERVAL, TimeUnit.SECONDS)); + timeUnit.sleep(sleepTime); } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.log(Level.SEVERE, "Caught exception during sleep", e); diff --git a/gcp-observability/src/test/java/io/grpc/gcp/observability/GcpObservabilityTest.java b/gcp-observability/src/test/java/io/grpc/gcp/observability/GcpObservabilityTest.java index 40f2fb01490..25467839dd6 100644 --- a/gcp-observability/src/test/java/io/grpc/gcp/observability/GcpObservabilityTest.java +++ b/gcp-observability/src/test/java/io/grpc/gcp/observability/GcpObservabilityTest.java @@ -45,6 +45,7 @@ import io.opencensus.trace.samplers.Samplers; import java.io.IOException; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import org.junit.Test; import org.junit.runner.RunWith; @@ -196,9 +197,9 @@ public void run() { mock(InternalLoggingServerInterceptor.Factory.class); when(serverInterceptorFactory.create()).thenReturn(serverInterceptor); - try (GcpObservability unused = - GcpObservability.grpcInit( - sink, config, channelInterceptorFactory, serverInterceptorFactory)) { + try { + GcpObservability gcpObservability = GcpObservability.grpcInit( + sink, config, channelInterceptorFactory, serverInterceptorFactory); List configurators = InternalConfiguratorRegistry.getConfigurators(); assertThat(configurators).hasSize(1); ObservabilityConfigurator configurator = (ObservabilityConfigurator) configurators.get(0); @@ -208,9 +209,11 @@ public void run() { assertThat(list.get(2)).isInstanceOf(ConditionalClientInterceptor.class); assertThat(configurator.serverInterceptors).hasSize(1); assertThat(configurator.tracerFactories).hasSize(2); + gcpObservability.closeWithSleepTime(3000, TimeUnit.MILLISECONDS); } catch (Exception e) { fail("Encountered exception: " + e); } + verify(sink).close(); } } From 87aa6deadfb7d48847ddddcbfbdbb5ca5ee9c459 Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Fri, 24 Jan 2025 16:42:56 -0800 Subject: [PATCH 159/591] core:Have acceptResolvedAddresses() do a seek when in CONNECTING state and cleanup removed subchannels when a seek was successful (#11849) * Have acceptResolvedAddresses() do a seek when in CONNECTING state and cleanup removed subchannels when a seek was successful. Move cleanup of removed subchannels into a method so it can be called from 2 places in acceptResolvedAddresses. Since the seek could mean we never looked at the first address, if we go off the end of the index and haven't looked at the all of the addresses then instead of scheduleBackoff() we reset the index and request a connection. --- .../internal/PickFirstLeafLoadBalancer.java | 59 +++++++++++++------ .../PickFirstLeafLoadBalancerTest.java | 14 +++-- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java index 042f9e63630..bbc144ea775 100644 --- a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java +++ b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java @@ -137,14 +137,15 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { final ImmutableList newImmutableAddressGroups = ImmutableList.builder().addAll(cleanServers).build(); - if (rawConnectivityState == READY) { - // If the previous ready subchannel exists in new address list, + if (rawConnectivityState == READY || rawConnectivityState == CONNECTING) { + // If the previous ready (or connecting) subchannel exists in new address list, // keep this connection and don't create new subchannels SocketAddress previousAddress = addressIndex.getCurrentAddress(); addressIndex.updateGroups(newImmutableAddressGroups); if (addressIndex.seekTo(previousAddress)) { SubchannelData subchannelData = subchannels.get(previousAddress); subchannelData.getSubchannel().updateAddresses(addressIndex.getCurrentEagAsList()); + shutdownRemovedAddresses(newImmutableAddressGroups); return Status.OK; } // Previous ready subchannel not in the new list of addresses @@ -152,23 +153,11 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { addressIndex.updateGroups(newImmutableAddressGroups); } - // remove old subchannels that were not in new address list - Set oldAddrs = new HashSet<>(subchannels.keySet()); - - // Flatten the new EAGs addresses - Set newAddrs = new HashSet<>(); - for (EquivalentAddressGroup endpoint : newImmutableAddressGroups) { - newAddrs.addAll(endpoint.getAddresses()); - } - - // Shut them down and remove them - for (SocketAddress oldAddr : oldAddrs) { - if (!newAddrs.contains(oldAddr)) { - subchannels.remove(oldAddr).getSubchannel().shutdown(); - } - } + // No old addresses means first time through, so we will do an explicit move to CONNECTING + // which is what we implicitly started with + boolean noOldAddrs = shutdownRemovedAddresses(newImmutableAddressGroups); - if (oldAddrs.size() == 0) { + if (noOldAddrs) { // Make tests happy; they don't properly assume starting in CONNECTING rawConnectivityState = CONNECTING; updateBalancingState(CONNECTING, new Picker(PickResult.withNoResult())); @@ -188,6 +177,31 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { return Status.OK; } + /** + * Compute the difference between the flattened new addresses and the old addresses that had been + * made into subchannels and then shutdown the matching subchannels. + * @return true if there were no old addresses + */ + private boolean shutdownRemovedAddresses( + ImmutableList newImmutableAddressGroups) { + + Set oldAddrs = new HashSet<>(subchannels.keySet()); + + // Flatten the new EAGs addresses + Set newAddrs = new HashSet<>(); + for (EquivalentAddressGroup endpoint : newImmutableAddressGroups) { + newAddrs.addAll(endpoint.getAddresses()); + } + + // Shut them down and remove them + for (SocketAddress oldAddr : oldAddrs) { + if (!newAddrs.contains(oldAddr)) { + subchannels.remove(oldAddr).getSubchannel().shutdown(); + } + } + return oldAddrs.isEmpty(); + } + private static List deDupAddresses(List groups) { Set seenAddresses = new HashSet<>(); List newGroups = new ArrayList<>(); @@ -290,7 +304,14 @@ void processSubchannelState(SubchannelData subchannelData, ConnectivityStateInfo cancelScheduleTask(); requestConnection(); // is recursive so might hit the end of the addresses } else { - scheduleBackoff(); + if (subchannels.size() >= addressIndex.size()) { + scheduleBackoff(); + } else { + // We must have done a seek to the middle of the list lets start over from the + // beginning + addressIndex.reset(); + requestConnection(); + } } } diff --git a/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java b/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java index 61bcb5c05ab..0e902bfdd56 100644 --- a/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java @@ -2133,18 +2133,20 @@ public void lastAddressFailingNotTransientFailure() { loadBalancer.acceptResolvedAddresses( ResolvedAddresses.newBuilder().setAddresses(newServers).setAttributes(affinity).build()); - // Verify that no new subchannels were created or started + // Subchannel 2 should be reused since it was trying to connect and is present. inOrder.verify(mockSubchannel1).shutdown(); - inOrder.verify(mockSubchannel3).start(stateListenerCaptor.capture()); - SubchannelStateListener stateListener3 = stateListenerCaptor.getValue(); - inOrder.verify(mockSubchannel3).requestConnection(); + inOrder.verify(mockSubchannel3, never()).start(stateListenerCaptor.capture()); assertEquals(CONNECTING, loadBalancer.getConcludedConnectivityState()); - // Second address connection attempt is unsuccessful, but should not go into transient failure + // Second address connection attempt is unsuccessful, so since at end, but don't have all + // subchannels, schedule a backoff for the first address stateListener2.onSubchannelState(ConnectivityStateInfo.forTransientFailure(CONNECTION_ERROR)); + fakeClock.forwardTime(1, TimeUnit.SECONDS); + inOrder.verify(mockSubchannel3).start(stateListenerCaptor.capture()); + SubchannelStateListener stateListener3 = stateListenerCaptor.getValue(); assertEquals(CONNECTING, loadBalancer.getConcludedConnectivityState()); - // Third address connection attempt is unsuccessful, now we enter transient failure + // Third address connection attempt is unsuccessful, now we enter TF, do name resolution stateListener3.onSubchannelState(ConnectivityStateInfo.forTransientFailure(CONNECTION_ERROR)); assertEquals(TRANSIENT_FAILURE, loadBalancer.getConcludedConnectivityState()); From 9e8629914fd12e08604ad8a6ae78853118acabe2 Mon Sep 17 00:00:00 2001 From: vinodhabib <47808007+vinodhabib@users.noreply.github.com> Date: Tue, 28 Jan 2025 07:32:00 +0000 Subject: [PATCH 160/591] examples: Added README files for all missing Examples (#11676) --- examples/README.md | 126 +++--------------- .../example-alts/{example-alts => }/README.md | 0 .../java/io/grpc/examples/advanced/README.md | 16 +++ .../io/grpc/examples/cancellation/README.md | 18 +++ .../grpc/examples/customloadbalance/README.md | 19 +++ .../java/io/grpc/examples/deadline/README.md | 15 +++ .../io/grpc/examples/errordetails/README.md | 16 +++ .../io/grpc/examples/errorhandling/README.md | 27 ++++ .../io/grpc/examples/experimental/README.md | 13 ++ .../java/io/grpc/examples/grpcproxy/README.md | 22 +++ .../java/io/grpc/examples/header/README.md | 16 +++ .../io/grpc/examples/healthservice/README.md | 10 ++ .../java/io/grpc/examples/hedging/README.md | 59 ++++++++ .../io/grpc/examples/helloworld/README.md | 7 + .../java/io/grpc/examples/keepalive/README.md | 16 +++ .../io/grpc/examples/loadbalance/README.md | 20 +++ .../java/io/grpc/examples/multiplex/README.md | 20 +++ .../io/grpc/examples/nameresolve/README.md | 22 +++ .../io/grpc/examples/preserialized/README.md | 18 +++ .../java/io/grpc/examples/retrying/README.md | 27 ++++ .../io/grpc/examples/routeguide/README.md | 24 ++++ .../io/grpc/examples/waitforready/README.md | 29 ++++ 22 files changed, 436 insertions(+), 104 deletions(-) rename examples/example-alts/{example-alts => }/README.md (100%) create mode 100644 examples/src/main/java/io/grpc/examples/advanced/README.md create mode 100644 examples/src/main/java/io/grpc/examples/cancellation/README.md create mode 100644 examples/src/main/java/io/grpc/examples/customloadbalance/README.md create mode 100644 examples/src/main/java/io/grpc/examples/deadline/README.md create mode 100644 examples/src/main/java/io/grpc/examples/errordetails/README.md create mode 100644 examples/src/main/java/io/grpc/examples/errorhandling/README.md create mode 100644 examples/src/main/java/io/grpc/examples/experimental/README.md create mode 100644 examples/src/main/java/io/grpc/examples/grpcproxy/README.md create mode 100644 examples/src/main/java/io/grpc/examples/header/README.md create mode 100644 examples/src/main/java/io/grpc/examples/healthservice/README.md create mode 100644 examples/src/main/java/io/grpc/examples/hedging/README.md create mode 100644 examples/src/main/java/io/grpc/examples/helloworld/README.md create mode 100644 examples/src/main/java/io/grpc/examples/keepalive/README.md create mode 100644 examples/src/main/java/io/grpc/examples/loadbalance/README.md create mode 100644 examples/src/main/java/io/grpc/examples/multiplex/README.md create mode 100644 examples/src/main/java/io/grpc/examples/nameresolve/README.md create mode 100644 examples/src/main/java/io/grpc/examples/preserialized/README.md create mode 100644 examples/src/main/java/io/grpc/examples/retrying/README.md create mode 100644 examples/src/main/java/io/grpc/examples/routeguide/README.md create mode 100644 examples/src/main/java/io/grpc/examples/waitforready/README.md diff --git a/examples/README.md b/examples/README.md index b51d560d7bb..a07d1b38a14 100644 --- a/examples/README.md +++ b/examples/README.md @@ -27,114 +27,32 @@ before trying out the examples. - [Json serialization](src/main/java/io/grpc/examples/advanced) --
- Hedging - - The [hedging example](src/main/java/io/grpc/examples/hedging) demonstrates that enabling hedging - can reduce tail latency. (Users should note that enabling hedging may introduce other overhead; - and in some scenarios, such as when some server resource gets exhausted for a period of time and - almost every RPC during that time has high latency or fails, hedging may make things worse. - Setting a throttle in the service config is recommended to protect the server from too many - inappropriate retry or hedging requests.) - - The server and the client in the example are basically the same as those in the - [hello world](src/main/java/io/grpc/examples/helloworld) example, except that the server mimics a - long tail of latency, and the client sends 2000 requests and can turn on and off hedging. - - To mimic the latency, the server randomly delays the RPC handling by 2 seconds at 10% chance, 5 - seconds at 5% chance, and 10 seconds at 1% chance. - - When running the client enabling the following hedging policy - - ```json - "hedgingPolicy": { - "maxAttempts": 3, - "hedgingDelay": "1s" - } - ``` - Then the latency summary in the client log is like the following - - ```text - Total RPCs sent: 2,000. Total RPCs failed: 0 - [Hedging enabled] - ======================== - 50% latency: 0ms - 90% latency: 6ms - 95% latency: 1,003ms - 99% latency: 2,002ms - 99.9% latency: 2,011ms - Max latency: 5,272ms - ======================== - ``` - - See [the section below](#to-build-the-examples) for how to build and run the example. The - executables for the server and the client are `hedging-hello-world-server` and - `hedging-hello-world-client`. - - To disable hedging, set environment variable `DISABLE_HEDGING_IN_HEDGING_EXAMPLE=true` before - running the client. That produces a latency summary in the client log like the following - - ```text - Total RPCs sent: 2,000. Total RPCs failed: 0 - [Hedging disabled] - ======================== - 50% latency: 0ms - 90% latency: 2,002ms - 95% latency: 5,002ms - 99% latency: 10,004ms - 99.9% latency: 10,007ms - Max latency: 10,007ms - ======================== - ``` - -
- --
- Retrying - - The [retrying example](src/main/java/io/grpc/examples/retrying) provides a HelloWorld gRPC client & - server which demos the effect of client retry policy configured on the [ManagedChannel]( - ../api/src/main/java/io/grpc/ManagedChannel.java) via [gRPC ServiceConfig]( - https://github.com/grpc/grpc/blob/master/doc/service_config.md). Retry policy implementation & - configuration details are outlined in the [proposal](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). - - This retrying example is very similar to the [hedging example](src/main/java/io/grpc/examples/hedging) in its setup. - The [RetryingHelloWorldServer](src/main/java/io/grpc/examples/retrying/RetryingHelloWorldServer.java) responds with - a status UNAVAILABLE error response to a specified percentage of requests to simulate server resource exhaustion and - general flakiness. The [RetryingHelloWorldClient](src/main/java/io/grpc/examples/retrying/RetryingHelloWorldClient.java) makes - a number of sequential requests to the server, several of which will be retried depending on the configured policy in - [retrying_service_config.json](src/main/resources/io/grpc/examples/retrying/retrying_service_config.json). Although - the requests are blocking unary calls for simplicity, these could easily be changed to future unary calls in order to - test the result of request concurrency with retry policy enabled. - - One can experiment with the [RetryingHelloWorldServer](src/main/java/io/grpc/examples/retrying/RetryingHelloWorldServer.java) - failure conditions to simulate server throttling, as well as alter policy values in the [retrying_service_config.json]( - src/main/resources/io/grpc/examples/retrying/retrying_service_config.json) to see their effects. To disable retrying - entirely, set environment variable `DISABLE_RETRYING_IN_RETRYING_EXAMPLE=true` before running the client. - Disabling the retry policy should produce many more failed gRPC calls as seen in the output log. - - See [the section below](#to-build-the-examples) for how to build and run the example. The - executables for the server and the client are `retrying-hello-world-server` and - `retrying-hello-world-client`. - -
- --
- Health Service - - The [health service example](src/main/java/io/grpc/examples/healthservice) - provides a HelloWorld gRPC server that doesn't like short names along with a - health service. It also provides a client application which makes HelloWorld - calls and checks the health status. - - The client application also shows how the round robin load balancer can - utilize the health status to avoid making calls to a service that is - not actively serving. -
+- [Hedging example](src/main/java/io/grpc/examples/hedging) +- [Retrying example](src/main/java/io/grpc/examples/retrying) + +- [Health Service example](src/main/java/io/grpc/examples/healthservice) - [Keep Alive](src/main/java/io/grpc/examples/keepalive) +- [Cancellation](src/main/java/io/grpc/examples/cancellation) + +- [Custom Load Balance](src/main/java/io/grpc/examples/customloadbalance) + +- [Deadline](src/main/java/io/grpc/examples/deadline) + +- [Error Details](src/main/java/io/grpc/examples/errordetails) + +- [GRPC Proxy](src/main/java/io/grpc/examples/grpcproxy) + +- [Load Balance](src/main/java/io/grpc/examples/loadbalance) + +- [Multiplex](src/main/java/io/grpc/examples/multiplex) + +- [Name Resolve](src/main/java/io/grpc/examples/nameresolve) + +- [Pre-Serialized Messages](src/main/java/io/grpc/examples/preserialized) + ### To build the examples 1. **[Install gRPC Java library SNAPSHOT locally, including code generation plugin](../COMPILING.md) (Only need this step for non-released versions, e.g. master HEAD).** diff --git a/examples/example-alts/example-alts/README.md b/examples/example-alts/README.md similarity index 100% rename from examples/example-alts/example-alts/README.md rename to examples/example-alts/README.md diff --git a/examples/src/main/java/io/grpc/examples/advanced/README.md b/examples/src/main/java/io/grpc/examples/advanced/README.md new file mode 100644 index 00000000000..f5b5c6cc7fc --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/advanced/README.md @@ -0,0 +1,16 @@ +gRPC JSON Serialization Example +===================== + +gRPC is a modern high-performance framework for building Remote Procedure Call (RPC) systems. +It commonly uses Protocol Buffers (Protobuf) as its serialization format, which is compact and efficient. +However, gRPC can also support JSON serialization when needed, typically for interoperability with +systems or clients that do not use Protobuf. +This is an advanced example of how to swap out the serialization logic, Normal users do not need to do this. +This code is not intended to be a production-ready implementation, since JSON encoding is slow. +Additionally, JSON serialization as implemented may be not resilient to malicious input. + +This advanced example uses Marshaller for JSON which marshals in the Protobuf 3 format described here +https://developers.google.com/protocol-buffers/docs/proto3#json + +If you are considering implementing your own serialization logic, contact the grpc team at +https://groups.google.com/forum/#!forum/grpc-io diff --git a/examples/src/main/java/io/grpc/examples/cancellation/README.md b/examples/src/main/java/io/grpc/examples/cancellation/README.md new file mode 100644 index 00000000000..6b11a17c517 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/cancellation/README.md @@ -0,0 +1,18 @@ +gRPC Cancellation Example +===================== + +When a gRPC client is no longer interested in the result of an RPC call, +it may cancel to signal this discontinuation of interest to the server. + +Any abort of an ongoing RPC is considered "cancellation" of that RPC. +The common causes of cancellation are the client explicitly cancelling, the deadline expires, and I/O failures. +The service is not informed the reason for the cancellation. + +There are two APIs for services to be notified of RPC cancellation: io.grpc.Context and ServerCallStreamObserver + +Context listeners are called on a different thread, so need to be thread-safe. +The ServerCallStreamObserver cancellation callback is called like other StreamObserver callbacks, +so the application may not need thread-safe handling. +Both APIs have thread-safe isCancelled() polling methods. + +Refer the gRPC documentation for details on Cancellation of RPCs https://grpc.io/docs/guides/cancellation/ diff --git a/examples/src/main/java/io/grpc/examples/customloadbalance/README.md b/examples/src/main/java/io/grpc/examples/customloadbalance/README.md new file mode 100644 index 00000000000..20dbccb81ac --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/customloadbalance/README.md @@ -0,0 +1,19 @@ +gRPC Custom Load Balance Example +===================== + +One of the key features of gRPC is load balancing, which allows requests from clients to be distributed across multiple servers. +This helps prevent any one server from becoming overloaded and allows the system to scale up by adding more servers. + +A gRPC load balancing policy is given a list of server IP addresses by the name resolver. +The policy is responsible for maintaining connections (subchannels) to the servers and picking a connection to use when an RPC is sent. + +This example gives the details about how we can implement our own custom load balance policy, If the built-in policies does not meet your requirements +and follow below steps for the same. + + - Register your implementation in the load balancer registry so that it can be referred to from the service config + - Parse the JSON configuration object of your implementation. This allows your load balancer to be configured in the service config with any arbitrary JSON you choose to support + - Manage what backends to maintain a connection with + - Implement a picker that will choose which backend to connect to when an RPC is made. Note that this needs to be a fast operation as it is on the RPC call path + - To enable your load balancer, configure it in your service config + +Refer the gRPC documentation for more details https://grpc.io/docs/guides/custom-load-balancing/ diff --git a/examples/src/main/java/io/grpc/examples/deadline/README.md b/examples/src/main/java/io/grpc/examples/deadline/README.md new file mode 100644 index 00000000000..3c7646f1e5f --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/deadline/README.md @@ -0,0 +1,15 @@ +gRPC Deadline Example +===================== + +A Deadline is used to specify a point in time past which a client is unwilling to wait for a response from a server. +This simple idea is very important in building robust distributed systems. +Clients that do not wait around unnecessarily and servers that know when to give up processing requests will improve the resource utilization and latency of your system. + +Note that while some language APIs have the concept of a deadline, others use the idea of a timeout. +When an API asks for a deadline, you provide a point in time which the call should not go past. +A timeout is the max duration of time that the call can take. +A timeout can be converted to a deadline by adding the timeout to the current time when the application starts a call. + +This Example gives usage and implementation of Deadline on Server, Client and Propagation. + +Refer the gRPC documentation for more details on Deadlines https://grpc.io/docs/guides/deadlines/ \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/errordetails/README.md b/examples/src/main/java/io/grpc/examples/errordetails/README.md new file mode 100644 index 00000000000..8f241ba37a7 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/errordetails/README.md @@ -0,0 +1,16 @@ +gRPC Error Details Example +===================== + +If a gRPC call completes successfully the server returns an OK status to the client (depending on the language the OK status may or may not be directly used in your code). +But what happens if the call isn’t successful? + +This Example gives the usage and implementation of how return the error details if gRPC call not successful or fails +and how to set and read com.google.rpc.Status objects as google.rpc.Status error details. + +gRPC allows detailed error information to be encapsulated in protobuf messages, which are sent alongside the status codes. + +If an error occurs, gRPC returns one of its error status codes with error message that provides further error details about what happened. + +Refer the below links for more details on error details and status codes +- https://grpc.io/docs/guides/error/ +- https://github.com/grpc/grpc-java/blob/master/api/src/main/java/io/grpc/Status.java \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/errorhandling/README.md b/examples/src/main/java/io/grpc/examples/errorhandling/README.md new file mode 100644 index 00000000000..a920e939c86 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/errorhandling/README.md @@ -0,0 +1,27 @@ +gRPC Error Handling Example +===================== + +Error handling in gRPC is a critical aspect of designing reliable and robust distributed systems. +gRPC provides a standardized mechanism for handling errors using status codes, error details, and optional metadata. + +This Example gives the usage and implementation of how to handle the Errors/Exceptions in gRPC, +shows how to extract error information from a failed RPC and setting and reading RPC error details. + +If a gRPC call completes successfully the server returns an OK status to the client (depending on the language the OK status may or may not be directly used in your code). + +If an error occurs gRPC returns one of its error status codes with error message that provides further error details about what happened. + +Error Propagation: +- When an error occurs on the server, gRPC stops processing the RPC and sends the error (status code, description, and optional details) to the client. +- On the client side, the error can be handled based on the status code. + +Client Side Error Handling: + - The gRPC client typically throws an exception or returns an error object when an RPC fails. + +Server Side Error Handling: +- Servers use the gRPC API to return errors explicitly using the grpc library's status functions. + +gRPC uses predefined status codes to represent the outcome of an RPC call. These status codes are part of the Status object that is sent from the server to the client. +Each status code is accompanied by a human-readable description(Please refer https://github.com/grpc/grpc-java/blob/master/api/src/main/java/io/grpc/Status.java) + +Refer the gRPC documentation for more details on Error Handling https://grpc.io/docs/guides/error/ \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/experimental/README.md b/examples/src/main/java/io/grpc/examples/experimental/README.md new file mode 100644 index 00000000000..295b0801538 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/experimental/README.md @@ -0,0 +1,13 @@ +gRPC Compression Example +===================== + +This example shows how clients can specify compression options when performing RPCs, +and how to enable compressed(i,e gzip) requests/responses for only particular method and in case of all methods by using the interceptors. + +Compression is used to reduce the amount of bandwidth used when communicating between client/server or peers and +can be enabled or disabled based on call or message level for all languages. + +gRPC allows asymmetrically compressed communication, whereby a response may be compressed differently with the request, +or not compressed at all. + +Refer the gRPC documentation for more details on Compression https://grpc.io/docs/guides/compression/ \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/grpcproxy/README.md b/examples/src/main/java/io/grpc/examples/grpcproxy/README.md new file mode 100644 index 00000000000..cc13dc3d9d0 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/grpcproxy/README.md @@ -0,0 +1,22 @@ +gRPC Proxy Example +===================== + +A gRPC proxy is a component or tool that acts as an intermediary between gRPC clients and servers, +facilitating communication while offering additional capabilities. +Proxies are used in scenarios where you need to handle tasks like load balancing, routing, monitoring, +or providing a bridge between gRPC and other protocols. + +GrpcProxy itself can be used unmodified to proxy any service for both unary and streaming. +It doesn't care what type of messages are being used. +The Registry class causes it to be called for any inbound RPC, and uses plain bytes for messages which avoids marshalling +messages and the need for Protobuf schema information. + +We can run the Grpc Proxy with Route guide example to see how it works by running the below + +Route guide has unary and streaming RPCs which makes it a nice showcase, and we can run each in a separate terminal window. + +./build/install/examples/bin/route-guide-server +./build/install/examples/bin/grpc-proxy +./build/install/examples/bin/route-guide-client localhost:8981 + +you can verify the proxy is being used by shutting down the proxy and seeing the client fail. \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/header/README.md b/examples/src/main/java/io/grpc/examples/header/README.md new file mode 100644 index 00000000000..1563a2799cc --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/header/README.md @@ -0,0 +1,16 @@ +gRPC Custom Header Example +===================== + +This example gives the usage and implementation of how to create and process(send/receive) the custom headers between Client and Server +using the interceptors (HeaderServerInterceptor, ClientServerInterceptor) along with Metadata. + +Metadata is a side channel that allows clients and servers to provide information to each other that is associated with an RPC. +gRPC metadata is a key-value pair of data that is sent with initial or final gRPC requests or responses. +It is used to provide additional information about the call, such as authentication credentials, +tracing information, or custom headers. + +gRPC metadata can be used to send custom headers to the server or from the server to the client. +This can be used to implement application-specific features, such as load balancing, +rate limiting or providing detailed error messages from the server to the client. + +Refer the gRPC documentation for more on Metadata/Headers https://grpc.io/docs/guides/metadata/ \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/healthservice/README.md b/examples/src/main/java/io/grpc/examples/healthservice/README.md new file mode 100644 index 00000000000..181bd70977f --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/healthservice/README.md @@ -0,0 +1,10 @@ +gRPC Health Service Example +===================== + +The Health Service example provides a HelloWorld gRPC server that doesn't like short names along with a +health service. It also provides a client application which makes HelloWorld +calls and checks the health status. + +The client application also shows how the round robin load balancer can +utilize the health status to avoid making calls to a service that is +not actively serving. diff --git a/examples/src/main/java/io/grpc/examples/hedging/README.md b/examples/src/main/java/io/grpc/examples/hedging/README.md new file mode 100644 index 00000000000..0154e5c2cee --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/hedging/README.md @@ -0,0 +1,59 @@ +gRPC Hedging Example +===================== + +The Hedging example demonstrates that enabling hedging +can reduce tail latency. (Users should note that enabling hedging may introduce other overhead; +and in some scenarios, such as when some server resource gets exhausted for a period of time and +almost every RPC during that time has high latency or fails, hedging may make things worse. +Setting a throttle in the service config is recommended to protect the server from too many +inappropriate retry or hedging requests.) + +The server and the client in the example are basically the same as those in the +[hello world](src/main/java/io/grpc/examples/helloworld) example, except that the server mimics a +long tail of latency, and the client sends 2000 requests and can turn on and off hedging. + +To mimic the latency, the server randomly delays the RPC handling by 2 seconds at 10% chance, 5 +seconds at 5% chance, and 10 seconds at 1% chance. + +When running the client enabling the following hedging policy + + ```json + "hedgingPolicy": { + "maxAttempts": 3, + "hedgingDelay": "1s" + } + ``` +Then the latency summary in the client log is like the following + + ```text + Total RPCs sent: 2,000. Total RPCs failed: 0 + [Hedging enabled] + ======================== + 50% latency: 0ms + 90% latency: 6ms + 95% latency: 1,003ms + 99% latency: 2,002ms + 99.9% latency: 2,011ms + Max latency: 5,272ms + ======================== + ``` + +See [the section below](#to-build-the-examples) for how to build and run the example. The +executables for the server and the client are `hedging-hello-world-server` and +`hedging-hello-world-client`. + +To disable hedging, set environment variable `DISABLE_HEDGING_IN_HEDGING_EXAMPLE=true` before +running the client. That produces a latency summary in the client log like the following + + ```text + Total RPCs sent: 2,000. Total RPCs failed: 0 + [Hedging disabled] + ======================== + 50% latency: 0ms + 90% latency: 2,002ms + 95% latency: 5,002ms + 99% latency: 10,004ms + 99.9% latency: 10,007ms + Max latency: 10,007ms + ======================== + ``` diff --git a/examples/src/main/java/io/grpc/examples/helloworld/README.md b/examples/src/main/java/io/grpc/examples/helloworld/README.md new file mode 100644 index 00000000000..5b11d4945c2 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/helloworld/README.md @@ -0,0 +1,7 @@ +gRPC Hello World Example +===================== +This Example gives the details about basic implementation of gRPC Client and Server along with +how the communication happens between them by sending a greeting message. + +Refer the gRPC documentation for more details on helloworld.proto specification, creation of gRPC services and +methods along with Execution process https://grpc.io/docs/languages/java/quickstart/ \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/keepalive/README.md b/examples/src/main/java/io/grpc/examples/keepalive/README.md new file mode 100644 index 00000000000..7b5b72665e7 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/keepalive/README.md @@ -0,0 +1,16 @@ +gRPC Keepalive Example +===================== + +This example gives the usage and implementation of the Keepalives methods, configurations in gRPC Client and +Server and how the communication happens between them. + +HTTP/2 PING-based keepalives are a way to keep an HTTP/2 connection alive even when there is no data being transferred. +This is done by periodically sending a PING Frames to the other end of the connection. +HTTP/2 keepalives can improve performance and reliability of HTTP/2 connections, +but it is important to configure the keepalive interval carefully. + +gRPC sends http2 pings on the transport to detect if the connection is down. +If the ping is not acknowledged by the other side within a certain period, the connection will be closed. +Note that pings are only necessary when there's no activity on the connection. + +Refer the gRPC documentation for more on Keepalive details and configurations https://grpc.io/docs/guides/keepalive/ \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/loadbalance/README.md b/examples/src/main/java/io/grpc/examples/loadbalance/README.md new file mode 100644 index 00000000000..0d19d2f3335 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/loadbalance/README.md @@ -0,0 +1,20 @@ +gRPC Load Balance Example +===================== + +One of the key features of gRPC is load balancing, which allows requests from clients to be distributed across multiple servers. +This helps prevent any one server from becoming overloaded and allows the system to scale up by adding more servers. + +A gRPC load balancing policy is given a list of server IP addresses by the name resolver. +The policy is responsible for maintaining connections (subchannels) to the servers and picking a connection to use when an RPC is sent. + +By default, the pick_first policy will be used. +This policy actually does no load balancing but just tries each address it gets from the name resolver and uses the first one it can connect to. +By updating the gRPC service config you can also switch to using round_robin that connects to every address it gets and rotates through the connected backends for each RPC. +There are also some other load balancing policies available, but the exact set varies by language. + +This example gives the details about how to implement Load Balance in gRPC, If the built-in policies does not meet your requirements +you can implement your own custom load balance [Custom Load Balance](src/main/java/io/grpc/examples/customloadbalance) + +gRPC supports both client side and server side load balancing but by default gRPC uses client side load balancing. + +Refer the gRPC documentation for more details on Load Balancing https://grpc.io/blog/grpc-load-balancing/ \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/multiplex/README.md b/examples/src/main/java/io/grpc/examples/multiplex/README.md new file mode 100644 index 00000000000..fb24642a41b --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/multiplex/README.md @@ -0,0 +1,20 @@ +gRPC Multiplex Example +===================== + +gRPC multiplexing refers to the ability of a single gRPC connection to handle multiple independent streams of communication simultaneously. +This is part of the HTTP/2 protocol on which gRPC is built. +Each gRPC connection supports multiple streams that can carry different RPCs, making it highly efficient for high-throughput, low-latency communication. + +In gRPC, sharing resources like channels and servers can improve efficiency and resource utilization. + +- Sharing gRPC Channels and Servers + + 1. Shared gRPC Channel: + - A single gRPC channel can be used by multiple stubs, enabling different service clients to communicate over the same connection. + - This minimizes the overhead of establishing and managing multiple connections + + 2. Shared gRPC Server: + - A single gRPC channel can be used by multiple stubs, enabling different service clients to communicate over the same connection. + - This minimizes the overhead of establishing and managing multiple connections + +This example demonstrates how to implement a gRPC server that serves both a GreetingService and an EchoService, and a client that shares a single channel across multiple stubs for both services. \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/nameresolve/README.md b/examples/src/main/java/io/grpc/examples/nameresolve/README.md new file mode 100644 index 00000000000..36c8d7e2a6b --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/nameresolve/README.md @@ -0,0 +1,22 @@ +gRPC Name Resolve Example +===================== + +This example explains standard name resolution process and how to implement it using the Name Resolver component. + +Name Resolution is fundamentally about Service Discovery. +Name Resolution refers to the process of converting a name into an address and +Name Resolver is the component that implements the Name Resolution process. + +When sending gRPC Request, Client must determine the IP address of the Service Name, +By Default DNS Name Resolution will be used when request received from the gRPC client. + +The Name Resolver in gRPC is necessary because clients often don’t know the exact IP address or port of the server +they need to connect to. + +The client registers an implementation of a **name resolver provider** to a process-global **registry** close to the start of the process. +The name resolver provider will be called by the **gRPC library** with a **target strings** intended for the custom name resolver. +Given that target string, the name resolver provider will return an instance of a **name resolver**, +which will interact with the client connection to direct the request according to the target string. + +Refer the gRPC documentation for more on Name Resolution and Custom Name Resolution +https://grpc.io/docs/guides/custom-name-resolution/ \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/preserialized/README.md b/examples/src/main/java/io/grpc/examples/preserialized/README.md new file mode 100644 index 00000000000..d49b3507d03 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/preserialized/README.md @@ -0,0 +1,18 @@ +gRPC Pre-Serialized Messages Example +===================== + +This example gives the usage and implementation of pre-serialized request and response messages +communication/exchange between grpc client and server by using ByteArrayMarshaller which produces +a byte[] instead of decoding into typical POJOs. + +This is a performance optimization that can be useful if you read the request/response from on-disk or a database +where it is already serialized, or if you need to send the same complicated message to many clients and servers. +The same approach can avoid deserializing requests/responses, to be stored in a database. + +It shows how to modify MethodDescriptor to use bytes as the response instead of HelloReply. By +adjusting toBuilder() you can choose which of the request and response are bytes. +The generated bindService() uses ServerCalls to make RPC handlers, Since the generated +bindService() won't expect byte[] in the AsyncService, this uses ServerCalls directly. + +Stubs use ClientCalls to send RPCs, Since the generated stub won't have byte[] in its +method signature, this uses ClientCalls directly. \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/retrying/README.md b/examples/src/main/java/io/grpc/examples/retrying/README.md new file mode 100644 index 00000000000..bb29ce75e43 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/retrying/README.md @@ -0,0 +1,27 @@ +gRPC Retrying Example +===================== + +The Retrying example provides a HelloWorld gRPC client & +server which demos the effect of client retry policy configured on the [ManagedChannel]( +https://github.com/grpc/grpc-java/blob/master/api/src/main/java/io/grpc/ManagedChannel.java) via [gRPC ServiceConfig]( +https://github.com/grpc/grpc/blob/master/doc/service_config.md). Retry policy implementation & +configuration details are outlined in the [proposal](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). + +This retrying example is very similar to the [hedging example](https://github.com/grpc/grpc-java/tree/master/examples/src/main/java/io/grpc/examples/hedging) in its setup. +The [RetryingHelloWorldServer](src/main/java/io/grpc/examples/retrying/RetryingHelloWorldServer.java) responds with +a status UNAVAILABLE error response to a specified percentage of requests to simulate server resource exhaustion and +general flakiness. The [RetryingHelloWorldClient](src/main/java/io/grpc/examples/retrying/RetryingHelloWorldClient.java) makes +a number of sequential requests to the server, several of which will be retried depending on the configured policy in +[retrying_service_config.json](https://github.com/grpc/grpc-java/blob/master/examples/src/main/resources/io/grpc/examples/retrying/retrying_service_config.json). Although +the requests are blocking unary calls for simplicity, these could easily be changed to future unary calls in order to +test the result of request concurrency with retry policy enabled. + +One can experiment with the [RetryingHelloWorldServer](src/main/java/io/grpc/examples/retrying/RetryingHelloWorldServer.java) +failure conditions to simulate server throttling, as well as alter policy values in the [retrying_service_config.json]( +https://github.com/grpc/grpc-java/blob/master/examples/src/main/resources/io/grpc/examples/retrying/retrying_service_config.json) to see their effects. To disable retrying +entirely, set environment variable `DISABLE_RETRYING_IN_RETRYING_EXAMPLE=true` before running the client. +Disabling the retry policy should produce many more failed gRPC calls as seen in the output log. + +See [the section](https://github.com/grpc/grpc-java/tree/master/examples#-to-build-the-examples) for how to build and run the example. The +executables for the server and the client are `retrying-hello-world-server` and +`retrying-hello-world-client`. diff --git a/examples/src/main/java/io/grpc/examples/routeguide/README.md b/examples/src/main/java/io/grpc/examples/routeguide/README.md new file mode 100644 index 00000000000..2528b26410c --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/routeguide/README.md @@ -0,0 +1,24 @@ +gRPC Route Guide Example +===================== + +This example illustrates how to implement and use a gRPC server and client for a RouteGuide service, +which demonstrates all 4 types of gRPC methods (unary, client streaming, server streaming, and bidirectional streaming). +Additionally, the service loads geographic features from a JSON file [route_guide_db.json](https://github.com/grpc/grpc-java/blob/master/examples/src/main/resources/io/grpc/examples/routeguide/route_guide_db.json) and retrieves features based on latitude and longitude. + +The route_guide.proto file defines a gRPC service with 4 types of RPC methods, showcasing different communication patterns between client and server. +1. Unary RPC + - rpc GetFeature(Point) returns (Feature) {} +2. Server-Side Streaming RPC + - rpc ListFeatures(Rectangle) returns (stream Feature) {} +3. Client-Side Streaming RPC + - rpc RecordRoute(stream Point) returns (RouteSummary) {} +4. Bidirectional Streaming RPC + - rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} + +These RPC methods illustrate the versatility of gRPC in handling various communication patterns, +from simple request-response interactions to complex bidirectional streaming scenarios. + +For more details, refer to the full route_guide.proto file on GitHub: https://github.com/grpc/grpc-java/blob/master/examples/src/main/proto/route_guide.proto + +Refer the gRPC documentation for more details on creation, build and execution of route guide example with explanation +https://grpc.io/docs/languages/java/basics/ \ No newline at end of file diff --git a/examples/src/main/java/io/grpc/examples/waitforready/README.md b/examples/src/main/java/io/grpc/examples/waitforready/README.md new file mode 100644 index 00000000000..1e294b453b6 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/waitforready/README.md @@ -0,0 +1,29 @@ +gRPC Wait-For-Ready Example +===================== + +This example gives the usage and implementation of the Wait-For-Ready feature. + +This feature can be activated on a client stub, ensuring that Remote Procedure Calls (RPCs) are held until the server is ready to receive them. +By waiting for the server to become available before sending requests, this mechanism enhances reliability, +particularly in situations where server availability may be delayed or unpredictable. + +When an RPC is initiated and the channel fails to connect to the server, its behavior depends on the Wait-for-Ready option: + +- Without Wait-for-Ready (Default Behavior): + + - The RPC will immediately fail if the channel cannot establish a connection, providing prompt feedback about the connectivity issue. + +- With Wait-for-Ready: + + - The RPC will not fail immediately. Instead, it will be queued and will wait until the connection is successfully established. + This approach is beneficial for handling temporary network disruptions more gracefully, ensuring the RPC is eventually executed once the connection is ready. + + +Example gives the Simple client that requests a greeting from the HelloWorldServer and defines waitForReady on the stub. + +To test this flow need to follow below steps: +- run this client without a server running(client rpc should hang) +- start the server (client rpc should complete) +- run this client again (client rpc should complete nearly immediately) + +Refer the gRPC documentation for more on Wait-For-Ready https://grpc.io/docs/guides/wait-for-ready/ \ No newline at end of file From b3db8c2489af25b0d10b6f45e60fd50771aa93d1 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 28 Jan 2025 16:38:32 -0800 Subject: [PATCH 161/591] xds: Allow FaultFilter's interceptor to be reused This is the only usage of PickSubchannelArgs when creating a filter's ClientInterceptor, and a follow-up commit will remove the argument and actually reuse the interceptors. Other filter's interceptors can already be reused. There doesn't seem to be any significant loss of legibility by making FaultFilter a more ordinary interceptor, but the change does cause the ForwardingClientCall to be present when faultDelay is configured, independent of whether the fault delay ends up being triggered. Reusing interceptors will move more state management out of the RPC path which will be more relevant with RLQS. --- .../main/java/io/grpc/xds/FaultFilter.java | 154 +++++++++--------- 1 file changed, 81 insertions(+), 73 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/FaultFilter.java b/xds/src/main/java/io/grpc/xds/FaultFilter.java index d46b3d30f5a..b7f7fa9c226 100644 --- a/xds/src/main/java/io/grpc/xds/FaultFilter.java +++ b/xds/src/main/java/io/grpc/xds/FaultFilter.java @@ -190,94 +190,102 @@ public ClientInterceptor buildClientInterceptor( config = overrideConfig; } FaultConfig faultConfig = (FaultConfig) config; - Long delayNanos = null; - Status abortStatus = null; - if (faultConfig.maxActiveFaults() == null - || activeFaultCounter.get() < faultConfig.maxActiveFaults()) { - Metadata headers = args.getHeaders(); - if (faultConfig.faultDelay() != null) { - delayNanos = determineFaultDelayNanos(faultConfig.faultDelay(), headers); - } - if (faultConfig.faultAbort() != null) { - abortStatus = determineFaultAbortStatus(faultConfig.faultAbort(), headers); - } - } - if (delayNanos == null && abortStatus == null) { - return null; - } - final Long finalDelayNanos = delayNanos; - final Status finalAbortStatus = getAbortStatusWithDescription(abortStatus); final class FaultInjectionInterceptor implements ClientInterceptor { @Override public ClientCall interceptCall( final MethodDescriptor method, final CallOptions callOptions, final Channel next) { - Executor callExecutor = callOptions.getExecutor(); - if (callExecutor == null) { // This should never happen in practice because - // ManagedChannelImpl.ConfigSelectingClientCall always provides CallOptions with - // a callExecutor. - // TODO(https://github.com/grpc/grpc-java/issues/7868) - callExecutor = MoreExecutors.directExecutor(); + boolean checkFault = false; + if (faultConfig.maxActiveFaults() == null + || activeFaultCounter.get() < faultConfig.maxActiveFaults()) { + checkFault = faultConfig.faultDelay() != null || faultConfig.faultAbort() != null; } - if (finalDelayNanos != null) { - Supplier> callSupplier; - if (finalAbortStatus != null) { - callSupplier = Suppliers.ofInstance( - new FailingClientCall(finalAbortStatus, callExecutor)); - } else { - callSupplier = new Supplier>() { - @Override - public ClientCall get() { - return next.newCall(method, callOptions); - } - }; + if (!checkFault) { + return next.newCall(method, callOptions); + } + final class DeadlineInsightForwardingCall extends ForwardingClientCall { + private ClientCall delegate; + + @Override + protected ClientCall delegate() { + return delegate; } - final DelayInjectedCall delayInjectedCall = new DelayInjectedCall<>( - finalDelayNanos, callExecutor, scheduler, callOptions.getDeadline(), callSupplier); - final class DeadlineInsightForwardingCall extends ForwardingClientCall { - @Override - protected ClientCall delegate() { - return delayInjectedCall; + @Override + public void start(Listener listener, Metadata headers) { + Executor callExecutor = callOptions.getExecutor(); + if (callExecutor == null) { // This should never happen in practice because + // ManagedChannelImpl.ConfigSelectingClientCall always provides CallOptions with + // a callExecutor. + // TODO(https://github.com/grpc/grpc-java/issues/7868) + callExecutor = MoreExecutors.directExecutor(); } - @Override - public void start(Listener listener, Metadata headers) { - Listener finalListener = - new SimpleForwardingClientCallListener(listener) { - @Override - public void onClose(Status status, Metadata trailers) { - if (status.getCode().equals(Code.DEADLINE_EXCEEDED)) { - // TODO(zdapeng:) check effective deadline locally, and - // do the following only if the local deadline is exceeded. - // (If the server sends DEADLINE_EXCEEDED for its own deadline, then the - // injected delay does not contribute to the error, because the request is - // only sent out after the delay. There could be a race between local and - // remote, but it is rather rare.) - String description = String.format( - Locale.US, - "Deadline exceeded after up to %d ns of fault-injected delay", - finalDelayNanos); - if (status.getDescription() != null) { - description = description + ": " + status.getDescription(); - } - status = Status.DEADLINE_EXCEEDED - .withDescription(description).withCause(status.getCause()); - // Replace trailers to prevent mixing sources of status and trailers. - trailers = new Metadata(); + Long delayNanos; + Status abortStatus = null; + if (faultConfig.faultDelay() != null) { + delayNanos = determineFaultDelayNanos(faultConfig.faultDelay(), headers); + } else { + delayNanos = null; + } + if (faultConfig.faultAbort() != null) { + abortStatus = getAbortStatusWithDescription( + determineFaultAbortStatus(faultConfig.faultAbort(), headers)); + } + + Supplier> callSupplier; + if (abortStatus != null) { + callSupplier = Suppliers.ofInstance( + new FailingClientCall(abortStatus, callExecutor)); + } else { + callSupplier = new Supplier>() { + @Override + public ClientCall get() { + return next.newCall(method, callOptions); + } + }; + } + if (delayNanos == null) { + delegate = callSupplier.get(); + delegate().start(listener, headers); + return; + } + + delegate = new DelayInjectedCall<>( + delayNanos, callExecutor, scheduler, callOptions.getDeadline(), callSupplier); + + Listener finalListener = + new SimpleForwardingClientCallListener(listener) { + @Override + public void onClose(Status status, Metadata trailers) { + if (status.getCode().equals(Code.DEADLINE_EXCEEDED)) { + // TODO(zdapeng:) check effective deadline locally, and + // do the following only if the local deadline is exceeded. + // (If the server sends DEADLINE_EXCEEDED for its own deadline, then the + // injected delay does not contribute to the error, because the request is + // only sent out after the delay. There could be a race between local and + // remote, but it is rather rare.) + String description = String.format( + Locale.US, + "Deadline exceeded after up to %d ns of fault-injected delay", + delayNanos); + if (status.getDescription() != null) { + description = description + ": " + status.getDescription(); } - delegate().onClose(status, trailers); + status = Status.DEADLINE_EXCEEDED + .withDescription(description).withCause(status.getCause()); + // Replace trailers to prevent mixing sources of status and trailers. + trailers = new Metadata(); } - }; - delegate().start(finalListener, headers); - } + delegate().onClose(status, trailers); + } + }; + delegate().start(finalListener, headers); } - - return new DeadlineInsightForwardingCall(); - } else { - return new FailingClientCall<>(finalAbortStatus, callExecutor); } + + return new DeadlineInsightForwardingCall(); } } From 7153ff8522f4aad9ae12c80984f0d5c56c94a949 Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:52:37 +0530 Subject: [PATCH 162/591] netty: Removed 4096 min buffer size (#11856) * netty: Removed 4096 min buffer size --- .../java/io/grpc/netty/NettyWritableBufferAllocator.java | 5 +---- .../io/grpc/netty/NettyWritableBufferAllocatorTest.java | 7 ------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/NettyWritableBufferAllocator.java b/netty/src/main/java/io/grpc/netty/NettyWritableBufferAllocator.java index 9e93ee1155c..40b84717160 100644 --- a/netty/src/main/java/io/grpc/netty/NettyWritableBufferAllocator.java +++ b/netty/src/main/java/io/grpc/netty/NettyWritableBufferAllocator.java @@ -33,9 +33,6 @@ */ class NettyWritableBufferAllocator implements WritableBufferAllocator { - // Use 4k as our minimum buffer size. - private static final int MIN_BUFFER = 4 * 1024; - // Set the maximum buffer size to 1MB. private static final int MAX_BUFFER = 1024 * 1024; @@ -47,7 +44,7 @@ class NettyWritableBufferAllocator implements WritableBufferAllocator { @Override public WritableBuffer allocate(int capacityHint) { - capacityHint = Math.min(MAX_BUFFER, Math.max(MIN_BUFFER, capacityHint)); + capacityHint = Math.min(MAX_BUFFER, capacityHint); return new NettyWritableBuffer(allocator.buffer(capacityHint, capacityHint)); } } diff --git a/netty/src/test/java/io/grpc/netty/NettyWritableBufferAllocatorTest.java b/netty/src/test/java/io/grpc/netty/NettyWritableBufferAllocatorTest.java index d577ec46b03..0b741ae24b3 100644 --- a/netty/src/test/java/io/grpc/netty/NettyWritableBufferAllocatorTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyWritableBufferAllocatorTest.java @@ -40,13 +40,6 @@ protected WritableBufferAllocator allocator() { return allocator; } - @Test - public void testCapacityHasMinimum() { - WritableBuffer buffer = allocator().allocate(100); - assertEquals(0, buffer.readableBytes()); - assertEquals(4096, buffer.writableBytes()); - } - @Test public void testCapacityIsExactAboveMinimum() { WritableBuffer buffer = allocator().allocate(9000); From 90aefb26e71fcc8d3f45fa5580dd3131b262db27 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 29 Jan 2025 14:26:35 -0800 Subject: [PATCH 163/591] core: Propagate authority override from LB exactly once Setting the authority is only useful when creating a real stream, as there will be a following pick otherwise. In addition, DelayedStream will buffer each call to setAuthority() in a list and we don't want that memory usage. Note that no LBs are using this feature yet, so users would not have been exposed to the memory use. We also needed to setAuthority() when the LB selected a subchannel on the first pick attempt. --- .../grpc/internal/DelayedClientTransport.java | 23 +++++--- .../internal/DelayedClientTransportTest.java | 54 +++++++------------ 2 files changed, 36 insertions(+), 41 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java index 2ff94b7804a..f3faa92d4a0 100644 --- a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java +++ b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java @@ -140,9 +140,15 @@ public final ClientStream newStream( ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult, callOptions.isWaitForReady()); if (transport != null) { - return transport.newStream( + ClientStream stream = transport.newStream( args.getMethodDescriptor(), args.getHeaders(), callOptions, tracers); + // User code provided authority takes precedence over the LB provided one; this will be + // overwritten by ClientCallImpl if the application sets an authority override + if (pickResult.getAuthorityOverride() != null) { + stream.setAuthority(pickResult.getAuthorityOverride()); + } + return stream; } } // This picker's conclusion is "buffer". If there hasn't been a newer picker set (possible @@ -287,10 +293,6 @@ final void reprocess(@Nullable SubchannelPicker picker) { for (final PendingStream stream : toProcess) { PickResult pickResult = picker.pickSubchannel(stream.args); CallOptions callOptions = stream.args.getCallOptions(); - // User code provided authority takes precedence over the LB provided one. - if (callOptions.getAuthority() == null && pickResult.getAuthorityOverride() != null) { - stream.setAuthority(pickResult.getAuthorityOverride()); - } final ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult, callOptions.isWaitForReady()); if (transport != null) { @@ -301,7 +303,7 @@ final void reprocess(@Nullable SubchannelPicker picker) { if (callOptions.getExecutor() != null) { executor = callOptions.getExecutor(); } - Runnable runnable = stream.createRealStream(transport); + Runnable runnable = stream.createRealStream(transport, pickResult.getAuthorityOverride()); if (runnable != null) { executor.execute(runnable); } @@ -354,7 +356,7 @@ private PendingStream(PickSubchannelArgs args, ClientStreamTracer[] tracers) { } /** Runnable may be null. */ - private Runnable createRealStream(ClientTransport transport) { + private Runnable createRealStream(ClientTransport transport, String authorityOverride) { ClientStream realStream; Context origContext = context.attach(); try { @@ -364,6 +366,13 @@ private Runnable createRealStream(ClientTransport transport) { } finally { context.detach(origContext); } + if (authorityOverride != null) { + // User code provided authority takes precedence over the LB provided one; this will be + // overwritten by an enqueud call from ClientCallImpl if the application sets an authority + // override. We must call the real stream directly because stream.start() has likely already + // been called on the delayed stream. + realStream.setAuthority(authorityOverride); + } return setStream(realStream); } diff --git a/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java b/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java index a5160552a9e..f65e6abcf1b 100644 --- a/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java +++ b/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java @@ -503,26 +503,11 @@ public void uncaughtException(Thread t, Throwable e) { } @Test - public void reprocess_authorityOverridePresentInCallOptions_authorityOverrideFromLbIsIgnored() { - DelayedStream delayedStream = (DelayedStream) delayedTransport.newStream( - method, headers, callOptions, tracers); - delayedStream.start(mock(ClientStreamListener.class)); - SubchannelPicker picker = mock(SubchannelPicker.class); - PickResult pickResult = PickResult.withSubchannel( - mockSubchannel, null, "authority-override-hostname-from-lb"); - when(picker.pickSubchannel(any(PickSubchannelArgs.class))).thenReturn(pickResult); - - delayedTransport.reprocess(picker); - fakeExecutor.runDueTasks(); - - verify(mockRealStream, never()).setAuthority("authority-override-hostname-from-lb"); - } - - @Test - public void - reprocess_authorityOverrideNotInCallOptions_authorityOverrideFromLbIsSetIntoStream() { + public void reprocess_authorityOverrideFromLb() { + InOrder inOrder = inOrder(mockRealStream); DelayedStream delayedStream = (DelayedStream) delayedTransport.newStream( method, headers, callOptions.withAuthority(null), tracers); + delayedStream.setAuthority("authority-override-from-calloptions"); delayedStream.start(mock(ClientStreamListener.class)); SubchannelPicker picker = mock(SubchannelPicker.class); PickResult pickResult = PickResult.withSubchannel( @@ -536,7 +521,10 @@ public void reprocess_authorityOverridePresentInCallOptions_authorityOverrideFro delayedTransport.reprocess(picker); fakeExecutor.runDueTasks(); - verify(mockRealStream).setAuthority("authority-override-hostname-from-lb"); + // Must be set before start(), and may be overwritten + inOrder.verify(mockRealStream).setAuthority("authority-override-hostname-from-lb"); + inOrder.verify(mockRealStream).setAuthority("authority-override-from-calloptions"); + inOrder.verify(mockRealStream).start(any(ClientStreamListener.class)); } @Test @@ -563,28 +551,26 @@ public void reprocess_NoPendingStream() { } @Test - public void newStream_assignsTransport_authorityFromCallOptionsSupersedesAuthorityFromLB() { + public void newStream_authorityOverrideFromLb() { + InOrder inOrder = inOrder(mockRealStream); SubchannelPicker picker = mock(SubchannelPicker.class); - AbstractSubchannel subchannel = mock(AbstractSubchannel.class); - when(subchannel.getInternalSubchannel()).thenReturn(mockInternalSubchannel); PickResult pickResult = PickResult.withSubchannel( - subchannel, null, "authority-override-hostname-from-lb"); + mockSubchannel, null, "authority-override-hostname-from-lb"); when(picker.pickSubchannel(any(PickSubchannelArgs.class))).thenReturn(pickResult); - ArgumentCaptor callOptionsArgumentCaptor = - ArgumentCaptor.forClass(CallOptions.class); when(mockRealTransport.newStream( - any(MethodDescriptor.class), any(Metadata.class), callOptionsArgumentCaptor.capture(), - ArgumentMatchers.any())) + any(MethodDescriptor.class), any(Metadata.class), any(CallOptions.class), any())) .thenReturn(mockRealStream); delayedTransport.reprocess(picker); - verifyNoMoreInteractions(picker); - verifyNoMoreInteractions(transportListener); - CallOptions callOptions = - CallOptions.DEFAULT.withAuthority("authority-override-hosstname-from-calloptions"); - delayedTransport.newStream(method, headers, callOptions, tracers); - assertThat(callOptionsArgumentCaptor.getValue().getAuthority()).isEqualTo( - "authority-override-hosstname-from-calloptions"); + ClientStream stream = delayedTransport.newStream(method, headers, callOptions, tracers); + assertThat(stream).isSameInstanceAs(mockRealStream); + stream.setAuthority("authority-override-from-calloptions"); + stream.start(mock(ClientStreamListener.class)); + + // Must be set before start(), and may be overwritten + inOrder.verify(mockRealStream).setAuthority("authority-override-hostname-from-lb"); + inOrder.verify(mockRealStream).setAuthority("authority-override-from-calloptions"); + inOrder.verify(mockRealStream).start(any(ClientStreamListener.class)); } @Test From c506190b0f1f19c389d9ef797d6c69fb41167850 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 30 Jan 2025 12:43:51 -0800 Subject: [PATCH 164/591] xds: Reuse filter interceptors across RPCs This moves the interceptor creation from the ConfigSelector to the resource update handling. The code structure changes will make adding support for filter lifecycles (for RLQS) a bit easier. The filter lifecycles will allow filters to share state across interceptors, and constructing all the interceptors on a single thread will mean filters wouldn't need to be thread-safe (but their interceptors would be thread-safe). --- .../main/java/io/grpc/xds/FaultFilter.java | 3 +- xds/src/main/java/io/grpc/xds/Filter.java | 3 +- .../io/grpc/xds/GcpAuthenticationFilter.java | 4 +- .../main/java/io/grpc/xds/RouterFilter.java | 3 +- .../java/io/grpc/xds/XdsNameResolver.java | 201 ++++++++++++------ .../grpc/xds/GcpAuthenticationFilterTest.java | 2 +- .../grpc/xds/GrpcXdsClientImplDataTest.java | 2 - 7 files changed, 136 insertions(+), 82 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/FaultFilter.java b/xds/src/main/java/io/grpc/xds/FaultFilter.java index b7f7fa9c226..c66861a9f15 100644 --- a/xds/src/main/java/io/grpc/xds/FaultFilter.java +++ b/xds/src/main/java/io/grpc/xds/FaultFilter.java @@ -37,7 +37,6 @@ import io.grpc.Deadline; import io.grpc.ForwardingClientCall; import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; -import io.grpc.LoadBalancer.PickSubchannelArgs; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.Status; @@ -183,7 +182,7 @@ public ConfigOrError parseFilterConfigOverride(Message rawProtoMess @Nullable @Override public ClientInterceptor buildClientInterceptor( - FilterConfig config, @Nullable FilterConfig overrideConfig, PickSubchannelArgs args, + FilterConfig config, @Nullable FilterConfig overrideConfig, final ScheduledExecutorService scheduler) { checkNotNull(config, "config"); if (overrideConfig != null) { diff --git a/xds/src/main/java/io/grpc/xds/Filter.java b/xds/src/main/java/io/grpc/xds/Filter.java index 4b2767687f3..29f8cc4e337 100644 --- a/xds/src/main/java/io/grpc/xds/Filter.java +++ b/xds/src/main/java/io/grpc/xds/Filter.java @@ -19,7 +19,6 @@ import com.google.common.base.MoreObjects; import com.google.protobuf.Message; import io.grpc.ClientInterceptor; -import io.grpc.LoadBalancer.PickSubchannelArgs; import io.grpc.ServerInterceptor; import java.util.Objects; import java.util.concurrent.ScheduledExecutorService; @@ -59,7 +58,7 @@ interface FilterConfig { interface ClientInterceptorBuilder { @Nullable ClientInterceptor buildClientInterceptor( - FilterConfig config, @Nullable FilterConfig overrideConfig, PickSubchannelArgs args, + FilterConfig config, @Nullable FilterConfig overrideConfig, ScheduledExecutorService scheduler); } diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java index 6d05e8ffa95..f73494d74db 100644 --- a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java +++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java @@ -31,7 +31,6 @@ import io.grpc.ClientCall; import io.grpc.ClientInterceptor; import io.grpc.CompositeCallCredentials; -import io.grpc.LoadBalancer.PickSubchannelArgs; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.Status; @@ -97,8 +96,7 @@ public ConfigOrError parseFilterConfigOverride(Message r @Nullable @Override public ClientInterceptor buildClientInterceptor(FilterConfig config, - @Nullable FilterConfig overrideConfig, PickSubchannelArgs args, - ScheduledExecutorService scheduler) { + @Nullable FilterConfig overrideConfig, ScheduledExecutorService scheduler) { ComputeEngineCredentials credentials = ComputeEngineCredentials.create(); LruCache callCredentialsCache = diff --git a/xds/src/main/java/io/grpc/xds/RouterFilter.java b/xds/src/main/java/io/grpc/xds/RouterFilter.java index 7f1adf86a6d..8038c1b98ae 100644 --- a/xds/src/main/java/io/grpc/xds/RouterFilter.java +++ b/xds/src/main/java/io/grpc/xds/RouterFilter.java @@ -18,7 +18,6 @@ import com.google.protobuf.Message; import io.grpc.ClientInterceptor; -import io.grpc.LoadBalancer.PickSubchannelArgs; import io.grpc.ServerInterceptor; import io.grpc.xds.Filter.ClientInterceptorBuilder; import io.grpc.xds.Filter.ServerInterceptorBuilder; @@ -64,7 +63,7 @@ public ConfigOrError parseFilterConfigOverride(Message r @Nullable @Override public ClientInterceptor buildClientInterceptor( - FilterConfig config, @Nullable FilterConfig overrideConfig, PickSubchannelArgs args, + FilterConfig config, @Nullable FilterConfig overrideConfig, ScheduledExecutorService scheduler) { return null; } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 3c7f4455fde..5ac53763373 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -59,6 +59,7 @@ import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight; import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy; import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy; +import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider; import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import io.grpc.xds.client.Bootstrapper.AuthorityInfo; @@ -384,20 +385,17 @@ private final class ConfigSelector extends InternalConfigSelector { @Override public Result selectConfig(PickSubchannelArgs args) { String cluster = null; - Route selectedRoute = null; + ClientInterceptor filters = null; // null iff cluster is null + RouteData selectedRoute = null; RoutingConfig routingCfg; - Map selectedOverrideConfigs; - List filterInterceptors = new ArrayList<>(); Metadata headers = args.getHeaders(); do { routingCfg = routingConfig; - selectedOverrideConfigs = new HashMap<>(routingCfg.virtualHostOverrideConfig); - for (Route route : routingCfg.routes) { + for (RouteData route : routingCfg.routes) { if (RoutingUtils.matchRoute( - route.routeMatch(), "/" + args.getMethodDescriptor().getFullMethodName(), - headers, random)) { + route.routeMatch, "/" + args.getMethodDescriptor().getFullMethodName(), + headers, random)) { selectedRoute = route; - selectedOverrideConfigs.putAll(route.filterConfigOverrides()); break; } } @@ -405,13 +403,14 @@ public Result selectConfig(PickSubchannelArgs args) { return Result.forError( Status.UNAVAILABLE.withDescription("Could not find xDS route matching RPC")); } - if (selectedRoute.routeAction() == null) { + if (selectedRoute.routeAction == null) { return Result.forError(Status.UNAVAILABLE.withDescription( "Could not route RPC to Route with non-forwarding action")); } - RouteAction action = selectedRoute.routeAction(); + RouteAction action = selectedRoute.routeAction; if (action.cluster() != null) { cluster = prefixedClusterName(action.cluster()); + filters = selectedRoute.filterChoices.get(0); } else if (action.weightedClusters() != null) { long totalWeight = 0; for (ClusterWeight weightedCluster : action.weightedClusters()) { @@ -419,23 +418,25 @@ public Result selectConfig(PickSubchannelArgs args) { } long select = random.nextLong(totalWeight); long accumulator = 0; - for (ClusterWeight weightedCluster : action.weightedClusters()) { + for (int i = 0; i < action.weightedClusters().size(); i++) { + ClusterWeight weightedCluster = action.weightedClusters().get(i); accumulator += weightedCluster.weight(); if (select < accumulator) { cluster = prefixedClusterName(weightedCluster.name()); - selectedOverrideConfigs.putAll(weightedCluster.filterConfigOverrides()); + filters = selectedRoute.filterChoices.get(i); break; } } } else if (action.namedClusterSpecifierPluginConfig() != null) { cluster = prefixedClusterSpecifierPluginName(action.namedClusterSpecifierPluginConfig().name()); + filters = selectedRoute.filterChoices.get(0); } } while (!retainCluster(cluster)); Long timeoutNanos = null; if (enableTimeout) { if (selectedRoute != null) { - timeoutNanos = selectedRoute.routeAction().timeoutNano(); + timeoutNanos = selectedRoute.routeAction.timeoutNano(); } if (timeoutNanos == null) { timeoutNanos = routingCfg.fallbackTimeoutNano; @@ -445,7 +446,7 @@ public Result selectConfig(PickSubchannelArgs args) { } } RetryPolicy retryPolicy = - selectedRoute == null ? null : selectedRoute.routeAction().retryPolicy(); + selectedRoute == null ? null : selectedRoute.routeAction.retryPolicy(); // TODO(chengyuanzhang): avoid service config generation and parsing for each call. Map rawServiceConfig = generateServiceConfigWithMethodConfig(timeoutNanos, retryPolicy); @@ -457,24 +458,9 @@ public Result selectConfig(PickSubchannelArgs args) { parsedServiceConfig.getError().augmentDescription( "Failed to parse service config (method config)")); } - if (routingCfg.filterChain != null) { - for (NamedFilterConfig namedFilter : routingCfg.filterChain) { - FilterConfig filterConfig = namedFilter.filterConfig; - Filter filter = filterRegistry.get(filterConfig.typeUrl()); - if (filter instanceof ClientInterceptorBuilder) { - ClientInterceptor interceptor = ((ClientInterceptorBuilder) filter) - .buildClientInterceptor( - filterConfig, selectedOverrideConfigs.get(namedFilter.name), - args, scheduler); - if (interceptor != null) { - filterInterceptors.add(interceptor); - } - } - } - } final String finalCluster = cluster; - final long hash = generateHash(selectedRoute.routeAction().hashPolicies(), headers); - Route finalSelectedRoute = selectedRoute; + final long hash = generateHash(selectedRoute.routeAction.hashPolicies(), headers); + RouteData finalSelectedRoute = selectedRoute; class ClusterSelectionInterceptor implements ClientInterceptor { @Override public ClientCall interceptCall( @@ -483,7 +469,7 @@ public ClientCall interceptCall( CallOptions callOptionsForCluster = callOptions.withOption(CLUSTER_SELECTION_KEY, finalCluster) .withOption(RPC_HASH_KEY, hash); - if (finalSelectedRoute.routeAction().autoHostRewrite()) { + if (finalSelectedRoute.routeAction.autoHostRewrite()) { callOptionsForCluster = callOptionsForCluster.withOption(AUTO_HOST_REWRITE_KEY, true); } return new SimpleForwardingClientCall( @@ -514,11 +500,11 @@ public void onClose(Status status, Metadata trailers) { } } - filterInterceptors.add(new ClusterSelectionInterceptor()); return Result.newBuilder() .setConfig(config) - .setInterceptor(combineInterceptors(filterInterceptors)) + .setInterceptor(combineInterceptors( + ImmutableList.of(filters, new ClusterSelectionInterceptor()))) .build(); } @@ -584,8 +570,18 @@ private long generateHash(List hashPolicies, Metadata headers) { } } + static final class PassthroughClientInterceptor implements ClientInterceptor { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return next.newCall(method, callOptions); + } + } + private static ClientInterceptor combineInterceptors(final List interceptors) { - checkArgument(!interceptors.isEmpty(), "empty interceptors"); + if (interceptors.size() == 0) { + return new PassthroughClientInterceptor(); + } if (interceptors.size() == 1) { return interceptors.get(0); } @@ -722,6 +718,7 @@ private void updateRoutes(List virtualHosts, long httpMaxStreamDura } List routes = virtualHost.routes(); + ImmutableList.Builder routesData = ImmutableList.builder(); // Populate all clusters to which requests can be routed to through the virtual host. Set clusters = new HashSet<>(); @@ -732,26 +729,34 @@ private void updateRoutes(List virtualHosts, long httpMaxStreamDura for (Route route : routes) { RouteAction action = route.routeAction(); String prefixedName; - if (action != null) { - if (action.cluster() != null) { - prefixedName = prefixedClusterName(action.cluster()); + if (action == null) { + routesData.add(new RouteData(route.routeMatch(), null, ImmutableList.of())); + } else if (action.cluster() != null) { + prefixedName = prefixedClusterName(action.cluster()); + clusters.add(prefixedName); + clusterNameMap.put(prefixedName, action.cluster()); + ClientInterceptor filters = createFilters(filterConfigs, virtualHost, route, null); + routesData.add(new RouteData(route.routeMatch(), route.routeAction(), filters)); + } else if (action.weightedClusters() != null) { + ImmutableList.Builder filterList = ImmutableList.builder(); + for (ClusterWeight weightedCluster : action.weightedClusters()) { + prefixedName = prefixedClusterName(weightedCluster.name()); clusters.add(prefixedName); - clusterNameMap.put(prefixedName, action.cluster()); - } else if (action.weightedClusters() != null) { - for (ClusterWeight weighedCluster : action.weightedClusters()) { - prefixedName = prefixedClusterName(weighedCluster.name()); - clusters.add(prefixedName); - clusterNameMap.put(prefixedName, weighedCluster.name()); - } - } else if (action.namedClusterSpecifierPluginConfig() != null) { - PluginConfig pluginConfig = action.namedClusterSpecifierPluginConfig().config(); - if (pluginConfig instanceof RlsPluginConfig) { - prefixedName = prefixedClusterSpecifierPluginName( - action.namedClusterSpecifierPluginConfig().name()); - clusters.add(prefixedName); - rlsPluginConfigMap.put(prefixedName, (RlsPluginConfig) pluginConfig); - } + clusterNameMap.put(prefixedName, weightedCluster.name()); + filterList.add(createFilters(filterConfigs, virtualHost, route, weightedCluster)); } + routesData.add( + new RouteData(route.routeMatch(), route.routeAction(), filterList.build())); + } else if (action.namedClusterSpecifierPluginConfig() != null) { + PluginConfig pluginConfig = action.namedClusterSpecifierPluginConfig().config(); + if (pluginConfig instanceof RlsPluginConfig) { + prefixedName = prefixedClusterSpecifierPluginName( + action.namedClusterSpecifierPluginConfig().name()); + clusters.add(prefixedName); + rlsPluginConfigMap.put(prefixedName, (RlsPluginConfig) pluginConfig); + } + ClientInterceptor filters = createFilters(filterConfigs, virtualHost, route, null); + routesData.add(new RouteData(route.routeMatch(), route.routeAction(), filters)); } } @@ -796,10 +801,7 @@ private void updateRoutes(List virtualHosts, long httpMaxStreamDura } // Make newly added clusters selectable by config selector and deleted clusters no longer // selectable. - routingConfig = - new RoutingConfig( - httpMaxStreamDurationNano, routes, filterConfigs, - virtualHost.filterConfigOverrides()); + routingConfig = new RoutingConfig(httpMaxStreamDurationNano, routesData.build()); shouldUpdateResult = false; for (String cluster : deletedClusters) { int count = clusterRefs.get(cluster).refCount.decrementAndGet(); @@ -813,6 +815,37 @@ private void updateRoutes(List virtualHosts, long httpMaxStreamDura } } + private ClientInterceptor createFilters( + @Nullable List filterConfigs, + VirtualHost virtualHost, + Route route, + @Nullable ClusterWeight weightedCluster) { + if (filterConfigs == null) { + return new PassthroughClientInterceptor(); + } + Map selectedOverrideConfigs = + new HashMap<>(virtualHost.filterConfigOverrides()); + selectedOverrideConfigs.putAll(route.filterConfigOverrides()); + if (weightedCluster != null) { + selectedOverrideConfigs.putAll(weightedCluster.filterConfigOverrides()); + } + ImmutableList.Builder filterInterceptors = ImmutableList.builder(); + for (NamedFilterConfig namedFilter : filterConfigs) { + FilterConfig filterConfig = namedFilter.filterConfig; + Filter filter = filterRegistry.get(filterConfig.typeUrl()); + if (filter instanceof ClientInterceptorBuilder) { + ClientInterceptor interceptor = ((ClientInterceptorBuilder) filter) + .buildClientInterceptor( + filterConfig, selectedOverrideConfigs.get(namedFilter.name), + scheduler); + if (interceptor != null) { + filterInterceptors.add(interceptor); + } + } + } + return combineInterceptors(filterInterceptors.build()); + } + private void cleanUpRoutes(String error) { if (existingClusters != null) { for (String cluster : existingClusters) { @@ -903,22 +936,50 @@ public void onResourceDoesNotExist(final String resourceName) { */ private static class RoutingConfig { private final long fallbackTimeoutNano; - final List routes; - // Null if HttpFilter is not supported. - @Nullable final List filterChain; - final Map virtualHostOverrideConfig; + final ImmutableList routes; - private static RoutingConfig empty = new RoutingConfig( - 0, Collections.emptyList(), null, Collections.emptyMap()); + private static RoutingConfig empty = new RoutingConfig(0, ImmutableList.of()); - private RoutingConfig( - long fallbackTimeoutNano, List routes, @Nullable List filterChain, - Map virtualHostOverrideConfig) { + private RoutingConfig(long fallbackTimeoutNano, ImmutableList routes) { this.fallbackTimeoutNano = fallbackTimeoutNano; - this.routes = routes; - checkArgument(filterChain == null || !filterChain.isEmpty(), "filterChain is empty"); - this.filterChain = filterChain == null ? null : Collections.unmodifiableList(filterChain); - this.virtualHostOverrideConfig = Collections.unmodifiableMap(virtualHostOverrideConfig); + this.routes = checkNotNull(routes, "routes"); + } + } + + static final class RouteData { + final RouteMatch routeMatch; + /** null implies non-forwarding action. */ + @Nullable + final RouteAction routeAction; + /** + * Only one of these interceptors should be used per-RPC. There are only multiple values in the + * list for weighted clusters, in which case the order of the list mirrors the weighted + * clusters. + */ + final ImmutableList filterChoices; + + RouteData(RouteMatch routeMatch, @Nullable RouteAction routeAction, ClientInterceptor filter) { + this(routeMatch, routeAction, ImmutableList.of(filter)); + } + + RouteData( + RouteMatch routeMatch, + @Nullable RouteAction routeAction, + ImmutableList filterChoices) { + this.routeMatch = checkNotNull(routeMatch, "routeMatch"); + checkArgument( + routeAction == null || !filterChoices.isEmpty(), + "filter may be empty only for non-forwarding action"); + this.routeAction = routeAction; + if (routeAction != null && routeAction.weightedClusters() != null) { + checkArgument( + routeAction.weightedClusters().size() == filterChoices.size(), + "filter choices must match size of weighted clusters"); + } + for (ClientInterceptor filter : filterChoices) { + checkNotNull(filter, "entry in filterChoices is null"); + } + this.filterChoices = checkNotNull(filterChoices, "filterChoices"); } } diff --git a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java index ddd244c8551..3ca240ab7c7 100644 --- a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java @@ -92,7 +92,7 @@ public void testClientInterceptor_createsAndReusesCachedCredentials() { GcpAuthenticationFilter filter = new GcpAuthenticationFilter(); // Create interceptor - ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null, null); + ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); // Mock channel and capture CallOptions diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 654e85143b8..5b9fdda1127 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -113,7 +113,6 @@ import io.grpc.ClientInterceptor; import io.grpc.EquivalentAddressGroup; import io.grpc.InsecureChannelCredentials; -import io.grpc.LoadBalancer; import io.grpc.LoadBalancerRegistry; import io.grpc.Status.Code; import io.grpc.internal.JsonUtil; @@ -1266,7 +1265,6 @@ public ConfigOrError parseFilterConfigOverride( @Override public ClientInterceptor buildClientInterceptor(FilterConfig config, @Nullable FilterConfig overrideConfig, - LoadBalancer.PickSubchannelArgs args, ScheduledExecutorService scheduler) { return null; } From 04f1cc5845fc52eaf1020c74cea6516d49205df2 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 30 Jan 2025 13:27:02 -0800 Subject: [PATCH 165/591] xds: Make XdsNR.RoutingConfig.empty a constant The field was made final in 4b52639aa but was soon reverted in 3ebb3e192 because of what I assume was a bad merge conflict resolution. The field has contained an immutable object since its introduction in d25f5acf1, so it is pretty likely to remain a constant in the future. --- xds/src/main/java/io/grpc/xds/XdsNameResolver.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 5ac53763373..2b4f206aef1 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -129,7 +129,7 @@ final class XdsNameResolver extends NameResolver { private final long randomChannelId; private final MetricRecorder metricRecorder; - private volatile RoutingConfig routingConfig = RoutingConfig.empty; + private volatile RoutingConfig routingConfig = RoutingConfig.EMPTY; private Listener2 listener; private ObjectPool xdsClientPool; private XdsClient xdsClient; @@ -856,7 +856,7 @@ private void cleanUpRoutes(String error) { } existingClusters = null; } - routingConfig = RoutingConfig.empty; + routingConfig = RoutingConfig.EMPTY; // Without addresses the default LB (normally pick_first) should become TRANSIENT_FAILURE, and // the config selector handles the error message itself. Once the LB API allows providing // failure information for addresses yet still providing a service config, the config seector @@ -938,7 +938,7 @@ private static class RoutingConfig { private final long fallbackTimeoutNano; final ImmutableList routes; - private static RoutingConfig empty = new RoutingConfig(0, ImmutableList.of()); + private static final RoutingConfig EMPTY = new RoutingConfig(0, ImmutableList.of()); private RoutingConfig(long fallbackTimeoutNano, ImmutableList routes) { this.fallbackTimeoutNano = fallbackTimeoutNano; From b1bc0a9d240c2cc6ccf9cf02543c9d9118132c14 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 30 Jan 2025 11:21:26 -0800 Subject: [PATCH 166/591] alts: Add ClientCall support to AltsContextUtil This adds a createFrom(Attributes) to mirror the check(Attributes) added in ba8ab79. It also adds conveniences for ClientCall for both createFrom() and check(). This allows getting peer information from ClientCall and CallCredentials.RequestInfo, as was already available from ServerCall. The tests were reworked to test the Attribute-based methods and then only basic tests for client/server. Fixes #11042 --- .../java/io/grpc/alts/AltsContextUtil.java | 37 ++++++++- .../io/grpc/alts/AltsContextUtilTest.java | 77 +++++++++++++++---- 2 files changed, 95 insertions(+), 19 deletions(-) diff --git a/alts/src/main/java/io/grpc/alts/AltsContextUtil.java b/alts/src/main/java/io/grpc/alts/AltsContextUtil.java index 5f2ce353761..f45179bbd91 100644 --- a/alts/src/main/java/io/grpc/alts/AltsContextUtil.java +++ b/alts/src/main/java/io/grpc/alts/AltsContextUtil.java @@ -17,6 +17,7 @@ package io.grpc.alts; import io.grpc.Attributes; +import io.grpc.ClientCall; import io.grpc.ExperimentalApi; import io.grpc.ServerCall; import io.grpc.alts.internal.AltsInternalContext; @@ -29,14 +30,36 @@ public final class AltsContextUtil { private AltsContextUtil() {} /** - * Creates a {@link AltsContext} from ALTS context information in the {@link ServerCall}. + * Creates an {@link AltsContext} from ALTS context information in the {@link ServerCall}. * * @param call the {@link ServerCall} containing the ALTS information * @return the created {@link AltsContext} * @throws IllegalArgumentException if the {@link ServerCall} has no ALTS information. */ public static AltsContext createFrom(ServerCall call) { - Object authContext = call.getAttributes().get(AltsProtocolNegotiator.AUTH_CONTEXT_KEY); + return createFrom(call.getAttributes()); + } + + /** + * Creates an {@link AltsContext} from ALTS context information in the {@link ClientCall}. + * + * @param call the {@link ClientCall} containing the ALTS information + * @return the created {@link AltsContext} + * @throws IllegalArgumentException if the {@link ClientCall} has no ALTS information. + */ + public static AltsContext createFrom(ClientCall call) { + return createFrom(call.getAttributes()); + } + + /** + * Creates an {@link AltsContext} from ALTS context information in the {@link Attributes}. + * + * @param attributes the {@link Attributes} containing the ALTS information + * @return the created {@link AltsContext} + * @throws IllegalArgumentException if the {@link Attributes} has no ALTS information. + */ + public static AltsContext createFrom(Attributes attributes) { + Object authContext = attributes.get(AltsProtocolNegotiator.AUTH_CONTEXT_KEY); if (!(authContext instanceof AltsInternalContext)) { throw new IllegalArgumentException("No ALTS context information found"); } @@ -53,6 +76,16 @@ public static boolean check(ServerCall call) { return check(call.getAttributes()); } + /** + * Checks if the {@link ClientCall} contains ALTS information. + * + * @param call the {@link ClientCall} to check + * @return true, if the {@link ClientCall} contains ALTS information and false otherwise. + */ + public static boolean check(ClientCall call) { + return check(call.getAttributes()); + } + /** * Checks if the {@link Attributes} contains ALTS information. * diff --git a/alts/src/test/java/io/grpc/alts/AltsContextUtilTest.java b/alts/src/test/java/io/grpc/alts/AltsContextUtilTest.java index 6fd2d840d45..675fa29fc99 100644 --- a/alts/src/test/java/io/grpc/alts/AltsContextUtilTest.java +++ b/alts/src/test/java/io/grpc/alts/AltsContextUtilTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.when; import io.grpc.Attributes; +import io.grpc.ClientCall; import io.grpc.ServerCall; import io.grpc.alts.AltsContext.SecurityLevel; import io.grpc.alts.internal.AltsInternalContext; @@ -37,27 +38,38 @@ /** Unit tests for {@link AltsContextUtil}. */ @RunWith(JUnit4.class) public class AltsContextUtilTest { - - private final ServerCall call = mock(ServerCall.class); - @Test public void check_noAttributeValue() { - when(call.getAttributes()).thenReturn(Attributes.newBuilder().build()); + assertFalse(AltsContextUtil.check(Attributes.newBuilder().build())); + } - assertFalse(AltsContextUtil.check(call)); + @Test + public void check_unexpectedAttributeValueType() { + assertFalse(AltsContextUtil.check(Attributes.newBuilder() + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new Object()) + .build())); } @Test - public void contains_unexpectedAttributeValueType() { + public void check_altsInternalContext() { + assertTrue(AltsContextUtil.check(Attributes.newBuilder() + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, AltsInternalContext.getDefaultInstance()) + .build())); + } + + @Test + public void checkServer_altsInternalContext() { + ServerCall call = mock(ServerCall.class); when(call.getAttributes()).thenReturn(Attributes.newBuilder() - .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new Object()) + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, AltsInternalContext.getDefaultInstance()) .build()); - assertFalse(AltsContextUtil.check(call)); + assertTrue(AltsContextUtil.check(call)); } @Test - public void contains_altsInternalContext() { + public void checkClient_altsInternalContext() { + ClientCall call = mock(ClientCall.class); when(call.getAttributes()).thenReturn(Attributes.newBuilder() .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, AltsInternalContext.getDefaultInstance()) .build()); @@ -66,26 +78,57 @@ public void contains_altsInternalContext() { } @Test - public void from_altsInternalContext() { + public void createFrom_altsInternalContext() { HandshakerResult handshakerResult = HandshakerResult.newBuilder() .setPeerIdentity(Identity.newBuilder().setServiceAccount("remote@peer")) .setLocalIdentity(Identity.newBuilder().setServiceAccount("local@peer")) .build(); - when(call.getAttributes()).thenReturn(Attributes.newBuilder() - .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new AltsInternalContext(handshakerResult)) - .build()); - AltsContext context = AltsContextUtil.createFrom(call); + AltsContext context = AltsContextUtil.createFrom(Attributes.newBuilder() + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new AltsInternalContext(handshakerResult)) + .build()); assertEquals("remote@peer", context.getPeerServiceAccount()); assertEquals("local@peer", context.getLocalServiceAccount()); assertEquals(SecurityLevel.INTEGRITY_AND_PRIVACY, context.getSecurityLevel()); } @Test(expected = IllegalArgumentException.class) - public void from_noAttributeValue() { - when(call.getAttributes()).thenReturn(Attributes.newBuilder().build()); + public void createFrom_noAttributeValue() { + AltsContextUtil.createFrom(Attributes.newBuilder().build()); + } - AltsContextUtil.createFrom(call); + @Test + public void createFromServer_altsInternalContext() { + HandshakerResult handshakerResult = + HandshakerResult.newBuilder() + .setPeerIdentity(Identity.newBuilder().setServiceAccount("remote@peer")) + .setLocalIdentity(Identity.newBuilder().setServiceAccount("local@peer")) + .build(); + + ServerCall call = mock(ServerCall.class); + when(call.getAttributes()).thenReturn(Attributes.newBuilder() + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new AltsInternalContext(handshakerResult)) + .build()); + + AltsContext context = AltsContextUtil.createFrom(call); + assertEquals("remote@peer", context.getPeerServiceAccount()); + } + + @Test + public void createFromClient_altsInternalContext() { + HandshakerResult handshakerResult = + HandshakerResult.newBuilder() + .setPeerIdentity(Identity.newBuilder().setServiceAccount("remote@peer")) + .setLocalIdentity(Identity.newBuilder().setServiceAccount("local@peer")) + .build(); + + ClientCall call = mock(ClientCall.class); + when(call.getAttributes()).thenReturn(Attributes.newBuilder() + .set(AltsProtocolNegotiator.AUTH_CONTEXT_KEY, new AltsInternalContext(handshakerResult)) + .build()); + + AltsContext context = AltsContextUtil.createFrom(call); + assertEquals("remote@peer", context.getPeerServiceAccount()); } } From 4a10a381666ff0025057beb9359778f038bab6b9 Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Tue, 4 Feb 2025 20:49:30 +0200 Subject: [PATCH 167/591] servlet: remove 4096 min buffer size Similar to 7153ff8 --- servlet/src/main/java/io/grpc/servlet/ServletServerStream.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java b/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java index 5d11abf0f90..bc367587cc6 100644 --- a/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java +++ b/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java @@ -168,7 +168,7 @@ private static final class ByteArrayWritableBuffer implements WritableBuffer { private int index; ByteArrayWritableBuffer(int capacityHint) { - this.bytes = new byte[min(1024 * 1024, max(4096, capacityHint))]; + this.bytes = new byte[min(1024 * 1024, capacityHint)]; this.capacity = bytes.length; } From ea3f644eefc0afb7ab66806e33a6b5189ff8f5f3 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 4 Feb 2025 10:51:13 -0800 Subject: [PATCH 168/591] Replace Kokoro ARM build with GitHub Actions The Kokoro aarch64 build runs on x86 with an emulator, and has always been flaky due to the slow execution speed. At present it is continually failing due to deadline exceededs. GitHub Actions is running on aarch64 hardware, so is much faster (4 minutes vs 30 minutes, without including the speedup from GitHub Action's caching). --- .github/workflows/branch-testing.yml | 41 ++++++++++++++++++++ buildscripts/kokoro/linux_aarch64.cfg | 13 ------- buildscripts/kokoro/linux_aarch64.sh | 17 -------- buildscripts/qemu_helpers/prepare_qemu.sh | 26 ------------- buildscripts/run_arm64_tests_in_docker.sh | 47 ----------------------- 5 files changed, 41 insertions(+), 103 deletions(-) create mode 100644 .github/workflows/branch-testing.yml delete mode 100644 buildscripts/kokoro/linux_aarch64.cfg delete mode 100755 buildscripts/kokoro/linux_aarch64.sh delete mode 100755 buildscripts/qemu_helpers/prepare_qemu.sh delete mode 100755 buildscripts/run_arm64_tests_in_docker.sh diff --git a/.github/workflows/branch-testing.yml b/.github/workflows/branch-testing.yml new file mode 100644 index 00000000000..ece8ec4cd58 --- /dev/null +++ b/.github/workflows/branch-testing.yml @@ -0,0 +1,41 @@ +name: GitHub Actions Branch Testing + +on: + push: + branches: + - master + - 'v1.*' + schedule: + - cron: '54 19 * * SUN' # weekly at a "random" time + +permissions: + contents: read + +jobs: + arm64: + runs-on: ubuntu-24.04-arm + strategy: + matrix: + jre: [17] + fail-fast: false # Should swap to true if we grow a large matrix + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.jre }} + distribution: 'temurin' + + - name: Gradle cache + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build + run: ./gradlew -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs='-Xmx1g' -PskipAndroid=true -PskipCodegen=true -PerrorProne=false test + diff --git a/buildscripts/kokoro/linux_aarch64.cfg b/buildscripts/kokoro/linux_aarch64.cfg deleted file mode 100644 index 325d910c5ea..00000000000 --- a/buildscripts/kokoro/linux_aarch64.cfg +++ /dev/null @@ -1,13 +0,0 @@ -# Config file for internal CI - -# Location of the continuous shell script in repository. -build_file: "grpc-java/buildscripts/kokoro/linux_aarch64.sh" -timeout_mins: 60 - -action { - define_artifacts { - regex: "github/grpc-java/**/build/test-results/**/sponge_log.xml" - regex: "github/grpc-java/mvn-artifacts/**" - regex: "github/grpc-java/artifacts/**" - } -} diff --git a/buildscripts/kokoro/linux_aarch64.sh b/buildscripts/kokoro/linux_aarch64.sh deleted file mode 100755 index f4a1292efb5..00000000000 --- a/buildscripts/kokoro/linux_aarch64.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -veux -o pipefail - -if [[ -f /VERSION ]]; then - cat /VERSION -fi - -readonly GRPC_JAVA_DIR="$(cd "$(dirname "$0")"/../.. && pwd)" - -. "$GRPC_JAVA_DIR"/buildscripts/kokoro/kokoro.sh -trap spongify_logs EXIT - -cd github/grpc-java - -buildscripts/qemu_helpers/prepare_qemu.sh - -buildscripts/run_arm64_tests_in_docker.sh diff --git a/buildscripts/qemu_helpers/prepare_qemu.sh b/buildscripts/qemu_helpers/prepare_qemu.sh deleted file mode 100755 index be0d733344f..00000000000 --- a/buildscripts/qemu_helpers/prepare_qemu.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# -# Setup and configure qemu userspace emulator on kokoro worker so that we can seamlessly emulate processes running -# inside docker containers. - -set -ex - -# show pre-existing qemu registration -cat /proc/sys/fs/binfmt_misc/qemu-aarch64 || true - -# Kokoro ubuntu1604 workers have already qemu-user and qemu-user-static packages installed, but it's and old version that: -# * prints warning about some syscalls (e.g "qemu: Unsupported syscall: 278") -# * doesn't register with binfmt_misc with the persistent ("F") flag we need (see below) -# -# To overcome the above limitations, we use the https://github.com/multiarch/qemu-user-static -# docker image to provide a new enough version of qemu-user-static and register it with -# the desired binfmt_misc flags. The most important flag we need is "F" (set by "--persistent yes"), -# which allows the qemu-aarch64-static binary to be loaded eagerly at the time of registration with binfmt_misc. -# That way, we can emulate aarch64 binaries running inside docker containers transparently, without needing the emulator -# binary to be accessible from the docker image we're emulating. -# Note that on newer distributions (such as glinux), simply "apt install qemu-user-static" is sufficient -# to install qemu-user-static with the right flags. -docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset --credential yes --persistent yes - -# Print current qemu reqistration to make sure everything is setup correctly. -cat /proc/sys/fs/binfmt_misc/qemu-aarch64 diff --git a/buildscripts/run_arm64_tests_in_docker.sh b/buildscripts/run_arm64_tests_in_docker.sh deleted file mode 100755 index 76ef64ac6b6..00000000000 --- a/buildscripts/run_arm64_tests_in_docker.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -set -ex - -readonly grpc_java_dir="$(dirname "$(readlink -f "$0")")/.." - -if [[ -t 0 ]]; then - DOCKER_ARGS="-it" -else - # The input device on kokoro is not a TTY, so -it does not work. - DOCKER_ARGS= -fi - - -cat <> "${grpc_java_dir}/gradle.properties" -skipAndroid=true -skipCodegen=true -org.gradle.parallel=true -org.gradle.jvmargs=-Xmx1024m -EOF - -export JAVA_OPTS="-Duser.home=/grpc-java/.current-user-home -Djava.util.prefs.userRoot=/grpc-java/.current-user-home/.java/.userPrefs" - -# build under x64 docker image to save time over building everything under -# aarch64 emulator. We've already built and tested the protoc binaries -# so for the rest of the build we will be using "-PskipCodegen=true" -# avoid further complicating the build. -docker run $DOCKER_ARGS --rm=true -v "${grpc_java_dir}":/grpc-java -w /grpc-java \ - --user "$(id -u):$(id -g)" -e JAVA_OPTS \ - openjdk:11-jdk-slim-buster \ - ./gradlew build -x test - -# Build and run java tests under aarch64 image. -# To be able to run this docker container on x64 machine, one needs to have -# qemu-user-static properly registered with binfmt_misc. -# The most important flag binfmt_misc flag we need is "F" (set by "--persistent yes"), -# which allows the qemu-aarch64-static binary to be loaded eagerly at the time of registration with binfmt_misc. -# That way, we can emulate aarch64 binaries running inside docker containers transparently, without needing the emulator -# binary to be accessible from the docker image we're emulating. -# Note that on newer distributions (such as glinux), simply "apt install qemu-user-static" is sufficient -# to install qemu-user-static with the right flags. -# A note on the "docker run" args used: -# - run docker container under current user's UID to avoid polluting the workspace -# - set the user.home property to avoid creating a "?" directory under grpc-java -docker run $DOCKER_ARGS --rm=true -v "${grpc_java_dir}":/grpc-java -w /grpc-java \ - --user "$(id -u):$(id -g)" -e JAVA_OPTS \ - arm64v8/openjdk:11-jdk-slim-buster \ - ./gradlew build From 199a7ea3e828fc0fc4ce0de48666b222bb22218d Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 5 Feb 2025 10:37:22 -0800 Subject: [PATCH 169/591] xds: Improve XdsNR's selectConfig() variable handling The variables from the do-while are no longer initialized to let the compiler verify that the loop sets each. Unnecessary comparisons to null are also removed and is more obvious as the variables are never set to null. Added a minor optimization of computing the RPCs path once instead of once for each route. The variable declarations were also sorted to match their initialization order. This does fix an unlikely bug where if the old code could successfully matched a route but fail to retain the cluster, then when trying a second time if the route was _not_ matched it would re-use the prior route and thus infinite-loop failing to retain that same cluster. It also adds a missing cast to unsigned long for a uint32 weight. The old code would detect if the _sum_ was negative, but a weight using 32 bits would have been negative and never selected. --- .../main/java/io/grpc/xds/VirtualHost.java | 5 +-- .../java/io/grpc/xds/XdsNameResolver.java | 34 +++++++++++-------- .../grpc/xds/XdsRouteConfigureResource.java | 9 +++-- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/VirtualHost.java b/xds/src/main/java/io/grpc/xds/VirtualHost.java index 7dfa0b34a35..5cc979984c6 100644 --- a/xds/src/main/java/io/grpc/xds/VirtualHost.java +++ b/xds/src/main/java/io/grpc/xds/VirtualHost.java @@ -218,12 +218,13 @@ private static RouteAction create( abstract static class ClusterWeight { abstract String name(); - abstract int weight(); + abstract long weight(); abstract ImmutableMap filterConfigOverrides(); static ClusterWeight create( - String name, int weight, Map filterConfigOverrides) { + String name, long weight, Map filterConfigOverrides) { + checkArgument(weight >= 0, "weight must not be negative"); return new AutoValue_VirtualHost_Route_RouteAction_ClusterWeight( name, weight, ImmutableMap.copyOf(filterConfigOverrides)); } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 2b4f206aef1..21f5d5efce6 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -384,17 +384,17 @@ static boolean matchHostName(String hostName, String pattern) { private final class ConfigSelector extends InternalConfigSelector { @Override public Result selectConfig(PickSubchannelArgs args) { - String cluster = null; - ClientInterceptor filters = null; // null iff cluster is null - RouteData selectedRoute = null; RoutingConfig routingCfg; + RouteData selectedRoute; + String cluster; + ClientInterceptor filters; Metadata headers = args.getHeaders(); + String path = "/" + args.getMethodDescriptor().getFullMethodName(); do { routingCfg = routingConfig; + selectedRoute = null; for (RouteData route : routingCfg.routes) { - if (RoutingUtils.matchRoute( - route.routeMatch, "/" + args.getMethodDescriptor().getFullMethodName(), - headers, random)) { + if (RoutingUtils.matchRoute(route.routeMatch, path, headers, random)) { selectedRoute = route; break; } @@ -412,13 +412,14 @@ public Result selectConfig(PickSubchannelArgs args) { cluster = prefixedClusterName(action.cluster()); filters = selectedRoute.filterChoices.get(0); } else if (action.weightedClusters() != null) { + // XdsRouteConfigureResource verifies the total weight will not be 0 or exceed uint32 long totalWeight = 0; for (ClusterWeight weightedCluster : action.weightedClusters()) { totalWeight += weightedCluster.weight(); } long select = random.nextLong(totalWeight); long accumulator = 0; - for (int i = 0; i < action.weightedClusters().size(); i++) { + for (int i = 0; ; i++) { ClusterWeight weightedCluster = action.weightedClusters().get(i); accumulator += weightedCluster.weight(); if (select < accumulator) { @@ -431,13 +432,16 @@ public Result selectConfig(PickSubchannelArgs args) { cluster = prefixedClusterSpecifierPluginName(action.namedClusterSpecifierPluginConfig().name()); filters = selectedRoute.filterChoices.get(0); + } else { + // updateRoutes() discards routes with unknown actions + throw new AssertionError(); } } while (!retainCluster(cluster)); + + final RouteAction routeAction = selectedRoute.routeAction; Long timeoutNanos = null; if (enableTimeout) { - if (selectedRoute != null) { - timeoutNanos = selectedRoute.routeAction.timeoutNano(); - } + timeoutNanos = routeAction.timeoutNano(); if (timeoutNanos == null) { timeoutNanos = routingCfg.fallbackTimeoutNano; } @@ -445,8 +449,7 @@ public Result selectConfig(PickSubchannelArgs args) { timeoutNanos = null; } } - RetryPolicy retryPolicy = - selectedRoute == null ? null : selectedRoute.routeAction.retryPolicy(); + RetryPolicy retryPolicy = routeAction.retryPolicy(); // TODO(chengyuanzhang): avoid service config generation and parsing for each call. Map rawServiceConfig = generateServiceConfigWithMethodConfig(timeoutNanos, retryPolicy); @@ -459,8 +462,7 @@ public Result selectConfig(PickSubchannelArgs args) { "Failed to parse service config (method config)")); } final String finalCluster = cluster; - final long hash = generateHash(selectedRoute.routeAction.hashPolicies(), headers); - RouteData finalSelectedRoute = selectedRoute; + final long hash = generateHash(routeAction.hashPolicies(), headers); class ClusterSelectionInterceptor implements ClientInterceptor { @Override public ClientCall interceptCall( @@ -469,7 +471,7 @@ public ClientCall interceptCall( CallOptions callOptionsForCluster = callOptions.withOption(CLUSTER_SELECTION_KEY, finalCluster) .withOption(RPC_HASH_KEY, hash); - if (finalSelectedRoute.routeAction.autoHostRewrite()) { + if (routeAction.autoHostRewrite()) { callOptionsForCluster = callOptionsForCluster.withOption(AUTO_HOST_REWRITE_KEY, true); } return new SimpleForwardingClientCall( @@ -757,6 +759,8 @@ private void updateRoutes(List virtualHosts, long httpMaxStreamDura } ClientInterceptor filters = createFilters(filterConfigs, virtualHost, route, null); routesData.add(new RouteData(route.routeMatch(), route.routeAction(), filters)); + } else { + // Discard route } } diff --git a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java index 587c7a437ad..c5ca8d45cb3 100644 --- a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java @@ -496,8 +496,9 @@ static StructOrError parseRouteAction( return StructOrError.fromError("RouteAction contains invalid ClusterWeight: " + clusterWeightOrError.getErrorDetail()); } - clusterWeightSum += clusterWeight.getWeight().getValue(); - weightedClusters.add(clusterWeightOrError.getStruct()); + ClusterWeight parsedWeight = clusterWeightOrError.getStruct(); + clusterWeightSum += parsedWeight.weight(); + weightedClusters.add(parsedWeight); } if (clusterWeightSum <= 0) { return StructOrError.fromError("Sum of cluster weights should be above 0."); @@ -609,7 +610,9 @@ static StructOrError parseClusterWe + overrideConfigs.getErrorDetail()); } return StructOrError.fromStruct(VirtualHost.Route.RouteAction.ClusterWeight.create( - proto.getName(), proto.getWeight().getValue(), overrideConfigs.getStruct())); + proto.getName(), + Integer.toUnsignedLong(proto.getWeight().getValue()), + overrideConfigs.getStruct())); } @Nullable // null if the plugin is not supported, but it's marked as optional. From 3142928fa3a530a2fe500bbc6a0873619238f3ee Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Wed, 5 Feb 2025 21:10:39 +0200 Subject: [PATCH 170/591] use gradle java-platform in grpc-bom; Fixes #5530 (#11875) * use gradle java-platform in grpc-bom; Fixes #5530 * fix withXml * explicitly exclude grpc-compiler --- bom/build.gradle | 44 ++++++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/bom/build.gradle b/bom/build.gradle index 1b1f98cff18..f7f3918372f 100644 --- a/bom/build.gradle +++ b/bom/build.gradle @@ -1,40 +1,32 @@ plugins { + id 'java-platform' id "maven-publish" } description = 'gRPC: BOM' +gradle.projectsEvaluated { + def projectsToInclude = rootProject.subprojects.findAll { + return it.name != 'grpc-compiler' + && it.plugins.hasPlugin('java') + && it.plugins.hasPlugin('maven-publish') + && it.tasks.findByName('publishMavenPublicationToMavenRepository')?.enabled + } + dependencies { + constraints { + projectsToInclude.each { api it } + } + } +} + publishing { publications { maven(MavenPublication) { - // remove all other artifacts since BOM doesn't generates any Jar - artifacts = [] - + from components.javaPlatform pom.withXml { - // Generate bom using subprojects - def internalProjects = [ - project.name, - 'grpc-compiler', - ] - - def dependencyManagement = asNode().appendNode('dependencyManagement') - def dependencies = dependencyManagement.appendNode('dependencies') - rootProject.subprojects.each { subproject -> - if (internalProjects.contains(subproject.name)) { - return - } - if (!subproject.hasProperty('publishMavenPublicationToMavenRepository')) { - return - } - if (!subproject.publishMavenPublicationToMavenRepository.enabled) { - return - } - def dependencyNode = dependencies.appendNode('dependency') - dependencyNode.appendNode('groupId', subproject.group) - dependencyNode.appendNode('artifactId', subproject.name) - dependencyNode.appendNode('version', subproject.version) - } + def dependencies = asNode().dependencyManagement.dependencies.last() // add protoc gen (produced by grpc-compiler with different artifact name) + // not sure how to express "pom" in gradle, kept in XML def dependencyNode = dependencies.appendNode('dependency') dependencyNode.appendNode('groupId', project.group) dependencyNode.appendNode('artifactId', 'protoc-gen-grpc-java') From 44e92e2c2c72cd9fc03b1fda1d5335ddf8a1b618 Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Thu, 6 Feb 2025 03:48:20 +0530 Subject: [PATCH 171/591] core: updates the backoff range as per the A6 redefinition (#11858) * core: updates the backoff range being used from [0, 1] to [0.8, 1.2] as per the A6 redefinition * adds a flag for experimental jitter * xds: Allow FaultFilter's interceptor to be reused This is the only usage of PickSubchannelArgs when creating a filter's ClientInterceptor, and a follow-up commit will remove the argument and actually reuse the interceptors. Other filter's interceptors can already be reused. There doesn't seem to be any significant loss of legibility by making FaultFilter a more ordinary interceptor, but the change does cause the ForwardingClientCall to be present when faultDelay is configured, independent of whether the fault delay ends up being triggered. Reusing interceptors will move more state management out of the RPC path which will be more relevant with RLQS. * netty: Removed 4096 min buffer size (#11856) * netty: Removed 4096 min buffer size * turns the flag in a var for better efficiency --------- Co-authored-by: Eric Anderson --- .../io/grpc/internal/RetriableStream.java | 11 ++- .../grpc/internal/ManagedChannelImplTest.java | 4 +- .../io/grpc/internal/RetriableStreamTest.java | 79 +++++++++---------- .../grpc/testing/integration/RetryTest.java | 10 +-- 4 files changed, 55 insertions(+), 49 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/RetriableStream.java b/core/src/main/java/io/grpc/internal/RetriableStream.java index 85d7bc86584..7765408a627 100644 --- a/core/src/main/java/io/grpc/internal/RetriableStream.java +++ b/core/src/main/java/io/grpc/internal/RetriableStream.java @@ -846,6 +846,15 @@ public void run() { } } + private static final boolean isExperimentalRetryJitterEnabled = GrpcUtil + .getFlag("GRPC_EXPERIMENTAL_XDS_RLS_LB", true); + + public static long intervalWithJitter(long intervalNanos) { + double inverseJitterFactor = isExperimentalRetryJitterEnabled + ? 0.8 * random.nextDouble() + 0.4 : random.nextDouble(); + return (long) (intervalNanos * inverseJitterFactor); + } + private static final class SavedCloseMasterListenerReason { private final Status status; private final RpcProgress progress; @@ -1066,7 +1075,7 @@ private RetryPlan makeRetryDecision(Status status, Metadata trailer) { if (pushbackMillis == null) { if (isRetryableStatusCode) { shouldRetry = true; - backoffNanos = (long) (nextBackoffIntervalNanos * random.nextDouble()); + backoffNanos = intervalWithJitter(nextBackoffIntervalNanos); nextBackoffIntervalNanos = Math.min( (long) (nextBackoffIntervalNanos * retryPolicy.backoffMultiplier), retryPolicy.maxBackoffNanos); diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 98900cecf2b..21ccf1095df 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -3875,7 +3875,7 @@ public double nextDouble() { Status.UNAVAILABLE, PROCESSED, new Metadata()); // in backoff - timer.forwardTime(5, TimeUnit.SECONDS); + timer.forwardTime(6, TimeUnit.SECONDS); assertThat(timer.getPendingTasks()).hasSize(1); verify(mockStream2, never()).start(any(ClientStreamListener.class)); @@ -3894,7 +3894,7 @@ public double nextDouble() { assertEquals("Channel shutdown invoked", statusCaptor.getValue().getDescription()); // backoff ends - timer.forwardTime(5, TimeUnit.SECONDS); + timer.forwardTime(6, TimeUnit.SECONDS); assertThat(timer.getPendingTasks()).isEmpty(); verify(mockStream2).start(streamListenerCaptor.capture()); verify(mockLoadBalancer, never()).shutdown(); diff --git a/core/src/test/java/io/grpc/internal/RetriableStreamTest.java b/core/src/test/java/io/grpc/internal/RetriableStreamTest.java index 658ed70a135..9b1ec343bb7 100644 --- a/core/src/test/java/io/grpc/internal/RetriableStreamTest.java +++ b/core/src/test/java/io/grpc/internal/RetriableStreamTest.java @@ -147,6 +147,17 @@ public double nextDouble() { private final ChannelBufferMeter channelBufferUsed = new ChannelBufferMeter(); private final FakeClock fakeClock = new FakeClock(); + private static long calculateBackoffWithRetries(int retryCount) { + // Calculate the exponential backoff delay with jitter + double exponent = retryCount > 0 ? Math.pow(BACKOFF_MULTIPLIER, retryCount) : 1; + long delay = (long) (INITIAL_BACKOFF_IN_SECONDS * exponent); + return RetriableStream.intervalWithJitter(delay); + } + + private static long calculateMaxBackoff() { + return RetriableStream.intervalWithJitter(MAX_BACKOFF_IN_SECONDS); + } + private final class RecordedRetriableStream extends RetriableStream { RecordedRetriableStream(MethodDescriptor method, Metadata headers, ChannelBufferMeter channelBufferUsed, long perRpcBufferLimit, long channelBufferLimit, @@ -307,7 +318,7 @@ public Void answer(InvocationOnMock in) { retriableStream.sendMessage("msg1 during backoff1"); retriableStream.sendMessage("msg2 during backoff1"); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM) - 1L, TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0) - 1L, TimeUnit.SECONDS); inOrder.verifyNoMoreInteractions(); assertEquals(1, fakeClock.numPendingTasks()); fakeClock.forwardTime(1L, TimeUnit.SECONDS); @@ -364,9 +375,7 @@ public Void answer(InvocationOnMock in) { retriableStream.sendMessage("msg2 during backoff2"); retriableStream.sendMessage("msg3 during backoff2"); - fakeClock.forwardTime( - (long) (INITIAL_BACKOFF_IN_SECONDS * BACKOFF_MULTIPLIER * FAKE_RANDOM) - 1L, - TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(1) - 1L, TimeUnit.SECONDS); inOrder.verifyNoMoreInteractions(); assertEquals(1, fakeClock.numPendingTasks()); fakeClock.forwardTime(1L, TimeUnit.SECONDS); @@ -459,7 +468,7 @@ public void retry_headersRead_cancel() { sublistenerCaptor1.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); ArgumentCaptor sublistenerCaptor2 = ArgumentCaptor.forClass(ClientStreamListener.class); @@ -518,7 +527,7 @@ public void retry_headersRead_closed() { doReturn(mockStream2).when(retriableStreamRecorder).newSubstream(1); sublistenerCaptor1.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); ArgumentCaptor sublistenerCaptor2 = ArgumentCaptor.forClass(ClientStreamListener.class); @@ -584,7 +593,7 @@ public void retry_cancel_closed() { doReturn(mockStream2).when(retriableStreamRecorder).newSubstream(1); sublistenerCaptor1.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); ArgumentCaptor sublistenerCaptor2 = ArgumentCaptor.forClass(ClientStreamListener.class); @@ -687,7 +696,7 @@ public void retry_unretriableClosed_cancel() { doReturn(mockStream2).when(retriableStreamRecorder).newSubstream(1); sublistenerCaptor1.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); ArgumentCaptor sublistenerCaptor2 = ArgumentCaptor.forClass(ClientStreamListener.class); @@ -821,7 +830,7 @@ public boolean isReady() { // send more requests during backoff retriableStream.request(789); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); inOrder.verify(mockStream2).start(sublistenerCaptor2.get()); inOrder.verify(mockStream2).request(3); @@ -875,7 +884,7 @@ public void request(int numMessages) { doReturn(mockStream2).when(retriableStreamRecorder).newSubstream(1); sublistenerCaptor1.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); inOrder.verify(mockStream2).start(sublistenerCaptor2.capture()); inOrder.verify(mockStream2).request(3); @@ -920,7 +929,7 @@ public void start(ClientStreamListener listener) { doReturn(mockStream2).when(retriableStreamRecorder).newSubstream(1); sublistenerCaptor1.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); inOrder.verify(mockStream2).start(sublistenerCaptor2.capture()); inOrder.verify(retriableStreamRecorder).postCommit(); @@ -1028,7 +1037,7 @@ public boolean isReady() { retriableStream.request(789); readiness.add(retriableStream.isReady()); // expected false b/c in backoff - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); verify(mockStream2).start(any(ClientStreamListener.class)); readiness.add(retriableStream.isReady()); // expected true @@ -1110,7 +1119,7 @@ public void addPrevRetryAttemptsToRespHeaders() { doReturn(mockStream2).when(retriableStreamRecorder).newSubstream(1); sublistenerCaptor1.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); ArgumentCaptor sublistenerCaptor2 = ArgumentCaptor.forClass(ClientStreamListener.class); @@ -1160,13 +1169,12 @@ public void start(ClientStreamListener listener) { listener1.closed( Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); assertEquals(1, fakeClock.numPendingTasks()); // send requests during backoff retriableStream.request(3); - fakeClock.forwardTime( - (long) (INITIAL_BACKOFF_IN_SECONDS * BACKOFF_MULTIPLIER * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(1), TimeUnit.SECONDS); retriableStream.request(1); verify(mockStream1, never()).request(anyInt()); @@ -1207,7 +1215,7 @@ public void start(ClientStreamListener listener) { // retry listener1.closed( Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); verify(mockStream2).start(any(ClientStreamListener.class)); verify(retriableStreamRecorder).postCommit(); @@ -1260,7 +1268,7 @@ public void perRpcBufferLimitExceededDuringBackoff() { bufferSizeTracer.outboundWireSize(2); verify(retriableStreamRecorder, never()).postCommit(); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); verify(mockStream2).start(any(ClientStreamListener.class)); verify(mockStream2).isReady(); @@ -1332,7 +1340,7 @@ public void expBackoff_maxBackoff_maxRetryAttempts() { sublistenerCaptor1.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM) - 1L, TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0) - 1L, TimeUnit.SECONDS); assertEquals(1, fakeClock.numPendingTasks()); fakeClock.forwardTime(1L, TimeUnit.SECONDS); assertEquals(0, fakeClock.numPendingTasks()); @@ -1347,9 +1355,7 @@ public void expBackoff_maxBackoff_maxRetryAttempts() { sublistenerCaptor2.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_2), PROCESSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime( - (long) (INITIAL_BACKOFF_IN_SECONDS * BACKOFF_MULTIPLIER * FAKE_RANDOM) - 1L, - TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(1) - 1L, TimeUnit.SECONDS); assertEquals(1, fakeClock.numPendingTasks()); fakeClock.forwardTime(1L, TimeUnit.SECONDS); assertEquals(0, fakeClock.numPendingTasks()); @@ -1364,10 +1370,7 @@ public void expBackoff_maxBackoff_maxRetryAttempts() { sublistenerCaptor3.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime( - (long) (INITIAL_BACKOFF_IN_SECONDS * BACKOFF_MULTIPLIER * BACKOFF_MULTIPLIER * FAKE_RANDOM) - - 1L, - TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(2) - 1L, TimeUnit.SECONDS); assertEquals(1, fakeClock.numPendingTasks()); fakeClock.forwardTime(1L, TimeUnit.SECONDS); assertEquals(0, fakeClock.numPendingTasks()); @@ -1382,7 +1385,7 @@ public void expBackoff_maxBackoff_maxRetryAttempts() { sublistenerCaptor4.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_2), PROCESSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime((long) (MAX_BACKOFF_IN_SECONDS * FAKE_RANDOM) - 1L, TimeUnit.SECONDS); + fakeClock.forwardTime(calculateMaxBackoff() - 1L, TimeUnit.SECONDS); assertEquals(1, fakeClock.numPendingTasks()); fakeClock.forwardTime(1L, TimeUnit.SECONDS); assertEquals(0, fakeClock.numPendingTasks()); @@ -1397,7 +1400,7 @@ public void expBackoff_maxBackoff_maxRetryAttempts() { sublistenerCaptor5.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_2), PROCESSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime((long) (MAX_BACKOFF_IN_SECONDS * FAKE_RANDOM) - 1L, TimeUnit.SECONDS); + fakeClock.forwardTime(calculateMaxBackoff() - 1L, TimeUnit.SECONDS); assertEquals(1, fakeClock.numPendingTasks()); fakeClock.forwardTime(1L, TimeUnit.SECONDS); assertEquals(0, fakeClock.numPendingTasks()); @@ -1480,7 +1483,7 @@ public void pushback() { sublistenerCaptor3.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM) - 1L, TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0) - 1L, TimeUnit.SECONDS); assertEquals(1, fakeClock.numPendingTasks()); fakeClock.forwardTime(1L, TimeUnit.SECONDS); assertEquals(0, fakeClock.numPendingTasks()); @@ -1495,9 +1498,7 @@ public void pushback() { sublistenerCaptor4.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_2), PROCESSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime( - (long) (INITIAL_BACKOFF_IN_SECONDS * BACKOFF_MULTIPLIER * FAKE_RANDOM) - 1L, - TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(1) - 1L, TimeUnit.SECONDS); assertEquals(1, fakeClock.numPendingTasks()); fakeClock.forwardTime(1L, TimeUnit.SECONDS); assertEquals(0, fakeClock.numPendingTasks()); @@ -1512,10 +1513,7 @@ public void pushback() { sublistenerCaptor5.getValue().closed( Status.fromCode(RETRIABLE_STATUS_CODE_2), PROCESSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime( - (long) (INITIAL_BACKOFF_IN_SECONDS * BACKOFF_MULTIPLIER * BACKOFF_MULTIPLIER * FAKE_RANDOM) - - 1L, - TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(2) - 1L, TimeUnit.SECONDS); assertEquals(1, fakeClock.numPendingTasks()); fakeClock.forwardTime(1L, TimeUnit.SECONDS); assertEquals(0, fakeClock.numPendingTasks()); @@ -1804,7 +1802,7 @@ public void transparentRetry_onlyOnceOnRefused() { .closed(Status.fromCode(RETRIABLE_STATUS_CODE_1), REFUSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); inOrder.verify(retriableStreamRecorder).newSubstream(1); ArgumentCaptor sublistenerCaptor3 = ArgumentCaptor.forClass(ClientStreamListener.class); @@ -1907,7 +1905,7 @@ public void normalRetry_thenNoTransparentRetry_butNormalRetry() { .closed(Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); inOrder.verify(retriableStreamRecorder).newSubstream(1); ArgumentCaptor sublistenerCaptor2 = ArgumentCaptor.forClass(ClientStreamListener.class); @@ -1923,8 +1921,7 @@ public void normalRetry_thenNoTransparentRetry_butNormalRetry() { .closed(Status.fromCode(RETRIABLE_STATUS_CODE_1), REFUSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime( - (long) (INITIAL_BACKOFF_IN_SECONDS * BACKOFF_MULTIPLIER * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(1), TimeUnit.SECONDS); inOrder.verify(retriableStreamRecorder).newSubstream(2); ArgumentCaptor sublistenerCaptor3 = ArgumentCaptor.forClass(ClientStreamListener.class); @@ -1960,7 +1957,7 @@ public void normalRetry_thenNoTransparentRetry_andNoMoreRetry() { .closed(Status.fromCode(RETRIABLE_STATUS_CODE_1), PROCESSED, new Metadata()); assertEquals(1, fakeClock.numPendingTasks()); - fakeClock.forwardTime((long) (INITIAL_BACKOFF_IN_SECONDS * FAKE_RANDOM), TimeUnit.SECONDS); + fakeClock.forwardTime(calculateBackoffWithRetries(0), TimeUnit.SECONDS); inOrder.verify(retriableStreamRecorder).newSubstream(1); ArgumentCaptor sublistenerCaptor2 = ArgumentCaptor.forClass(ClientStreamListener.class); diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/RetryTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/RetryTest.java index edd2a57ab9d..669ce1c69db 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/RetryTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/RetryTest.java @@ -303,7 +303,7 @@ public void retryUntilBufferLimitExceeded() throws Exception { serverCall.close( Status.UNAVAILABLE.withDescription("original attempt failed"), new Metadata()); - elapseBackoff(10, SECONDS); + elapseBackoff(12, SECONDS); // 2nd attempt received serverCall = serverCalls.poll(5, SECONDS); serverCall.request(2); @@ -348,7 +348,7 @@ public void statsRecorded() throws Exception { Status.UNAVAILABLE.withDescription("original attempt failed"), new Metadata()); assertRpcStatusRecorded(Status.Code.UNAVAILABLE, 1000, 1); - elapseBackoff(10, SECONDS); + elapseBackoff(12, SECONDS); assertRpcStartedRecorded(); assertOutboundMessageRecorded(); serverCall = serverCalls.poll(5, SECONDS); @@ -366,7 +366,7 @@ public void statsRecorded() throws Exception { call.request(1); assertInboundMessageRecorded(); assertInboundWireSizeRecorded(1); - assertRpcStatusRecorded(Status.Code.OK, 12000, 2); + assertRpcStatusRecorded(Status.Code.OK, 14000, 2); assertRetryStatsRecorded(1, 0, 0); } @@ -418,7 +418,7 @@ public void streamClosed(Status status) { Status.UNAVAILABLE.withDescription("original attempt failed"), new Metadata()); assertRpcStatusRecorded(Code.UNAVAILABLE, 5000, 1); - elapseBackoff(10, SECONDS); + elapseBackoff(12, SECONDS); assertRpcStartedRecorded(); assertOutboundMessageRecorded(); serverCall = serverCalls.poll(5, SECONDS); @@ -431,7 +431,7 @@ public void streamClosed(Status status) { streamClosedLatch.countDown(); // The call listener is closed. verify(mockCallListener, timeout(5000)).onClose(any(Status.class), any(Metadata.class)); - assertRpcStatusRecorded(Code.CANCELLED, 17_000, 1); + assertRpcStatusRecorded(Code.CANCELLED, 19_000, 1); assertRetryStatsRecorded(1, 0, 0); } From 90b1c4fe94963fc0263d9b2e9a36470437f763e5 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Fri, 7 Feb 2025 14:16:13 +0530 Subject: [PATCH 172/591] protobuf: Stabilize marshallerWithRecursionLimit (#11884) --- .../src/main/java/io/grpc/protobuf/lite/ProtoLiteUtils.java | 3 +-- protobuf/src/main/java/io/grpc/protobuf/ProtoUtils.java | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/protobuf-lite/src/main/java/io/grpc/protobuf/lite/ProtoLiteUtils.java b/protobuf-lite/src/main/java/io/grpc/protobuf/lite/ProtoLiteUtils.java index 7e33fc67622..ef4b16bd476 100644 --- a/protobuf-lite/src/main/java/io/grpc/protobuf/lite/ProtoLiteUtils.java +++ b/protobuf-lite/src/main/java/io/grpc/protobuf/lite/ProtoLiteUtils.java @@ -89,12 +89,11 @@ public static Marshaller marshaller(T defaultInstance /** * Creates a {@link Marshaller} for protos of the same type as {@code defaultInstance} and a - * custom limit for the recursion depth. Any negative number will leave the limit to its default + * custom limit for the recursion depth. Any negative number will leave the limit as its default * value as defined by the protobuf library. * * @since 1.56.0 */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10108") public static Marshaller marshallerWithRecursionLimit( T defaultInstance, int recursionLimit) { return new MessageMarshaller<>(defaultInstance, recursionLimit); diff --git a/protobuf/src/main/java/io/grpc/protobuf/ProtoUtils.java b/protobuf/src/main/java/io/grpc/protobuf/ProtoUtils.java index 933d598996c..d403789eb5f 100644 --- a/protobuf/src/main/java/io/grpc/protobuf/ProtoUtils.java +++ b/protobuf/src/main/java/io/grpc/protobuf/ProtoUtils.java @@ -18,7 +18,6 @@ import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; -import io.grpc.ExperimentalApi; import io.grpc.Metadata; import io.grpc.MethodDescriptor.Marshaller; import io.grpc.protobuf.lite.ProtoLiteUtils; @@ -58,12 +57,11 @@ public static Marshaller marshaller(final T defaultInstan /** * Creates a {@link Marshaller} for protos of the same type as {@code defaultInstance} and a - * custom limit for the recursion depth. Any negative number will leave the limit to its default + * custom limit for the recursion depth. Any negative number will leave the limit as its default * value as defined by the protobuf library. * * @since 1.56.0 */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10108") public static Marshaller marshallerWithRecursionLimit(T defaultInstance, int recursionLimit) { return ProtoLiteUtils.marshallerWithRecursionLimit(defaultInstance, recursionLimit); From 67fc2e156a8cc5aee79906f461d92ef3a772fe27 Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Fri, 7 Feb 2025 16:33:17 -0800 Subject: [PATCH 173/591] Add new classes for eliminating xds config tears (#11740) * Framework definition to support A74 --- xds/build.gradle | 2 + xds/src/main/java/io/grpc/xds/XdsConfig.java | 192 +++++ .../io/grpc/xds/XdsDependencyManager.java | 769 +++++++++++++++++ .../io/grpc/xds/client/XdsClientImpl.java | 3 +- .../java/io/grpc/xds/ControlPlaneRule.java | 21 +- .../io/grpc/xds/XdsClientFallbackTest.java | 2 +- .../io/grpc/xds/XdsDependencyManagerTest.java | 784 ++++++++++++++++++ .../grpc/xds/XdsTestControlPlaneService.java | 9 +- .../test/java/io/grpc/xds/XdsTestUtils.java | 423 ++++++++++ .../client/CommonBootstrapperTestUtils.java | 6 + 10 files changed, 2186 insertions(+), 25 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/XdsConfig.java create mode 100644 xds/src/main/java/io/grpc/xds/XdsDependencyManager.java create mode 100644 xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java create mode 100644 xds/src/test/java/io/grpc/xds/XdsTestUtils.java diff --git a/xds/build.gradle b/xds/build.gradle index c51fc2819d7..cdd4924cab3 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -46,6 +46,7 @@ dependencies { thirdpartyImplementation project(':grpc-protobuf'), project(':grpc-stub') compileOnly sourceSets.thirdparty.output + testCompileOnly sourceSets.thirdparty.output implementation project(':grpc-stub'), project(':grpc-core'), project(':grpc-util'), @@ -59,6 +60,7 @@ dependencies { libraries.protobuf.java.util def nettyDependency = implementation project(':grpc-netty') + testImplementation project(':grpc-api') testImplementation project(':grpc-rls') testImplementation project(':grpc-inprocess') testImplementation testFixtures(project(':grpc-core')), diff --git a/xds/src/main/java/io/grpc/xds/XdsConfig.java b/xds/src/main/java/io/grpc/xds/XdsConfig.java new file mode 100644 index 00000000000..999ee0d4b0c --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/XdsConfig.java @@ -0,0 +1,192 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableMap; +import io.grpc.StatusOr; +import io.grpc.xds.XdsClusterResource.CdsUpdate; +import io.grpc.xds.XdsEndpointResource.EdsUpdate; +import io.grpc.xds.XdsListenerResource.LdsUpdate; +import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; +import java.io.Closeable; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Represents the xDS configuration tree for a specified Listener. + */ +final class XdsConfig { + private final LdsUpdate listener; + private final RdsUpdate route; + private final VirtualHost virtualHost; + private final ImmutableMap> clusters; + private final int hashCode; + + XdsConfig(LdsUpdate listener, RdsUpdate route, Map> clusters, + VirtualHost virtualHost) { + this(listener, route, virtualHost, ImmutableMap.copyOf(clusters)); + } + + public XdsConfig(LdsUpdate listener, RdsUpdate route, VirtualHost virtualHost, + ImmutableMap> clusters) { + this.listener = listener; + this.route = route; + this.virtualHost = virtualHost; + this.clusters = clusters; + + hashCode = Objects.hash(listener, route, virtualHost, clusters); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof XdsConfig)) { + return false; + } + + XdsConfig o = (XdsConfig) obj; + + return hashCode() == o.hashCode() && Objects.equals(listener, o.listener) + && Objects.equals(route, o.route) && Objects.equals(virtualHost, o.virtualHost) + && Objects.equals(clusters, o.clusters); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("XdsConfig{") + .append("\n listener=").append(listener) + .append(",\n route=").append(route) + .append(",\n virtualHost=").append(virtualHost) + .append(",\n clusters=").append(clusters) + .append("\n}"); + return builder.toString(); + } + + public LdsUpdate getListener() { + return listener; + } + + public RdsUpdate getRoute() { + return route; + } + + public VirtualHost getVirtualHost() { + return virtualHost; + } + + public ImmutableMap> getClusters() { + return clusters; + } + + static final class XdsClusterConfig { + private final String clusterName; + private final CdsUpdate clusterResource; + private final StatusOr endpoint; //Will be null for non-EDS clusters + + XdsClusterConfig(String clusterName, CdsUpdate clusterResource, + StatusOr endpoint) { + this.clusterName = checkNotNull(clusterName, "clusterName"); + this.clusterResource = checkNotNull(clusterResource, "clusterResource"); + this.endpoint = endpoint; + } + + @Override + public int hashCode() { + int endpointHash = (endpoint != null) ? endpoint.hashCode() : 0; + return clusterName.hashCode() + clusterResource.hashCode() + endpointHash; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof XdsClusterConfig)) { + return false; + } + XdsClusterConfig o = (XdsClusterConfig) obj; + return Objects.equals(clusterName, o.clusterName) + && Objects.equals(clusterResource, o.clusterResource) + && Objects.equals(endpoint, o.endpoint); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("XdsClusterConfig{clusterName=").append(clusterName) + .append(", clusterResource=").append(clusterResource) + .append(", endpoint=").append(endpoint).append("}"); + return builder.toString(); + } + + public String getClusterName() { + return clusterName; + } + + public CdsUpdate getClusterResource() { + return clusterResource; + } + + public StatusOr getEndpoint() { + return endpoint; + } + } + + static final class XdsConfigBuilder { + private LdsUpdate listener; + private RdsUpdate route; + private Map> clusters = new HashMap<>(); + private VirtualHost virtualHost; + + XdsConfigBuilder setListener(LdsUpdate listener) { + this.listener = checkNotNull(listener, "listener"); + return this; + } + + XdsConfigBuilder setRoute(RdsUpdate route) { + this.route = checkNotNull(route, "route"); + return this; + } + + XdsConfigBuilder addCluster(String name, StatusOr clusterConfig) { + checkNotNull(name, "name"); + checkNotNull(clusterConfig, "clusterConfig"); + clusters.put(name, clusterConfig); + return this; + } + + XdsConfigBuilder setVirtualHost(VirtualHost virtualHost) { + this.virtualHost = checkNotNull(virtualHost, "virtualHost"); + return this; + } + + XdsConfig build() { + checkNotNull(listener, "listener"); + checkNotNull(route, "route"); + return new XdsConfig(listener, route, clusters, virtualHost); + } + } + + public interface XdsClusterSubscriptionRegistry { + Closeable subscribeToCluster(String clusterName); + } +} diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java new file mode 100644 index 00000000000..d2af47bc9db --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -0,0 +1,769 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.xds.client.XdsClient.ResourceUpdate; +import static io.grpc.xds.client.XdsLogger.XdsLogLevel.DEBUG; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; +import io.grpc.InternalLogId; +import io.grpc.Status; +import io.grpc.StatusOr; +import io.grpc.SynchronizationContext; +import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight; +import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; +import io.grpc.xds.client.XdsClient; +import io.grpc.xds.client.XdsClient.ResourceWatcher; +import io.grpc.xds.client.XdsLogger; +import io.grpc.xds.client.XdsResourceType; +import java.io.Closeable; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * This class acts as a layer of indirection between the XdsClient and the NameResolver. It + * maintains the watchers for the xds resources and when an update is received, it either requests + * referenced resources or updates the XdsConfig and notifies the XdsConfigWatcher. Each instance + * applies to a single data plane authority. + */ +final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegistry { + public static final XdsClusterResource CLUSTER_RESOURCE = XdsClusterResource.getInstance(); + public static final XdsEndpointResource ENDPOINT_RESOURCE = XdsEndpointResource.getInstance(); + private static final int MAX_CLUSTER_RECURSION_DEPTH = 16; // Matches C++ + private final XdsClient xdsClient; + private final XdsConfigWatcher xdsConfigWatcher; + private final SynchronizationContext syncContext; + private final String dataPlaneAuthority; + + private final InternalLogId logId; + private final XdsLogger logger; + private XdsConfig lastXdsConfig = null; + private final Map, TypeWatchers> resourceWatchers = new HashMap<>(); + + XdsDependencyManager(XdsClient xdsClient, XdsConfigWatcher xdsConfigWatcher, + SynchronizationContext syncContext, String dataPlaneAuthority, + String listenerName) { + logId = InternalLogId.allocate("xds-dependency-manager", listenerName); + logger = XdsLogger.withLogId(logId); + this.xdsClient = checkNotNull(xdsClient, "xdsClient"); + this.xdsConfigWatcher = checkNotNull(xdsConfigWatcher, "xdsConfigWatcher"); + this.syncContext = checkNotNull(syncContext, "syncContext"); + this.dataPlaneAuthority = checkNotNull(dataPlaneAuthority, "dataPlaneAuthority"); + + // start the ball rolling + syncContext.execute(() -> addWatcher(new LdsWatcher(listenerName))); + } + + public static String toContextStr(String typeName, String resourceName) { + return typeName + " resource: " + resourceName; + } + + @Override + public Closeable subscribeToCluster(String clusterName) { + + checkNotNull(clusterName, "clusterName"); + ClusterSubscription subscription = new ClusterSubscription(clusterName); + + syncContext.execute(() -> { + addClusterWatcher(clusterName, subscription, 1); + maybePublishConfig(); + }); + + return subscription; + } + + private void addWatcher(XdsWatcherBase watcher) { + syncContext.throwIfNotInThisSynchronizationContext(); + XdsResourceType type = watcher.type; + String resourceName = watcher.resourceName; + + @SuppressWarnings("unchecked") + TypeWatchers typeWatchers = (TypeWatchers)resourceWatchers.get(type); + if (typeWatchers == null) { + typeWatchers = new TypeWatchers<>(type); + resourceWatchers.put(type, typeWatchers); + } + + typeWatchers.add(resourceName, watcher); + xdsClient.watchXdsResource(type, resourceName, watcher, syncContext); + } + + private void cancelCdsWatcher(CdsWatcher watcher, Object parentContext) { + if (watcher == null) { + return; + } + watcher.parentContexts.remove(parentContext); + if (watcher.parentContexts.isEmpty()) { + cancelWatcher(watcher); + } + } + + private void cancelEdsWatcher(EdsWatcher watcher, CdsWatcher parentContext) { + if (watcher == null) { + return; + } + watcher.parentContexts.remove(parentContext); + if (watcher.parentContexts.isEmpty()) { + cancelWatcher(watcher); + } + } + + + + private void cancelWatcher(XdsWatcherBase watcher) { + syncContext.throwIfNotInThisSynchronizationContext(); + + if (watcher == null) { + return; + } + + if (watcher instanceof CdsWatcher || watcher instanceof EdsWatcher) { + throwIfParentContextsNotEmpty(watcher); + } + + XdsResourceType type = watcher.type; + String resourceName = watcher.resourceName; + + @SuppressWarnings("unchecked") + TypeWatchers typeWatchers = (TypeWatchers)resourceWatchers.get(type); + if (typeWatchers == null) { + logger.log(DEBUG, "Trying to cancel watcher {0}, but type not watched", watcher); + return; + } + + typeWatchers.watchers.remove(resourceName); + xdsClient.cancelXdsResourceWatch(type, resourceName, watcher); + + } + + private static void throwIfParentContextsNotEmpty(XdsWatcherBase watcher) { + if (watcher instanceof CdsWatcher) { + CdsWatcher cdsWatcher = (CdsWatcher) watcher; + if (!cdsWatcher.parentContexts.isEmpty()) { + String msg = String.format("CdsWatcher %s has parent contexts %s", + cdsWatcher.resourceName(), cdsWatcher.parentContexts.keySet()); + throw new IllegalStateException(msg); + } + } else if (watcher instanceof EdsWatcher) { + EdsWatcher edsWatcher = (EdsWatcher) watcher; + if (!edsWatcher.parentContexts.isEmpty()) { + String msg = String.format("CdsWatcher %s has parent contexts %s", + edsWatcher.resourceName(), edsWatcher.parentContexts); + throw new IllegalStateException(msg); + } + } + } + + public void shutdown() { + syncContext.execute(() -> { + for (TypeWatchers watchers : resourceWatchers.values()) { + shutdownWatchersForType(watchers); + } + resourceWatchers.clear(); + }); + } + + private void shutdownWatchersForType(TypeWatchers watchers) { + for (Map.Entry> watcherEntry : watchers.watchers.entrySet()) { + xdsClient.cancelXdsResourceWatch(watchers.resourceType, watcherEntry.getKey(), + watcherEntry.getValue()); + } + } + + private void releaseSubscription(ClusterSubscription subscription) { + checkNotNull(subscription, "subscription"); + String clusterName = subscription.getClusterName(); + syncContext.execute(() -> { + XdsWatcherBase cdsWatcher = + resourceWatchers.get(CLUSTER_RESOURCE).watchers.get(clusterName); + if (cdsWatcher == null) { + return; // already released while waiting for the syncContext + } + cancelClusterWatcherTree((CdsWatcher) cdsWatcher, subscription); + maybePublishConfig(); + }); + } + + private void cancelClusterWatcherTree(CdsWatcher root, Object parentContext) { + checkNotNull(root, "root"); + + cancelCdsWatcher(root, parentContext); + + if (!root.hasDataValue() || !root.parentContexts.isEmpty()) { + return; + } + + XdsClusterResource.CdsUpdate cdsUpdate = root.getData().getValue(); + switch (cdsUpdate.clusterType()) { + case EDS: + String edsServiceName = cdsUpdate.edsServiceName(); + EdsWatcher edsWatcher = + (EdsWatcher) resourceWatchers.get(ENDPOINT_RESOURCE).watchers.get(edsServiceName); + cancelEdsWatcher(edsWatcher, root); + break; + case AGGREGATE: + for (String cluster : cdsUpdate.prioritizedClusterNames()) { + CdsWatcher clusterWatcher = + (CdsWatcher) resourceWatchers.get(CLUSTER_RESOURCE).watchers.get(cluster); + if (clusterWatcher != null) { + cancelClusterWatcherTree(clusterWatcher, root); + } + } + break; + case LOGICAL_DNS: + // no eds needed + break; + default: + throw new AssertionError("Unknown cluster type: " + cdsUpdate.clusterType()); + } + } + + /** + * Check if all resources have results, and if so, generate a new XdsConfig and send it to all + * the watchers. + */ + private void maybePublishConfig() { + syncContext.throwIfNotInThisSynchronizationContext(); + boolean waitingOnResource = resourceWatchers.values().stream() + .flatMap(typeWatchers -> typeWatchers.watchers.values().stream()) + .anyMatch(XdsWatcherBase::missingResult); + if (waitingOnResource) { + return; + } + + XdsConfig newConfig = buildConfig(); + if (Objects.equals(newConfig, lastXdsConfig)) { + return; + } + lastXdsConfig = newConfig; + xdsConfigWatcher.onUpdate(lastXdsConfig); + } + + @VisibleForTesting + XdsConfig buildConfig() { + XdsConfig.XdsConfigBuilder builder = new XdsConfig.XdsConfigBuilder(); + + // Iterate watchers and build the XdsConfig + + // Will only be 1 listener and 1 route resource + VirtualHost activeVirtualHost = getActiveVirtualHost(); + for (XdsWatcherBase xdsWatcherBase : + resourceWatchers.get(XdsListenerResource.getInstance()).watchers.values()) { + XdsListenerResource.LdsUpdate ldsUpdate = ((LdsWatcher) xdsWatcherBase).getData().getValue(); + builder.setListener(ldsUpdate); + if (activeVirtualHost == null) { + activeVirtualHost = RoutingUtils.findVirtualHostForHostName( + ldsUpdate.httpConnectionManager().virtualHosts(), dataPlaneAuthority); + } + + if (ldsUpdate.httpConnectionManager() != null + && ldsUpdate.httpConnectionManager().virtualHosts() != null) { + RdsUpdate rdsUpdate = new RdsUpdate(ldsUpdate.httpConnectionManager().virtualHosts()); + builder.setRoute(rdsUpdate); + } + } + + resourceWatchers.get(XdsRouteConfigureResource.getInstance()).watchers.values().stream() + .map(watcher -> (RdsWatcher) watcher) + .forEach(watcher -> builder.setRoute(watcher.getData().getValue())); + + builder.setVirtualHost(activeVirtualHost); + + Map> edsWatchers = + resourceWatchers.get(ENDPOINT_RESOURCE).watchers; + Map> cdsWatchers = + resourceWatchers.get(CLUSTER_RESOURCE).watchers; + + // Iterate CDS watchers + for (XdsWatcherBase watcher : cdsWatchers.values()) { + CdsWatcher cdsWatcher = (CdsWatcher) watcher; + String clusterName = cdsWatcher.resourceName(); + StatusOr cdsUpdate = cdsWatcher.getData(); + if (cdsUpdate.hasValue()) { + XdsConfig.XdsClusterConfig clusterConfig; + String edsName = cdsUpdate.getValue().edsServiceName(); + EdsWatcher edsWatcher = (EdsWatcher) edsWatchers.get(edsName); + + // Only EDS type clusters have endpoint data + StatusOr data = + edsWatcher != null ? edsWatcher.getData() : null; + clusterConfig = new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate.getValue(), data); + builder.addCluster(clusterName, StatusOr.fromValue(clusterConfig)); + } else { + builder.addCluster(clusterName, StatusOr.fromStatus(cdsUpdate.getStatus())); + } + } + + return builder.build(); + } + + @Override + public String toString() { + return logId.toString(); + } + + private static class TypeWatchers { + // Key is resource name + final Map> watchers = new HashMap<>(); + final XdsResourceType resourceType; + + TypeWatchers(XdsResourceType resourceType) { + this.resourceType = resourceType; + } + + public void add(String resourceName, XdsWatcherBase watcher) { + watchers.put(resourceName, watcher); + } + } + + public interface XdsConfigWatcher { + + void onUpdate(XdsConfig config); + + // These 2 methods are invoked when there is an error or + // does-not-exist on LDS or RDS only. The context will be a + // human-readable string indicating the scope in which the error + // occurred (e.g., the resource type and name). + void onError(String resourceContext, Status status); + + void onResourceDoesNotExist(String resourceContext); + } + + private class ClusterSubscription implements Closeable { + String clusterName; + + public ClusterSubscription(String clusterName) { + this.clusterName = clusterName; + } + + public String getClusterName() { + return clusterName; + } + + @Override + public void close() throws IOException { + releaseSubscription(this); + } + } + + private abstract static class XdsWatcherBase + implements ResourceWatcher { + private final XdsResourceType type; + private final String resourceName; + @Nullable + private StatusOr data; + + + private XdsWatcherBase(XdsResourceType type, String resourceName) { + this.type = checkNotNull(type, "type"); + this.resourceName = checkNotNull(resourceName, "resourceName"); + } + + @Override + public void onError(Status error) { + checkNotNull(error, "error"); + setDataAsStatus(error); + } + + protected void handleDoesNotExist(String resourceName) { + checkArgument(this.resourceName.equals(resourceName), "Resource name does not match"); + setDataAsStatus(Status.UNAVAILABLE.withDescription("No " + toContextString())); + } + + boolean missingResult() { + return data == null; + } + + @Nullable + StatusOr getData() { + return data; + } + + boolean hasDataValue() { + return data != null && data.hasValue(); + } + + String resourceName() { + return resourceName; + } + + protected void setData(T data) { + checkNotNull(data, "data"); + this.data = StatusOr.fromValue(data); + } + + protected void setDataAsStatus(Status status) { + checkNotNull(status, "status"); + this.data = StatusOr.fromStatus(status); + } + + String toContextString() { + return toContextStr(type.typeName(), resourceName); + } + } + + private class LdsWatcher extends XdsWatcherBase { + String rdsName; + + private LdsWatcher(String resourceName) { + super(XdsListenerResource.getInstance(), resourceName); + } + + @Override + public void onChanged(XdsListenerResource.LdsUpdate update) { + checkNotNull(update, "update"); + + HttpConnectionManager httpConnectionManager = update.httpConnectionManager(); + List virtualHosts = httpConnectionManager.virtualHosts(); + String rdsName = httpConnectionManager.rdsName(); + VirtualHost activeVirtualHost = getActiveVirtualHost(); + + boolean changedRdsName = !Objects.equals(rdsName, this.rdsName); + if (changedRdsName) { + cleanUpRdsWatcher(); + } + + if (virtualHosts != null) { + // No RDS watcher since we are getting RDS updates via LDS + updateRoutes(virtualHosts, this, activeVirtualHost, this.rdsName == null); + this.rdsName = null; + } else if (changedRdsName) { + cleanUpRdsWatcher(); + this.rdsName = rdsName; + addWatcher(new RdsWatcher(rdsName)); + logger.log(XdsLogger.XdsLogLevel.INFO, "Start watching RDS resource {0}", rdsName); + } + + setData(update); + maybePublishConfig(); + } + + @Override + public void onError(Status error) { + super.onError(checkNotNull(error, "error")); + xdsConfigWatcher.onError(toContextString(), error); + } + + @Override + public void onResourceDoesNotExist(String resourceName) { + handleDoesNotExist(resourceName); + xdsConfigWatcher.onResourceDoesNotExist(toContextString()); + } + + private void cleanUpRdsWatcher() { + RdsWatcher oldRdsWatcher = getRdsWatcher(); + if (oldRdsWatcher != null) { + cancelWatcher(oldRdsWatcher); + logger.log(XdsLogger.XdsLogLevel.DEBUG, "Stop watching RDS resource {0}", rdsName); + + // Cleanup clusters (as appropriate) that had the old rds watcher as a parent + if (!oldRdsWatcher.hasDataValue() || !oldRdsWatcher.getData().hasValue() + || resourceWatchers.get(CLUSTER_RESOURCE) == null) { + return; + } + for (XdsWatcherBase watcher : + resourceWatchers.get(CLUSTER_RESOURCE).watchers.values()) { + cancelCdsWatcher((CdsWatcher) watcher, oldRdsWatcher); + } + } + } + + private RdsWatcher getRdsWatcher() { + TypeWatchers watchers = resourceWatchers.get(XdsRouteConfigureResource.getInstance()); + if (watchers == null || rdsName == null || watchers.watchers.isEmpty()) { + return null; + } + + return (RdsWatcher) watchers.watchers.get(rdsName); + } + } + + private class RdsWatcher extends XdsWatcherBase { + + public RdsWatcher(String resourceName) { + super(XdsRouteConfigureResource.getInstance(), checkNotNull(resourceName, "resourceName")); + } + + @Override + public void onChanged(RdsUpdate update) { + checkNotNull(update, "update"); + RdsUpdate oldData = hasDataValue() ? getData().getValue() : null; + VirtualHost oldVirtualHost = + (oldData != null) + ? RoutingUtils.findVirtualHostForHostName(oldData.virtualHosts, dataPlaneAuthority) + : null; + setData(update); + updateRoutes(update.virtualHosts, this, oldVirtualHost, true); + maybePublishConfig(); + } + + @Override + public void onError(Status error) { + super.onError(checkNotNull(error, "error")); + xdsConfigWatcher.onError(toContextString(), error); + } + + @Override + public void onResourceDoesNotExist(String resourceName) { + handleDoesNotExist(checkNotNull(resourceName, "resourceName")); + xdsConfigWatcher.onResourceDoesNotExist(toContextString()); + } + + ImmutableList getCdsNames() { + if (!hasDataValue() || getData().getValue().virtualHosts == null) { + return ImmutableList.of(); + } + + return ImmutableList.copyOf(getClusterNamesFromVirtualHost(getActiveVirtualHost())); + } + } + + private class CdsWatcher extends XdsWatcherBase { + Map parentContexts = new HashMap<>(); + + CdsWatcher(String resourceName, Object parentContext, int depth) { + super(CLUSTER_RESOURCE, checkNotNull(resourceName, "resourceName")); + this.parentContexts.put(checkNotNull(parentContext, "parentContext"), depth); + } + + @Override + public void onChanged(XdsClusterResource.CdsUpdate update) { + checkNotNull(update, "update"); + switch (update.clusterType()) { + case EDS: + setData(update); + if (!addEdsWatcher(update.edsServiceName(), this)) { + maybePublishConfig(); + } + break; + case LOGICAL_DNS: + setData(update); + maybePublishConfig(); + // no eds needed + break; + case AGGREGATE: + Object parentContext = this; + int depth = parentContexts.values().stream().max(Integer::compare).orElse(0) + 1; + if (depth > MAX_CLUSTER_RECURSION_DEPTH) { + logger.log(XdsLogger.XdsLogLevel.WARNING, + "Cluster recursion depth limit exceeded for cluster {0}", resourceName()); + Status error = Status.UNAVAILABLE.withDescription( + "aggregate cluster graph exceeds max depth"); + setDataAsStatus(error); + } + if (hasDataValue()) { + Set oldNames = new HashSet<>(getData().getValue().prioritizedClusterNames()); + Set newNames = new HashSet<>(update.prioritizedClusterNames()); + + + Set deletedClusters = Sets.difference(oldNames, newNames); + deletedClusters.forEach((cluster) + -> cancelClusterWatcherTree(getCluster(cluster), parentContext)); + + if (depth <= MAX_CLUSTER_RECURSION_DEPTH) { + setData(update); + Set addedClusters = Sets.difference(newNames, oldNames); + addedClusters.forEach((cluster) -> addClusterWatcher(cluster, parentContext, depth)); + + if (addedClusters.isEmpty()) { + maybePublishConfig(); + } + } else { // data was set to error status above + maybePublishConfig(); + } + + } else if (depth <= MAX_CLUSTER_RECURSION_DEPTH) { + setData(update); + update.prioritizedClusterNames() + .forEach(name -> addClusterWatcher(name, parentContext, depth)); + maybePublishConfig(); + } + break; + default: + Status error = Status.UNAVAILABLE.withDescription( + "aggregate cluster graph exceeds max depth"); + setDataAsStatus(error); + maybePublishConfig(); + } + } + + @Override + public void onResourceDoesNotExist(String resourceName) { + handleDoesNotExist(checkNotNull(resourceName, "resourceName")); + maybePublishConfig(); + } + } + + // Returns true if the watcher was added, false if it already exists + private boolean addEdsWatcher(String edsServiceName, CdsWatcher parentContext) { + TypeWatchers typeWatchers = resourceWatchers.get(XdsEndpointResource.getInstance()); + if (typeWatchers == null || !typeWatchers.watchers.containsKey(edsServiceName)) { + addWatcher(new EdsWatcher(edsServiceName, parentContext)); + return true; + } + + EdsWatcher watcher = (EdsWatcher) typeWatchers.watchers.get(edsServiceName); + watcher.addParentContext(parentContext); // Is a set, so don't need to check for existence + return false; + } + + private void addClusterWatcher(String clusterName, Object parentContext, int depth) { + TypeWatchers clusterWatchers = resourceWatchers.get(CLUSTER_RESOURCE); + if (clusterWatchers != null) { + CdsWatcher watcher = (CdsWatcher) clusterWatchers.watchers.get(clusterName); + if (watcher != null) { + watcher.parentContexts.put(parentContext, depth); + return; + } + } + + addWatcher(new CdsWatcher(clusterName, parentContext, depth)); + } + + private class EdsWatcher extends XdsWatcherBase { + private final Set parentContexts = new HashSet<>(); + + private EdsWatcher(String resourceName, CdsWatcher parentContext) { + super(ENDPOINT_RESOURCE, checkNotNull(resourceName, "resourceName")); + parentContexts.add(checkNotNull(parentContext, "parentContext")); + } + + @Override + public void onChanged(XdsEndpointResource.EdsUpdate update) { + setData(checkNotNull(update, "update")); + maybePublishConfig(); + } + + @Override + public void onResourceDoesNotExist(String resourceName) { + handleDoesNotExist(checkNotNull(resourceName, "resourceName")); + maybePublishConfig(); + } + + void addParentContext(CdsWatcher parentContext) { + parentContexts.add(checkNotNull(parentContext, "parentContext")); + } + } + + private void updateRoutes(List virtualHosts, Object newParentContext, + VirtualHost oldVirtualHost, boolean sameParentContext) { + VirtualHost virtualHost = + RoutingUtils.findVirtualHostForHostName(virtualHosts, dataPlaneAuthority); + if (virtualHost == null) { + String error = "Failed to find virtual host matching hostname: " + dataPlaneAuthority; + logger.log(XdsLogger.XdsLogLevel.WARNING, error); + cleanUpRoutes(); + xdsConfigWatcher.onError( + "xDS node ID:" + dataPlaneAuthority, Status.UNAVAILABLE.withDescription(error)); + return; + } + + Set newClusters = getClusterNamesFromVirtualHost(virtualHost); + Set oldClusters = getClusterNamesFromVirtualHost(oldVirtualHost); + + if (sameParentContext) { + // Calculate diffs. + Set addedClusters = Sets.difference(newClusters, oldClusters); + Set deletedClusters = Sets.difference(oldClusters, newClusters); + + deletedClusters.forEach(watcher -> + cancelClusterWatcherTree(getCluster(watcher), newParentContext)); + addedClusters.forEach((cluster) -> addClusterWatcher(cluster, newParentContext, 1)); + } else { + newClusters.forEach((cluster) -> addClusterWatcher(cluster, newParentContext, 1)); + } + } + + private static Set getClusterNamesFromVirtualHost(VirtualHost virtualHost) { + if (virtualHost == null) { + return Collections.emptySet(); + } + + // Get all cluster names to which requests can be routed through the virtual host. + Set clusters = new HashSet<>(); + for (VirtualHost.Route route : virtualHost.routes()) { + VirtualHost.Route.RouteAction action = route.routeAction(); + if (action == null) { + continue; + } + if (action.cluster() != null) { + clusters.add(action.cluster()); + } else if (action.weightedClusters() != null) { + for (ClusterWeight weighedCluster : action.weightedClusters()) { + clusters.add(weighedCluster.name()); + } + } + } + + return clusters; + } + + @Nullable + private VirtualHost getActiveVirtualHost() { + TypeWatchers rdsWatchers = resourceWatchers.get(XdsRouteConfigureResource.getInstance()); + if (rdsWatchers == null) { + return null; + } + + RdsWatcher activeRdsWatcher = + (RdsWatcher) rdsWatchers.watchers.values().stream().findFirst().orElse(null); + if (activeRdsWatcher == null || activeRdsWatcher.missingResult() + || !activeRdsWatcher.getData().hasValue()) { + return null; + } + + return RoutingUtils.findVirtualHostForHostName( + activeRdsWatcher.getData().getValue().virtualHosts, dataPlaneAuthority); + } + + // Must be in SyncContext + private void cleanUpRoutes() { + // Remove RdsWatcher & CDS Watchers + TypeWatchers rdsResourceWatcher = + resourceWatchers.get(XdsRouteConfigureResource.getInstance()); + if (rdsResourceWatcher == null || rdsResourceWatcher.watchers.isEmpty()) { + return; + } + + XdsWatcherBase watcher = rdsResourceWatcher.watchers.values().stream().findFirst().get(); + cancelWatcher(watcher); + + // Remove CdsWatchers pointed to by the RdsWatcher + RdsWatcher rdsWatcher = (RdsWatcher) watcher; + for (String cName : rdsWatcher.getCdsNames()) { + CdsWatcher cdsWatcher = getCluster(cName); + if (cdsWatcher != null) { + cancelClusterWatcherTree(cdsWatcher, rdsWatcher); + } + } + } + + private CdsWatcher getCluster(String clusterName) { + return (CdsWatcher) resourceWatchers.get(CLUSTER_RESOURCE).watchers.get(clusterName); + } +} diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index 4304d1d9e6f..034779ed023 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -582,8 +582,7 @@ private void handleResourceUpdate( String errorDetail = null; if (errors.isEmpty()) { checkArgument(invalidResources.isEmpty(), "found invalid resources but missing errors"); - controlPlaneClient.ackResponse(xdsResourceType, args.versionInfo, - args.nonce); + controlPlaneClient.ackResponse(xdsResourceType, args.versionInfo, args.nonce); } else { errorDetail = Joiner.on('\n').join(errors); logger.log(XdsLogLevel.WARNING, diff --git a/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java b/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java index ac1c4829c74..39761912ea5 100644 --- a/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java +++ b/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java @@ -24,7 +24,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.protobuf.Any; -import com.google.protobuf.BoolValue; import com.google.protobuf.Message; import com.google.protobuf.UInt32Value; import io.envoyproxy.envoy.config.cluster.v3.Cluster; @@ -45,7 +44,6 @@ import io.envoyproxy.envoy.config.listener.v3.Listener; import io.envoyproxy.envoy.config.route.v3.NonForwardingAction; 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; @@ -239,24 +237,7 @@ void setEdsConfig(String edsName, ClusterLoadAssignment clusterLoadAssignment) { * Builds a new default RDS configuration. */ static RouteConfiguration buildRouteConfiguration(String authority) { - return buildRouteConfiguration(authority, RDS_NAME, CLUSTER_NAME); - } - - static RouteConfiguration buildRouteConfiguration(String authority, String rdsName, - String clusterName) { - VirtualHost.Builder vhBuilder = VirtualHost.newBuilder() - .setName(rdsName) - .addDomains(authority) - .addRoutes( - Route.newBuilder() - .setMatch( - RouteMatch.newBuilder().setPrefix("/").build()) - .setRoute( - RouteAction.newBuilder().setCluster(clusterName) - .setAutoHostRewrite(BoolValue.newBuilder().setValue(true).build()) - .build())); - VirtualHost virtualHost = vhBuilder.build(); - return RouteConfiguration.newBuilder().setName(rdsName).addVirtualHosts(virtualHost).build(); + return XdsTestUtils.buildRouteConfiguration(authority, RDS_NAME, CLUSTER_NAME); } /** diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java index 7cf6280711f..97c2695f209 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java @@ -205,7 +205,7 @@ private static void setAdsConfig(ControlPlaneRule controlPlane, String serverNam ControlPlaneRule.buildClientListener(MAIN_SERVER, serverName)); controlPlane.setRdsConfig(rdsName, - ControlPlaneRule.buildRouteConfiguration(MAIN_SERVER, rdsName, clusterName)); + XdsTestUtils.buildRouteConfiguration(MAIN_SERVER, rdsName, clusterName)); controlPlane.setCdsConfig(clusterName, ControlPlaneRule.buildCluster(clusterName, edsName)); controlPlane.setEdsConfig(edsName, diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java new file mode 100644 index 00000000000..96aeb0f41fb --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -0,0 +1,784 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; +import static io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType.AGGREGATE; +import static io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType.EDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_EDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_LDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_RDS; +import static io.grpc.xds.XdsTestUtils.CLUSTER_NAME; +import static io.grpc.xds.XdsTestUtils.ENDPOINT_HOSTNAME; +import static io.grpc.xds.XdsTestUtils.ENDPOINT_PORT; +import static io.grpc.xds.XdsTestUtils.RDS_NAME; +import static io.grpc.xds.XdsTestUtils.getEdsNameForCluster; +import static io.grpc.xds.client.CommonBootstrapperTestUtils.SERVER_URI; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Message; +import io.envoyproxy.envoy.config.cluster.v3.Cluster; +import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; +import io.envoyproxy.envoy.config.listener.v3.Listener; +import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; +import io.grpc.BindableService; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.StatusOr; +import io.grpc.SynchronizationContext; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.internal.ExponentialBackoffPolicy; +import io.grpc.internal.FakeClock; +import io.grpc.testing.GrpcCleanupRule; +import io.grpc.xds.XdsListenerResource.LdsUpdate; +import io.grpc.xds.client.CommonBootstrapperTestUtils; +import io.grpc.xds.client.XdsClient; +import io.grpc.xds.client.XdsClientImpl; +import io.grpc.xds.client.XdsClientMetricReporter; +import io.grpc.xds.client.XdsResourceType; +import io.grpc.xds.client.XdsTransportFactory; +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit tests for {@link XdsDependencyManager}. */ +@RunWith(JUnit4.class) +public class XdsDependencyManagerTest { + private static final Logger log = Logger.getLogger(XdsDependencyManagerTest.class.getName()); + public static final String CLUSTER_TYPE_NAME = XdsClusterResource.getInstance().typeName(); + public static final String ENDPOINT_TYPE_NAME = XdsEndpointResource.getInstance().typeName(); + + @Mock + private XdsClientMetricReporter xdsClientMetricReporter; + + private final SynchronizationContext syncContext = + new SynchronizationContext(mock(Thread.UncaughtExceptionHandler.class)); + + private ManagedChannel channel; + private XdsClientImpl xdsClient; + private XdsDependencyManager xdsDependencyManager; + private TestWatcher xdsConfigWatcher; + private Server xdsServer; + + private final FakeClock fakeClock = new FakeClock(); + private final String serverName = InProcessServerBuilder.generateName(); + private final Queue loadReportCalls = new ArrayDeque<>(); + private final AtomicBoolean adsEnded = new AtomicBoolean(true); + private final AtomicBoolean lrsEnded = new AtomicBoolean(true); + private final XdsTestControlPlaneService controlPlaneService = new XdsTestControlPlaneService(); + private final BindableService lrsService = + XdsTestUtils.createLrsService(lrsEnded, loadReportCalls); + + @Rule + public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule(); + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + private TestWatcher testWatcher; + private XdsConfig defaultXdsConfig; // set in setUp() + + @Captor + private ArgumentCaptor xdsConfigCaptor; + @Captor + private ArgumentCaptor statusCaptor; + + @Before + public void setUp() throws Exception { + xdsServer = cleanupRule.register(InProcessServerBuilder + .forName(serverName) + .addService(controlPlaneService) + .addService(lrsService) + .directExecutor() + .build() + .start()); + + XdsTestUtils.setAdsConfig(controlPlaneService, serverName); + + channel = cleanupRule.register( + InProcessChannelBuilder.forName(serverName).directExecutor().build()); + XdsTransportFactory xdsTransportFactory = + ignore -> new GrpcXdsTransportFactory.GrpcXdsTransport(channel); + + xdsClient = CommonBootstrapperTestUtils.createXdsClient( + Collections.singletonList(SERVER_URI), xdsTransportFactory, fakeClock, + new ExponentialBackoffPolicy.Provider(), MessagePrinter.INSTANCE, xdsClientMetricReporter); + + testWatcher = new TestWatcher(); + xdsConfigWatcher = mock(TestWatcher.class, delegatesTo(testWatcher)); + defaultXdsConfig = XdsTestUtils.getDefaultXdsConfig(serverName); + } + + @After + public void tearDown() throws InterruptedException { + if (xdsDependencyManager != null) { + xdsDependencyManager.shutdown(); + } + xdsClient.shutdown(); + channel.shutdown(); // channel not owned by XdsClient + + xdsServer.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); + + assertThat(adsEnded.get()).isTrue(); + assertThat(lrsEnded.get()).isTrue(); + assertThat(fakeClock.getPendingTasks()).isEmpty(); + } + + @Test + public void verify_basic_config() { + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + + verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); + testWatcher.verifyStats(1, 0, 0); + } + + @Test + public void verify_config_update() { + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + + InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); + testWatcher.verifyStats(1, 0, 0); + assertThat(testWatcher.lastConfig).isEqualTo(defaultXdsConfig); + + XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS2", "CDS2", "EDS2", + ENDPOINT_HOSTNAME + "2", ENDPOINT_PORT + 2); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(ArgumentMatchers.notNull()); + testWatcher.verifyStats(2, 0, 0); + assertThat(testWatcher.lastConfig).isNotEqualTo(defaultXdsConfig); + } + + @Test + public void verify_simple_aggregate() { + InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); + + List childNames = Arrays.asList("clusterC", "clusterB", "clusterA"); + String rootName = "root_c"; + + RouteConfiguration routeConfig = + XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, rootName); + controlPlaneService.setXdsConfig( + ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig)); + + XdsTestUtils.setAggregateCdsConfig(controlPlaneService, serverName, rootName, childNames); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + + Map> lastConfigClusters = + testWatcher.lastConfig.getClusters(); + assertThat(lastConfigClusters).hasSize(childNames.size() + 1); + StatusOr rootC = lastConfigClusters.get(rootName); + XdsClusterResource.CdsUpdate rootUpdate = rootC.getValue().getClusterResource(); + assertThat(rootUpdate.clusterType()).isEqualTo(AGGREGATE); + assertThat(rootUpdate.prioritizedClusterNames()).isEqualTo(childNames); + + for (String childName : childNames) { + assertThat(lastConfigClusters).containsKey(childName); + XdsClusterResource.CdsUpdate childResource = + lastConfigClusters.get(childName).getValue().getClusterResource(); + assertThat(childResource.clusterType()).isEqualTo(EDS); + assertThat(childResource.edsServiceName()).isEqualTo(getEdsNameForCluster(childName)); + + StatusOr endpoint = + lastConfigClusters.get(childName).getValue().getEndpoint(); + assertThat(endpoint.hasValue()).isTrue(); + assertThat(endpoint.getValue().clusterName).isEqualTo(getEdsNameForCluster(childName)); + } + } + + @Test + public void testComplexRegisteredAggregate() throws IOException { + InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); + + // Do initialization + String rootName1 = "root_c"; + List childNames = Arrays.asList("clusterC", "clusterB", "clusterA"); + XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName1, childNames); + + String rootName2 = "root_2"; + List childNames2 = Arrays.asList("clusterA", "clusterX"); + XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName2, childNames2); + + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + + Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + + Closeable subscription2 = xdsDependencyManager.subscribeToCluster(rootName2); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); + testWatcher.verifyStats(3, 0, 0); + ImmutableSet.Builder builder = ImmutableSet.builder(); + Set expectedClusters = builder.add(rootName1).add(rootName2).add(CLUSTER_NAME) + .addAll(childNames).addAll(childNames2).build(); + assertThat(xdsConfigCaptor.getValue().getClusters().keySet()).isEqualTo(expectedClusters); + + // Close 1 subscription shouldn't affect the other or RDS subscriptions + subscription1.close(); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); + builder = ImmutableSet.builder(); + Set expectedClusters2 = + builder.add(rootName2).add(CLUSTER_NAME).addAll(childNames2).build(); + assertThat(xdsConfigCaptor.getValue().getClusters().keySet()).isEqualTo(expectedClusters2); + + subscription2.close(); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); + } + + @Test + public void testDelayedSubscription() { + InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); + + String rootName1 = "root_c"; + List childNames = Arrays.asList("clusterC", "clusterB", "clusterA"); + + Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1); + assertThat(subscription1).isNotNull(); + fakeClock.forwardTime(16, TimeUnit.SECONDS); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsConfigCaptor.capture()); + assertThat(xdsConfigCaptor.getValue().getClusters().get(rootName1).toString()).isEqualTo( + StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( + "No " + toContextStr(CLUSTER_TYPE_NAME, rootName1))).toString()); + + XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName1, childNames); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsConfigCaptor.capture()); + assertThat(xdsConfigCaptor.getValue().getClusters().get(rootName1).hasValue()).isTrue(); + } + + @Test + public void testMissingCdsAndEds() { + // update config so that agg cluster references 2 existing & 1 non-existing cluster + List childNames = Arrays.asList("clusterC", "clusterB", "clusterA"); + Cluster cluster = XdsTestUtils.buildAggCluster(CLUSTER_NAME, childNames); + Map clusterMap = new HashMap<>(); + Map edsMap = new HashMap<>(); + + clusterMap.put(CLUSTER_NAME, cluster); + for (int i = 0; i < childNames.size() - 1; i++) { + String edsName = XdsTestUtils.EDS_NAME + i; + Cluster child = ControlPlaneRule.buildCluster(childNames.get(i), edsName); + clusterMap.put(childNames.get(i), child); + } + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); + + // Update config so that one of the 2 "valid" clusters has an EDS resource, the other does not + // and there is an EDS that doesn't have matching clusters + ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( + serverName, ENDPOINT_HOSTNAME, ENDPOINT_PORT, XdsTestUtils.EDS_NAME + 0); + edsMap.put(XdsTestUtils.EDS_NAME + 0, clusterLoadAssignment); + clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( + serverName, ENDPOINT_HOSTNAME, ENDPOINT_PORT, "garbageEds"); + edsMap.put("garbageEds", clusterLoadAssignment); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); + + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + + fakeClock.forwardTime(16, TimeUnit.SECONDS); + verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); + + List> returnedClusters = new ArrayList<>(); + for (String childName : childNames) { + returnedClusters.add(xdsConfigCaptor.getValue().getClusters().get(childName)); + } + + // Check that missing cluster reported Status and the other 2 are present + Status expectedClusterStatus = Status.UNAVAILABLE.withDescription( + "No " + toContextStr(CLUSTER_TYPE_NAME, childNames.get(2))); + StatusOr missingCluster = returnedClusters.get(2); + assertThat(missingCluster.getStatus().toString()).isEqualTo(expectedClusterStatus.toString()); + assertThat(returnedClusters.get(0).hasValue()).isTrue(); + assertThat(returnedClusters.get(1).hasValue()).isTrue(); + + // Check that missing EDS reported Status, the other one is present and the garbage EDS is not + Status expectedEdsStatus = Status.UNAVAILABLE.withDescription( + "No " + toContextStr(ENDPOINT_TYPE_NAME, XdsTestUtils.EDS_NAME + 1)); + assertThat(returnedClusters.get(0).getValue().getEndpoint().hasValue()).isTrue(); + assertThat(returnedClusters.get(1).getValue().getEndpoint().hasValue()).isFalse(); + assertThat(returnedClusters.get(1).getValue().getEndpoint().getStatus().toString()) + .isEqualTo(expectedEdsStatus.toString()); + + verify(xdsConfigWatcher, never()).onResourceDoesNotExist(any()); + testWatcher.verifyStats(1, 0, 0); + } + + @Test + public void testMissingLds() { + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, "badLdsName"); + + fakeClock.forwardTime(16, TimeUnit.SECONDS); + verify(xdsConfigWatcher, timeout(1000)).onResourceDoesNotExist( + toContextStr(XdsListenerResource.getInstance().typeName(), "badLdsName")); + + testWatcher.verifyStats(0, 0, 1); + } + + @Test + public void testMissingRds() { + Listener serverListener = ControlPlaneRule.buildServerListener(); + Listener clientListener = + ControlPlaneRule.buildClientListener(serverName, serverName, "badRdsName"); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, + ImmutableMap.of(XdsTestUtils.SERVER_LISTENER, serverListener, serverName, clientListener)); + + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + + fakeClock.forwardTime(16, TimeUnit.SECONDS); + verify(xdsConfigWatcher, timeout(1000)).onResourceDoesNotExist( + toContextStr(XdsRouteConfigureResource.getInstance().typeName(), "badRdsName")); + + testWatcher.verifyStats(0, 0, 1); + } + + @Test + public void testUpdateToMissingVirtualHost() { + InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); + WrappedXdsClient wrappedXdsClient = new WrappedXdsClient(xdsClient, syncContext); + xdsDependencyManager = new XdsDependencyManager( + wrappedXdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); + + // Update with a config that has a virtual host that doesn't match the server name + wrappedXdsClient.deliverLdsUpdate(0L, buildUnmatchedVirtualHosts()); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onError(any(), statusCaptor.capture()); + assertThat(statusCaptor.getValue().getDescription()) + .isEqualTo("Failed to find virtual host matching hostname: " + serverName); + + testWatcher.verifyStats(1, 1, 0); + + wrappedXdsClient.shutdown(); + } + + private List buildUnmatchedVirtualHosts() { + io.grpc.xds.VirtualHost.Route route1 = + io.grpc.xds.VirtualHost.Route.forAction( + io.grpc.xds.VirtualHost.Route.RouteMatch.withPathExactOnly("/GreetService/bye"), + io.grpc.xds.VirtualHost.Route.RouteAction.forCluster( + "cluster-bar.googleapis.com", Collections.emptyList(), + TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()); + io.grpc.xds.VirtualHost.Route route2 = + io.grpc.xds.VirtualHost.Route.forAction( + io.grpc.xds.VirtualHost.Route.RouteMatch.withPathExactOnly("/HelloService/hi"), + io.grpc.xds.VirtualHost.Route.RouteAction.forCluster( + "cluster-foo.googleapis.com", Collections.emptyList(), + TimeUnit.SECONDS.toNanos(15L), null, false), + ImmutableMap.of()); + return Arrays.asList( + io.grpc.xds.VirtualHost.create("virtualhost-foo", Collections.singletonList("hello" + + ".googleapis.com"), + Collections.singletonList(route1), + ImmutableMap.of()), + io.grpc.xds.VirtualHost.create("virtualhost-bar", Collections.singletonList("hi" + + ".googleapis.com"), + Collections.singletonList(route2), + ImmutableMap.of())); + } + + @Test + public void testCorruptLds() { + String ldsResourceName = + "xdstp://unknown.example.com/envoy.config.listener.v3.Listener/listener1"; + + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, ldsResourceName); + + Status expectedStatus = Status.INVALID_ARGUMENT.withDescription( + "Wrong configuration: xds server does not exist for resource " + ldsResourceName); + String context = toContextStr(XdsListenerResource.getInstance().typeName(), ldsResourceName); + verify(xdsConfigWatcher, timeout(1000)) + .onError(eq(context), argThat(new XdsTestUtils.StatusMatcher(expectedStatus))); + + fakeClock.forwardTime(16, TimeUnit.SECONDS); + testWatcher.verifyStats(0, 1, 0); + } + + @Test + public void testChangeRdsName_fromLds() { + // TODO implement + InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); + Listener serverListener = ControlPlaneRule.buildServerListener(); + + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); + + String newRdsName = "newRdsName1"; + + Listener clientListener = buildInlineClientListener(newRdsName, CLUSTER_NAME); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, + ImmutableMap.of(XdsTestUtils.SERVER_LISTENER, serverListener, serverName, clientListener)); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); + assertThat(xdsConfigCaptor.getValue()).isNotEqualTo(defaultXdsConfig); + assertThat(xdsConfigCaptor.getValue().getVirtualHost().name()).isEqualTo(newRdsName); + } + + @Test + public void testMultipleParentsInCdsTree() throws IOException { + /* + * Configure Xds server with the following cluster tree and point RDS to root: + 2 aggregates under root A & B + B has EDS Cluster B1 && shared agg AB1; A has agg A1 && shared agg AB1 + A1 has shared EDS Cluster A11 && shared agg AB1 + AB1 has shared EDS Clusters A11 && AB11 + + As an alternate visualization, parents are: + A -> root, B -> root, A1 -> A, AB1 -> A|B|A1, B1 -> B, A11 -> A1|AB1, AB11 -> AB1 + */ + Cluster rootCluster = + XdsTestUtils.buildAggCluster("root", Arrays.asList("clusterA", "clusterB")); + Cluster clusterA = + XdsTestUtils.buildAggCluster("clusterA", Arrays.asList("clusterA1", "clusterAB1")); + Cluster clusterB = + XdsTestUtils.buildAggCluster("clusterB", Arrays.asList("clusterB1", "clusterAB1")); + Cluster clusterA1 = + XdsTestUtils.buildAggCluster("clusterA1", Arrays.asList("clusterA11", "clusterAB1")); + Cluster clusterAB1 = + XdsTestUtils.buildAggCluster("clusterAB1", Arrays.asList("clusterA11", "clusterAB11")); + + Map clusterMap = new HashMap<>(); + Map edsMap = new HashMap<>(); + + clusterMap.put("root", rootCluster); + clusterMap.put("clusterA", clusterA); + clusterMap.put("clusterB", clusterB); + clusterMap.put("clusterA1", clusterA1); + clusterMap.put("clusterAB1", clusterAB1); + + XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterA11", "clusterAB11", "clusterB1"); + RouteConfiguration routeConfig = + XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "root"); + controlPlaneService.setXdsConfig( + ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig)); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); + + // Start the actual test + InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); + XdsConfig initialConfig = xdsConfigCaptor.getValue(); + + // Make sure that adding subscriptions that rds points at doesn't change the config + Closeable rootSub = xdsDependencyManager.subscribeToCluster("root"); + assertThat(xdsDependencyManager.buildConfig()).isEqualTo(initialConfig); + Closeable clusterAB11Sub = xdsDependencyManager.subscribeToCluster("clusterAB11"); + assertThat(xdsDependencyManager.buildConfig()).isEqualTo(initialConfig); + + // Make sure that closing subscriptions that rds points at doesn't change the config + rootSub.close(); + assertThat(xdsDependencyManager.buildConfig()).isEqualTo(initialConfig); + clusterAB11Sub.close(); + assertThat(xdsDependencyManager.buildConfig()).isEqualTo(initialConfig); + + // Make an explicit root subscription and then change RDS to point to A11 + rootSub = xdsDependencyManager.subscribeToCluster("root"); + RouteConfiguration newRouteConfig = + XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "clusterA11"); + controlPlaneService.setXdsConfig( + ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, newRouteConfig)); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); + assertThat(xdsConfigCaptor.getValue().getClusters().keySet().size()).isEqualTo(8); + + // Now that it is released, we should only have A11 + rootSub.close(); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); + assertThat(xdsConfigCaptor.getValue().getClusters().keySet()).containsExactly("clusterA11"); + } + + @Test + public void testMultipleCdsReferToSameEds() { + // Create the maps and Update the config to have 2 clusters that refer to the same EDS resource + String edsName = "sharedEds"; + + Cluster rootCluster = + XdsTestUtils.buildAggCluster("root", Arrays.asList("clusterA", "clusterB")); + Cluster clusterA = ControlPlaneRule.buildCluster("clusterA", edsName); + Cluster clusterB = ControlPlaneRule.buildCluster("clusterB", edsName); + + Map clusterMap = new HashMap<>(); + clusterMap.put("root", rootCluster); + clusterMap.put("clusterA", clusterA); + clusterMap.put("clusterB", clusterB); + + Map edsMap = new HashMap<>(); + ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( + serverName, ENDPOINT_HOSTNAME, ENDPOINT_PORT, edsName); + edsMap.put(edsName, clusterLoadAssignment); + + RouteConfiguration routeConfig = + XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "root"); + controlPlaneService.setXdsConfig( + ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig)); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); + + // Start the actual test + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); + XdsConfig initialConfig = xdsConfigCaptor.getValue(); + assertThat(initialConfig.getClusters().keySet()) + .containsExactly("root", "clusterA", "clusterB"); + + XdsEndpointResource.EdsUpdate edsForA = + initialConfig.getClusters().get("clusterA").getValue().getEndpoint().getValue(); + assertThat(edsForA.clusterName).isEqualTo(edsName); + XdsEndpointResource.EdsUpdate edsForB = + initialConfig.getClusters().get("clusterB").getValue().getEndpoint().getValue(); + assertThat(edsForB.clusterName).isEqualTo(edsName); + assertThat(edsForA).isEqualTo(edsForB); + edsForA.localityLbEndpointsMap.values().forEach( + localityLbEndpoints -> assertThat(localityLbEndpoints.endpoints()).hasSize(1)); + } + + @Test + public void testChangeRdsName_FromLds_complexTree() { + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + + // Create the same tree as in testMultipleParentsInCdsTree + Cluster rootCluster = + XdsTestUtils.buildAggCluster("root", Arrays.asList("clusterA", "clusterB")); + Cluster clusterA = + XdsTestUtils.buildAggCluster("clusterA", Arrays.asList("clusterA1", "clusterAB1")); + Cluster clusterB = + XdsTestUtils.buildAggCluster("clusterB", Arrays.asList("clusterB1", "clusterAB1")); + Cluster clusterA1 = + XdsTestUtils.buildAggCluster("clusterA1", Arrays.asList("clusterA11", "clusterAB1")); + Cluster clusterAB1 = + XdsTestUtils.buildAggCluster("clusterAB1", Arrays.asList("clusterA11", "clusterAB11")); + + Map clusterMap = new HashMap<>(); + Map edsMap = new HashMap<>(); + + clusterMap.put("root", rootCluster); + clusterMap.put("clusterA", clusterA); + clusterMap.put("clusterB", clusterB); + clusterMap.put("clusterA1", clusterA1); + clusterMap.put("clusterAB1", clusterAB1); + + XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterA11", "clusterAB11", "clusterB1"); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); + + InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); + inOrder.verify(xdsConfigWatcher, atLeastOnce()).onUpdate(any()); + + // Do the test + String newRdsName = "newRdsName1"; + Listener clientListener = buildInlineClientListener(newRdsName, "root"); + Listener serverListener = ControlPlaneRule.buildServerListener(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, + ImmutableMap.of(XdsTestUtils.SERVER_LISTENER, serverListener, serverName, clientListener)); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); + XdsConfig config = xdsConfigCaptor.getValue(); + assertThat(config.getVirtualHost().name()).isEqualTo(newRdsName); + assertThat(config.getClusters().size()).isEqualTo(8); + } + + @Test + public void testChangeAggCluster() { + InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); + + xdsDependencyManager = new XdsDependencyManager( + xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + inOrder.verify(xdsConfigWatcher, atLeastOnce()).onUpdate(any()); + + // Setup initial config A -> A1 -> (A11, A12) + Cluster rootCluster = + XdsTestUtils.buildAggCluster("root", Arrays.asList("clusterA")); + Cluster clusterA = + XdsTestUtils.buildAggCluster("clusterA", Arrays.asList("clusterA1")); + Cluster clusterA1 = + XdsTestUtils.buildAggCluster("clusterA1", Arrays.asList("clusterA11", "clusterA12")); + + Map clusterMap = new HashMap<>(); + Map edsMap = new HashMap<>(); + + clusterMap.put("root", rootCluster); + clusterMap.put("clusterA", clusterA); + clusterMap.put("clusterA1", clusterA1); + + XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterA11", "clusterA12"); + Listener clientListener = buildInlineClientListener(RDS_NAME, "root"); + Listener serverListener = ControlPlaneRule.buildServerListener(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, + ImmutableMap.of(XdsTestUtils.SERVER_LISTENER, serverListener, serverName, clientListener)); + + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); + + inOrder.verify(xdsConfigWatcher).onUpdate(any()); + + // Update the cluster to A -> A2 -> (A21, A22) + Cluster clusterA2 = + XdsTestUtils.buildAggCluster("clusterA2", Arrays.asList("clusterA21", "clusterA22")); + clusterA = + XdsTestUtils.buildAggCluster("clusterA", Arrays.asList("clusterA2")); + clusterMap.clear(); + edsMap.clear(); + clusterMap.put("root", rootCluster); + clusterMap.put("clusterA", clusterA); + clusterMap.put("clusterA2", clusterA2); + XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterA21", "clusterA22"); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); + + // Verify that the config is updated as expected + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); + XdsConfig config = xdsConfigCaptor.getValue(); + assertThat(config.getClusters().keySet()).containsExactly("root", "clusterA", "clusterA2", + "clusterA21", "clusterA22"); + } + + private Listener buildInlineClientListener(String rdsName, String clusterName) { + return XdsTestUtils.buildInlineClientListener(rdsName, clusterName, serverName); + } + + + private static String toContextStr(String type, String resourceName) { + return type + " resource: " + resourceName; + } + + private static class TestWatcher implements XdsDependencyManager.XdsConfigWatcher { + XdsConfig lastConfig; + int numUpdates = 0; + int numError = 0; + int numDoesNotExist = 0; + + @Override + public void onUpdate(XdsConfig config) { + log.fine("Config changed: " + config); + lastConfig = config; + numUpdates++; + } + + @Override + public void onError(String resourceContext, Status status) { + log.fine(String.format("Error %s for %s: ", status, resourceContext)); + numError++; + } + + @Override + public void onResourceDoesNotExist(String resourceName) { + log.fine("Resource does not exist: " + resourceName); + numDoesNotExist++; + } + + private List getStats() { + return Arrays.asList(numUpdates, numError, numDoesNotExist); + } + + private void verifyStats(int updt, int err, int notExist) { + assertThat(getStats()).isEqualTo(Arrays.asList(updt, err, notExist)); + } + } + + private static class WrappedXdsClient extends XdsClient { + private final XdsClient delegate; + private final SynchronizationContext syncContext; + private ResourceWatcher ldsWatcher; + + WrappedXdsClient(XdsClient delegate, SynchronizationContext syncContext) { + this.delegate = delegate; + this.syncContext = syncContext; + } + + @Override + public void shutdown() { + delegate.shutdown(); + } + + @Override + @SuppressWarnings("unchecked") + public void watchXdsResource( + XdsResourceType type, String resourceName, ResourceWatcher watcher, + Executor executor) { + if (type.equals(XdsListenerResource.getInstance())) { + ldsWatcher = (ResourceWatcher) watcher; + } + delegate.watchXdsResource(type, resourceName, watcher, executor); + } + + + + @Override + public void cancelXdsResourceWatch(XdsResourceType type, + String resourceName, + ResourceWatcher watcher) { + delegate.cancelXdsResourceWatch(type, resourceName, watcher); + } + + void deliverLdsUpdate(long httpMaxStreamDurationNano, + List virtualHosts) { + syncContext.execute(() -> { + LdsUpdate ldsUpdate = LdsUpdate.forApiListener( + io.grpc.xds.HttpConnectionManager.forVirtualHosts( + httpMaxStreamDurationNano, virtualHosts, null)); + ldsWatcher.onChanged(ldsUpdate); + }); + } + } +} diff --git a/xds/src/test/java/io/grpc/xds/XdsTestControlPlaneService.java b/xds/src/test/java/io/grpc/xds/XdsTestControlPlaneService.java index 98f5fcbfef9..a54893c9075 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestControlPlaneService.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestControlPlaneService.java @@ -106,7 +106,7 @@ public void setXdsConfig(final String type, final Map copyResources = new HashMap<>(resources); xdsResources.put(type, copyResources); - String newVersionInfo = String.valueOf(xdsVersions.get(type).getAndDecrement()); + String newVersionInfo = String.valueOf(xdsVersions.get(type).getAndIncrement()); for (Map.Entry, Set> entry : subscribers.get(type).entrySet()) { @@ -119,6 +119,11 @@ public void run() { }); } + ImmutableMap getCurrentConfig(String type) { + HashMap hashMap = xdsResources.get(type); + return (hashMap != null) ? ImmutableMap.copyOf(hashMap) : ImmutableMap.of(); + } + @Override public StreamObserver streamAggregatedResources( final StreamObserver responseObserver) { @@ -159,7 +164,7 @@ public void run() { DiscoveryResponse response = generateResponse(resourceType, String.valueOf(xdsVersions.get(resourceType)), - String.valueOf(xdsNonces.get(resourceType).get(responseObserver)), + String.valueOf(xdsNonces.get(resourceType).get(responseObserver).addAndGet(1)), requestedResourceNames); responseObserver.onNext(response); subscribers.get(resourceType).put(responseObserver, requestedResourceNames); diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java new file mode 100644 index 00000000000..7f5ec0b27c6 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -0,0 +1,423 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_EDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_LDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_RDS; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.Message; +import com.google.protobuf.util.Durations; +import io.envoyproxy.envoy.config.cluster.v3.Cluster; +import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; +import io.envoyproxy.envoy.config.endpoint.v3.ClusterStats; +import io.envoyproxy.envoy.config.listener.v3.ApiListener; +import io.envoyproxy.envoy.config.listener.v3.Listener; +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.extensions.clusters.aggregate.v3.ClusterConfig; +import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router; +import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter; +import io.envoyproxy.envoy.service.load_stats.v3.LoadReportingServiceGrpc; +import io.envoyproxy.envoy.service.load_stats.v3.LoadStatsRequest; +import io.envoyproxy.envoy.service.load_stats.v3.LoadStatsResponse; +import io.grpc.BindableService; +import io.grpc.Context; +import io.grpc.Context.CancellationListener; +import io.grpc.Status; +import io.grpc.StatusOr; +import io.grpc.internal.JsonParser; +import io.grpc.stub.StreamObserver; +import io.grpc.xds.Endpoints.LbEndpoint; +import io.grpc.xds.Endpoints.LocalityLbEndpoints; +import io.grpc.xds.client.Bootstrapper; +import io.grpc.xds.client.Locality; +import io.grpc.xds.client.XdsResourceType; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.mockito.ArgumentMatcher; +import org.mockito.InOrder; + +public class XdsTestUtils { + private static final Logger log = Logger.getLogger(XdsTestUtils.class.getName()); + static final String RDS_NAME = "route-config.googleapis.com"; + static final String CLUSTER_NAME = "cluster0"; + static final String EDS_NAME = "eds-service-0"; + static final String SERVER_LISTENER = "grpc/server?udpa.resource.listening_address="; + static final String HTTP_CONNECTION_MANAGER_TYPE_URL = + "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3" + + ".HttpConnectionManager"; + public static final String ENDPOINT_HOSTNAME = "data-host"; + public static final int ENDPOINT_PORT = 1234; + + static BindableService createLrsService(AtomicBoolean lrsEnded, + Queue loadReportCalls) { + return new LoadReportingServiceGrpc.LoadReportingServiceImplBase() { + @Override + public StreamObserver streamLoadStats( + StreamObserver responseObserver) { + assertThat(lrsEnded.get()).isTrue(); + lrsEnded.set(false); + @SuppressWarnings("unchecked") + StreamObserver requestObserver = mock(StreamObserver.class); + LrsRpcCall call = new LrsRpcCall(requestObserver, responseObserver); + Context.current().addListener( + new CancellationListener() { + @Override + public void cancelled(Context context) { + lrsEnded.set(true); + } + }, MoreExecutors.directExecutor()); + loadReportCalls.offer(call); + return requestObserver; + } + }; + } + + static boolean matchErrorDetail( + com.google.rpc.Status errorDetail, int expectedCode, List expectedMessages) { + if (expectedCode != errorDetail.getCode()) { + return false; + } + List errors = Splitter.on('\n').splitToList(errorDetail.getMessage()); + if (errors.size() != expectedMessages.size()) { + return false; + } + for (int i = 0; i < errors.size(); i++) { + if (!errors.get(i).startsWith(expectedMessages.get(i))) { + return false; + } + } + return true; + } + + static void setAdsConfig(XdsTestControlPlaneService service, String serverName) { + setAdsConfig(service, serverName, RDS_NAME, CLUSTER_NAME, EDS_NAME, ENDPOINT_HOSTNAME, + ENDPOINT_PORT); + } + + static void setAdsConfig(XdsTestControlPlaneService service, String serverName, String rdsName, + String clusterName, String edsName, String endpointHostname, + int endpointPort) { + + Listener serverListener = ControlPlaneRule.buildServerListener(); + Listener clientListener = ControlPlaneRule.buildClientListener(serverName, serverName, rdsName); + service.setXdsConfig(ADS_TYPE_URL_LDS, + ImmutableMap.of(SERVER_LISTENER, serverListener, serverName, clientListener)); + + RouteConfiguration routeConfig = + buildRouteConfiguration(serverName, rdsName, clusterName); + service.setXdsConfig(ADS_TYPE_URL_RDS, ImmutableMap.of(rdsName, routeConfig));; + + Cluster cluster = ControlPlaneRule.buildCluster(clusterName, edsName); + service.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of(clusterName, cluster)); + + ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( + serverName, endpointHostname, endpointPort, edsName); + service.setXdsConfig(ADS_TYPE_URL_EDS, + ImmutableMap.of(edsName, clusterLoadAssignment)); + + log.log(Level.FINE, String.format("Set ADS config for %s with address %s:%d", + serverName, endpointHostname, endpointPort)); + + } + + static String getEdsNameForCluster(String clusterName) { + return "eds_" + clusterName; + } + + static void setAggregateCdsConfig(XdsTestControlPlaneService service, String serverName, + String clusterName, List children) { + Map clusterMap = new HashMap<>(); + + ClusterConfig rootConfig = ClusterConfig.newBuilder().addAllClusters(children).build(); + Cluster.CustomClusterType type = + Cluster.CustomClusterType.newBuilder() + .setName(XdsClusterResource.AGGREGATE_CLUSTER_TYPE_NAME) + .setTypedConfig(Any.pack(rootConfig)) + .build(); + Cluster.Builder builder = Cluster.newBuilder().setName(clusterName).setClusterType(type); + builder.setLbPolicy(Cluster.LbPolicy.ROUND_ROBIN); + Cluster cluster = builder.build(); + clusterMap.put(clusterName, cluster); + + for (String child : children) { + Cluster childCluster = ControlPlaneRule.buildCluster(child, getEdsNameForCluster(child)); + clusterMap.put(child, childCluster); + } + + service.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); + + Map edsMap = new HashMap<>(); + for (String child : children) { + ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( + serverName, ENDPOINT_HOSTNAME, ENDPOINT_PORT, getEdsNameForCluster(child)); + edsMap.put(getEdsNameForCluster(child), clusterLoadAssignment); + } + service.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); + } + + static void addAggregateToExistingConfig(XdsTestControlPlaneService service, String rootName, + List children) { + Map clusterMap = new HashMap<>(service.getCurrentConfig(ADS_TYPE_URL_CDS)); + if (clusterMap.containsKey(rootName)) { + throw new IllegalArgumentException("Root cluster " + rootName + " already exists"); + } + ClusterConfig rootConfig = ClusterConfig.newBuilder().addAllClusters(children).build(); + Cluster.CustomClusterType type = + Cluster.CustomClusterType.newBuilder() + .setName(XdsClusterResource.AGGREGATE_CLUSTER_TYPE_NAME) + .setTypedConfig(Any.pack(rootConfig)) + .build(); + Cluster.Builder builder = Cluster.newBuilder().setName(rootName).setClusterType(type); + builder.setLbPolicy(Cluster.LbPolicy.ROUND_ROBIN); + Cluster cluster = builder.build(); + clusterMap.put(rootName, cluster); + + for (String child : children) { + if (clusterMap.containsKey(child)) { + continue; + } + Cluster childCluster = ControlPlaneRule.buildCluster(child, getEdsNameForCluster(child)); + clusterMap.put(child, childCluster); + } + + service.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); + + Map edsMap = new HashMap<>(service.getCurrentConfig(ADS_TYPE_URL_EDS)); + for (String child : children) { + if (edsMap.containsKey(getEdsNameForCluster(child))) { + continue; + } + ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( + child, ENDPOINT_HOSTNAME, ENDPOINT_PORT, getEdsNameForCluster(child)); + edsMap.put(getEdsNameForCluster(child), clusterLoadAssignment); + } + service.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); + } + + static XdsConfig getDefaultXdsConfig(String serverHostName) + throws XdsResourceType.ResourceInvalidException, IOException { + XdsConfig.XdsConfigBuilder builder = new XdsConfig.XdsConfigBuilder(); + + Filter.NamedFilterConfig routerFilterConfig = new Filter.NamedFilterConfig( + serverHostName, RouterFilter.ROUTER_CONFIG); + + HttpConnectionManager httpConnectionManager = HttpConnectionManager.forRdsName( + 0L, RDS_NAME, Collections.singletonList(routerFilterConfig)); + XdsListenerResource.LdsUpdate ldsUpdate = + XdsListenerResource.LdsUpdate.forApiListener(httpConnectionManager); + + RouteConfiguration routeConfiguration = + buildRouteConfiguration(serverHostName, RDS_NAME, CLUSTER_NAME); + Bootstrapper.ServerInfo serverInfo = null; + XdsResourceType.Args args = new XdsResourceType.Args(serverInfo, "0", "0", null, null, null); + XdsRouteConfigureResource.RdsUpdate rdsUpdate = + XdsRouteConfigureResource.getInstance().doParse(args, routeConfiguration); + + // Take advantage of knowing that there is only 1 virtual host in the route configuration + assertThat(rdsUpdate.virtualHosts).hasSize(1); + VirtualHost virtualHost = rdsUpdate.virtualHosts.get(0); + + // Need to create endpoints to create locality endpoints map to create edsUpdate + Map lbEndpointsMap = new HashMap<>(); + LbEndpoint lbEndpoint = + LbEndpoint.create(serverHostName, ENDPOINT_PORT, 0, true, ENDPOINT_HOSTNAME); + lbEndpointsMap.put( + Locality.create("", "", ""), + LocalityLbEndpoints.create(ImmutableList.of(lbEndpoint), 10, 0)); + + // Need to create EdsUpdate to create CdsUpdate to create XdsClusterConfig for builder + XdsEndpointResource.EdsUpdate edsUpdate = new XdsEndpointResource.EdsUpdate( + EDS_NAME, lbEndpointsMap, Collections.emptyList()); + XdsClusterResource.CdsUpdate cdsUpdate = XdsClusterResource.CdsUpdate.forEds( + CLUSTER_NAME, EDS_NAME, serverInfo, null, null, null) + .lbPolicyConfig(getWrrLbConfigAsMap()).build(); + XdsConfig.XdsClusterConfig clusterConfig = new XdsConfig.XdsClusterConfig( + CLUSTER_NAME, cdsUpdate, StatusOr.fromValue(edsUpdate)); + + builder + .setListener(ldsUpdate) + .setRoute(rdsUpdate) + .setVirtualHost(virtualHost) + .addCluster(CLUSTER_NAME, StatusOr.fromValue(clusterConfig)); + + return builder.build(); + } + + @SuppressWarnings("unchecked") + private static ImmutableMap getWrrLbConfigAsMap() throws IOException { + String lbConfigStr = "{\"wrr_locality_experimental\" : " + + "{ \"childPolicy\" : [{\"round_robin\" : {}}]}}"; + + return ImmutableMap.copyOf((Map) JsonParser.parse(lbConfigStr)); + } + + static RouteConfiguration buildRouteConfiguration(String authority, String rdsName, + String clusterName) { + io.envoyproxy.envoy.config.route.v3.VirtualHost.Builder vhBuilder = + io.envoyproxy.envoy.config.route.v3.VirtualHost.newBuilder() + .setName(rdsName) + .addDomains(authority) + .addRoutes( + Route.newBuilder() + .setMatch( + RouteMatch.newBuilder().setPrefix("/").build()) + .setRoute( + RouteAction.newBuilder().setCluster(clusterName) + .setAutoHostRewrite(BoolValue.newBuilder().setValue(true).build()) + .build())); + io.envoyproxy.envoy.config.route.v3.VirtualHost virtualHost = vhBuilder.build(); + return RouteConfiguration.newBuilder().setName(rdsName).addVirtualHosts(virtualHost).build(); + } + + static Cluster buildAggCluster(String name, List childNames) { + ClusterConfig rootConfig = ClusterConfig.newBuilder().addAllClusters(childNames).build(); + Cluster.CustomClusterType type = + Cluster.CustomClusterType.newBuilder() + .setName(XdsClusterResource.AGGREGATE_CLUSTER_TYPE_NAME) + .setTypedConfig(Any.pack(rootConfig)) + .build(); + Cluster.Builder builder = + Cluster.newBuilder().setName(name).setClusterType(type); + builder.setLbPolicy(Cluster.LbPolicy.ROUND_ROBIN); + Cluster cluster = builder.build(); + return cluster; + } + + static void addEdsClusters(Map clusterMap, Map edsMap, + String... clusterNames) { + for (String clusterName : clusterNames) { + String edsName = getEdsNameForCluster(clusterName); + Cluster cluster = ControlPlaneRule.buildCluster(clusterName, edsName); + clusterMap.put(clusterName, cluster); + + ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( + clusterName, ENDPOINT_HOSTNAME, ENDPOINT_PORT, edsName); + edsMap.put(edsName, clusterLoadAssignment); + } + } + + static Listener buildInlineClientListener(String rdsName, String clusterName, String serverName) { + HttpFilter + httpFilter = HttpFilter.newBuilder() + .setName(serverName) + .setTypedConfig(Any.pack(Router.newBuilder().build())) + .setIsOptional(true) + .build(); + ApiListener.Builder clientListenerBuilder = + ApiListener.newBuilder().setApiListener(Any.pack( + io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3 + .HttpConnectionManager.newBuilder() + .setRouteConfig( + buildRouteConfiguration(serverName, rdsName, clusterName)) + .addAllHttpFilters(Collections.singletonList(httpFilter)) + .build(), + HTTP_CONNECTION_MANAGER_TYPE_URL)); + return Listener.newBuilder() + .setName(serverName) + .setApiListener(clientListenerBuilder.build()).build(); + + } + + /** + * Matches a {@link LoadStatsRequest} containing a collection of {@link ClusterStats} with + * the same list of clusterName:clusterServiceName pair. + */ + static class LrsRequestMatcher implements ArgumentMatcher { + private final List expected; + + private LrsRequestMatcher(List clusterNames) { + expected = new ArrayList<>(); + for (String[] pair : clusterNames) { + expected.add(pair[0] + ":" + (pair[1] == null ? "" : pair[1])); + } + Collections.sort(expected); + } + + @Override + public boolean matches(LoadStatsRequest argument) { + List actual = new ArrayList<>(); + for (ClusterStats clusterStats : argument.getClusterStatsList()) { + actual.add(clusterStats.getClusterName() + ":" + clusterStats.getClusterServiceName()); + } + Collections.sort(actual); + return actual.equals(expected); + } + } + + static class LrsRpcCall { + private final StreamObserver requestObserver; + private final StreamObserver responseObserver; + private final InOrder inOrder; + + private LrsRpcCall(StreamObserver requestObserver, + StreamObserver responseObserver) { + this.requestObserver = requestObserver; + this.responseObserver = responseObserver; + inOrder = inOrder(requestObserver); + } + + protected void verifyNextReportClusters(List clusters) { + inOrder.verify(requestObserver).onNext(argThat(new LrsRequestMatcher(clusters))); + } + + protected void sendResponse(List clusters, long loadReportIntervalNano) { + LoadStatsResponse response = + LoadStatsResponse.newBuilder() + .addAllClusters(clusters) + .setLoadReportingInterval(Durations.fromNanos(loadReportIntervalNano)) + .build(); + responseObserver.onNext(response); + } + } + + static class StatusMatcher implements ArgumentMatcher { + private final Status expectedStatus; + + StatusMatcher(Status expectedStatus) { + this.expectedStatus = expectedStatus; + } + + @Override + public boolean matches(Status status) { + return status != null && expectedStatus.getCode().equals(status.getCode()) + && expectedStatus.getDescription().equals(status.getDescription()); + } + } +} diff --git a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java index f3de4549ba9..485970741c1 100644 --- a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java @@ -34,9 +34,15 @@ import javax.annotation.Nullable; public class CommonBootstrapperTestUtils { + public static final String SERVER_URI = "trafficdirector.googleapis.com"; private static final ChannelCredentials CHANNEL_CREDENTIALS = InsecureChannelCredentials.create(); private static final String SERVER_URI_CUSTOM_AUTHORITY = "trafficdirector2.googleapis.com"; private static final String SERVER_URI_EMPTY_AUTHORITY = "trafficdirector3.googleapis.com"; + public static final String LDS_RESOURCE = "listener.googleapis.com"; + public static final String RDS_RESOURCE = "route-configuration.googleapis.com"; + public static final String CDS_RESOURCE = "cluster.googleapis.com"; + public static final String EDS_RESOURCE = "cluster-load-assignment.googleapis.com"; + private static final String FILE_WATCHER_CONFIG = "{\"path\": \"/etc/secret/certs\"}"; private static final String MESHCA_CONFIG = "{\n" From bd6af59221fa4f0572193b363308c952eee7d884 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Mon, 10 Feb 2025 17:14:07 -0800 Subject: [PATCH 174/591] xds: improve code readability of server FilterChain parsing - Improve code flow and variable names - Reduce nesting - Add comments between logical blocks - Add comments explaining some xDS/gRPC nuances --- .../java/io/grpc/xds/XdsListenerResource.java | 86 ++++++++++++------- .../java/io/grpc/xds/XdsServerWrapper.java | 77 +++++++++++------ .../grpc/xds/GrpcXdsClientImplDataTest.java | 36 +++++--- 3 files changed, 127 insertions(+), 72 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java index 141580af73d..a18b093e38f 100644 --- a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java @@ -146,11 +146,11 @@ static EnvoyServerProtoData.Listener parseServerSideListener( Listener proto, TlsContextManager tlsContextManager, FilterRegistry filterRegistry, Set certProviderInstances, XdsResourceType.Args args) throws ResourceInvalidException { - if (!proto.getTrafficDirection().equals(TrafficDirection.INBOUND) - && !proto.getTrafficDirection().equals(TrafficDirection.UNSPECIFIED)) { + TrafficDirection trafficDirection = proto.getTrafficDirection(); + if (!trafficDirection.equals(TrafficDirection.INBOUND) + && !trafficDirection.equals(TrafficDirection.UNSPECIFIED)) { throw new ResourceInvalidException( - "Listener " + proto.getName() + " with invalid traffic direction: " - + proto.getTrafficDirection()); + "Listener " + proto.getName() + " with invalid traffic direction: " + trafficDirection); } if (!proto.getListenerFiltersList().isEmpty()) { throw new ResourceInvalidException( @@ -178,16 +178,29 @@ static EnvoyServerProtoData.Listener parseServerSideListener( } ImmutableList.Builder filterChains = ImmutableList.builder(); - Set uniqueSet = new HashSet<>(); + Set filterChainMatchSet = new HashSet<>(); + int i = 0; for (io.envoyproxy.envoy.config.listener.v3.FilterChain fc : proto.getFilterChainsList()) { + // May be empty. If it's not empty, required to be unique. + String filterChainName = fc.getName(); + if (filterChainName.isEmpty()) { + // Generate a name, so we can identify it in the logs. + filterChainName = "chain_" + i; + } filterChains.add( - parseFilterChain(fc, tlsContextManager, filterRegistry, uniqueSet, - certProviderInstances, args)); + parseFilterChain(fc, filterChainName, tlsContextManager, filterRegistry, + filterChainMatchSet, certProviderInstances, args)); + i++; } + FilterChain defaultFilterChain = null; if (proto.hasDefaultFilterChain()) { + String defaultFilterChainName = proto.getDefaultFilterChain().getName(); + if (defaultFilterChainName.isEmpty()) { + defaultFilterChainName = "chain_default"; + } defaultFilterChain = parseFilterChain( - proto.getDefaultFilterChain(), tlsContextManager, filterRegistry, + proto.getDefaultFilterChain(), defaultFilterChainName, tlsContextManager, filterRegistry, null, certProviderInstances, args); } @@ -198,36 +211,44 @@ static EnvoyServerProtoData.Listener parseServerSideListener( @VisibleForTesting static FilterChain parseFilterChain( io.envoyproxy.envoy.config.listener.v3.FilterChain proto, - TlsContextManager tlsContextManager, FilterRegistry filterRegistry, - Set uniqueSet, Set certProviderInstances, XdsResourceType.Args args) + String filterChainName, + TlsContextManager tlsContextManager, + FilterRegistry filterRegistry, + // null disables FilterChainMatch uniqueness check, used for defaultFilterChain + @Nullable Set filterChainMatchSet, + Set certProviderInstances, + XdsResourceType.Args args) throws ResourceInvalidException { + // FilterChain contains L4 filters, so we ensure it contains only HCM. if (proto.getFiltersCount() != 1) { - throw new ResourceInvalidException("FilterChain " + proto.getName() + throw new ResourceInvalidException("FilterChain " + filterChainName + " should contain exact one HttpConnectionManager filter"); } - io.envoyproxy.envoy.config.listener.v3.Filter filter = proto.getFiltersList().get(0); - if (!filter.hasTypedConfig()) { + io.envoyproxy.envoy.config.listener.v3.Filter l4Filter = proto.getFiltersList().get(0); + if (!l4Filter.hasTypedConfig()) { throw new ResourceInvalidException( - "FilterChain " + proto.getName() + " contains filter " + filter.getName() + "FilterChain " + filterChainName + " contains filter " + l4Filter.getName() + " without typed_config"); } - Any any = filter.getTypedConfig(); - // HttpConnectionManager is the only supported network filter at the moment. + Any any = l4Filter.getTypedConfig(); if (!any.getTypeUrl().equals(TYPE_URL_HTTP_CONNECTION_MANAGER)) { throw new ResourceInvalidException( - "FilterChain " + proto.getName() + " contains filter " + filter.getName() + "FilterChain " + filterChainName + " contains filter " + l4Filter.getName() + " with unsupported typed_config type " + any.getTypeUrl()); } + + // Parse HCM. HttpConnectionManager hcmProto; try { hcmProto = any.unpack(HttpConnectionManager.class); } catch (InvalidProtocolBufferException e) { - throw new ResourceInvalidException("FilterChain " + proto.getName() + " with filter " - + filter.getName() + " failed to unpack message", e); + throw new ResourceInvalidException("FilterChain " + filterChainName + " with filter " + + l4Filter.getName() + " failed to unpack message", e); } io.grpc.xds.HttpConnectionManager httpConnectionManager = parseHttpConnectionManager( hcmProto, filterRegistry, false /* isForClient */, args); + // Parse Transport Socket. EnvoyServerProtoData.DownstreamTlsContext downstreamTlsContext = null; if (proto.hasTransportSocket()) { if (!TRANSPORT_SOCKET_NAME_TLS.equals(proto.getTransportSocket().getName())) { @@ -239,7 +260,7 @@ static FilterChain parseFilterChain( downstreamTlsContextProto = proto.getTransportSocket().getTypedConfig().unpack(DownstreamTlsContext.class); } catch (InvalidProtocolBufferException e) { - throw new ResourceInvalidException("FilterChain " + proto.getName() + throw new ResourceInvalidException("FilterChain " + filterChainName + " failed to unpack message", e); } downstreamTlsContext = @@ -247,10 +268,15 @@ static FilterChain parseFilterChain( validateDownstreamTlsContext(downstreamTlsContextProto, certProviderInstances)); } + // Parse FilterChainMatch. FilterChainMatch filterChainMatch = parseFilterChainMatch(proto.getFilterChainMatch()); - checkForUniqueness(uniqueSet, filterChainMatch); + // null used to skip this check for defaultFilterChain. + if (filterChainMatchSet != null) { + validateFilterChainMatchForUniqueness(filterChainMatchSet, filterChainMatch); + } + return FilterChain.create( - proto.getName(), + filterChainName, filterChainMatch, httpConnectionManager, downstreamTlsContext, @@ -284,15 +310,15 @@ static DownstreamTlsContext validateDownstreamTlsContext( return downstreamTlsContext; } - private static void checkForUniqueness(Set uniqueSet, + private static void validateFilterChainMatchForUniqueness( + Set filterChainMatchSet, FilterChainMatch filterChainMatch) throws ResourceInvalidException { - if (uniqueSet != null) { - List crossProduct = getCrossProduct(filterChainMatch); - for (FilterChainMatch cur : crossProduct) { - if (!uniqueSet.add(cur)) { - throw new ResourceInvalidException("FilterChainMatch must be unique. " - + "Found duplicate: " + cur); - } + // Flattens complex FilterChainMatch into a list of simple FilterChainMatch'es. + List crossProduct = getCrossProduct(filterChainMatch); + for (FilterChainMatch cur : crossProduct) { + if (!filterChainMatchSet.add(cur)) { + throw new ResourceInvalidException("FilterChainMatch must be unique. " + + "Found duplicate: " + cur); } } } diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index 392f4c1a313..3a9b98ee321 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -60,7 +60,6 @@ import java.io.IOException; import java.net.SocketAddress; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -459,47 +458,69 @@ private void shutdown() { } private void updateSelector() { - Map> filterChainRouting = new HashMap<>(); + // This is regenerated in generateRoutingConfig() calls below. savedRdsRoutingConfigRef.clear(); + + // Prepare server routing config map. + ImmutableMap.Builder> routingConfigs = + ImmutableMap.builder(); for (FilterChain filterChain: filterChains) { - filterChainRouting.put(filterChain, generateRoutingConfig(filterChain)); + routingConfigs.put(filterChain, generateRoutingConfig(filterChain)); } - FilterChainSelector selector = new FilterChainSelector( - Collections.unmodifiableMap(filterChainRouting), - defaultFilterChain == null ? null : defaultFilterChain.sslContextProviderSupplier(), - defaultFilterChain == null ? new AtomicReference() : - generateRoutingConfig(defaultFilterChain)); - List toRelease = getSuppliersInUse(); + + // Prepare the new selector. + FilterChainSelector selector; + if (defaultFilterChain != null) { + selector = new FilterChainSelector( + routingConfigs.build(), + defaultFilterChain.sslContextProviderSupplier(), + generateRoutingConfig(defaultFilterChain)); + } else { + selector = new FilterChainSelector(routingConfigs.build(), null, new AtomicReference<>()); + } + + // Prepare the list of current selector's resources to close later. + List oldSslSuppliers = getSuppliersInUse(); + + // Swap the selectors, initiate a graceful shutdown of the old one. logger.log(Level.FINEST, "Updating selector {0}", selector); filterChainSelectorManager.updateSelector(selector); - for (SslContextProviderSupplier e: toRelease) { - e.close(); + + // Release old resources. + for (SslContextProviderSupplier supplier: oldSslSuppliers) { + supplier.close(); } + + // Now that we have valid Transport Socket config, we can start/restart listening on a port. startDelegateServer(); } private AtomicReference generateRoutingConfig(FilterChain filterChain) { HttpConnectionManager hcm = filterChain.httpConnectionManager(); + ImmutableMap interceptors; + + // Inlined routes. if (hcm.virtualHosts() != null) { - ImmutableMap interceptors = generatePerRouteInterceptors( - hcm.httpFilterConfigs(), hcm.virtualHosts()); - return new AtomicReference<>(ServerRoutingConfig.create(hcm.virtualHosts(),interceptors)); + interceptors = generatePerRouteInterceptors(hcm.httpFilterConfigs(), hcm.virtualHosts()); + return new AtomicReference<>(ServerRoutingConfig.create(hcm.virtualHosts(), interceptors)); + } + + // Routes from RDS. + RouteDiscoveryState rds = routeDiscoveryStates.get(hcm.rdsName()); + checkNotNull(rds, "rds"); + + ServerRoutingConfig routingConfig; + ImmutableList savedVhosts = rds.savedVirtualHosts; + if (savedVhosts != null) { + interceptors = generatePerRouteInterceptors(hcm.httpFilterConfigs(), savedVhosts); + routingConfig = ServerRoutingConfig.create(savedVhosts, interceptors); } else { - RouteDiscoveryState rds = routeDiscoveryStates.get(hcm.rdsName()); - checkNotNull(rds, "rds"); - AtomicReference serverRoutingConfigRef = new AtomicReference<>(); - if (rds.savedVirtualHosts != null) { - ImmutableMap interceptors = generatePerRouteInterceptors( - hcm.httpFilterConfigs(), rds.savedVirtualHosts); - ServerRoutingConfig serverRoutingConfig = - ServerRoutingConfig.create(rds.savedVirtualHosts, interceptors); - serverRoutingConfigRef.set(serverRoutingConfig); - } else { - serverRoutingConfigRef.set(ServerRoutingConfig.FAILING_ROUTING_CONFIG); - } - savedRdsRoutingConfigRef.put(filterChain, serverRoutingConfigRef); - return serverRoutingConfigRef; + routingConfig = ServerRoutingConfig.FAILING_ROUTING_CONFIG; } + + AtomicReference routingConfigRef = new AtomicReference<>(routingConfig); + savedRdsRoutingConfigRef.put(filterChain, routingConfigRef); + return routingConfigRef; } private ImmutableMap generatePerRouteInterceptors( diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 5b9fdda1127..314b2094480 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -2719,7 +2719,7 @@ public void parseFilterChain_noHcm() throws ResourceInvalidException { thrown.expectMessage( "FilterChain filter-chain-foo should contain exact one HttpConnectionManager filter"); XdsListenerResource.parseFilterChain( - filterChain, null, filterRegistry, null, null, + filterChain, "filter-chain-foo", null, filterRegistry, null, null, getXdsResourceTypeArgs(true)); } @@ -2738,7 +2738,7 @@ public void parseFilterChain_duplicateFilter() throws ResourceInvalidException { thrown.expectMessage( "FilterChain filter-chain-foo should contain exact one HttpConnectionManager filter"); XdsListenerResource.parseFilterChain( - filterChain, null, filterRegistry, null, null, + filterChain, "filter-chain-foo", null, filterRegistry, null, null, getXdsResourceTypeArgs(true)); } @@ -2757,7 +2757,7 @@ public void parseFilterChain_filterMissingTypedConfig() throws ResourceInvalidEx "FilterChain filter-chain-foo contains filter envoy.http_connection_manager " + "without typed_config"); XdsListenerResource.parseFilterChain( - filterChain, null, filterRegistry, null, null, + filterChain, "filter-chain-foo", null, filterRegistry, null, null, getXdsResourceTypeArgs(true)); } @@ -2780,13 +2780,13 @@ public void parseFilterChain_unsupportedFilter() throws ResourceInvalidException "FilterChain filter-chain-foo contains filter unsupported with unsupported " + "typed_config type unsupported-type-url"); XdsListenerResource.parseFilterChain( - filterChain, null, filterRegistry, null, null, + filterChain, "filter-chain-foo", null, filterRegistry, null, null, getXdsResourceTypeArgs(true)); } @Test public void parseFilterChain_noName() throws ResourceInvalidException { - FilterChain filterChain1 = + FilterChain filterChain0 = FilterChain.newBuilder() .setFilterChainMatch(FilterChainMatch.getDefaultInstance()) .addFilters(buildHttpConnectionManagerFilter( @@ -2796,9 +2796,11 @@ public void parseFilterChain_noName() throws ResourceInvalidException { .setTypedConfig(Any.pack(Router.newBuilder().build())) .build())) .build(); - FilterChain filterChain2 = + + FilterChain filterChain1 = FilterChain.newBuilder() - .setFilterChainMatch(FilterChainMatch.getDefaultInstance()) + .setFilterChainMatch( + FilterChainMatch.newBuilder().addAllSourcePorts(Arrays.asList(443, 8080))) .addFilters(buildHttpConnectionManagerFilter( HttpFilter.newBuilder() .setName("http-filter-bar") @@ -2807,13 +2809,19 @@ public void parseFilterChain_noName() throws ResourceInvalidException { .build())) .build(); - EnvoyServerProtoData.FilterChain parsedFilterChain1 = XdsListenerResource.parseFilterChain( - filterChain1, null, filterRegistry, null, - null, getXdsResourceTypeArgs(true)); - EnvoyServerProtoData.FilterChain parsedFilterChain2 = XdsListenerResource.parseFilterChain( - filterChain2, null, filterRegistry, null, - null, getXdsResourceTypeArgs(true)); - assertThat(parsedFilterChain1.name()).isEqualTo(parsedFilterChain2.name()); + Listener listenerProto = + Listener.newBuilder() + .setName("listener1") + .setTrafficDirection(TrafficDirection.INBOUND) + .addAllFilterChains(Arrays.asList(filterChain0, filterChain1)) + .setDefaultFilterChain(filterChain0) + .build(); + EnvoyServerProtoData.Listener listener = XdsListenerResource.parseServerSideListener( + listenerProto, null, filterRegistry, null, getXdsResourceTypeArgs(true)); + + assertThat(listener.filterChains().get(0).name()).isEqualTo("chain_0"); + assertThat(listener.filterChains().get(1).name()).isEqualTo("chain_1"); + assertThat(listener.defaultFilterChain().name()).isEqualTo("chain_default"); } @Test From dc316f7fd974675a466325b3f4a69ba0d72db53b Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Thu, 30 Jan 2025 09:26:55 -0800 Subject: [PATCH 175/591] Add missing frame release to Http2ClientStreamTransportState. If a data frame is received before headers, processing of the frame is abandoned. The frame must be released in that case. --- .../java/io/grpc/internal/Http2ClientStreamTransportState.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/io/grpc/internal/Http2ClientStreamTransportState.java b/core/src/main/java/io/grpc/internal/Http2ClientStreamTransportState.java index e92bb7a4af1..5560a1abb6d 100644 --- a/core/src/main/java/io/grpc/internal/Http2ClientStreamTransportState.java +++ b/core/src/main/java/io/grpc/internal/Http2ClientStreamTransportState.java @@ -140,6 +140,7 @@ protected void transportDataReceived(ReadableBuffer frame, boolean endOfStream) } } else { if (!headersReceived) { + frame.close(); http2ProcessingFailed( Status.INTERNAL.withDescription("headers not received before payload"), false, From 302342cfce50361bc1b12a37a3627d34c25dbf88 Mon Sep 17 00:00:00 2001 From: Naveen Prasanna V <80662475+NaveenPrasannaV@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:38:07 +0530 Subject: [PATCH 176/591] core: logging the error message when onClose() itself fails (#11880) --- .../java/io/grpc/internal/ClientCallImpl.java | 6 ++++- .../io/grpc/internal/ClientCallImplTest.java | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/io/grpc/internal/ClientCallImpl.java b/core/src/main/java/io/grpc/internal/ClientCallImpl.java index 07f2701d1c1..b9e8dd79f09 100644 --- a/core/src/main/java/io/grpc/internal/ClientCallImpl.java +++ b/core/src/main/java/io/grpc/internal/ClientCallImpl.java @@ -561,7 +561,11 @@ public Attributes getAttributes() { } private void closeObserver(Listener observer, Status status, Metadata trailers) { - observer.onClose(status, trailers); + try { + observer.onClose(status, trailers); + } catch (RuntimeException ex) { + log.log(Level.WARNING, "Exception thrown by onClose() in ClientCall", ex); + } } @Override diff --git a/core/src/test/java/io/grpc/internal/ClientCallImplTest.java b/core/src/test/java/io/grpc/internal/ClientCallImplTest.java index 66d626ec2b6..03e613e13d9 100644 --- a/core/src/test/java/io/grpc/internal/ClientCallImplTest.java +++ b/core/src/test/java/io/grpc/internal/ClientCallImplTest.java @@ -1105,6 +1105,32 @@ public void getAttributes() { assertEquals(attrs, call.getAttributes()); } + @Test + public void onCloseExceptionCaughtAndLogged() { + DelayedExecutor executor = new DelayedExecutor(); + ClientCallImpl call = new ClientCallImpl<>( + method, + executor, + baseCallOptions, + clientStreamProvider, + deadlineCancellationExecutor, + channelCallTracer, configSelector); + + call.start(callListener, new Metadata()); + verify(stream).start(listenerArgumentCaptor.capture()); + final ClientStreamListener streamListener = listenerArgumentCaptor.getValue(); + streamListener.headersRead(new Metadata()); + + doThrow(new RuntimeException("Exception thrown by onClose() in ClientCall")).when(callListener) + .onClose(any(Status.class), any(Metadata.class)); + + Status status = Status.RESOURCE_EXHAUSTED.withDescription("simulated"); + streamListener.closed(status, PROCESSED, new Metadata()); + executor.release(); + + verify(callListener).onClose(same(status), any(Metadata.class)); + } + private static final class DelayedExecutor implements Executor { private final BlockingQueue commands = new LinkedBlockingQueue<>(); From fc8571a0e53fa4d1e63ea2c118c05bbf3b5b32cc Mon Sep 17 00:00:00 2001 From: Kannan J Date: Wed, 12 Feb 2025 01:08:46 +0530 Subject: [PATCH 177/591] Version upgrades (#11874) --- MODULE.bazel | 4 ++-- gradle/libs.versions.toml | 38 +++++++++++++++++++++--------------- repositories.bzl | 4 ++-- servlet/jakarta/build.gradle | 27 ++++++++++++++++++------- 4 files changed, 46 insertions(+), 27 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 4c7e1b3dca5..7131affbdff 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,7 +8,7 @@ module( # GRPC_DEPS_START IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.android:annotations:4.1.1.4", - "com.google.api.grpc:proto-google-common-protos:2.48.0", + "com.google.api.grpc:proto-google-common-protos:2.51.0", "com.google.auth:google-auth-library-credentials:1.24.1", "com.google.auth:google-auth-library-oauth2-http:1.24.1", "com.google.auto.value:auto-value-annotations:1.11.0", @@ -18,7 +18,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.errorprone:error_prone_annotations:2.30.0", "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:33.3.1-android", - "com.google.re2j:re2j:1.7", + "com.google.re2j:re2j:1.8", "com.google.truth:truth:1.4.2", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43ec3368b76..c1554d6b2e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,18 +11,20 @@ protobuf = "3.25.5" [libraries] android-annotations = "com.google.android:annotations:4.1.1.4" androidx-annotation = "androidx.annotation:annotation:1.9.0" +# 1.15.0 requires libraries and applications that depend on it to compile against +# version 35 or later of the Android APIs. androidx-core = "androidx.core:core:1.13.1" -androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.8.6" -androidx-lifecycle-service = "androidx.lifecycle:lifecycle-service:2.8.6" +androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.8.7" +androidx-lifecycle-service = "androidx.lifecycle:lifecycle-service:2.8.7" androidx-test-core = "androidx.test:core:1.6.1" androidx-test-ext-junit = "androidx.test.ext:junit:1.2.1" androidx-test-rules = "androidx.test:rules:1.6.1" animalsniffer = "org.codehaus.mojo:animal-sniffer:1.24" animalsniffer-annotations = "org.codehaus.mojo:animal-sniffer-annotations:1.24" -assertj-core = "org.assertj:assertj-core:3.26.3" +assertj-core = "org.assertj:assertj-core:3.27.3" auto-value = "com.google.auto.value:auto-value:1.11.0" auto-value-annotations = "com.google.auto.value:auto-value-annotations:1.11.0" -checkstyle = "com.puppycrawl.tools:checkstyle:10.19.0" +checkstyle = "com.puppycrawl.tools:checkstyle:10.21.2" commons-math3 = "org.apache.commons:commons-math3:3.6.1" conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2" cronet-api = "org.chromium.net:cronet-api:119.6045.31" @@ -34,14 +36,16 @@ cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31" errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.30.0" # error-prone 2.32.0+ require Java 17+ errorprone-core = "com.google.errorprone:error_prone_core:2.31.0" -google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.48.0" +google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.51.0" # google-auth-library 1.25.0+ requires error_prone_annotations 2.31.0+, which # breaks the Android build google-auth-credentials = "com.google.auth:google-auth-library-credentials:1.24.1" google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.24.1" # Release notes: https://cloud.google.com/logging/docs/release-notes -google-cloud-logging = "com.google.cloud:google-cloud-logging:3.20.6" +google-cloud-logging = "com.google.cloud:google-cloud-logging:3.21.2" +# 2.12.1 requires error_prone_annotations:2.36.0 but we are stuck with 2.30.0 gson = "com.google.code.gson:gson:2.11.0" +# 33.4.0 requires com.google.errorprone:error_prone_annotations:2.36.0 but we are stuck with 2.30.0 (see above) guava = "com.google.guava:guava:33.3.1-android" guava-betaChecker = "com.google.guava:guava-beta-checker:1.0" guava-testlib = "com.google.guava:guava-testlib:33.3.1-android" @@ -51,13 +55,15 @@ guava-jre = "com.google.guava:guava:33.3.1-jre" hdrhistogram = "org.hdrhistogram:HdrHistogram:2.2.2" # 6.0.0+ use java.lang.Deprecated forRemoval and since from Java 9 jakarta-servlet-api = "jakarta.servlet:jakarta.servlet-api:5.0.0" +# Using javax.annotation is fine as it is part of the JDK, we don't want to depend on J2EE +# where it is relocated to as org.apache.tomcat:tomcat-annotations-api. See issue #9179. javax-annotation = "org.apache.tomcat:annotations-api:6.0.53" javax-servlet-api = "javax.servlet:javax.servlet-api:4.0.1" # 12.0.0+ require Java 17+ jetty-client = "org.eclipse.jetty:jetty-client:11.0.24" -jetty-http2-server = "org.eclipse.jetty.http2:http2-server:11.0.24" +jetty-http2-server = "org.eclipse.jetty.http2:jetty-http2-server:12.0.16" jetty-http2-server10 = "org.eclipse.jetty.http2:http2-server:10.0.20" -jetty-servlet = "org.eclipse.jetty:jetty-servlet:11.0.24" +jetty-servlet = "org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.16" jetty-servlet10 = "org.eclipse.jetty:jetty-servlet:10.0.20" jsr305 = "com.google.code.findbugs:jsr305:3.0.2" junit = "junit:junit:4.13.2" @@ -85,18 +91,18 @@ opencensus-contrib-grpc-metrics = { module = "io.opencensus:opencensus-contrib-g opencensus-exporter-stats-stackdriver = { module = "io.opencensus:opencensus-exporter-stats-stackdriver", version.ref = "opencensus" } opencensus-exporter-trace-stackdriver = { module = "io.opencensus:opencensus-exporter-trace-stackdriver", version.ref = "opencensus" } opencensus-impl = { module = "io.opencensus:opencensus-impl", version.ref = "opencensus" } -opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.43.0" -opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.43.0-alpha" -opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.40.0-alpha" -opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.43.0" -opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.43.0" +opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.46.0" +opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.46.0-alpha" +opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.43.0-alpha" +opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.46.0" +opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.46.0" perfmark-api = "io.perfmark:perfmark-api:0.27.0" protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobuf" } protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" } protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } -re2j = "com.google.re2j:re2j:1.7" -robolectric = "org.robolectric:robolectric:4.13" +re2j = "com.google.re2j:re2j:1.8" +robolectric = "org.robolectric:robolectric:4.14.1" signature-android = "net.sf.androidscents.signature:android-api-level-21:5.0.1_r2" signature-java = "org.codehaus.mojo.signature:java18:1.0" # 11.0.0+ require Java 17+ @@ -109,5 +115,5 @@ undertow-servlet = "io.undertow:undertow-servlet:2.3.18.Final" # Do not update: Pinned to the last version supporting Java 8. # See https://checkstyle.sourceforge.io/releasenotes.html#Release_10.1 checkstylejava8 = "com.puppycrawl.tools:checkstyle:9.3" -# See https://github.com/google/error-prone/releases/tag/v2.11.0 +# 2.11.0+ requires JDK 11+ (See https://github.com/google/error-prone/releases/tag/v2.11.0) errorprone-corejava8 = "com.google.errorprone:error_prone_core:2.10.0" diff --git a/repositories.bzl b/repositories.bzl index 3f4cd11c1a6..b431b283a91 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -12,7 +12,7 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # GRPC_DEPS_START IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.android:annotations:4.1.1.4", - "com.google.api.grpc:proto-google-common-protos:2.48.0", + "com.google.api.grpc:proto-google-common-protos:2.51.0", "com.google.auth:google-auth-library-credentials:1.24.1", "com.google.auth:google-auth-library-oauth2-http:1.24.1", "com.google.auto.value:auto-value-annotations:1.11.0", @@ -22,7 +22,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.errorprone:error_prone_annotations:2.30.0", "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:33.3.1-android", - "com.google.re2j:re2j:1.7", + "com.google.re2j:re2j:1.8", "com.google.truth:truth:1.4.2", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day diff --git a/servlet/jakarta/build.gradle b/servlet/jakarta/build.gradle index 51333856ddf..d1ebc1d4dc1 100644 --- a/servlet/jakarta/build.gradle +++ b/servlet/jakarta/build.gradle @@ -8,13 +8,15 @@ description = "gRPC: Jakarta Servlet" // Set up classpaths and source directories for different servlet tests sourceSets { - // Only run these tests if java 11+ is being used - if (JavaVersion.current().isJava11Compatible()) { + // Only run these tests if the required minimum Java version is being used + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { jettyTest { java { include '**/Jetty*.java' } } + } + if (JavaVersion.current().isJava11Compatible()) { tomcatTest { java { include '**/Tomcat*.java' @@ -50,6 +52,8 @@ def migrate(String name, String inputDir, SourceSet sourceSet) { filter { String line -> line.replace('javax.servlet', 'jakarta.servlet') .replace('io.grpc.servlet', 'io.grpc.servlet.jakarta') + .replace('org.eclipse.jetty.http2.parser', 'org.eclipse.jetty.http2') + .replace('org.eclipse.jetty.servlet', 'org.eclipse.jetty.ee10.servlet') } } } @@ -57,9 +61,11 @@ def migrate(String name, String inputDir, SourceSet sourceSet) { migrate('main', '../src/main/java', sourceSets.main) -// Only build sourceSets and classpaths for tests if using Java 11 -if (JavaVersion.current().isJava11Compatible()) { +// Only build sourceSets and classpaths for tests if using the required minimum Java version +if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { migrate('jettyTest', '../src/jettyTest/java', sourceSets.jettyTest) +} +if (JavaVersion.current().isJava11Compatible()) { migrate('tomcatTest', '../src/tomcatTest/java', sourceSets.tomcatTest) migrate('undertowTest', '../src/undertowTest/java', sourceSets.undertowTest) } @@ -104,12 +110,19 @@ dependencies { // Set up individual classpaths for each test, to avoid any mismatch, // and ensure they are only used when supported by the current jvm -if (JavaVersion.current().isJava11Compatible()) { +if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { def jetty11Test = tasks.register('jetty11Test', Test) { classpath = sourceSets.jettyTest.runtimeClasspath testClassesDirs = sourceSets.jettyTest.output.classesDirs } - + tasks.named('compileJettyTestJava') { JavaCompile task -> + task.options.release.set 9 + } + tasks.named("check").configure { + dependsOn jetty11Test + } +} +if (JavaVersion.current().isJava11Compatible()) { def tomcat10Test = tasks.register('tomcat10Test', Test) { classpath = sourceSets.tomcatTest.runtimeClasspath testClassesDirs = sourceSets.tomcatTest.output.classesDirs @@ -134,6 +147,6 @@ if (JavaVersion.current().isJava11Compatible()) { } tasks.named("check").configure { - dependsOn jetty11Test, tomcat10Test, undertowTest + dependsOn tomcat10Test, undertowTest } } From ade2dd20382ab47867bd38c3f21fa58930ebb97c Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Tue, 11 Feb 2025 12:38:52 -0800 Subject: [PATCH 178/591] xds: Change XdsClusterConfig to have children field instead of endpoint (#11888) * Change XdsConfig to match spec with a `children` object holding either `a list of leaf cluster names` or `an EdsUpdate`. Removed intermediate aggregate nodes from `XdsConfig.clusters`. --- xds/src/main/java/io/grpc/xds/XdsConfig.java | 72 ++++++++-- .../io/grpc/xds/XdsDependencyManager.java | 135 +++++++++++++++--- .../io/grpc/xds/XdsDependencyManagerTest.java | 46 +++--- .../test/java/io/grpc/xds/XdsTestUtils.java | 3 +- 4 files changed, 209 insertions(+), 47 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsConfig.java b/xds/src/main/java/io/grpc/xds/XdsConfig.java index 999ee0d4b0c..7af03caf4be 100644 --- a/xds/src/main/java/io/grpc/xds/XdsConfig.java +++ b/xds/src/main/java/io/grpc/xds/XdsConfig.java @@ -26,6 +26,7 @@ import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import java.io.Closeable; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -103,19 +104,17 @@ public ImmutableMap> getClusters() { static final class XdsClusterConfig { private final String clusterName; private final CdsUpdate clusterResource; - private final StatusOr endpoint; //Will be null for non-EDS clusters + private final ClusterChild children; // holds details - XdsClusterConfig(String clusterName, CdsUpdate clusterResource, - StatusOr endpoint) { + XdsClusterConfig(String clusterName, CdsUpdate clusterResource, ClusterChild details) { this.clusterName = checkNotNull(clusterName, "clusterName"); this.clusterResource = checkNotNull(clusterResource, "clusterResource"); - this.endpoint = endpoint; + this.children = checkNotNull(details, "details"); } @Override public int hashCode() { - int endpointHash = (endpoint != null) ? endpoint.hashCode() : 0; - return clusterName.hashCode() + clusterResource.hashCode() + endpointHash; + return clusterName.hashCode() + clusterResource.hashCode() + children.hashCode(); } @Override @@ -126,7 +125,7 @@ public boolean equals(Object obj) { XdsClusterConfig o = (XdsClusterConfig) obj; return Objects.equals(clusterName, o.clusterName) && Objects.equals(clusterResource, o.clusterResource) - && Objects.equals(endpoint, o.endpoint); + && Objects.equals(children, o.children); } @Override @@ -134,7 +133,8 @@ public String toString() { StringBuilder builder = new StringBuilder(); builder.append("XdsClusterConfig{clusterName=").append(clusterName) .append(", clusterResource=").append(clusterResource) - .append(", endpoint=").append(endpoint).append("}"); + .append(", children={").append(children) + .append("}"); return builder.toString(); } @@ -146,8 +146,60 @@ public CdsUpdate getClusterResource() { return clusterResource; } - public StatusOr getEndpoint() { - return endpoint; + public ClusterChild getChildren() { + return children; + } + + interface ClusterChild {} + + /** Endpoint info for EDS and LOGICAL_DNS clusters. If there was an + * error, endpoints will be null and resolution_note will be set. + */ + static final class EndpointConfig implements ClusterChild { + private final StatusOr endpoint; + + public EndpointConfig(StatusOr endpoint) { + this.endpoint = checkNotNull(endpoint, "endpoint"); + } + + @Override + public int hashCode() { + return endpoint.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof EndpointConfig)) { + return false; + } + return Objects.equals(endpoint, ((EndpointConfig)obj).endpoint); + } + + public StatusOr getEndpoint() { + return endpoint; + } + } + + // The list of leaf clusters for an aggregate cluster. + static final class AggregateConfig implements ClusterChild { + private final List leafNames; + + public AggregateConfig(List leafNames) { + this.leafNames = checkNotNull(leafNames, "leafNames"); + } + + @Override + public int hashCode() { + return leafNames.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AggregateConfig)) { + return false; + } + return Objects.equals(leafNames, ((AggregateConfig) obj).leafNames); + } } } diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index d2af47bc9db..a7400950375 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -29,6 +29,9 @@ import io.grpc.StatusOr; import io.grpc.SynchronizationContext; import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight; +import io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType; +import io.grpc.xds.XdsConfig.XdsClusterConfig.AggregateConfig; +import io.grpc.xds.XdsConfig.XdsClusterConfig.EndpointConfig; import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsClient.ResourceWatcher; @@ -36,6 +39,7 @@ import io.grpc.xds.client.XdsResourceType; import java.io.Closeable; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -43,6 +47,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** @@ -299,27 +304,123 @@ XdsConfig buildConfig() { Map> cdsWatchers = resourceWatchers.get(CLUSTER_RESOURCE).watchers; - // Iterate CDS watchers - for (XdsWatcherBase watcher : cdsWatchers.values()) { - CdsWatcher cdsWatcher = (CdsWatcher) watcher; - String clusterName = cdsWatcher.resourceName(); - StatusOr cdsUpdate = cdsWatcher.getData(); - if (cdsUpdate.hasValue()) { - XdsConfig.XdsClusterConfig clusterConfig; - String edsName = cdsUpdate.getValue().edsServiceName(); - EdsWatcher edsWatcher = (EdsWatcher) edsWatchers.get(edsName); - - // Only EDS type clusters have endpoint data - StatusOr data = - edsWatcher != null ? edsWatcher.getData() : null; - clusterConfig = new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate.getValue(), data); - builder.addCluster(clusterName, StatusOr.fromValue(clusterConfig)); + // Only care about aggregates from LDS/RDS or subscriptions and the leaf clusters + List topLevelClusters = + cdsWatchers.values().stream() + .filter(XdsDependencyManager::isTopLevelCluster) + .map(w -> w.resourceName()) + .collect(Collectors.toList()); + + // Flatten multi-level aggregates into lists of leaf clusters + Set leafNames = + addTopLevelClustersToBuilder(builder, edsWatchers, cdsWatchers, topLevelClusters); + + addLeavesToBuilder(builder, edsWatchers, leafNames); + + return builder.build(); + } + + private void addLeavesToBuilder(XdsConfig.XdsConfigBuilder builder, + Map> edsWatchers, + Set leafNames) { + for (String clusterName : leafNames) { + CdsWatcher cdsWatcher = getCluster(clusterName); + StatusOr cdsUpdateOr = cdsWatcher.getData(); + + if (cdsUpdateOr.hasValue()) { + XdsClusterResource.CdsUpdate cdsUpdate = cdsUpdateOr.getValue(); + if (cdsUpdate.clusterType() == ClusterType.EDS) { + EdsWatcher edsWatcher = (EdsWatcher) edsWatchers.get(cdsUpdate.edsServiceName()); + if (edsWatcher != null) { + EndpointConfig child = new EndpointConfig(edsWatcher.getData()); + builder.addCluster(clusterName, StatusOr.fromValue( + new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, child))); + } else { + builder.addCluster(clusterName, StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( + "EDS resource not found for cluster " + clusterName))); + } + } else if (cdsUpdate.clusterType() == ClusterType.LOGICAL_DNS) { + // TODO get the resolved endpoint configuration + builder.addCluster(clusterName, StatusOr.fromValue( + new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, new EndpointConfig(null)))); + } } else { - builder.addCluster(clusterName, StatusOr.fromStatus(cdsUpdate.getStatus())); + builder.addCluster(clusterName, StatusOr.fromStatus(cdsUpdateOr.getStatus())); } } + } - return builder.build(); + // Adds the top-level clusters to the builder and returns the leaf cluster names + private Set addTopLevelClustersToBuilder( + XdsConfig.XdsConfigBuilder builder, Map> edsWatchers, + Map> cdsWatchers, List topLevelClusters) { + + Set leafClusterNames = new HashSet<>(); + for (String clusterName : topLevelClusters) { + CdsWatcher cdsWatcher = (CdsWatcher) cdsWatchers.get(clusterName); + StatusOr cdsWatcherDataOr = cdsWatcher.getData(); + if (!cdsWatcher.hasDataValue()) { + builder.addCluster(clusterName, StatusOr.fromStatus(cdsWatcherDataOr.getStatus())); + continue; + } + + XdsClusterResource.CdsUpdate cdsUpdate = cdsWatcherDataOr.getValue(); + XdsConfig.XdsClusterConfig.ClusterChild child; + switch (cdsUpdate.clusterType()) { + case AGGREGATE: + List leafNames = getLeafNames(cdsUpdate); + child = new AggregateConfig(leafNames); + leafClusterNames.addAll(leafNames); + break; + case EDS: + EdsWatcher edsWatcher = (EdsWatcher) edsWatchers.get(cdsUpdate.edsServiceName()); + if (edsWatcher != null) { + child = new EndpointConfig(edsWatcher.getData()); + } else { + builder.addCluster(clusterName, StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( + "EDS resource not found for cluster " + clusterName))); + continue; + } + break; + case LOGICAL_DNS: + // TODO get the resolved endpoint configuration + child = new EndpointConfig(null); + break; + default: + throw new IllegalStateException("Unexpected value: " + cdsUpdate.clusterType()); + } + builder.addCluster(clusterName, StatusOr.fromValue( + new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, child))); + } + + return leafClusterNames; + } + + private List getLeafNames(XdsClusterResource.CdsUpdate cdsUpdate) { + List childNames = new ArrayList<>(); + + for (String cluster : cdsUpdate.prioritizedClusterNames()) { + StatusOr data = getCluster(cluster).getData(); + if (data == null || !data.hasValue() || data.getValue() == null) { + childNames.add(cluster); + continue; + } + if (data.getValue().clusterType() == ClusterType.AGGREGATE) { + childNames.addAll(getLeafNames(data.getValue())); + } else { + childNames.add(cluster); + } + } + + return childNames; + } + + private static boolean isTopLevelCluster(XdsWatcherBase cdsWatcher) { + if (! (cdsWatcher instanceof CdsWatcher)) { + return false; + } + return ((CdsWatcher)cdsWatcher).parentContexts.values().stream() + .anyMatch(depth -> depth == 1); } @Override diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index 96aeb0f41fb..c4b7c6c8ace 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -57,6 +57,8 @@ import io.grpc.internal.ExponentialBackoffPolicy; import io.grpc.internal.FakeClock; import io.grpc.testing.GrpcCleanupRule; +import io.grpc.xds.XdsConfig.XdsClusterConfig; +import io.grpc.xds.XdsEndpointResource.EdsUpdate; import io.grpc.xds.XdsListenerResource.LdsUpdate; import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.client.XdsClient; @@ -219,28 +221,37 @@ public void verify_simple_aggregate() { XdsTestUtils.setAggregateCdsConfig(controlPlaneService, serverName, rootName, childNames); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); - Map> lastConfigClusters = + Map> lastConfigClusters = testWatcher.lastConfig.getClusters(); assertThat(lastConfigClusters).hasSize(childNames.size() + 1); - StatusOr rootC = lastConfigClusters.get(rootName); + StatusOr rootC = lastConfigClusters.get(rootName); XdsClusterResource.CdsUpdate rootUpdate = rootC.getValue().getClusterResource(); assertThat(rootUpdate.clusterType()).isEqualTo(AGGREGATE); assertThat(rootUpdate.prioritizedClusterNames()).isEqualTo(childNames); for (String childName : childNames) { assertThat(lastConfigClusters).containsKey(childName); + StatusOr childConfigOr = lastConfigClusters.get(childName); XdsClusterResource.CdsUpdate childResource = - lastConfigClusters.get(childName).getValue().getClusterResource(); + childConfigOr.getValue().getClusterResource(); assertThat(childResource.clusterType()).isEqualTo(EDS); assertThat(childResource.edsServiceName()).isEqualTo(getEdsNameForCluster(childName)); - StatusOr endpoint = - lastConfigClusters.get(childName).getValue().getEndpoint(); + StatusOr endpoint = getEndpoint(childConfigOr); assertThat(endpoint.hasValue()).isTrue(); assertThat(endpoint.getValue().clusterName).isEqualTo(getEdsNameForCluster(childName)); } } + private static StatusOr getEndpoint(StatusOr childConfigOr) { + XdsClusterConfig.ClusterChild clusterChild = childConfigOr.getValue() + .getChildren(); + assertThat(clusterChild).isInstanceOf(XdsClusterConfig.EndpointConfig.class); + StatusOr endpoint = ((XdsClusterConfig.EndpointConfig) clusterChild).getEndpoint(); + assertThat(endpoint).isNotNull(); + return endpoint; + } + @Test public void testComplexRegisteredAggregate() throws IOException { InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); @@ -289,7 +300,6 @@ public void testDelayedSubscription() { inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); String rootName1 = "root_c"; - List childNames = Arrays.asList("clusterC", "clusterB", "clusterA"); Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1); assertThat(subscription1).isNotNull(); @@ -299,6 +309,7 @@ public void testDelayedSubscription() { StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( "No " + toContextStr(CLUSTER_TYPE_NAME, rootName1))).toString()); + List childNames = Arrays.asList("clusterC", "clusterB", "clusterA"); XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName1, childNames); inOrder.verify(xdsConfigWatcher).onUpdate(xdsConfigCaptor.capture()); assertThat(xdsConfigCaptor.getValue().getClusters().get(rootName1).hasValue()).isTrue(); @@ -336,7 +347,7 @@ public void testMissingCdsAndEds() { fakeClock.forwardTime(16, TimeUnit.SECONDS); verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); - List> returnedClusters = new ArrayList<>(); + List> returnedClusters = new ArrayList<>(); for (String childName : childNames) { returnedClusters.add(xdsConfigCaptor.getValue().getClusters().get(childName)); } @@ -344,7 +355,7 @@ public void testMissingCdsAndEds() { // Check that missing cluster reported Status and the other 2 are present Status expectedClusterStatus = Status.UNAVAILABLE.withDescription( "No " + toContextStr(CLUSTER_TYPE_NAME, childNames.get(2))); - StatusOr missingCluster = returnedClusters.get(2); + StatusOr missingCluster = returnedClusters.get(2); assertThat(missingCluster.getStatus().toString()).isEqualTo(expectedClusterStatus.toString()); assertThat(returnedClusters.get(0).hasValue()).isTrue(); assertThat(returnedClusters.get(1).hasValue()).isTrue(); @@ -352,9 +363,9 @@ public void testMissingCdsAndEds() { // Check that missing EDS reported Status, the other one is present and the garbage EDS is not Status expectedEdsStatus = Status.UNAVAILABLE.withDescription( "No " + toContextStr(ENDPOINT_TYPE_NAME, XdsTestUtils.EDS_NAME + 1)); - assertThat(returnedClusters.get(0).getValue().getEndpoint().hasValue()).isTrue(); - assertThat(returnedClusters.get(1).getValue().getEndpoint().hasValue()).isFalse(); - assertThat(returnedClusters.get(1).getValue().getEndpoint().getStatus().toString()) + assertThat(getEndpoint(returnedClusters.get(0)).hasValue()).isTrue(); + assertThat(getEndpoint(returnedClusters.get(1)).hasValue()).isFalse(); + assertThat(getEndpoint(returnedClusters.get(1)).getStatus().toString()) .isEqualTo(expectedEdsStatus.toString()); verify(xdsConfigWatcher, never()).onResourceDoesNotExist(any()); @@ -539,7 +550,7 @@ public void testMultipleParentsInCdsTree() throws IOException { controlPlaneService.setXdsConfig( ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, newRouteConfig)); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); - assertThat(xdsConfigCaptor.getValue().getClusters().keySet().size()).isEqualTo(8); + assertThat(xdsConfigCaptor.getValue().getClusters().keySet().size()).isEqualTo(4); // Now that it is released, we should only have A11 rootSub.close(); @@ -582,11 +593,9 @@ public void testMultipleCdsReferToSameEds() { assertThat(initialConfig.getClusters().keySet()) .containsExactly("root", "clusterA", "clusterB"); - XdsEndpointResource.EdsUpdate edsForA = - initialConfig.getClusters().get("clusterA").getValue().getEndpoint().getValue(); + EdsUpdate edsForA = getEndpoint(initialConfig.getClusters().get("clusterA")).getValue(); assertThat(edsForA.clusterName).isEqualTo(edsName); - XdsEndpointResource.EdsUpdate edsForB = - initialConfig.getClusters().get("clusterB").getValue().getEndpoint().getValue(); + EdsUpdate edsForB = getEndpoint(initialConfig.getClusters().get("clusterB")).getValue(); assertThat(edsForB.clusterName).isEqualTo(edsName); assertThat(edsForA).isEqualTo(edsForB); edsForA.localityLbEndpointsMap.values().forEach( @@ -635,7 +644,7 @@ public void testChangeRdsName_FromLds_complexTree() { inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); XdsConfig config = xdsConfigCaptor.getValue(); assertThat(config.getVirtualHost().name()).isEqualTo(newRdsName); - assertThat(config.getClusters().size()).isEqualTo(8); + assertThat(config.getClusters().size()).isEqualTo(4); } @Test @@ -689,8 +698,7 @@ public void testChangeAggCluster() { // Verify that the config is updated as expected inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); XdsConfig config = xdsConfigCaptor.getValue(); - assertThat(config.getClusters().keySet()).containsExactly("root", "clusterA", "clusterA2", - "clusterA21", "clusterA22"); + assertThat(config.getClusters().keySet()).containsExactly("root", "clusterA21", "clusterA22"); } private Listener buildInlineClientListener(String rdsName, String clusterName) { diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java index 7f5ec0b27c6..ea28734ec6a 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -57,6 +57,7 @@ import io.grpc.stub.StreamObserver; import io.grpc.xds.Endpoints.LbEndpoint; import io.grpc.xds.Endpoints.LocalityLbEndpoints; +import io.grpc.xds.XdsConfig.XdsClusterConfig.EndpointConfig; import io.grpc.xds.client.Bootstrapper; import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsResourceType; @@ -269,7 +270,7 @@ static XdsConfig getDefaultXdsConfig(String serverHostName) CLUSTER_NAME, EDS_NAME, serverInfo, null, null, null) .lbPolicyConfig(getWrrLbConfigAsMap()).build(); XdsConfig.XdsClusterConfig clusterConfig = new XdsConfig.XdsClusterConfig( - CLUSTER_NAME, cdsUpdate, StatusOr.fromValue(edsUpdate)); + CLUSTER_NAME, cdsUpdate, new EndpointConfig(StatusOr.fromValue(edsUpdate))); builder .setListener(ldsUpdate) From 764a4e3f08ec91eda231e59cbb1d75e80a2be6aa Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Tue, 11 Feb 2025 14:34:46 -0800 Subject: [PATCH 179/591] xds: Cleanup by moving methods in XdsDependencyManager ahead of classes (#11890) * Move private methods ahead of classes --- .../io/grpc/xds/XdsDependencyManager.java | 248 +++++++++--------- 1 file changed, 123 insertions(+), 125 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index a7400950375..4adfebfe74d 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -138,8 +138,6 @@ private void cancelEdsWatcher(EdsWatcher watcher, CdsWatcher parentContext) { } } - - private void cancelWatcher(XdsWatcherBase watcher) { syncContext.throwIfNotInThisSynchronizationContext(); @@ -428,6 +426,129 @@ public String toString() { return logId.toString(); } + // Returns true if the watcher was added, false if it already exists + private boolean addEdsWatcher(String edsServiceName, CdsWatcher parentContext) { + TypeWatchers typeWatchers = resourceWatchers.get(XdsEndpointResource.getInstance()); + if (typeWatchers == null || !typeWatchers.watchers.containsKey(edsServiceName)) { + addWatcher(new EdsWatcher(edsServiceName, parentContext)); + return true; + } + + EdsWatcher watcher = (EdsWatcher) typeWatchers.watchers.get(edsServiceName); + watcher.addParentContext(parentContext); // Is a set, so don't need to check for existence + return false; + } + + private void addClusterWatcher(String clusterName, Object parentContext, int depth) { + TypeWatchers clusterWatchers = resourceWatchers.get(CLUSTER_RESOURCE); + if (clusterWatchers != null) { + CdsWatcher watcher = (CdsWatcher) clusterWatchers.watchers.get(clusterName); + if (watcher != null) { + watcher.parentContexts.put(parentContext, depth); + return; + } + } + + addWatcher(new CdsWatcher(clusterName, parentContext, depth)); + } + + private void updateRoutes(List virtualHosts, Object newParentContext, + VirtualHost oldVirtualHost, boolean sameParentContext) { + VirtualHost virtualHost = + RoutingUtils.findVirtualHostForHostName(virtualHosts, dataPlaneAuthority); + if (virtualHost == null) { + String error = "Failed to find virtual host matching hostname: " + dataPlaneAuthority; + logger.log(XdsLogger.XdsLogLevel.WARNING, error); + cleanUpRoutes(); + xdsConfigWatcher.onError( + "xDS node ID:" + dataPlaneAuthority, Status.UNAVAILABLE.withDescription(error)); + return; + } + + Set newClusters = getClusterNamesFromVirtualHost(virtualHost); + Set oldClusters = getClusterNamesFromVirtualHost(oldVirtualHost); + + if (sameParentContext) { + // Calculate diffs. + Set addedClusters = Sets.difference(newClusters, oldClusters); + Set deletedClusters = Sets.difference(oldClusters, newClusters); + + deletedClusters.forEach(watcher -> + cancelClusterWatcherTree(getCluster(watcher), newParentContext)); + addedClusters.forEach((cluster) -> addClusterWatcher(cluster, newParentContext, 1)); + } else { + newClusters.forEach((cluster) -> addClusterWatcher(cluster, newParentContext, 1)); + } + } + + private static Set getClusterNamesFromVirtualHost(VirtualHost virtualHost) { + if (virtualHost == null) { + return Collections.emptySet(); + } + + // Get all cluster names to which requests can be routed through the virtual host. + Set clusters = new HashSet<>(); + for (VirtualHost.Route route : virtualHost.routes()) { + VirtualHost.Route.RouteAction action = route.routeAction(); + if (action == null) { + continue; + } + if (action.cluster() != null) { + clusters.add(action.cluster()); + } else if (action.weightedClusters() != null) { + for (ClusterWeight weighedCluster : action.weightedClusters()) { + clusters.add(weighedCluster.name()); + } + } + } + + return clusters; + } + + @Nullable + private VirtualHost getActiveVirtualHost() { + TypeWatchers rdsWatchers = resourceWatchers.get(XdsRouteConfigureResource.getInstance()); + if (rdsWatchers == null) { + return null; + } + + RdsWatcher activeRdsWatcher = + (RdsWatcher) rdsWatchers.watchers.values().stream().findFirst().orElse(null); + if (activeRdsWatcher == null || activeRdsWatcher.missingResult() + || !activeRdsWatcher.getData().hasValue()) { + return null; + } + + return RoutingUtils.findVirtualHostForHostName( + activeRdsWatcher.getData().getValue().virtualHosts, dataPlaneAuthority); + } + + // Must be in SyncContext + private void cleanUpRoutes() { + // Remove RdsWatcher & CDS Watchers + TypeWatchers rdsResourceWatcher = + resourceWatchers.get(XdsRouteConfigureResource.getInstance()); + if (rdsResourceWatcher == null || rdsResourceWatcher.watchers.isEmpty()) { + return; + } + + XdsWatcherBase watcher = rdsResourceWatcher.watchers.values().stream().findFirst().get(); + cancelWatcher(watcher); + + // Remove CdsWatchers pointed to by the RdsWatcher + RdsWatcher rdsWatcher = (RdsWatcher) watcher; + for (String cName : rdsWatcher.getCdsNames()) { + CdsWatcher cdsWatcher = getCluster(cName); + if (cdsWatcher != null) { + cancelClusterWatcherTree(cdsWatcher, rdsWatcher); + } + } + } + + private CdsWatcher getCluster(String clusterName) { + return (CdsWatcher) resourceWatchers.get(CLUSTER_RESOURCE).watchers.get(clusterName); + } + private static class TypeWatchers { // Key is resource name final Map> watchers = new HashMap<>(); @@ -720,32 +841,6 @@ public void onResourceDoesNotExist(String resourceName) { } } - // Returns true if the watcher was added, false if it already exists - private boolean addEdsWatcher(String edsServiceName, CdsWatcher parentContext) { - TypeWatchers typeWatchers = resourceWatchers.get(XdsEndpointResource.getInstance()); - if (typeWatchers == null || !typeWatchers.watchers.containsKey(edsServiceName)) { - addWatcher(new EdsWatcher(edsServiceName, parentContext)); - return true; - } - - EdsWatcher watcher = (EdsWatcher) typeWatchers.watchers.get(edsServiceName); - watcher.addParentContext(parentContext); // Is a set, so don't need to check for existence - return false; - } - - private void addClusterWatcher(String clusterName, Object parentContext, int depth) { - TypeWatchers clusterWatchers = resourceWatchers.get(CLUSTER_RESOURCE); - if (clusterWatchers != null) { - CdsWatcher watcher = (CdsWatcher) clusterWatchers.watchers.get(clusterName); - if (watcher != null) { - watcher.parentContexts.put(parentContext, depth); - return; - } - } - - addWatcher(new CdsWatcher(clusterName, parentContext, depth)); - } - private class EdsWatcher extends XdsWatcherBase { private final Set parentContexts = new HashSet<>(); @@ -770,101 +865,4 @@ void addParentContext(CdsWatcher parentContext) { parentContexts.add(checkNotNull(parentContext, "parentContext")); } } - - private void updateRoutes(List virtualHosts, Object newParentContext, - VirtualHost oldVirtualHost, boolean sameParentContext) { - VirtualHost virtualHost = - RoutingUtils.findVirtualHostForHostName(virtualHosts, dataPlaneAuthority); - if (virtualHost == null) { - String error = "Failed to find virtual host matching hostname: " + dataPlaneAuthority; - logger.log(XdsLogger.XdsLogLevel.WARNING, error); - cleanUpRoutes(); - xdsConfigWatcher.onError( - "xDS node ID:" + dataPlaneAuthority, Status.UNAVAILABLE.withDescription(error)); - return; - } - - Set newClusters = getClusterNamesFromVirtualHost(virtualHost); - Set oldClusters = getClusterNamesFromVirtualHost(oldVirtualHost); - - if (sameParentContext) { - // Calculate diffs. - Set addedClusters = Sets.difference(newClusters, oldClusters); - Set deletedClusters = Sets.difference(oldClusters, newClusters); - - deletedClusters.forEach(watcher -> - cancelClusterWatcherTree(getCluster(watcher), newParentContext)); - addedClusters.forEach((cluster) -> addClusterWatcher(cluster, newParentContext, 1)); - } else { - newClusters.forEach((cluster) -> addClusterWatcher(cluster, newParentContext, 1)); - } - } - - private static Set getClusterNamesFromVirtualHost(VirtualHost virtualHost) { - if (virtualHost == null) { - return Collections.emptySet(); - } - - // Get all cluster names to which requests can be routed through the virtual host. - Set clusters = new HashSet<>(); - for (VirtualHost.Route route : virtualHost.routes()) { - VirtualHost.Route.RouteAction action = route.routeAction(); - if (action == null) { - continue; - } - if (action.cluster() != null) { - clusters.add(action.cluster()); - } else if (action.weightedClusters() != null) { - for (ClusterWeight weighedCluster : action.weightedClusters()) { - clusters.add(weighedCluster.name()); - } - } - } - - return clusters; - } - - @Nullable - private VirtualHost getActiveVirtualHost() { - TypeWatchers rdsWatchers = resourceWatchers.get(XdsRouteConfigureResource.getInstance()); - if (rdsWatchers == null) { - return null; - } - - RdsWatcher activeRdsWatcher = - (RdsWatcher) rdsWatchers.watchers.values().stream().findFirst().orElse(null); - if (activeRdsWatcher == null || activeRdsWatcher.missingResult() - || !activeRdsWatcher.getData().hasValue()) { - return null; - } - - return RoutingUtils.findVirtualHostForHostName( - activeRdsWatcher.getData().getValue().virtualHosts, dataPlaneAuthority); - } - - // Must be in SyncContext - private void cleanUpRoutes() { - // Remove RdsWatcher & CDS Watchers - TypeWatchers rdsResourceWatcher = - resourceWatchers.get(XdsRouteConfigureResource.getInstance()); - if (rdsResourceWatcher == null || rdsResourceWatcher.watchers.isEmpty()) { - return; - } - - XdsWatcherBase watcher = rdsResourceWatcher.watchers.values().stream().findFirst().get(); - cancelWatcher(watcher); - - // Remove CdsWatchers pointed to by the RdsWatcher - RdsWatcher rdsWatcher = (RdsWatcher) watcher; - for (String cName : rdsWatcher.getCdsNames()) { - CdsWatcher cdsWatcher = getCluster(cName); - if (cdsWatcher != null) { - cancelClusterWatcherTree(cdsWatcher, rdsWatcher); - } - } - } - - private CdsWatcher getCluster(String clusterName) { - return (CdsWatcher) resourceWatchers.get(CLUSTER_RESOURCE).watchers.get(clusterName); - } } From 122b6837175c66b728e379f8a826472def34ba18 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 12 Feb 2025 14:54:46 -0800 Subject: [PATCH 180/591] Upgrade netty-tcnative to 2.0.70 --- MODULE.bazel | 5 +++-- SECURITY.md | 3 ++- gradle/libs.versions.toml | 2 +- repositories.bzl | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 7131affbdff..9c18d918a2c 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -31,8 +31,8 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.netty:netty-handler-proxy:4.1.110.Final", "io.netty:netty-handler:4.1.110.Final", "io.netty:netty-resolver:4.1.110.Final", - "io.netty:netty-tcnative-boringssl-static:2.0.65.Final", - "io.netty:netty-tcnative-classes:2.0.65.Final", + "io.netty:netty-tcnative-boringssl-static:2.0.70.Final", + "io.netty:netty-tcnative-classes:2.0.70.Final", "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.110.Final", "io.netty:netty-transport-native-unix-common:4.1.110.Final", "io.netty:netty-transport:4.1.110.Final", @@ -48,6 +48,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") + # CEL Spec may be removed when cncf/xds MODULE is no longer using protobuf 27.x bazel_dep(name = "cel-spec", repo_name = "dev_cel", version = "0.15.0") bazel_dep(name = "grpc", repo_name = "com_github_grpc_grpc", version = "1.56.3.bcr.1") diff --git a/SECURITY.md b/SECURITY.md index 5c5e3598b29..48d6e0919a1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -399,7 +399,8 @@ grpc-netty version | netty-handler version | netty-tcnative-boringssl-static ver 1.57.x-1.58.x | 4.1.93.Final | 2.0.61.Final 1.59.x | 4.1.97.Final | 2.0.61.Final 1.60.x-1.66.x | 4.1.100.Final | 2.0.61.Final -1.67.x | 4.1.110.Final | 2.0.65.Final +1.67.x-1.70.x | 4.1.110.Final | 2.0.65.Final +1.71.x- | 4.1.110.Final | 2.0.70.Final _(grpc-netty-shaded avoids issues with keeping these versions in sync.)_ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1554d6b2e0..b6b2e5e0e45 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ netty = '4.1.110.Final' # Keep the following references of tcnative version in sync whenever it's updated: # SECURITY.md -nettytcnative = '2.0.65.Final' +nettytcnative = '2.0.70.Final' opencensus = "0.31.1" # Not upgrading to 4.x as it is not yet ABI compatible. # https://github.com/protocolbuffers/protobuf/issues/17247 diff --git a/repositories.bzl b/repositories.bzl index b431b283a91..a4f5b0de1c6 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -35,8 +35,8 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.netty:netty-handler-proxy:4.1.110.Final", "io.netty:netty-handler:4.1.110.Final", "io.netty:netty-resolver:4.1.110.Final", - "io.netty:netty-tcnative-boringssl-static:2.0.65.Final", - "io.netty:netty-tcnative-classes:2.0.65.Final", + "io.netty:netty-tcnative-boringssl-static:2.0.70.Final", + "io.netty:netty-tcnative-classes:2.0.70.Final", "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.110.Final", "io.netty:netty-transport-native-unix-common:4.1.110.Final", "io.netty:netty-transport:4.1.110.Final", From 7585b1607dca2ccb3735616a1a05f83b53f814ef Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Fri, 14 Feb 2025 11:38:06 +0530 Subject: [PATCH 181/591] core: remember last pick status in no real stream (#11851) --- .../grpc/internal/DelayedClientTransport.java | 20 +++++++++++++++---- .../internal/DelayedClientTransportTest.java | 17 ++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java index f3faa92d4a0..8ff755af3eb 100644 --- a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java +++ b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java @@ -129,8 +129,9 @@ public final ClientStream newStream( if (state.shutdownStatus != null) { return new FailingClientStream(state.shutdownStatus, tracers); } + PickResult pickResult = null; if (state.lastPicker != null) { - PickResult pickResult = state.lastPicker.pickSubchannel(args); + pickResult = state.lastPicker.pickSubchannel(args); callOptions = args.getCallOptions(); // User code provided authority takes precedence over the LB provided one. if (callOptions.getAuthority() == null @@ -156,7 +157,7 @@ public final ClientStream newStream( synchronized (lock) { PickerState newerState = pickerState; if (state == newerState) { - return createPendingStream(args, tracers); + return createPendingStream(args, tracers, pickResult); } state = newerState; } @@ -171,9 +172,12 @@ public final ClientStream newStream( * schedule tasks on syncContext. */ @GuardedBy("lock") - private PendingStream createPendingStream( - PickSubchannelArgs args, ClientStreamTracer[] tracers) { + private PendingStream createPendingStream(PickSubchannelArgs args, ClientStreamTracer[] tracers, + PickResult pickResult) { PendingStream pendingStream = new PendingStream(args, tracers); + if (args.getCallOptions().isWaitForReady() && pickResult != null && pickResult.hasResult()) { + pendingStream.lastPickStatus = pickResult.getStatus(); + } pendingStreams.add(pendingStream); if (getPendingStreamsCount() == 1) { syncContext.executeLater(reportTransportInUse); @@ -293,6 +297,9 @@ final void reprocess(@Nullable SubchannelPicker picker) { for (final PendingStream stream : toProcess) { PickResult pickResult = picker.pickSubchannel(stream.args); CallOptions callOptions = stream.args.getCallOptions(); + if (callOptions.isWaitForReady() && pickResult.hasResult()) { + stream.lastPickStatus = pickResult.getStatus(); + } final ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult, callOptions.isWaitForReady()); if (transport != null) { @@ -349,6 +356,7 @@ private class PendingStream extends DelayedStream { private final PickSubchannelArgs args; private final Context context = Context.current(); private final ClientStreamTracer[] tracers; + private volatile Status lastPickStatus; private PendingStream(PickSubchannelArgs args, ClientStreamTracer[] tracers) { this.args = args; @@ -405,6 +413,10 @@ protected void onEarlyCancellation(Status reason) { public void appendTimeoutInsight(InsightBuilder insight) { if (args.getCallOptions().isWaitForReady()) { insight.append("wait_for_ready"); + Status status = lastPickStatus; + if (status != null && !status.isOk()) { + insight.appendKeyValue("Last Pick Failure", status); + } } super.appendTimeoutInsight(insight); } diff --git a/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java b/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java index f65e6abcf1b..902c2835a92 100644 --- a/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java +++ b/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java @@ -745,6 +745,23 @@ public void pendingStream_appendTimeoutInsight_waitForReady() { .matches("\\[wait_for_ready, buffered_nanos=[0-9]+\\, waiting_for_connection]"); } + @Test + public void pendingStream_appendTimeoutInsight_waitForReady_withLastPickFailure() { + ClientStream stream = delayedTransport.newStream( + method, headers, callOptions.withWaitForReady(), tracers); + stream.start(streamListener); + SubchannelPicker picker = mock(SubchannelPicker.class); + when(picker.pickSubchannel(any(PickSubchannelArgs.class))) + .thenReturn(PickResult.withError(Status.PERMISSION_DENIED)); + delayedTransport.reprocess(picker); + InsightBuilder insight = new InsightBuilder(); + stream.appendTimeoutInsight(insight); + assertThat(insight.toString()) + .matches("\\[wait_for_ready, " + + "Last Pick Failure=Status\\{code=PERMISSION_DENIED, description=null, cause=null\\}," + + " buffered_nanos=[0-9]+, waiting_for_connection]"); + } + private static TransportProvider newTransportProvider(final ClientTransport transport) { return new TransportProvider() { @Override From 5a7f350537bbdbba3e3f49de53d298d99b5c1913 Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:59:21 +0200 Subject: [PATCH 182/591] optimize number of buffer allocations (#11879) Currently this improves 2 flows 1. Known length message which length is greater than 1Mb. Previously the first buffer was 1Mb, and then many buffers of 4096 bytes (from CodedOutputStream), now subsequent buffers are also up to 1Mb 2. In case of compression, the first write is always 10 bytes buffer (gzip header), but worth allocating more space --- .../java/io/grpc/internal/MessageFramer.java | 19 +++++++---- .../testing/integration/CompressionTest.java | 33 +++++++------------ .../io/grpc/netty/NettyClientStreamTest.java | 2 +- .../io/grpc/netty/NettyServerStreamTest.java | 2 +- .../okhttp/OkHttpWritableBufferAllocator.java | 8 ++--- .../OkHttpWritableBufferAllocatorTest.java | 7 ++-- 6 files changed, 34 insertions(+), 37 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/MessageFramer.java b/core/src/main/java/io/grpc/internal/MessageFramer.java index 5e75fa2e6fe..8b5ccb864a4 100644 --- a/core/src/main/java/io/grpc/internal/MessageFramer.java +++ b/core/src/main/java/io/grpc/internal/MessageFramer.java @@ -75,6 +75,10 @@ void deliverFrame( // effectively final. Can only be set once. private int maxOutboundMessageSize = NO_MAX_OUTBOUND_MESSAGE_SIZE; private WritableBuffer buffer; + /** + * if > 0 - the number of bytes to allocate for the current known-length message. + */ + private int knownLengthPendingAllocation; private Compressor compressor = Codec.Identity.NONE; private boolean messageCompression = true; private final OutputStreamAdapter outputStreamAdapter = new OutputStreamAdapter(); @@ -222,9 +226,7 @@ private int writeKnownLengthUncompressed(InputStream message, int messageLength) headerScratch.put(UNCOMPRESSED).putInt(messageLength); // Allocate the initial buffer chunk based on frame header + payload length. // Note that the allocator may allocate a buffer larger or smaller than this length - if (buffer == null) { - buffer = bufferAllocator.allocate(headerScratch.position() + messageLength); - } + knownLengthPendingAllocation = HEADER_LENGTH + messageLength; writeRaw(headerScratch.array(), 0, headerScratch.position()); return writeToOutputStream(message, outputStreamAdapter); } @@ -288,8 +290,9 @@ private void writeRaw(byte[] b, int off, int len) { commitToSink(false, false); } if (buffer == null) { - // Request a buffer allocation using the message length as a hint. - buffer = bufferAllocator.allocate(len); + checkState(knownLengthPendingAllocation > 0, "knownLengthPendingAllocation reached 0"); + buffer = bufferAllocator.allocate(knownLengthPendingAllocation); + knownLengthPendingAllocation -= min(knownLengthPendingAllocation, buffer.writableBytes()); } int toWrite = min(len, buffer.writableBytes()); buffer.write(b, off, toWrite); @@ -388,6 +391,8 @@ public void write(byte[] b, int off, int len) { * {@link OutputStream}. */ private final class BufferChainOutputStream extends OutputStream { + private static final int FIRST_BUFFER_SIZE = 4096; + private final List bufferList = new ArrayList<>(); private WritableBuffer current; @@ -397,7 +402,7 @@ private final class BufferChainOutputStream extends OutputStream { * {@link #write(byte[], int, int)}. */ @Override - public void write(int b) throws IOException { + public void write(int b) { if (current != null && current.writableBytes() > 0) { current.write((byte)b); return; @@ -410,7 +415,7 @@ public void write(int b) throws IOException { public void write(byte[] b, int off, int len) { if (current == null) { // Request len bytes initially from the allocator, it may give us more. - current = bufferAllocator.allocate(len); + current = bufferAllocator.allocate(Math.max(FIRST_BUFFER_SIZE, len)); bufferList.add(current); } while (len > 0) { diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/CompressionTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/CompressionTest.java index 208eb40c438..5307c26949b 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/CompressionTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/CompressionTest.java @@ -24,6 +24,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import com.google.protobuf.ByteString; import io.grpc.CallOptions; import io.grpc.Channel; @@ -53,8 +55,6 @@ import io.grpc.testing.integration.TestServiceGrpc.TestServiceBlockingStub; import io.grpc.testing.integration.TransportCompressionTest.Fzip; import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -146,25 +146,16 @@ public void tearDown() { * Parameters for test. */ @Parameters - public static Collection params() { - boolean[] bools = new boolean[]{false, true}; - List combos = new ArrayList<>(64); - for (boolean enableClientMessageCompression : bools) { - for (boolean clientAcceptEncoding : bools) { - for (boolean clientEncoding : bools) { - for (boolean enableServerMessageCompression : bools) { - for (boolean serverAcceptEncoding : bools) { - for (boolean serverEncoding : bools) { - combos.add(new Object[] { - enableClientMessageCompression, clientAcceptEncoding, clientEncoding, - enableServerMessageCompression, serverAcceptEncoding, serverEncoding}); - } - } - } - } - } - } - return combos; + public static Iterable params() { + List bools = Lists.newArrayList(false, true); + return Iterables.transform(Lists.cartesianProduct( + bools, // enableClientMessageCompression + bools, // clientAcceptEncoding + bools, // clientEncoding + bools, // enableServerMessageCompression + bools, // serverAcceptEncoding + bools // serverEncoding + ), List::toArray); } @Test diff --git a/netty/src/test/java/io/grpc/netty/NettyClientStreamTest.java b/netty/src/test/java/io/grpc/netty/NettyClientStreamTest.java index a44a196ac8c..4dd24c3fd4d 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientStreamTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientStreamTest.java @@ -233,7 +233,7 @@ public void writeFrameFutureFailedShouldCancelRpc() { // Verify that failed SendGrpcFrameCommand results in immediate CancelClientStreamCommand. inOrder.verify(writeQueue).enqueue(any(CancelClientStreamCommand.class), eq(true)); // Verify that any other failures do not produce another CancelClientStreamCommand in the queue. - inOrder.verify(writeQueue, atLeast(1)).enqueue(any(SendGrpcFrameCommand.class), eq(false)); + inOrder.verify(writeQueue, atLeast(0)).enqueue(any(SendGrpcFrameCommand.class), eq(false)); inOrder.verify(writeQueue).enqueue(any(SendGrpcFrameCommand.class), eq(true)); inOrder.verifyNoMoreInteractions(); diff --git a/netty/src/test/java/io/grpc/netty/NettyServerStreamTest.java b/netty/src/test/java/io/grpc/netty/NettyServerStreamTest.java index 0723e359752..2f2933ae103 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerStreamTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerStreamTest.java @@ -158,7 +158,7 @@ public void writeFrameFutureFailedShouldCancelRpc() { // Verify that failed SendGrpcFrameCommand results in immediate CancelServerStreamCommand. inOrder.verify(writeQueue).enqueue(any(CancelServerStreamCommand.class), eq(true)); // Verify that any other failures do not produce another CancelServerStreamCommand in the queue. - inOrder.verify(writeQueue, atLeast(1)).enqueue(any(SendGrpcFrameCommand.class), eq(false)); + inOrder.verify(writeQueue, atLeast(0)).enqueue(any(SendGrpcFrameCommand.class), eq(false)); inOrder.verify(writeQueue).enqueue(any(SendGrpcFrameCommand.class), eq(true)); inOrder.verifyNoMoreInteractions(); } diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpWritableBufferAllocator.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpWritableBufferAllocator.java index 481ada61c96..f5d490818ba 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpWritableBufferAllocator.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpWritableBufferAllocator.java @@ -19,6 +19,7 @@ import io.grpc.internal.WritableBuffer; import io.grpc.internal.WritableBufferAllocator; import okio.Buffer; +import okio.Segment; /** * The default allocator for {@link OkHttpWritableBuffer}s used by the OkHttp transport. OkHttp @@ -27,9 +28,6 @@ */ class OkHttpWritableBufferAllocator implements WritableBufferAllocator { - // Use 4k as our minimum buffer size. - private static final int MIN_BUFFER = 4096; - // Set the maximum buffer size to 1MB private static final int MAX_BUFFER = 1024 * 1024; @@ -45,7 +43,9 @@ class OkHttpWritableBufferAllocator implements WritableBufferAllocator { */ @Override public WritableBuffer allocate(int capacityHint) { - capacityHint = Math.min(MAX_BUFFER, Math.max(MIN_BUFFER, capacityHint)); + // okio buffer uses fixed size Segments, round capacityHint up + capacityHint = Math.min(MAX_BUFFER, + (capacityHint + Segment.SIZE - 1) / Segment.SIZE * Segment.SIZE); return new OkHttpWritableBuffer(new Buffer(), capacityHint); } } diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpWritableBufferAllocatorTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpWritableBufferAllocatorTest.java index e606b6b9a50..c444e0ee11d 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpWritableBufferAllocatorTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpWritableBufferAllocatorTest.java @@ -21,6 +21,7 @@ import io.grpc.internal.WritableBuffer; import io.grpc.internal.WritableBufferAllocator; import io.grpc.internal.WritableBufferAllocatorTestBase; +import okio.Segment; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -42,7 +43,7 @@ protected WritableBufferAllocator allocator() { public void testCapacity() { WritableBuffer buffer = allocator().allocate(4096); assertEquals(0, buffer.readableBytes()); - assertEquals(4096, buffer.writableBytes()); + assertEquals(Segment.SIZE, buffer.writableBytes()); } @Test @@ -54,8 +55,8 @@ public void testInitialCapacityHasMaximum() { @Test public void testIsExactBelowMaxCapacity() { - WritableBuffer buffer = allocator().allocate(4097); + WritableBuffer buffer = allocator().allocate(Segment.SIZE + 1); assertEquals(0, buffer.readableBytes()); - assertEquals(4097, buffer.writableBytes()); + assertEquals(Segment.SIZE * 2, buffer.writableBytes()); } } From 41dd0c6d7358cc12c83c559c4059f8a8cb86d44e Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Fri, 14 Feb 2025 10:23:54 -0800 Subject: [PATCH 183/591] xds:Cleanup to reduce test flakiness (#11895) * don't process resourceDoesNotExist for watchers that have been cancelled. * Change test to use an ArgumentMatcher instead of expecting that only the final result will be sent since depending on timing there may be configs sent for clusters being removed with their entries as errors. --- .../io/grpc/xds/XdsDependencyManager.java | 16 +++++++++++++ .../io/grpc/xds/XdsDependencyManagerTest.java | 24 ++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index 4adfebfe74d..7eff0c549e2 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -149,6 +149,7 @@ private void cancelWatcher(XdsWatcherBase watcher) throwIfParentContextsNotEmpty(watcher); } + watcher.cancelled = true; XdsResourceType type = watcher.type; String resourceName = watcher.resourceName; @@ -597,6 +598,8 @@ private abstract static class XdsWatcherBase implements ResourceWatcher { private final XdsResourceType type; private final String resourceName; + boolean cancelled; + @Nullable private StatusOr data; @@ -693,6 +696,10 @@ public void onError(Status error) { @Override public void onResourceDoesNotExist(String resourceName) { + if (cancelled) { + return; + } + handleDoesNotExist(resourceName); xdsConfigWatcher.onResourceDoesNotExist(toContextString()); } @@ -752,6 +759,9 @@ public void onError(Status error) { @Override public void onResourceDoesNotExist(String resourceName) { + if (cancelled) { + return; + } handleDoesNotExist(checkNotNull(resourceName, "resourceName")); xdsConfigWatcher.onResourceDoesNotExist(toContextString()); } @@ -836,6 +846,9 @@ public void onChanged(XdsClusterResource.CdsUpdate update) { @Override public void onResourceDoesNotExist(String resourceName) { + if (cancelled) { + return; + } handleDoesNotExist(checkNotNull(resourceName, "resourceName")); maybePublishConfig(); } @@ -857,6 +870,9 @@ public void onChanged(XdsEndpointResource.EdsUpdate update) { @Override public void onResourceDoesNotExist(String resourceName) { + if (cancelled) { + return; + } handleDoesNotExist(checkNotNull(resourceName, "resourceName")); maybePublishConfig(); } diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index c4b7c6c8ace..f94a94a9446 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -88,6 +88,7 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatchers; import org.mockito.Captor; import org.mockito.InOrder; @@ -696,9 +697,9 @@ public void testChangeAggCluster() { controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); // Verify that the config is updated as expected - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); - XdsConfig config = xdsConfigCaptor.getValue(); - assertThat(config.getClusters().keySet()).containsExactly("root", "clusterA21", "clusterA22"); + ClusterNameMatcher nameMatcher + = new ClusterNameMatcher(Arrays.asList("root", "clusterA21", "clusterA22")); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(argThat(nameMatcher)); } private Listener buildInlineClientListener(String rdsName, String clusterName) { @@ -789,4 +790,21 @@ void deliverLdsUpdate(long httpMaxStreamDurationNano, }); } } + + static class ClusterNameMatcher implements ArgumentMatcher { + private final List expectedNames; + + ClusterNameMatcher(List expectedNames) { + this.expectedNames = expectedNames; + } + + @Override + public boolean matches(XdsConfig xdsConfig) { + if (xdsConfig == null || xdsConfig.getClusters() == null) { + return false; + } + return xdsConfig.getClusters().size() == expectedNames.size() + && xdsConfig.getClusters().keySet().containsAll(expectedNames); + } + } } From a5347b2bc4ac6dbe77b916a2a3dcf16199002603 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:55:42 -0800 Subject: [PATCH 184/591] s2a: inject Optional in tests --- .../handshaker/GetAuthenticationMechanisms.java | 10 ++++++---- .../s2a/internal/handshaker/SslContextFactory.java | 3 ++- .../handshaker/GetAuthenticationMechanismsTest.java | 12 ++++++++++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanisms.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanisms.java index 2d089183f91..88dfd62675f 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanisms.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanisms.java @@ -24,19 +24,21 @@ /** Retrieves the authentication mechanism for a given local identity. */ @Immutable final class GetAuthenticationMechanisms { - private static final Optional TOKEN_MANAGER = AccessTokenManager.create(); + static final Optional TOKEN_MANAGER = AccessTokenManager.create(); /** * Retrieves the authentication mechanism for a given local identity. * * @param localIdentity the identity for which to fetch a token. + * @param tokenManager the token manager to use for fetching tokens. * @return an {@link AuthenticationMechanism} for the given local identity. */ - static Optional getAuthMechanism(Optional localIdentity) { - if (!TOKEN_MANAGER.isPresent()) { + static Optional getAuthMechanism(Optional localIdentity, + Optional tokenManager) { + if (!tokenManager.isPresent()) { return Optional.empty(); } - AccessTokenManager manager = TOKEN_MANAGER.get(); + AccessTokenManager manager = tokenManager.get(); // If no identity is provided, fetch the default access token and DO NOT attach an identity // to the request. if (!localIdentity.isPresent()) { diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java index 3e5481daa9e..153f4de6918 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java @@ -105,7 +105,8 @@ private static GetTlsConfigurationResp.ClientTlsConfiguration getClientTlsConfig reqBuilder.setLocalIdentity(localIdentity.get().getIdentity()); } Optional authMechanism = - GetAuthenticationMechanisms.getAuthMechanism(localIdentity); + GetAuthenticationMechanisms.getAuthMechanism(localIdentity, + GetAuthenticationMechanisms.TOKEN_MANAGER); if (authMechanism.isPresent()) { reqBuilder.addAuthenticationMechanisms(authMechanism.get()); } diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java index d17d9ba99e8..d69d84bf459 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java @@ -18,9 +18,11 @@ import com.google.common.truth.Expect; import io.grpc.s2a.internal.handshaker.S2AIdentity; +import io.grpc.s2a.internal.handshaker.tokenmanager.AccessTokenManager; import io.grpc.s2a.internal.handshaker.tokenmanager.SingleTokenFetcher; import java.util.Optional; import org.junit.AfterClass; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -33,6 +35,7 @@ public final class GetAuthenticationMechanismsTest { @Rule public final Expect expect = Expect.create(); private static final String TOKEN = "access_token"; private static String originalAccessToken; + private Optional tokenManager; @BeforeClass public static void setUpClass() { @@ -41,6 +44,11 @@ public static void setUpClass() { SingleTokenFetcher.setAccessToken(TOKEN); } + @Before + public void setUp() { + tokenManager = AccessTokenManager.create(); + } + @AfterClass public static void tearDownClass() { SingleTokenFetcher.setAccessToken(originalAccessToken); @@ -49,7 +57,7 @@ public static void tearDownClass() { @Test public void getAuthMechanisms_emptyIdentity_success() { expect - .that(GetAuthenticationMechanisms.getAuthMechanism(Optional.empty())) + .that(GetAuthenticationMechanisms.getAuthMechanism(Optional.empty(), tokenManager)) .isEqualTo( Optional.of(AuthenticationMechanism.newBuilder().setToken("access_token").build())); } @@ -58,7 +66,7 @@ public void getAuthMechanisms_emptyIdentity_success() { public void getAuthMechanisms_nonEmptyIdentity_success() { S2AIdentity fakeIdentity = S2AIdentity.fromSpiffeId("fake-spiffe-id"); expect - .that(GetAuthenticationMechanisms.getAuthMechanism(Optional.of(fakeIdentity))) + .that(GetAuthenticationMechanisms.getAuthMechanism(Optional.of(fakeIdentity), tokenManager)) .isEqualTo( Optional.of( AuthenticationMechanism.newBuilder() From 57af63ad0a0203aa87b20bb29bc56cd9215fb2ef Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 14 Feb 2025 11:03:40 -0800 Subject: [PATCH 185/591] kokoro: Increase gradle mem in android-interop To try to aid failure when building android-interop-testing ``` The Daemon will expire after the build after running out of JVM heap space. The project memory settings are likely not configured or are configured to an insufficient value. The daemon will restart for the next build, which may increase subsequent build times. These settings can be adjusted by setting 'org.gradle.jvmargs' in 'gradle.properties'. The currently configured max heap space is '512 MiB' and the configured max metaspace is '384 MiB'. ... Exception in thread "Daemon client event forwarder" java.lang.OutOfMemoryError: Java heap space ... > Task :grpc-android-interop-testing:mergeDexDebug FAILED ERROR:D8: java.lang.OutOfMemoryError: Java heap space com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: ``` --- buildscripts/kokoro/android-interop.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/buildscripts/kokoro/android-interop.sh b/buildscripts/kokoro/android-interop.sh index 43bad26f1ec..b4adc8bed43 100755 --- a/buildscripts/kokoro/android-interop.sh +++ b/buildscripts/kokoro/android-interop.sh @@ -7,7 +7,6 @@ set -exu -o pipefail cd github/grpc-java -export GRADLE_OPTS=-Xmx512m export LDFLAGS=-L/tmp/protobuf/lib export CXXFLAGS=-I/tmp/protobuf/include export LD_LIBRARY_PATH=/tmp/protobuf/lib @@ -30,7 +29,7 @@ sudo update-java-alternatives --set java-1.11.0-openjdk-amd64 # Unset any existing JAVA_HOME env var to stop Gradle from using it unset JAVA_HOME -GRADLE_FLAGS="-Pandroid.useAndroidX=true" +GRADLE_FLAGS="-Pandroid.useAndroidX=true -Dorg.gradle.jvmargs=-Xmx1024m" ./gradlew $GRADLE_FLAGS :grpc-android-interop-testing:assembleDebug ./gradlew $GRADLE_FLAGS :grpc-android-interop-testing:assembleDebugAndroidTest From c1d703546a8ed4a052769aa78203f44658a3083f Mon Sep 17 00:00:00 2001 From: Larry Safran Date: Fri, 14 Feb 2025 14:46:54 -0800 Subject: [PATCH 186/591] okhttp:Use a locally specified value instead of Segment.SIZE in okhttp (#11899) Switched to using 8192 which is the current value of Segment.SIZE and just have a test check that they are equal. The reason for doing this is that Segment.SIZE is Kotlin internal so shouldn't be used outside of its module. --- .../grpc/okhttp/OkHttpWritableBufferAllocator.java | 4 ++-- .../okhttp/OkHttpWritableBufferAllocatorTest.java | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpWritableBufferAllocator.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpWritableBufferAllocator.java index f5d490818ba..58896a5dbb0 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpWritableBufferAllocator.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpWritableBufferAllocator.java @@ -19,7 +19,6 @@ import io.grpc.internal.WritableBuffer; import io.grpc.internal.WritableBufferAllocator; import okio.Buffer; -import okio.Segment; /** * The default allocator for {@link OkHttpWritableBuffer}s used by the OkHttp transport. OkHttp @@ -30,6 +29,7 @@ class OkHttpWritableBufferAllocator implements WritableBufferAllocator { // Set the maximum buffer size to 1MB private static final int MAX_BUFFER = 1024 * 1024; + public static final int SEGMENT_SIZE_COPY = 8192; // Should equal Segment.SIZE /** * Construct a new instance. @@ -45,7 +45,7 @@ class OkHttpWritableBufferAllocator implements WritableBufferAllocator { public WritableBuffer allocate(int capacityHint) { // okio buffer uses fixed size Segments, round capacityHint up capacityHint = Math.min(MAX_BUFFER, - (capacityHint + Segment.SIZE - 1) / Segment.SIZE * Segment.SIZE); + (capacityHint + SEGMENT_SIZE_COPY - 1) / SEGMENT_SIZE_COPY * SEGMENT_SIZE_COPY); return new OkHttpWritableBuffer(new Buffer(), capacityHint); } } diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpWritableBufferAllocatorTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpWritableBufferAllocatorTest.java index c444e0ee11d..c19224822a8 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpWritableBufferAllocatorTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpWritableBufferAllocatorTest.java @@ -16,6 +16,7 @@ package io.grpc.okhttp; +import static io.grpc.okhttp.OkHttpWritableBufferAllocator.SEGMENT_SIZE_COPY; import static org.junit.Assert.assertEquals; import io.grpc.internal.WritableBuffer; @@ -39,11 +40,12 @@ protected WritableBufferAllocator allocator() { return allocator; } + @SuppressWarnings("KotlinInternal") @Test public void testCapacity() { WritableBuffer buffer = allocator().allocate(4096); assertEquals(0, buffer.readableBytes()); - assertEquals(Segment.SIZE, buffer.writableBytes()); + assertEquals(SEGMENT_SIZE_COPY, buffer.writableBytes()); } @Test @@ -55,8 +57,14 @@ public void testInitialCapacityHasMaximum() { @Test public void testIsExactBelowMaxCapacity() { - WritableBuffer buffer = allocator().allocate(Segment.SIZE + 1); + WritableBuffer buffer = allocator().allocate(SEGMENT_SIZE_COPY + 1); assertEquals(0, buffer.readableBytes()); - assertEquals(Segment.SIZE * 2, buffer.writableBytes()); + assertEquals(SEGMENT_SIZE_COPY * 2, buffer.writableBytes()); + } + + @SuppressWarnings("KotlinInternal") + @Test + public void testSegmentSizeMatchesKotlin() { + assertEquals(Segment.SIZE, SEGMENT_SIZE_COPY); } } From 9e54e8e5e93034fbea5db686cdbfbd985d0c6f3a Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 14 Feb 2025 13:41:32 -0800 Subject: [PATCH 187/591] servlet: Provide Gradle a filter version number The version number is simply a unique string per version. --- servlet/jakarta/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/servlet/jakarta/build.gradle b/servlet/jakarta/build.gradle index d1ebc1d4dc1..456b2b75e3e 100644 --- a/servlet/jakarta/build.gradle +++ b/servlet/jakarta/build.gradle @@ -47,6 +47,8 @@ def migrate(String name, String inputDir, SourceSet sourceSet) { def outputDir = layout.buildDirectory.dir('generated/sources/jakarta-' + name) sourceSet.java.srcDir tasks.register('migrateSources' + name.capitalize(), Sync) { task -> into(outputDir) + // Increment when changing the filter, to inform Gradle it needs to rebuild + inputs.property("filter-version", "1") from("$inputDir/io/grpc/servlet") { into('io/grpc/servlet/jakarta') filter { String line -> From 16d26726cfeb44510c8d9f92b121a9fbe32a6301 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 14 Feb 2025 15:47:19 -0800 Subject: [PATCH 188/591] s2a: Don't allow S2AStub to be set S2AStub is an internal API and shouldn't be used outside of s2a. It is still available for tests. IntegrationTest was moved to io.grpc.s2a. It uses a io.grpc.s2a class, so shouldn't be in internal.handler --- s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java | 6 ++++-- .../main/java/io/grpc/s2a/internal/handshaker/S2AStub.java | 5 +++-- .../grpc/s2a/{internal/handshaker => }/IntegrationTest.java | 4 +++- 3 files changed, 10 insertions(+), 5 deletions(-) rename s2a/src/test/java/io/grpc/s2a/{internal/handshaker => }/IntegrationTest.java (98%) diff --git a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java index 2cbdf7e4c5f..4be32475205 100644 --- a/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java +++ b/s2a/src/main/java/io/grpc/s2a/S2AChannelCredentials.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.isNullOrEmpty; +import com.google.common.annotations.VisibleForTesting; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.grpc.Channel; import io.grpc.ChannelCredentials; @@ -110,7 +111,8 @@ public Builder setLocalUid(String localUid) { * Sets the stub to use to communicate with S2A. This is only used for testing that the * stream to S2A gets closed. */ - public Builder setStub(S2AStub stub) { + @VisibleForTesting + Builder setStub(S2AStub stub) { checkNotNull(stub); this.stub = stub; return this; @@ -130,4 +132,4 @@ InternalProtocolNegotiator.ClientFactory buildProtocolNegotiatorFactory() { } private S2AChannelCredentials() {} -} \ No newline at end of file +} diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java index c5ac8f96d96..956ec485229 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java @@ -44,7 +44,8 @@ public class S2AStub implements AutoCloseable { private boolean doneWriting = false; private boolean isClosed = false; - static S2AStub newInstance(S2AServiceGrpc.S2AServiceStub serviceStub) { + @VisibleForTesting + public static S2AStub newInstance(S2AServiceGrpc.S2AServiceStub serviceStub) { checkNotNull(serviceStub); return new S2AStub(serviceStub); } @@ -224,4 +225,4 @@ SessionResp getResultOrThrow() throws IOException { return response.get(); } } -} \ No newline at end of file +} diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java b/s2a/src/test/java/io/grpc/s2a/IntegrationTest.java similarity index 98% rename from s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java rename to s2a/src/test/java/io/grpc/s2a/IntegrationTest.java index 613983d9b39..d8d2fdd4d03 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/IntegrationTest.java +++ b/s2a/src/test/java/io/grpc/s2a/IntegrationTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.s2a.internal.handshaker; +package io.grpc.s2a; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; @@ -37,6 +37,8 @@ import io.grpc.s2a.S2AChannelCredentials; import io.grpc.s2a.internal.channel.S2AHandshakerServiceChannel; import io.grpc.s2a.internal.handshaker.FakeS2AServer; +import io.grpc.s2a.internal.handshaker.S2AServiceGrpc; +import io.grpc.s2a.internal.handshaker.S2AStub; import io.grpc.stub.StreamObserver; import io.grpc.testing.protobuf.SimpleRequest; import io.grpc.testing.protobuf.SimpleResponse; From 16edf7ac4e21d3bcf516bfde0e018915172dfe7d Mon Sep 17 00:00:00 2001 From: Naveen Prasanna V <80662475+NaveenPrasannaV@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:40:18 +0530 Subject: [PATCH 189/591] Examples: Updated HelloWorldServer to use Executor (#11850) --- .../examples/helloworld/HelloWorldServer.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java index 81027587031..0e39581c98f 100644 --- a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java +++ b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java @@ -23,6 +23,8 @@ import java.io.IOException; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * Server that manages startup/shutdown of a {@code Greeter} server. @@ -31,11 +33,20 @@ public class HelloWorldServer { private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName()); private Server server; - private void start() throws IOException { /* The port on which the server should run */ int port = 50051; + /* + * By default gRPC uses a global, shared Executor.newCachedThreadPool() for gRPC callbacks into + * your application. This is convenient, but can cause an excessive number of threads to be + * created if there are many RPCs. It is often better to limit the number of threads your + * application uses for processing and let RPCs queue when the CPU is saturated. + * The appropriate number of threads varies heavily between applications. + * Async application code generally does not need more threads than CPU cores. + */ + ExecutorService executor = Executors.newFixedThreadPool(2); server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()) + .executor(executor) .addService(new GreeterImpl()) .build() .start(); @@ -48,7 +59,12 @@ public void run() { try { HelloWorldServer.this.stop(); } catch (InterruptedException e) { + if (server != null) { + server.shutdownNow(); + } e.printStackTrace(System.err); + } finally { + executor.shutdown(); } System.err.println("*** server shut down"); } From a132123c93d1ba46763d852b9057b70f32c86215 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 18 Feb 2025 14:16:02 +0000 Subject: [PATCH 190/591] Start 1.72.0 development cycle (#11907) --- MODULE.bazel | 2 +- build.gradle | 2 +- .../src/test/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/test/golden/TestService.java.txt | 2 +- core/src/main/java/io/grpc/internal/GrpcUtil.java | 2 +- examples/MODULE.bazel | 2 +- examples/android/clientcache/app/build.gradle | 10 +++++----- examples/android/helloworld/app/build.gradle | 8 ++++---- examples/android/routeguide/app/build.gradle | 8 ++++---- examples/android/strictmode/app/build.gradle | 8 ++++---- examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 4 ++-- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 4 ++-- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 4 ++-- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 4 ++-- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 4 ++-- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- 34 files changed, 55 insertions(+), 55 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 9c18d918a2c..666fda73202 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "grpc-java", compatibility_level = 0, repo_name = "io_grpc_grpc_java", - version = "1.71.0-SNAPSHOT", # CURRENT_GRPC_VERSION + version = "1.72.0-SNAPSHOT", # CURRENT_GRPC_VERSION ) # GRPC_DEPS_START diff --git a/build.gradle b/build.gradle index 09c78735776..93ce60054bc 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.71.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.72.0-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index e696def7b99..82fe6a81d18 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; *
*/ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.71.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.72.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index c052093cbbc..912bd50da12 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; *
*/ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.71.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.72.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index fc420195667..937854ac3ff 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -219,7 +219,7 @@ public byte[] parseAsciiString(byte[] serialized) { public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); - public static final String IMPLEMENTATION_VERSION = "1.71.0-SNAPSHOT"; // CURRENT_GRPC_VERSION + public static final String IMPLEMENTATION_VERSION = "1.72.0-SNAPSHOT"; // CURRENT_GRPC_VERSION /** * The default timeout in nanos for a keepalive ping request. diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index 72541817979..4d72ae9c395 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,5 +1,5 @@ bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") -bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.70.0-SNAPSHOT") # CURRENT_GRPC_VERSION +bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.72.0-SNAPSHOT") # CURRENT_GRPC_VERSION bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") bazel_dep(name = "rules_jvm_external", version = "6.0") diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index 7700b2248eb..1f2a17ae6bb 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -34,7 +34,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -54,12 +54,12 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.5' - testImplementation 'io.grpc:grpc-testing:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + testImplementation 'io.grpc:grpc-testing:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index 0fb396bbe39..09b994a4954 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index 1a8209913a2..bdad129845b 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index 02b17c189f9..f38110a741b 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -33,7 +33,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -53,8 +53,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/build.gradle b/examples/build.gradle index d4991f02f43..e807d09f407 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index 17b2568c7ea..e7142f3fb5a 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index 7701465dee2..1d07a7cb8ec 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index 2976782a5d7..00fdecdb6c4 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -6,13 +6,13 @@ jar - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT example-debug https://github.com/grpc/grpc-java UTF-8 - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index a0f29660afc..b73902095a1 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index 99f98cc5b48..28539851934 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -6,13 +6,13 @@ jar - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT example-dualstack https://github.com/grpc/grpc-java UTF-8 - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index aea626a5193..0ef0dcaefe2 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index 00e7ee0e3ad..d2a32578550 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,13 +6,13 @@ jar - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index 5ccb5bb0d3a..e16e32e3bc1 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 3c7b0587e8f..1cad10bbb87 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 9eb5f38c364..86aa42f8ed0 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index d7ac43e79ae..6993c40a1ac 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,13 +6,13 @@ jar - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index 64ea928456b..ca12c3f7872 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index 6c1e172b2e0..7e9a915bfbd 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,13 +7,13 @@ jar - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 4de1183e0d7..6d06f097ccb 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index f01b362347a..a9fea928a34 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -7,13 +7,13 @@ jar - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT example-oauth https://github.com/grpc/grpc-java UTF-8 - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index 2e0cbfbe0b6..f575d24d19b 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index ffa8295f849..45235fa1e08 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index 8c885d9cb99..ad68e891436 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index 163276aec10..2176df5afc5 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -15,7 +15,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index 87e37c5a3b1..e741bfe1c3f 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index ade33ee8769..de9298cf0e1 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,13 +6,13 @@ jar - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT example-tls https://github.com/grpc/grpc-java UTF-8 - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index ef1bc185816..c0159dce258 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.71.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/pom.xml b/examples/pom.xml index 6370ce7d56a..edc9c4cda14 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,13 +6,13 @@ jar - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT examples https://github.com/grpc/grpc-java UTF-8 - 1.71.0-SNAPSHOT + 1.72.0-SNAPSHOT 3.25.5 3.25.5 From 713607056e1761b9c17fbdc3c506e5596c48692a Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 12 Feb 2025 21:52:19 -0800 Subject: [PATCH 191/591] util: Use acceptResolvedAddresses() for MultiChildLb children A failing Status from acceptResolvedAddresses means something is wrong with the config, but parts of the config may still have been applied. Thus there are now two possible flows: errors that should prevent updateOverallBalancingState() and errors that should have no effect other than the return code. To manage that, MultChildLb must always be responsible for calling updateOverallBalancingState(). acceptResolvedAddressesInternal() was inlined to make that error processing easier. No existing usages actually needed to have logic between updating the children and regenerating the picker. RingHashLb already was verifying that the address list was not empty, so the short-circuiting when acceptResolvedAddressesInternal() returned an error was impossible to trigger. WrrLb's updateWeightTask() calls the last picker, so it can run before acceptResolvedAddressesInternal(); the only part that matters is re-creating the weightUpdateTimer. --- .../io/grpc/util/MultiChildLoadBalancer.java | 74 +++++---------- .../io/grpc/xds/RingHashLoadBalancer.java | 90 ++++++++----------- .../xds/WeightedRoundRobinLoadBalancer.java | 28 ++---- 3 files changed, 66 insertions(+), 126 deletions(-) diff --git a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java index b51d2772d3e..ed338343c11 100644 --- a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java @@ -107,21 +107,22 @@ protected ChildLbState createChildLbState(Object key) { */ @Override public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + logger.log(Level.FINE, "Received resolution result: {0}", resolvedAddresses); try { resolvingAddresses = true; // process resolvedAddresses to update children - AcceptResolvedAddrRetVal acceptRetVal = acceptResolvedAddressesInternal(resolvedAddresses); - if (!acceptRetVal.status.isOk()) { - return acceptRetVal.status; + Map newChildAddresses = createChildAddressesMap(resolvedAddresses); + + // Handle error case + if (newChildAddresses.isEmpty()) { + Status unavailableStatus = Status.UNAVAILABLE.withDescription( + "NameResolver returned no usable address. " + resolvedAddresses); + handleNameResolutionError(unavailableStatus); + return unavailableStatus; } - // Update the picker and our connectivity state - updateOverallBalancingState(); - - // shutdown removed children - shutdownRemoved(acceptRetVal.removedChildren); - return acceptRetVal.status; + return updateChildrenWithResolvedAddresses(newChildAddresses); } finally { resolvingAddresses = false; } @@ -149,31 +150,7 @@ public void shutdown() { childLbStates.clear(); } - /** - * This does the work to update the child map and calculate which children have been removed. - * You must call {@link #updateOverallBalancingState} to update the picker - * and call {@link #shutdownRemoved(List)} to shutdown the endpoints that have been removed. - */ - protected final AcceptResolvedAddrRetVal acceptResolvedAddressesInternal( - ResolvedAddresses resolvedAddresses) { - logger.log(Level.FINE, "Received resolution result: {0}", resolvedAddresses); - - Map newChildAddresses = createChildAddressesMap(resolvedAddresses); - - // Handle error case - if (newChildAddresses.isEmpty()) { - Status unavailableStatus = Status.UNAVAILABLE.withDescription( - "NameResolver returned no usable address. " + resolvedAddresses); - handleNameResolutionError(unavailableStatus); - return new AcceptResolvedAddrRetVal(unavailableStatus, null); - } - - List removed = updateChildrenWithResolvedAddresses(newChildAddresses); - return new AcceptResolvedAddrRetVal(Status.OK, removed); - } - - /** Returns removed children. */ - private List updateChildrenWithResolvedAddresses( + private Status updateChildrenWithResolvedAddresses( Map newChildAddresses) { // Create a map with the old values Map oldStatesMap = @@ -183,6 +160,7 @@ private List updateChildrenWithResolvedAddresses( } // Move ChildLbStates from the map to a new list (preserving the new map's order) + Status status = Status.OK; List newChildLbStates = new ArrayList<>(newChildAddresses.size()); for (Map.Entry entry : newChildAddresses.entrySet()) { ChildLbState childLbState = oldStatesMap.remove(entry.getKey()); @@ -191,21 +169,23 @@ private List updateChildrenWithResolvedAddresses( } newChildLbStates.add(childLbState); if (entry.getValue() != null) { - childLbState.lb.handleResolvedAddresses(entry.getValue()); // update child LB + // update child LB + Status newStatus = childLbState.lb.acceptResolvedAddresses(entry.getValue()); + if (!newStatus.isOk()) { + status = newStatus; + } } } childLbStates = newChildLbStates; - // Remaining entries in map are orphaned - return new ArrayList<>(oldStatesMap.values()); - } + // Update the picker and our connectivity state + updateOverallBalancingState(); - protected final void shutdownRemoved(List removedChildren) { - // Do shutdowns after updating picker to reduce the chance of failing an RPC by picking a - // subchannel that has been shutdown. - for (ChildLbState childLbState : removedChildren) { + // Remaining entries in map are orphaned + for (ChildLbState childLbState : oldStatesMap.values()) { childLbState.shutdown(); } + return status; } @Nullable @@ -406,14 +386,4 @@ public String toString() { return addrs.toString(); } } - - protected static class AcceptResolvedAddrRetVal { - public final Status status; - public final List removedChildren; - - public AcceptResolvedAddrRetVal(Status status, List removedChildren) { - this.status = status; - this.removedChildren = removedChildren; - } - } } diff --git a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java index 0c4792cb924..3793482c86b 100644 --- a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java @@ -87,62 +87,46 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { return addressValidityStatus; } - try { - resolvingAddresses = true; - AcceptResolvedAddrRetVal acceptRetVal = acceptResolvedAddressesInternal(resolvedAddresses); - if (!acceptRetVal.status.isOk()) { - return acceptRetVal.status; - } - - // Now do the ringhash specific logic with weights and building the ring - RingHashConfig config = (RingHashConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); - if (config == null) { - throw new IllegalArgumentException("Missing RingHash configuration"); + // Now do the ringhash specific logic with weights and building the ring + RingHashConfig config = (RingHashConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); + if (config == null) { + throw new IllegalArgumentException("Missing RingHash configuration"); + } + Map serverWeights = new HashMap<>(); + long totalWeight = 0L; + for (EquivalentAddressGroup eag : addrList) { + Long weight = eag.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT); + // Support two ways of server weighing: either multiple instances of the same address + // or each address contains a per-address weight attribute. If a weight is not provided, + // each occurrence of the address will be counted a weight value of one. + if (weight == null) { + weight = 1L; } - Map serverWeights = new HashMap<>(); - long totalWeight = 0L; - for (EquivalentAddressGroup eag : addrList) { - Long weight = eag.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT); - // Support two ways of server weighing: either multiple instances of the same address - // or each address contains a per-address weight attribute. If a weight is not provided, - // each occurrence of the address will be counted a weight value of one. - if (weight == null) { - weight = 1L; - } - totalWeight += weight; - EquivalentAddressGroup addrKey = stripAttrs(eag); - if (serverWeights.containsKey(addrKey)) { - serverWeights.put(addrKey, serverWeights.get(addrKey) + weight); - } else { - serverWeights.put(addrKey, weight); - } + totalWeight += weight; + EquivalentAddressGroup addrKey = stripAttrs(eag); + if (serverWeights.containsKey(addrKey)) { + serverWeights.put(addrKey, serverWeights.get(addrKey) + weight); + } else { + serverWeights.put(addrKey, weight); } - // Calculate scale - long minWeight = Collections.min(serverWeights.values()); - double normalizedMinWeight = (double) minWeight / totalWeight; - // Scale up the number of hashes per host such that the least-weighted host gets a whole - // number of hashes on the the ring. Other hosts might not end up with whole numbers, and - // that's fine (the ring-building algorithm can handle this). This preserves the original - // implementation's behavior: when weights aren't provided, all hosts should get an equal - // number of hashes. In the case where this number exceeds the max_ring_size, it's scaled - // back down to fit. - double scale = Math.min( - Math.ceil(normalizedMinWeight * config.minRingSize) / normalizedMinWeight, - (double) config.maxRingSize); - - // Build the ring - ring = buildRing(serverWeights, totalWeight, scale); - - // Must update channel picker before return so that new RPCs will not be routed to deleted - // clusters and resolver can remove them in service config. - updateOverallBalancingState(); - - shutdownRemoved(acceptRetVal.removedChildren); - } finally { - this.resolvingAddresses = false; } - - return Status.OK; + // Calculate scale + long minWeight = Collections.min(serverWeights.values()); + double normalizedMinWeight = (double) minWeight / totalWeight; + // Scale up the number of hashes per host such that the least-weighted host gets a whole + // number of hashes on the the ring. Other hosts might not end up with whole numbers, and + // that's fine (the ring-building algorithm can handle this). This preserves the original + // implementation's behavior: when weights aren't provided, all hosts should get an equal + // number of hashes. In the case where this number exceeds the max_ring_size, it's scaled + // back down to fit. + double scale = Math.min( + Math.ceil(normalizedMinWeight * config.minRingSize) / normalizedMinWeight, + (double) config.maxRingSize); + + // Build the ring + ring = buildRing(serverWeights, totalWeight, scale); + + return super.acceptResolvedAddresses(resolvedAddresses); } diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 73764c63c80..b7b4fd51afe 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -170,31 +170,17 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { } config = (WeightedRoundRobinLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); - AcceptResolvedAddrRetVal acceptRetVal; - try { - resolvingAddresses = true; - acceptRetVal = acceptResolvedAddressesInternal(resolvedAddresses); - if (!acceptRetVal.status.isOk()) { - return acceptRetVal.status; - } - - if (weightUpdateTimer != null && weightUpdateTimer.isPending()) { - weightUpdateTimer.cancel(); - } - updateWeightTask.run(); - createAndApplyOrcaListeners(); + if (weightUpdateTimer != null && weightUpdateTimer.isPending()) { + weightUpdateTimer.cancel(); + } + updateWeightTask.run(); - // Must update channel picker before return so that new RPCs will not be routed to deleted - // clusters and resolver can remove them in service config. - updateOverallBalancingState(); + Status status = super.acceptResolvedAddresses(resolvedAddresses); - shutdownRemoved(acceptRetVal.removedChildren); - } finally { - resolvingAddresses = false; - } + createAndApplyOrcaListeners(); - return acceptRetVal.status; + return status; } /** From 2b87b01651fa5d99ab1b1d9cbc4f87af5a1f5d6a Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 18 Feb 2025 10:47:01 -0800 Subject: [PATCH 192/591] xds: Change how xDS filters are created by introducing Filter.Provider (#11883) This is the first step towards supporting filter state retention in Java. The mechanism will be similar to the one described in [A83] (https://github.com/grpc/proposal/blob/master/A83-xds-gcp-authn-filter.md#filter-call-credentials-cache) for C-core, and will serve the same purpose. However, the implementation details are very different due to the different nature of xDS HTTP filter support in C-core and Java. In Java, xDS HTTP filters are backed by classes implementing `io.grpc.xds.Filter`, from here just called "Filters". To support Filter state retention (next PR), Java's xDS implementation must be able to create unique Filter instances per: - Per HCM `envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager` - Per filter name as specified in `envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter.name` This PR **does not** implements Filter state retention, but lays the groundwork for it by changing how filters are registered and instantiated. To achieve this, all existing Filter classes had to be updated to the new instantiation mechanism described below. Prior to these this PR, Filters had no livecycle. FilterRegistry provided singleton instances for a given typeUrl. This PR introduces a new interface `Filter.Provider`, which instantiates Filter classes. All functionality that doesn't need an instance of a Filter is moved to the Filter.Provider. This includes parsing filter config proto into FilterConfig and determining the filter kind (client-side, server-side, or both). This PR is limited to refactoring, and there's no changes to the existing behavior. Note that all Filter Providers still return singleton Filter instances. However, with this PR, it is now possible to create Providers that return a new Filter instance each time `newInstance` is called. --- .../main/java/io/grpc/xds/FaultFilter.java | 170 ++++++++++-------- xds/src/main/java/io/grpc/xds/Filter.java | 90 +++++++--- .../main/java/io/grpc/xds/FilterRegistry.java | 16 +- .../io/grpc/xds/GcpAuthenticationFilter.java | 78 ++++---- .../java/io/grpc/xds/InternalRbacFilter.java | 7 +- xds/src/main/java/io/grpc/xds/RbacFilter.java | 158 ++++++++-------- .../main/java/io/grpc/xds/RouterFilter.java | 65 ++++--- .../java/io/grpc/xds/XdsListenerResource.java | 29 ++- .../java/io/grpc/xds/XdsNameResolver.java | 31 ++-- .../grpc/xds/XdsRouteConfigureResource.java | 6 +- .../java/io/grpc/xds/XdsServerWrapper.java | 66 ++++--- .../java/io/grpc/xds/FaultFilterTest.java | 19 +- .../grpc/xds/GcpAuthenticationFilterTest.java | 25 +-- .../grpc/xds/GrpcXdsClientImplDataTest.java | 78 ++++---- .../test/java/io/grpc/xds/RbacFilterTest.java | 35 ++-- .../java/io/grpc/xds/RouterFilterTest.java | 36 ++++ .../java/io/grpc/xds/XdsNameResolverTest.java | 19 +- .../io/grpc/xds/XdsServerWrapperTest.java | 61 ++++--- 18 files changed, 579 insertions(+), 410 deletions(-) create mode 100644 xds/src/test/java/io/grpc/xds/RouterFilterTest.java diff --git a/xds/src/main/java/io/grpc/xds/FaultFilter.java b/xds/src/main/java/io/grpc/xds/FaultFilter.java index c66861a9f15..2012fd36b62 100644 --- a/xds/src/main/java/io/grpc/xds/FaultFilter.java +++ b/xds/src/main/java/io/grpc/xds/FaultFilter.java @@ -45,7 +45,6 @@ import io.grpc.internal.GrpcUtil; import io.grpc.xds.FaultConfig.FaultAbort; import io.grpc.xds.FaultConfig.FaultDelay; -import io.grpc.xds.Filter.ClientInterceptorBuilder; import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl; import java.util.Locale; import java.util.concurrent.Executor; @@ -56,10 +55,11 @@ import javax.annotation.Nullable; /** HttpFault filter implementation. */ -final class FaultFilter implements Filter, ClientInterceptorBuilder { +final class FaultFilter implements Filter { - static final FaultFilter INSTANCE = + private static final FaultFilter INSTANCE = new FaultFilter(ThreadSafeRandomImpl.instance, new AtomicLong()); + @VisibleForTesting static final Metadata.Key HEADER_DELAY_KEY = Metadata.Key.of("x-envoy-fault-delay-request", Metadata.ASCII_STRING_MARSHALLER); @@ -87,96 +87,108 @@ final class FaultFilter implements Filter, ClientInterceptorBuilder { this.activeFaultCounter = activeFaultCounter; } - @Override - public String[] typeUrls() { - return new String[] { TYPE_URL }; - } - - @Override - public ConfigOrError parseFilterConfig(Message rawProtoMessage) { - HTTPFault httpFaultProto; - if (!(rawProtoMessage instanceof Any)) { - return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); + static final class Provider implements Filter.Provider { + @Override + public String[] typeUrls() { + return new String[]{TYPE_URL}; } - Any anyMessage = (Any) rawProtoMessage; - try { - httpFaultProto = anyMessage.unpack(HTTPFault.class); - } catch (InvalidProtocolBufferException e) { - return ConfigOrError.fromError("Invalid proto: " + e); + + @Override + public boolean isClientFilter() { + return true; } - return parseHttpFault(httpFaultProto); - } - private static ConfigOrError parseHttpFault(HTTPFault httpFault) { - FaultDelay faultDelay = null; - FaultAbort faultAbort = null; - if (httpFault.hasDelay()) { - faultDelay = parseFaultDelay(httpFault.getDelay()); + @Override + public FaultFilter newInstance() { + return INSTANCE; } - if (httpFault.hasAbort()) { - ConfigOrError faultAbortOrError = parseFaultAbort(httpFault.getAbort()); - if (faultAbortOrError.errorDetail != null) { - return ConfigOrError.fromError( - "HttpFault contains invalid FaultAbort: " + faultAbortOrError.errorDetail); + + @Override + public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + HTTPFault httpFaultProto; + if (!(rawProtoMessage instanceof Any)) { + return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); } - faultAbort = faultAbortOrError.config; - } - Integer maxActiveFaults = null; - if (httpFault.hasMaxActiveFaults()) { - maxActiveFaults = httpFault.getMaxActiveFaults().getValue(); - if (maxActiveFaults < 0) { - maxActiveFaults = Integer.MAX_VALUE; + Any anyMessage = (Any) rawProtoMessage; + try { + httpFaultProto = anyMessage.unpack(HTTPFault.class); + } catch (InvalidProtocolBufferException e) { + return ConfigOrError.fromError("Invalid proto: " + e); } + return parseHttpFault(httpFaultProto); } - return ConfigOrError.fromConfig(FaultConfig.create(faultDelay, faultAbort, maxActiveFaults)); - } - private static FaultDelay parseFaultDelay( - io.envoyproxy.envoy.extensions.filters.common.fault.v3.FaultDelay faultDelay) { - FaultConfig.FractionalPercent percent = parsePercent(faultDelay.getPercentage()); - if (faultDelay.hasHeaderDelay()) { - return FaultDelay.forHeader(percent); + @Override + public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { + return parseFilterConfig(rawProtoMessage); } - return FaultDelay.forFixedDelay(Durations.toNanos(faultDelay.getFixedDelay()), percent); - } - @VisibleForTesting - static ConfigOrError parseFaultAbort( - io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort faultAbort) { - FaultConfig.FractionalPercent percent = parsePercent(faultAbort.getPercentage()); - switch (faultAbort.getErrorTypeCase()) { - case HEADER_ABORT: - return ConfigOrError.fromConfig(FaultAbort.forHeader(percent)); - case HTTP_STATUS: - return ConfigOrError.fromConfig(FaultAbort.forStatus( - GrpcUtil.httpStatusToGrpcStatus(faultAbort.getHttpStatus()), percent)); - case GRPC_STATUS: - return ConfigOrError.fromConfig(FaultAbort.forStatus( - Status.fromCodeValue(faultAbort.getGrpcStatus()), percent)); - case ERRORTYPE_NOT_SET: - default: - return ConfigOrError.fromError( - "Unknown error type case: " + faultAbort.getErrorTypeCase()); + private static ConfigOrError parseHttpFault(HTTPFault httpFault) { + FaultDelay faultDelay = null; + FaultAbort faultAbort = null; + if (httpFault.hasDelay()) { + faultDelay = parseFaultDelay(httpFault.getDelay()); + } + if (httpFault.hasAbort()) { + ConfigOrError faultAbortOrError = parseFaultAbort(httpFault.getAbort()); + if (faultAbortOrError.errorDetail != null) { + return ConfigOrError.fromError( + "HttpFault contains invalid FaultAbort: " + faultAbortOrError.errorDetail); + } + faultAbort = faultAbortOrError.config; + } + Integer maxActiveFaults = null; + if (httpFault.hasMaxActiveFaults()) { + maxActiveFaults = httpFault.getMaxActiveFaults().getValue(); + if (maxActiveFaults < 0) { + maxActiveFaults = Integer.MAX_VALUE; + } + } + return ConfigOrError.fromConfig(FaultConfig.create(faultDelay, faultAbort, maxActiveFaults)); } - } - private static FaultConfig.FractionalPercent parsePercent(FractionalPercent proto) { - switch (proto.getDenominator()) { - case HUNDRED: - return FaultConfig.FractionalPercent.perHundred(proto.getNumerator()); - case TEN_THOUSAND: - return FaultConfig.FractionalPercent.perTenThousand(proto.getNumerator()); - case MILLION: - return FaultConfig.FractionalPercent.perMillion(proto.getNumerator()); - case UNRECOGNIZED: - default: - throw new IllegalArgumentException("Unknown denominator type: " + proto.getDenominator()); + private static FaultDelay parseFaultDelay( + io.envoyproxy.envoy.extensions.filters.common.fault.v3.FaultDelay faultDelay) { + FaultConfig.FractionalPercent percent = parsePercent(faultDelay.getPercentage()); + if (faultDelay.hasHeaderDelay()) { + return FaultDelay.forHeader(percent); + } + return FaultDelay.forFixedDelay(Durations.toNanos(faultDelay.getFixedDelay()), percent); } - } - @Override - public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { - return parseFilterConfig(rawProtoMessage); + @VisibleForTesting + static ConfigOrError parseFaultAbort( + io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort faultAbort) { + FaultConfig.FractionalPercent percent = parsePercent(faultAbort.getPercentage()); + switch (faultAbort.getErrorTypeCase()) { + case HEADER_ABORT: + return ConfigOrError.fromConfig(FaultAbort.forHeader(percent)); + case HTTP_STATUS: + return ConfigOrError.fromConfig(FaultAbort.forStatus( + GrpcUtil.httpStatusToGrpcStatus(faultAbort.getHttpStatus()), percent)); + case GRPC_STATUS: + return ConfigOrError.fromConfig(FaultAbort.forStatus( + Status.fromCodeValue(faultAbort.getGrpcStatus()), percent)); + case ERRORTYPE_NOT_SET: + default: + return ConfigOrError.fromError( + "Unknown error type case: " + faultAbort.getErrorTypeCase()); + } + } + + private static FaultConfig.FractionalPercent parsePercent(FractionalPercent proto) { + switch (proto.getDenominator()) { + case HUNDRED: + return FaultConfig.FractionalPercent.perHundred(proto.getNumerator()); + case TEN_THOUSAND: + return FaultConfig.FractionalPercent.perTenThousand(proto.getNumerator()); + case MILLION: + return FaultConfig.FractionalPercent.perMillion(proto.getNumerator()); + case UNRECOGNIZED: + default: + throw new IllegalArgumentException("Unknown denominator type: " + proto.getDenominator()); + } + } } @Nullable diff --git a/xds/src/main/java/io/grpc/xds/Filter.java b/xds/src/main/java/io/grpc/xds/Filter.java index 29f8cc4e337..ab61ba2b570 100644 --- a/xds/src/main/java/io/grpc/xds/Filter.java +++ b/xds/src/main/java/io/grpc/xds/Filter.java @@ -25,48 +25,82 @@ import javax.annotation.Nullable; /** - * Defines the parsing functionality of an HTTP filter. A Filter may optionally implement either - * {@link ClientInterceptorBuilder} or {@link ServerInterceptorBuilder} or both, indicating it is - * capable of working on the client side or server side or both, respectively. + * Defines the parsing functionality of an HTTP filter. + * + *

A Filter may optionally implement either {@link Filter#buildClientInterceptor} or + * {@link Filter#buildServerInterceptor} or both, and return true from corresponding + * {@link Provider#isClientFilter()}, {@link Provider#isServerFilter()} to indicate that the filter + * is capable of working on the client side or server side or both, respectively. */ interface Filter { - /** - * The proto message types supported by this filter. A filter will be registered by each of its - * supported message types. - */ - String[] typeUrls(); + /** Represents an opaque data structure holding configuration for a filter. */ + interface FilterConfig { + String typeUrl(); + } /** - * Parses the top-level filter config from raw proto message. The message may be either a {@link - * com.google.protobuf.Any} or a {@link com.google.protobuf.Struct}. + * Common interface for filter providers. */ - ConfigOrError parseFilterConfig(Message rawProtoMessage); + interface Provider { + /** + * The proto message types supported by this filter. A filter will be registered by each of its + * supported message types. + */ + String[] typeUrls(); - /** - * Parses the per-filter override filter config from raw proto message. The message may be either - * a {@link com.google.protobuf.Any} or a {@link com.google.protobuf.Struct}. - */ - ConfigOrError parseFilterConfigOverride(Message rawProtoMessage); + /** + * Whether the filter can be installed on the client side. + * + *

Returns true if the filter implements {@link Filter#buildClientInterceptor}. + */ + default boolean isClientFilter() { + return false; + } - /** Represents an opaque data structure holding configuration for a filter. */ - interface FilterConfig { - String typeUrl(); + /** + * Whether the filter can be installed into xDS-enabled servers. + * + *

Returns true if the filter implements {@link Filter#buildServerInterceptor}. + */ + default boolean isServerFilter() { + return false; + } + + /** + * Creates a new instance of the filter. + * + *

Returns a filter instance registered with the same typeUrls as the provider, + * capable of working with the same FilterConfig type returned by provider's parse functions. + */ + Filter newInstance(); + + /** + * Parses the top-level filter config from raw proto message. The message may be either a {@link + * com.google.protobuf.Any} or a {@link com.google.protobuf.Struct}. + */ + ConfigOrError parseFilterConfig(Message rawProtoMessage); + + /** + * Parses the per-filter override filter config from raw proto message. The message may be + * either a {@link com.google.protobuf.Any} or a {@link com.google.protobuf.Struct}. + */ + ConfigOrError parseFilterConfigOverride(Message rawProtoMessage); } /** Uses the FilterConfigs produced above to produce an HTTP filter interceptor for clients. */ - interface ClientInterceptorBuilder { - @Nullable - ClientInterceptor buildClientInterceptor( - FilterConfig config, @Nullable FilterConfig overrideConfig, - ScheduledExecutorService scheduler); + @Nullable + default ClientInterceptor buildClientInterceptor( + FilterConfig config, @Nullable FilterConfig overrideConfig, + ScheduledExecutorService scheduler) { + return null; } /** Uses the FilterConfigs produced above to produce an HTTP filter interceptor for the server. */ - interface ServerInterceptorBuilder { - @Nullable - ServerInterceptor buildServerInterceptor( - FilterConfig config, @Nullable FilterConfig overrideConfig); + @Nullable + default ServerInterceptor buildServerInterceptor( + FilterConfig config, @Nullable FilterConfig overrideConfig) { + return null; } /** Filter config with instance name. */ diff --git a/xds/src/main/java/io/grpc/xds/FilterRegistry.java b/xds/src/main/java/io/grpc/xds/FilterRegistry.java index 7f1fe82c6c3..426c6d1b3f6 100644 --- a/xds/src/main/java/io/grpc/xds/FilterRegistry.java +++ b/xds/src/main/java/io/grpc/xds/FilterRegistry.java @@ -23,21 +23,21 @@ /** * A registry for all supported {@link Filter}s. Filters can be queried from the registry - * by any of the {@link Filter#typeUrls() type URLs}. + * by any of the {@link Filter.Provider#typeUrls() type URLs}. */ final class FilterRegistry { private static FilterRegistry instance; - private final Map supportedFilters = new HashMap<>(); + private final Map supportedFilters = new HashMap<>(); private FilterRegistry() {} static synchronized FilterRegistry getDefaultRegistry() { if (instance == null) { instance = newRegistry().register( - FaultFilter.INSTANCE, - RouterFilter.INSTANCE, - RbacFilter.INSTANCE); + new FaultFilter.Provider(), + new RouterFilter.Provider(), + new RbacFilter.Provider()); } return instance; } @@ -48,8 +48,8 @@ static FilterRegistry newRegistry() { } @VisibleForTesting - FilterRegistry register(Filter... filters) { - for (Filter filter : filters) { + FilterRegistry register(Filter.Provider... filters) { + for (Filter.Provider filter : filters) { for (String typeUrl : filter.typeUrls()) { supportedFilters.put(typeUrl, filter); } @@ -58,7 +58,7 @@ FilterRegistry register(Filter... filters) { } @Nullable - Filter get(String typeUrl) { + Filter.Provider get(String typeUrl) { return supportedFilters.get(typeUrl); } } diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java index f73494d74db..7ed617c9843 100644 --- a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java +++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java @@ -35,7 +35,6 @@ import io.grpc.MethodDescriptor; import io.grpc.Status; import io.grpc.auth.MoreCallCredentials; -import io.grpc.xds.Filter.ClientInterceptorBuilder; import io.grpc.xds.MetadataRegistry.MetadataValueParser; import java.util.LinkedHashMap; import java.util.Map; @@ -47,50 +46,63 @@ * A {@link Filter} that injects a {@link CallCredentials} to handle * authentication for xDS credentials. */ -final class GcpAuthenticationFilter implements Filter, ClientInterceptorBuilder { +final class GcpAuthenticationFilter implements Filter { static final String TYPE_URL = "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig"; - @Override - public String[] typeUrls() { - return new String[] { TYPE_URL }; - } + static final class Provider implements Filter.Provider { + @Override + public String[] typeUrls() { + return new String[]{TYPE_URL}; + } - @Override - public ConfigOrError parseFilterConfig(Message rawProtoMessage) { - GcpAuthnFilterConfig gcpAuthnProto; - if (!(rawProtoMessage instanceof Any)) { - return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); + @Override + public boolean isClientFilter() { + return true; } - Any anyMessage = (Any) rawProtoMessage; - try { - gcpAuthnProto = anyMessage.unpack(GcpAuthnFilterConfig.class); - } catch (InvalidProtocolBufferException e) { - return ConfigOrError.fromError("Invalid proto: " + e); + @Override + public GcpAuthenticationFilter newInstance() { + return new GcpAuthenticationFilter(); } - long cacheSize = 10; - // Validate cache_config - if (gcpAuthnProto.hasCacheConfig()) { - TokenCacheConfig cacheConfig = gcpAuthnProto.getCacheConfig(); - cacheSize = cacheConfig.getCacheSize().getValue(); - if (cacheSize == 0) { - return ConfigOrError.fromError( - "cache_config.cache_size must be greater than zero"); + @Override + public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + GcpAuthnFilterConfig gcpAuthnProto; + if (!(rawProtoMessage instanceof Any)) { + return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); } - // LruCache's size is an int and briefly exceeds its maximum size before evicting entries - cacheSize = UnsignedLongs.min(cacheSize, Integer.MAX_VALUE - 1); - } + Any anyMessage = (Any) rawProtoMessage; - GcpAuthenticationConfig config = new GcpAuthenticationConfig((int) cacheSize); - return ConfigOrError.fromConfig(config); - } + try { + gcpAuthnProto = anyMessage.unpack(GcpAuthnFilterConfig.class); + } catch (InvalidProtocolBufferException e) { + return ConfigOrError.fromError("Invalid proto: " + e); + } - @Override - public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { - return parseFilterConfig(rawProtoMessage); + long cacheSize = 10; + // Validate cache_config + if (gcpAuthnProto.hasCacheConfig()) { + TokenCacheConfig cacheConfig = gcpAuthnProto.getCacheConfig(); + cacheSize = cacheConfig.getCacheSize().getValue(); + if (cacheSize == 0) { + return ConfigOrError.fromError( + "cache_config.cache_size must be greater than zero"); + } + // LruCache's size is an int and briefly exceeds its maximum size before evicting entries + cacheSize = UnsignedLongs.min(cacheSize, Integer.MAX_VALUE - 1); + } + + GcpAuthenticationConfig config = new GcpAuthenticationConfig((int) cacheSize); + return ConfigOrError.fromConfig(config); + } + + @Override + public ConfigOrError parseFilterConfigOverride( + Message rawProtoMessage) { + return parseFilterConfig(rawProtoMessage); + } } @Nullable diff --git a/xds/src/main/java/io/grpc/xds/InternalRbacFilter.java b/xds/src/main/java/io/grpc/xds/InternalRbacFilter.java index 54e6c748cd5..cedb3f4c85b 100644 --- a/xds/src/main/java/io/grpc/xds/InternalRbacFilter.java +++ b/xds/src/main/java/io/grpc/xds/InternalRbacFilter.java @@ -19,8 +19,6 @@ import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC; import io.grpc.Internal; import io.grpc.ServerInterceptor; -import io.grpc.xds.RbacConfig; -import io.grpc.xds.RbacFilter; /** This class exposes some functionality in RbacFilter to other packages. */ @Internal @@ -30,11 +28,12 @@ private InternalRbacFilter() {} /** Parses RBAC filter config and creates AuthorizationServerInterceptor. */ public static ServerInterceptor createInterceptor(RBAC rbac) { - ConfigOrError filterConfig = RbacFilter.parseRbacConfig(rbac); + ConfigOrError filterConfig = RbacFilter.Provider.parseRbacConfig(rbac); if (filterConfig.errorDetail != null) { throw new IllegalArgumentException( String.format("Failed to parse Rbac policy: %s", filterConfig.errorDetail)); } - return new RbacFilter().buildServerInterceptor(filterConfig.config, null); + return new RbacFilter.Provider().newInstance() + .buildServerInterceptor(filterConfig.config, null); } } diff --git a/xds/src/main/java/io/grpc/xds/RbacFilter.java b/xds/src/main/java/io/grpc/xds/RbacFilter.java index 6a55f7f193e..2bc4eeb846b 100644 --- a/xds/src/main/java/io/grpc/xds/RbacFilter.java +++ b/xds/src/main/java/io/grpc/xds/RbacFilter.java @@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkNotNull; -import com.google.common.annotations.VisibleForTesting; import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; @@ -34,7 +33,6 @@ import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Status; -import io.grpc.xds.Filter.ServerInterceptorBuilder; import io.grpc.xds.internal.MatcherParser; import io.grpc.xds.internal.Matchers; import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine; @@ -66,10 +64,10 @@ import javax.annotation.Nullable; /** RBAC Http filter implementation. */ -final class RbacFilter implements Filter, ServerInterceptorBuilder { +final class RbacFilter implements Filter { private static final Logger logger = Logger.getLogger(RbacFilter.class.getName()); - static final RbacFilter INSTANCE = new RbacFilter(); + private static final RbacFilter INSTANCE = new RbacFilter(); static final String TYPE_URL = "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC"; @@ -77,87 +75,99 @@ final class RbacFilter implements Filter, ServerInterceptorBuilder { private static final String TYPE_URL_OVERRIDE_CONFIG = "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute"; - RbacFilter() {} + private RbacFilter() {} - @Override - public String[] typeUrls() { - return new String[] { TYPE_URL, TYPE_URL_OVERRIDE_CONFIG }; - } + static final class Provider implements Filter.Provider { + @Override + public String[] typeUrls() { + return new String[] {TYPE_URL, TYPE_URL_OVERRIDE_CONFIG}; + } - @Override - public ConfigOrError parseFilterConfig(Message rawProtoMessage) { - RBAC rbacProto; - if (!(rawProtoMessage instanceof Any)) { - return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); + @Override + public boolean isServerFilter() { + return true; } - Any anyMessage = (Any) rawProtoMessage; - try { - rbacProto = anyMessage.unpack(RBAC.class); - } catch (InvalidProtocolBufferException e) { - return ConfigOrError.fromError("Invalid proto: " + e); + + @Override + public RbacFilter newInstance() { + return INSTANCE; } - return parseRbacConfig(rbacProto); - } - @VisibleForTesting - static ConfigOrError parseRbacConfig(RBAC rbac) { - if (!rbac.hasRules()) { - return ConfigOrError.fromConfig(RbacConfig.create(null)); + @Override + public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + RBAC rbacProto; + if (!(rawProtoMessage instanceof Any)) { + return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); + } + Any anyMessage = (Any) rawProtoMessage; + try { + rbacProto = anyMessage.unpack(RBAC.class); + } catch (InvalidProtocolBufferException e) { + return ConfigOrError.fromError("Invalid proto: " + e); + } + return parseRbacConfig(rbacProto); } - io.envoyproxy.envoy.config.rbac.v3.RBAC rbacConfig = rbac.getRules(); - GrpcAuthorizationEngine.Action authAction; - switch (rbacConfig.getAction()) { - case ALLOW: - authAction = GrpcAuthorizationEngine.Action.ALLOW; - break; - case DENY: - authAction = GrpcAuthorizationEngine.Action.DENY; - break; - case LOG: + + @Override + public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { + RBACPerRoute rbacPerRoute; + if (!(rawProtoMessage instanceof Any)) { + return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); + } + Any anyMessage = (Any) rawProtoMessage; + try { + rbacPerRoute = anyMessage.unpack(RBACPerRoute.class); + } catch (InvalidProtocolBufferException e) { + return ConfigOrError.fromError("Invalid proto: " + e); + } + if (rbacPerRoute.hasRbac()) { + return parseRbacConfig(rbacPerRoute.getRbac()); + } else { return ConfigOrError.fromConfig(RbacConfig.create(null)); - case UNRECOGNIZED: - default: - return ConfigOrError.fromError("Unknown rbacConfig action type: " + rbacConfig.getAction()); + } } - List policyMatchers = new ArrayList<>(); - List> sortedPolicyEntries = rbacConfig.getPoliciesMap().entrySet() - .stream() - .sorted((a,b) -> a.getKey().compareTo(b.getKey())) - .collect(Collectors.toList()); - for (Map.Entry entry: sortedPolicyEntries) { - try { - Policy policy = entry.getValue(); - if (policy.hasCondition() || policy.hasCheckedCondition()) { + + static ConfigOrError parseRbacConfig(RBAC rbac) { + if (!rbac.hasRules()) { + return ConfigOrError.fromConfig(RbacConfig.create(null)); + } + io.envoyproxy.envoy.config.rbac.v3.RBAC rbacConfig = rbac.getRules(); + GrpcAuthorizationEngine.Action authAction; + switch (rbacConfig.getAction()) { + case ALLOW: + authAction = GrpcAuthorizationEngine.Action.ALLOW; + break; + case DENY: + authAction = GrpcAuthorizationEngine.Action.DENY; + break; + case LOG: + return ConfigOrError.fromConfig(RbacConfig.create(null)); + case UNRECOGNIZED: + default: return ConfigOrError.fromError( - "Policy.condition and Policy.checked_condition must not set: " + entry.getKey()); + "Unknown rbacConfig action type: " + rbacConfig.getAction()); + } + List policyMatchers = new ArrayList<>(); + List> sortedPolicyEntries = rbacConfig.getPoliciesMap().entrySet() + .stream() + .sorted((a,b) -> a.getKey().compareTo(b.getKey())) + .collect(Collectors.toList()); + for (Map.Entry entry: sortedPolicyEntries) { + try { + Policy policy = entry.getValue(); + if (policy.hasCondition() || policy.hasCheckedCondition()) { + return ConfigOrError.fromError( + "Policy.condition and Policy.checked_condition must not set: " + entry.getKey()); + } + policyMatchers.add(PolicyMatcher.create(entry.getKey(), + parsePermissionList(policy.getPermissionsList()), + parsePrincipalList(policy.getPrincipalsList()))); + } catch (Exception e) { + return ConfigOrError.fromError("Encountered error parsing policy: " + e); } - policyMatchers.add(PolicyMatcher.create(entry.getKey(), - parsePermissionList(policy.getPermissionsList()), - parsePrincipalList(policy.getPrincipalsList()))); - } catch (Exception e) { - return ConfigOrError.fromError("Encountered error parsing policy: " + e); } - } - return ConfigOrError.fromConfig(RbacConfig.create( - AuthConfig.create(policyMatchers, authAction))); - } - - @Override - public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { - RBACPerRoute rbacPerRoute; - if (!(rawProtoMessage instanceof Any)) { - return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); - } - Any anyMessage = (Any) rawProtoMessage; - try { - rbacPerRoute = anyMessage.unpack(RBACPerRoute.class); - } catch (InvalidProtocolBufferException e) { - return ConfigOrError.fromError("Invalid proto: " + e); - } - if (rbacPerRoute.hasRbac()) { - return parseRbacConfig(rbacPerRoute.getRbac()); - } else { - return ConfigOrError.fromConfig(RbacConfig.create(null)); + return ConfigOrError.fromConfig(RbacConfig.create( + AuthConfig.create(policyMatchers, authAction))); } } diff --git a/xds/src/main/java/io/grpc/xds/RouterFilter.java b/xds/src/main/java/io/grpc/xds/RouterFilter.java index 8038c1b98ae..939bd0b12ab 100644 --- a/xds/src/main/java/io/grpc/xds/RouterFilter.java +++ b/xds/src/main/java/io/grpc/xds/RouterFilter.java @@ -17,18 +17,12 @@ package io.grpc.xds; import com.google.protobuf.Message; -import io.grpc.ClientInterceptor; -import io.grpc.ServerInterceptor; -import io.grpc.xds.Filter.ClientInterceptorBuilder; -import io.grpc.xds.Filter.ServerInterceptorBuilder; -import java.util.concurrent.ScheduledExecutorService; -import javax.annotation.Nullable; /** * Router filter implementation. Currently this filter does not parse any field in the config. */ -enum RouterFilter implements Filter, ClientInterceptorBuilder, ServerInterceptorBuilder { - INSTANCE; +final class RouterFilter implements Filter { + private static final RouterFilter INSTANCE = new RouterFilter(); static final String TYPE_URL = "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"; @@ -36,7 +30,7 @@ enum RouterFilter implements Filter, ClientInterceptorBuilder, ServerInterceptor static final FilterConfig ROUTER_CONFIG = new FilterConfig() { @Override public String typeUrl() { - return RouterFilter.TYPE_URL; + return TYPE_URL; } @Override @@ -45,33 +39,38 @@ public String toString() { } }; - @Override - public String[] typeUrls() { - return new String[] { TYPE_URL }; - } + static final class Provider implements Filter.Provider { + @Override + public String[] typeUrls() { + return new String[]{TYPE_URL}; + } - @Override - public ConfigOrError parseFilterConfig(Message rawProtoMessage) { - return ConfigOrError.fromConfig(ROUTER_CONFIG); - } + @Override + public boolean isClientFilter() { + return true; + } - @Override - public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { - return ConfigOrError.fromError("Router Filter should not have override config"); - } + @Override + public boolean isServerFilter() { + return true; + } - @Nullable - @Override - public ClientInterceptor buildClientInterceptor( - FilterConfig config, @Nullable FilterConfig overrideConfig, - ScheduledExecutorService scheduler) { - return null; - } + @Override + public RouterFilter newInstance() { + return INSTANCE; + } - @Nullable - @Override - public ServerInterceptor buildServerInterceptor( - FilterConfig config, @Nullable Filter.FilterConfig overrideConfig) { - return null; + @Override + public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + return ConfigOrError.fromConfig(ROUTER_CONFIG); + } + + @Override + public ConfigOrError parseFilterConfigOverride( + Message rawProtoMessage) { + return ConfigOrError.fromError("Router Filter should not have override config"); + } } + + private RouterFilter() {} } diff --git a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java index a18b093e38f..4b554be1743 100644 --- a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java @@ -575,12 +575,8 @@ static StructOrError parseHttpFilter( String filterName = httpFilter.getName(); boolean isOptional = httpFilter.getIsOptional(); if (!httpFilter.hasTypedConfig()) { - if (isOptional) { - return null; - } else { - return StructOrError.fromError( - "HttpFilter [" + filterName + "] is not optional and has no typed config"); - } + return isOptional ? null : StructOrError.fromError( + "HttpFilter [" + filterName + "] is not optional and has no typed config"); } Message rawConfig = httpFilter.getTypedConfig(); String typeUrl = httpFilter.getTypedConfig().getTypeUrl(); @@ -600,18 +596,17 @@ static StructOrError parseHttpFilter( return StructOrError.fromError( "HttpFilter [" + filterName + "] contains invalid proto: " + e); } - Filter filter = filterRegistry.get(typeUrl); - if ((isForClient && !(filter instanceof Filter.ClientInterceptorBuilder)) - || (!isForClient && !(filter instanceof Filter.ServerInterceptorBuilder))) { - if (isOptional) { - return null; - } else { - return StructOrError.fromError( - "HttpFilter [" + filterName + "](" + typeUrl + ") is required but unsupported for " - + (isForClient ? "client" : "server")); - } + + Filter.Provider provider = filterRegistry.get(typeUrl); + if (provider == null + || (isForClient && !provider.isClientFilter()) + || (!isForClient && !provider.isServerFilter())) { + // Filter type not supported. + return isOptional ? null : StructOrError.fromError( + "HttpFilter [" + filterName + "](" + typeUrl + ") is required but unsupported for " + ( + isForClient ? "client" : "server")); } - ConfigOrError filterConfig = filter.parseFilterConfig(rawConfig); + ConfigOrError filterConfig = provider.parseFilterConfig(rawConfig); if (filterConfig.errorDetail != null) { return StructOrError.fromError( "Invalid filter config for HttpFilter [" + filterName + "]: " + filterConfig.errorDetail); diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 21f5d5efce6..b7b1ed0bdba 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -49,7 +49,6 @@ import io.grpc.internal.GrpcUtil; import io.grpc.internal.ObjectPool; import io.grpc.xds.ClusterSpecifierPlugin.PluginConfig; -import io.grpc.xds.Filter.ClientInterceptorBuilder; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.Filter.NamedFilterConfig; import io.grpc.xds.RouteLookupServiceClusterSpecifierPlugin.RlsPluginConfig; @@ -827,26 +826,36 @@ private ClientInterceptor createFilters( if (filterConfigs == null) { return new PassthroughClientInterceptor(); } + Map selectedOverrideConfigs = new HashMap<>(virtualHost.filterConfigOverrides()); selectedOverrideConfigs.putAll(route.filterConfigOverrides()); if (weightedCluster != null) { selectedOverrideConfigs.putAll(weightedCluster.filterConfigOverrides()); } + ImmutableList.Builder filterInterceptors = ImmutableList.builder(); for (NamedFilterConfig namedFilter : filterConfigs) { - FilterConfig filterConfig = namedFilter.filterConfig; - Filter filter = filterRegistry.get(filterConfig.typeUrl()); - if (filter instanceof ClientInterceptorBuilder) { - ClientInterceptor interceptor = ((ClientInterceptorBuilder) filter) - .buildClientInterceptor( - filterConfig, selectedOverrideConfigs.get(namedFilter.name), - scheduler); - if (interceptor != null) { - filterInterceptors.add(interceptor); - } + FilterConfig config = namedFilter.filterConfig; + String name = namedFilter.name; + String typeUrl = config.typeUrl(); + + Filter.Provider provider = filterRegistry.get(typeUrl); + if (provider == null || !provider.isClientFilter()) { + continue; + } + + Filter filter = provider.newInstance(); + + ClientInterceptor interceptor = + filter.buildClientInterceptor(config, selectedOverrideConfigs.get(name), scheduler); + if (interceptor != null) { + filterInterceptors.add(interceptor); } } + + // Combine interceptors produced by different filters into a single one that executes + // them sequentially. The order is preserved. return combineInterceptors(filterInterceptors.build()); } diff --git a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java index c5ca8d45cb3..80a77cbb1d4 100644 --- a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java @@ -245,8 +245,8 @@ static StructOrError> parseOverrideFilterConfigs( return StructOrError.fromError( "FilterConfig [" + name + "] contains invalid proto: " + e); } - Filter filter = filterRegistry.get(typeUrl); - if (filter == null) { + Filter.Provider provider = filterRegistry.get(typeUrl); + if (provider == null) { if (isOptional) { continue; } @@ -254,7 +254,7 @@ static StructOrError> parseOverrideFilterConfigs( "HttpFilter [" + name + "](" + typeUrl + ") is required but unsupported"); } ConfigOrError filterConfig = - filter.parseFilterConfigOverride(rawConfig); + provider.parseFilterConfigOverride(rawConfig); if (filterConfig.errorDetail != null) { return StructOrError.fromError( "Invalid filter config for HttpFilter [" + name + "]: " + filterConfig.errorDetail); diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index 3a9b98ee321..bbb17d9b616 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -47,7 +47,6 @@ import io.grpc.xds.EnvoyServerProtoData.FilterChain; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.Filter.NamedFilterConfig; -import io.grpc.xds.Filter.ServerInterceptorBuilder; import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler.FilterChainSelector; import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl; import io.grpc.xds.VirtualHost.Route; @@ -524,37 +523,56 @@ private AtomicReference generateRoutingConfig(FilterChain f } private ImmutableMap generatePerRouteInterceptors( - List namedFilterConfigs, List virtualHosts) { + @Nullable List filterConfigs, List virtualHosts) { + // This should always be called from the sync context. + // Ideally we'd want to throw otherwise, but this breaks the tests now. + // syncContext.throwIfNotInThisSynchronizationContext(); + ImmutableMap.Builder perRouteInterceptors = new ImmutableMap.Builder<>(); + for (VirtualHost virtualHost : virtualHosts) { for (Route route : virtualHost.routes()) { - List filterInterceptors = new ArrayList<>(); - Map selectedOverrideConfigs = - new HashMap<>(virtualHost.filterConfigOverrides()); - selectedOverrideConfigs.putAll(route.filterConfigOverrides()); - if (namedFilterConfigs != null) { - for (NamedFilterConfig namedFilterConfig : namedFilterConfigs) { - FilterConfig filterConfig = namedFilterConfig.filterConfig; - Filter filter = filterRegistry.get(filterConfig.typeUrl()); - if (filter instanceof ServerInterceptorBuilder) { - ServerInterceptor interceptor = - ((ServerInterceptorBuilder) filter).buildServerInterceptor( - filterConfig, selectedOverrideConfigs.get(namedFilterConfig.name)); - if (interceptor != null) { - filterInterceptors.add(interceptor); - } - } else { - logger.log(Level.WARNING, "HttpFilterConfig(type URL: " - + filterConfig.typeUrl() + ") is not supported on server-side. " - + "Probably a bug at ClientXdsClient verification."); - } + // Short circuit. + if (filterConfigs == null) { + perRouteInterceptors.put(route, noopInterceptor); + continue; + } + + // Override vhost filter configs with more specific per-route configs. + Map perRouteOverrides = ImmutableMap.builder() + .putAll(virtualHost.filterConfigOverrides()) + .putAll(route.filterConfigOverrides()) + .buildKeepingLast(); + + // Interceptors for this vhost/route combo. + List interceptors = new ArrayList<>(filterConfigs.size()); + + for (NamedFilterConfig namedFilter : filterConfigs) { + FilterConfig config = namedFilter.filterConfig; + String name = namedFilter.name; + String typeUrl = config.typeUrl(); + + Filter.Provider provider = filterRegistry.get(typeUrl); + if (provider == null || !provider.isServerFilter()) { + logger.warning("HttpFilter[" + name + "]: not supported on server-side: " + typeUrl); + continue; + } + + Filter filter = provider.newInstance(); + ServerInterceptor interceptor = + filter.buildServerInterceptor(config, perRouteOverrides.get(name)); + if (interceptor != null) { + interceptors.add(interceptor); } } - ServerInterceptor interceptor = combineInterceptors(filterInterceptors); - perRouteInterceptors.put(route, interceptor); + + // Combine interceptors produced by different filters into a single one that executes + // them sequentially. The order is preserved. + perRouteInterceptors.put(route, combineInterceptors(interceptors)); } } + return perRouteInterceptors.buildOrThrow(); } diff --git a/xds/src/test/java/io/grpc/xds/FaultFilterTest.java b/xds/src/test/java/io/grpc/xds/FaultFilterTest.java index f85f29ec0a3..8f0a33951b0 100644 --- a/xds/src/test/java/io/grpc/xds/FaultFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/FaultFilterTest.java @@ -33,16 +33,23 @@ /** Tests for {@link FaultFilter}. */ @RunWith(JUnit4.class) public class FaultFilterTest { + private static final FaultFilter.Provider FILTER_PROVIDER = new FaultFilter.Provider(); + + @Test + public void filterType_clientOnly() { + assertThat(FILTER_PROVIDER.isClientFilter()).isTrue(); + assertThat(FILTER_PROVIDER.isServerFilter()).isFalse(); + } @Test public void parseFaultAbort_convertHttpStatus() { Any rawConfig = Any.pack( HTTPFault.newBuilder().setAbort(FaultAbort.newBuilder().setHttpStatus(404)).build()); - FaultConfig faultConfig = FaultFilter.INSTANCE.parseFilterConfig(rawConfig).config; + FaultConfig faultConfig = FILTER_PROVIDER.parseFilterConfig(rawConfig).config; assertThat(faultConfig.faultAbort().status().getCode()) .isEqualTo(GrpcUtil.httpStatusToGrpcStatus(404).getCode()); - FaultConfig faultConfigOverride = - FaultFilter.INSTANCE.parseFilterConfigOverride(rawConfig).config; + + FaultConfig faultConfigOverride = FILTER_PROVIDER.parseFilterConfigOverride(rawConfig).config; assertThat(faultConfigOverride.faultAbort().status().getCode()) .isEqualTo(GrpcUtil.httpStatusToGrpcStatus(404).getCode()); } @@ -54,7 +61,7 @@ public void parseFaultAbort_withHeaderAbort() { .setPercentage(FractionalPercent.newBuilder() .setNumerator(20).setDenominator(DenominatorType.HUNDRED)) .setHeaderAbort(HeaderAbort.getDefaultInstance()).build(); - FaultConfig.FaultAbort faultAbort = FaultFilter.parseFaultAbort(proto).config; + FaultConfig.FaultAbort faultAbort = FaultFilter.Provider.parseFaultAbort(proto).config; assertThat(faultAbort.headerAbort()).isTrue(); assertThat(faultAbort.percent().numerator()).isEqualTo(20); assertThat(faultAbort.percent().denominatorType()) @@ -68,7 +75,7 @@ public void parseFaultAbort_withHttpStatus() { .setPercentage(FractionalPercent.newBuilder() .setNumerator(100).setDenominator(DenominatorType.TEN_THOUSAND)) .setHttpStatus(400).build(); - FaultConfig.FaultAbort res = FaultFilter.parseFaultAbort(proto).config; + FaultConfig.FaultAbort res = FaultFilter.Provider.parseFaultAbort(proto).config; assertThat(res.percent().numerator()).isEqualTo(100); assertThat(res.percent().denominatorType()) .isEqualTo(FaultConfig.FractionalPercent.DenominatorType.TEN_THOUSAND); @@ -82,7 +89,7 @@ public void parseFaultAbort_withGrpcStatus() { .setPercentage(FractionalPercent.newBuilder() .setNumerator(600).setDenominator(DenominatorType.MILLION)) .setGrpcStatus(Code.DEADLINE_EXCEEDED.value()).build(); - FaultConfig.FaultAbort faultAbort = FaultFilter.parseFaultAbort(proto).config; + FaultConfig.FaultAbort faultAbort = FaultFilter.Provider.parseFaultAbort(proto).config; assertThat(faultAbort.percent().numerator()).isEqualTo(600); assertThat(faultAbort.percent().denominatorType()) .isEqualTo(FaultConfig.FractionalPercent.DenominatorType.MILLION); diff --git a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java index 3ca240ab7c7..52efaf9bd7b 100644 --- a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java @@ -35,6 +35,7 @@ import io.grpc.ClientInterceptor; import io.grpc.MethodDescriptor; import io.grpc.testing.TestMethodDescriptors; +import io.grpc.xds.GcpAuthenticationFilter.GcpAuthenticationConfig; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -43,6 +44,14 @@ @RunWith(JUnit4.class) public class GcpAuthenticationFilterTest { + private static final GcpAuthenticationFilter.Provider FILTER_PROVIDER = + new GcpAuthenticationFilter.Provider(); + + @Test + public void filterType_clientOnly() { + assertThat(FILTER_PROVIDER.isClientFilter()).isTrue(); + assertThat(FILTER_PROVIDER.isServerFilter()).isFalse(); + } @Test public void testParseFilterConfig_withValidConfig() { @@ -51,13 +60,11 @@ public void testParseFilterConfig_withValidConfig() { .build(); Any anyMessage = Any.pack(config); - GcpAuthenticationFilter filter = new GcpAuthenticationFilter(); - ConfigOrError result = filter.parseFilterConfig(anyMessage); + ConfigOrError result = FILTER_PROVIDER.parseFilterConfig(anyMessage); assertNotNull(result.config); assertNull(result.errorDetail); - assertEquals(20L, - ((GcpAuthenticationFilter.GcpAuthenticationConfig) result.config).getCacheSize()); + assertEquals(20L, result.config.getCacheSize()); } @Test @@ -67,8 +74,7 @@ public void testParseFilterConfig_withZeroCacheSize() { .build(); Any anyMessage = Any.pack(config); - GcpAuthenticationFilter filter = new GcpAuthenticationFilter(); - ConfigOrError result = filter.parseFilterConfig(anyMessage); + ConfigOrError result = FILTER_PROVIDER.parseFilterConfig(anyMessage); assertNull(result.config); assertNotNull(result.errorDetail); @@ -77,9 +83,9 @@ public void testParseFilterConfig_withZeroCacheSize() { @Test public void testParseFilterConfig_withInvalidMessageType() { - GcpAuthenticationFilter filter = new GcpAuthenticationFilter(); Message invalidMessage = Empty.getDefaultInstance(); - ConfigOrError result = filter.parseFilterConfig(invalidMessage); + ConfigOrError result = + FILTER_PROVIDER.parseFilterConfig(invalidMessage); assertNull(result.config); assertThat(result.errorDetail).contains("Invalid config type"); @@ -87,8 +93,7 @@ public void testParseFilterConfig_withInvalidMessageType() { @Test public void testClientInterceptor_createsAndReusesCachedCredentials() { - GcpAuthenticationFilter.GcpAuthenticationConfig config = - new GcpAuthenticationFilter.GcpAuthenticationConfig(10); + GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); GcpAuthenticationFilter filter = new GcpAuthenticationFilter(); // Create interceptor diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 314b2094480..610d147ccf9 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -110,7 +110,6 @@ import io.envoyproxy.envoy.type.v3.FractionalPercent; import io.envoyproxy.envoy.type.v3.FractionalPercent.DenominatorType; import io.envoyproxy.envoy.type.v3.Int64Range; -import io.grpc.ClientInterceptor; import io.grpc.EquivalentAddressGroup; import io.grpc.InsecureChannelCredentials; import io.grpc.LoadBalancerRegistry; @@ -150,9 +149,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -165,6 +162,10 @@ @RunWith(JUnit4.class) public class GrpcXdsClientImplDataTest { + private static final FaultFilter.Provider FAULT_FILTER_PROVIDER = new FaultFilter.Provider(); + private static final RbacFilter.Provider RBAC_FILTER_PROVIDER = new RbacFilter.Provider(); + private static final RouterFilter.Provider ROUTER_FILTER_PROVIDER = new RouterFilter.Provider(); + private static final ServerInfo LRS_SERVER_INFO = ServerInfo.create("lrs.googleapis.com", InsecureChannelCredentials.create()); private static final String GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE = @@ -1243,36 +1244,39 @@ public String typeUrl() { } } - private static class TestFilter implements io.grpc.xds.Filter, - io.grpc.xds.Filter.ClientInterceptorBuilder { - @Override - public String[] typeUrls() { - return new String[]{"test-url"}; - } + private static class TestFilter implements io.grpc.xds.Filter { - @Override - public ConfigOrError parseFilterConfig(Message rawProtoMessage) { - return ConfigOrError.fromConfig(new SimpleFilterConfig(rawProtoMessage)); - } + static final class Provider implements io.grpc.xds.Filter.Provider { + @Override + public String[] typeUrls() { + return new String[]{"test-url"}; + } - @Override - public ConfigOrError parseFilterConfigOverride( - Message rawProtoMessage) { - return ConfigOrError.fromConfig(new SimpleFilterConfig(rawProtoMessage)); - } + @Override + public boolean isClientFilter() { + return true; + } - @Nullable - @Override - public ClientInterceptor buildClientInterceptor(FilterConfig config, - @Nullable FilterConfig overrideConfig, - ScheduledExecutorService scheduler) { - return null; + @Override + public TestFilter newInstance() { + return new TestFilter(); + } + + @Override + public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + return ConfigOrError.fromConfig(new SimpleFilterConfig(rawProtoMessage)); + } + + @Override + public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { + return ConfigOrError.fromConfig(new SimpleFilterConfig(rawProtoMessage)); + } } } @Test public void parseHttpFilter_typedStructMigration() { - filterRegistry.register(new TestFilter()); + filterRegistry.register(new TestFilter.Provider()); Struct rawStruct = Struct.newBuilder() .putFields("name", Value.newBuilder().setStringValue("default").build()) .build(); @@ -1301,7 +1305,7 @@ public void parseHttpFilter_typedStructMigration() { @Test public void parseOverrideHttpFilter_typedStructMigration() { - filterRegistry.register(new TestFilter()); + filterRegistry.register(new TestFilter.Provider()); Struct rawStruct0 = Struct.newBuilder() .putFields("name", Value.newBuilder().setStringValue("default0").build()) .build(); @@ -1342,7 +1346,7 @@ public void parseHttpFilter_unsupportedAndRequired() { @Test public void parseHttpFilter_routerFilterForClient() { - filterRegistry.register(RouterFilter.INSTANCE); + filterRegistry.register(ROUTER_FILTER_PROVIDER); HttpFilter httpFilter = HttpFilter.newBuilder() .setIsOptional(false) @@ -1356,7 +1360,7 @@ public void parseHttpFilter_routerFilterForClient() { @Test public void parseHttpFilter_routerFilterForServer() { - filterRegistry.register(RouterFilter.INSTANCE); + filterRegistry.register(ROUTER_FILTER_PROVIDER); HttpFilter httpFilter = HttpFilter.newBuilder() .setIsOptional(false) @@ -1370,7 +1374,7 @@ public void parseHttpFilter_routerFilterForServer() { @Test public void parseHttpFilter_faultConfigForClient() { - filterRegistry.register(FaultFilter.INSTANCE); + filterRegistry.register(FAULT_FILTER_PROVIDER); HttpFilter httpFilter = HttpFilter.newBuilder() .setIsOptional(false) @@ -1397,7 +1401,7 @@ public void parseHttpFilter_faultConfigForClient() { @Test public void parseHttpFilter_faultConfigUnsupportedForServer() { - filterRegistry.register(FaultFilter.INSTANCE); + filterRegistry.register(FAULT_FILTER_PROVIDER); HttpFilter httpFilter = HttpFilter.newBuilder() .setIsOptional(false) @@ -1426,7 +1430,7 @@ public void parseHttpFilter_faultConfigUnsupportedForServer() { @Test public void parseHttpFilter_rbacConfigForServer() { - filterRegistry.register(RbacFilter.INSTANCE); + filterRegistry.register(RBAC_FILTER_PROVIDER); HttpFilter httpFilter = HttpFilter.newBuilder() .setIsOptional(false) @@ -1453,7 +1457,7 @@ public void parseHttpFilter_rbacConfigForServer() { @Test public void parseHttpFilter_rbacConfigUnsupportedForClient() { - filterRegistry.register(RbacFilter.INSTANCE); + filterRegistry.register(RBAC_FILTER_PROVIDER); HttpFilter httpFilter = HttpFilter.newBuilder() .setIsOptional(false) @@ -1482,7 +1486,7 @@ public void parseHttpFilter_rbacConfigUnsupportedForClient() { @Test public void parseOverrideRbacFilterConfig() { - filterRegistry.register(RbacFilter.INSTANCE); + filterRegistry.register(RBAC_FILTER_PROVIDER); RBACPerRoute rbacPerRoute = RBACPerRoute.newBuilder() .setRbac( @@ -1508,7 +1512,7 @@ public void parseOverrideRbacFilterConfig() { @Test public void parseOverrideFilterConfigs_unsupportedButOptional() { - filterRegistry.register(FaultFilter.INSTANCE); + filterRegistry.register(FAULT_FILTER_PROVIDER); HTTPFault httpFault = HTTPFault.newBuilder() .setDelay(FaultDelay.newBuilder().setFixedDelay(Durations.fromNanos(3000))) .build(); @@ -1528,7 +1532,7 @@ public void parseOverrideFilterConfigs_unsupportedButOptional() { @Test public void parseOverrideFilterConfigs_unsupportedAndRequired() { - filterRegistry.register(FaultFilter.INSTANCE); + filterRegistry.register(FAULT_FILTER_PROVIDER); HTTPFault httpFault = HTTPFault.newBuilder() .setDelay(FaultDelay.newBuilder().setFixedDelay(Durations.fromNanos(3000))) .build(); @@ -1620,7 +1624,7 @@ public void parseHttpConnectionManager_duplicateHttpFilters() throws ResourceInv @Test public void parseHttpConnectionManager_lastNotTerminal() throws ResourceInvalidException { - filterRegistry.register(FaultFilter.INSTANCE); + filterRegistry.register(FAULT_FILTER_PROVIDER); HttpConnectionManager hcm = HttpConnectionManager.newBuilder() .addHttpFilters( @@ -1638,7 +1642,7 @@ public void parseHttpConnectionManager_lastNotTerminal() throws ResourceInvalidE @Test public void parseHttpConnectionManager_terminalNotLast() throws ResourceInvalidException { - filterRegistry.register(RouterFilter.INSTANCE); + filterRegistry.register(ROUTER_FILTER_PROVIDER); HttpConnectionManager hcm = HttpConnectionManager.newBuilder() .addHttpFilters( diff --git a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java index 013b21e3f45..7f195693d84 100644 --- a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java @@ -78,6 +78,13 @@ public class RbacFilterTest { private static final String PATH = "auth"; private static final StringMatcher STRING_MATCHER = StringMatcher.newBuilder().setExact("/" + PATH).setIgnoreCase(true).build(); + private static final RbacFilter.Provider FILTER_PROVIDER = new RbacFilter.Provider(); + + @Test + public void filterType_serverOnly() { + assertThat(FILTER_PROVIDER.isClientFilter()).isFalse(); + assertThat(FILTER_PROVIDER.isServerFilter()).isTrue(); + } @Test @SuppressWarnings({"unchecked", "deprecation"}) @@ -252,7 +259,7 @@ public void testAuthorizationInterceptor() { OrMatcher.create(AlwaysTrueMatcher.INSTANCE)); AuthConfig authconfig = AuthConfig.create(Collections.singletonList(policyMatcher), GrpcAuthorizationEngine.Action.ALLOW); - new RbacFilter().buildServerInterceptor(RbacConfig.create(authconfig), null) + FILTER_PROVIDER.newInstance().buildServerInterceptor(RbacConfig.create(authconfig), null) .interceptCall(mockServerCall, new Metadata(), mockHandler); verify(mockHandler, never()).startCall(eq(mockServerCall), any(Metadata.class)); ArgumentCaptor captor = ArgumentCaptor.forClass(Status.class); @@ -264,7 +271,7 @@ public void testAuthorizationInterceptor() { authconfig = AuthConfig.create(Collections.singletonList(policyMatcher), GrpcAuthorizationEngine.Action.DENY); - new RbacFilter().buildServerInterceptor(RbacConfig.create(authconfig), null) + FILTER_PROVIDER.newInstance().buildServerInterceptor(RbacConfig.create(authconfig), null) .interceptCall(mockServerCall, new Metadata(), mockHandler); verify(mockHandler).startCall(eq(mockServerCall), any(Metadata.class)); } @@ -290,7 +297,7 @@ public void handleException() { .putPolicies("policy-name", Policy.newBuilder().setCondition(Expr.newBuilder().build()).build()) .build()).build(); - result = new RbacFilter().parseFilterConfig(Any.pack(rawProto)); + result = FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto)); assertThat(result.errorDetail).isNotNull(); } @@ -312,10 +319,10 @@ public void overrideConfig() { RbacConfig original = RbacConfig.create(authconfig); RBACPerRoute rbacPerRoute = RBACPerRoute.newBuilder().build(); - RbacConfig override = - new RbacFilter().parseFilterConfigOverride(Any.pack(rbacPerRoute)).config; + RbacConfig override = FILTER_PROVIDER.parseFilterConfigOverride(Any.pack(rbacPerRoute)).config; assertThat(override).isEqualTo(RbacConfig.create(null)); - ServerInterceptor interceptor = new RbacFilter().buildServerInterceptor(original, override); + ServerInterceptor interceptor = + FILTER_PROVIDER.newInstance().buildServerInterceptor(original, override); assertThat(interceptor).isNull(); policyMatcher = PolicyMatcher.create("policy-matcher-override", @@ -325,7 +332,7 @@ public void overrideConfig() { GrpcAuthorizationEngine.Action.ALLOW); override = RbacConfig.create(authconfig); - new RbacFilter().buildServerInterceptor(original, override) + FILTER_PROVIDER.newInstance().buildServerInterceptor(original, override) .interceptCall(mockServerCall, new Metadata(), mockHandler); verify(mockHandler).startCall(eq(mockServerCall), any(Metadata.class)); verify(mockServerCall).getAttributes(); @@ -337,22 +344,22 @@ public void ignoredConfig() { Message rawProto = io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC.newBuilder() .setRules(RBAC.newBuilder().setAction(Action.LOG) .putPolicies("policy-name", Policy.newBuilder().build()).build()).build(); - ConfigOrError result = new RbacFilter().parseFilterConfig(Any.pack(rawProto)); + ConfigOrError result = FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto)); assertThat(result.config).isEqualTo(RbacConfig.create(null)); } @Test public void testOrderIndependenceOfPolicies() { Message rawProto = buildComplexRbac(ImmutableList.of(1, 2, 3, 4, 5, 6), true); - ConfigOrError ascFirst = new RbacFilter().parseFilterConfig(Any.pack(rawProto)); + ConfigOrError ascFirst = FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto)); rawProto = buildComplexRbac(ImmutableList.of(1, 2, 3, 4, 5, 6), false); - ConfigOrError ascLast = new RbacFilter().parseFilterConfig(Any.pack(rawProto)); + ConfigOrError ascLast = FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto)); assertThat(ascFirst.config).isEqualTo(ascLast.config); rawProto = buildComplexRbac(ImmutableList.of(6, 5, 4, 3, 2, 1), true); - ConfigOrError decFirst = new RbacFilter().parseFilterConfig(Any.pack(rawProto)); + ConfigOrError decFirst = FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto)); assertThat(ascFirst.config).isEqualTo(decFirst.config); } @@ -374,14 +381,14 @@ private MethodDescriptor.Builder method() { private ConfigOrError parse(List permissionList, List principalList) { - return RbacFilter.parseRbacConfig(buildRbac(permissionList, principalList)); + return RbacFilter.Provider.parseRbacConfig(buildRbac(permissionList, principalList)); } private ConfigOrError parseRaw(List permissionList, List principalList) { Message rawProto = buildRbac(permissionList, principalList); Any proto = Any.pack(rawProto); - return new RbacFilter().parseFilterConfig(proto); + return FILTER_PROVIDER.parseFilterConfig(proto); } private io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC buildRbac( @@ -449,6 +456,6 @@ private ConfigOrError parseOverride(List permissionList, RBACPerRoute rbacPerRoute = RBACPerRoute.newBuilder().setRbac( buildRbac(permissionList, principalList)).build(); Any proto = Any.pack(rbacPerRoute); - return new RbacFilter().parseFilterConfigOverride(proto); + return FILTER_PROVIDER.parseFilterConfigOverride(proto); } } diff --git a/xds/src/test/java/io/grpc/xds/RouterFilterTest.java b/xds/src/test/java/io/grpc/xds/RouterFilterTest.java new file mode 100644 index 00000000000..30fd8a6dc38 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/RouterFilterTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link RouterFilter}. */ +@RunWith(JUnit4.class) +public class RouterFilterTest { + private static final RouterFilter.Provider FILTER_PROVIDER = new RouterFilter.Provider(); + + @Test + public void filterType_clientAndServer() { + assertThat(FILTER_PROVIDER.isClientFilter()).isTrue(); + assertThat(FILTER_PROVIDER.isServerFilter()).isTrue(); + } + +} diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index d895cecdb10..f7309051f92 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -22,10 +22,12 @@ import static io.grpc.xds.FaultFilter.HEADER_ABORT_PERCENTAGE_KEY; import static io.grpc.xds.FaultFilter.HEADER_DELAY_KEY; import static io.grpc.xds.FaultFilter.HEADER_DELAY_PERCENTAGE_KEY; +import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -130,6 +132,9 @@ public class XdsNameResolverTest { private static final String RDS_RESOURCE_NAME = "route-configuration.googleapis.com"; private static final String FAULT_FILTER_INSTANCE_NAME = "envoy.fault"; private static final String ROUTER_FILTER_INSTANCE_NAME = "envoy.router"; + private static final FaultFilter.Provider FAULT_FILTER_PROVIDER = new FaultFilter.Provider(); + private static final RouterFilter.Provider ROUTER_FILTER_PROVIDER = new RouterFilter.Provider(); + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); private final SynchronizationContext syncContext = new SynchronizationContext( @@ -184,9 +189,19 @@ public void setUp() { originalEnableTimeout = XdsNameResolver.enableTimeout; XdsNameResolver.enableTimeout = true; + + // Replace FaultFilter.Provider with the one returning FaultFilter injected with mockRandom. + Filter.Provider faultFilterProvider = + mock(Filter.Provider.class, delegatesTo(FAULT_FILTER_PROVIDER)); + // Lenient: suppress [MockitoHint] Unused warning, only used in resolved_fault* tests. + lenient() + .doReturn(new FaultFilter(mockRandom, new AtomicLong())) + .when(faultFilterProvider).newInstance(); + FilterRegistry filterRegistry = FilterRegistry.newRegistry().register( - new FaultFilter(mockRandom, new AtomicLong()), - RouterFilter.INSTANCE); + ROUTER_FILTER_PROVIDER, + faultFilterProvider); + resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, null, metricRecorder); diff --git a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java index 66ac1475d8e..41f005ba583 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java @@ -31,7 +31,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -53,7 +52,6 @@ import io.grpc.xds.EnvoyServerProtoData.FilterChain; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.Filter.NamedFilterConfig; -import io.grpc.xds.Filter.ServerInterceptorBuilder; import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler.FilterChainSelector; import io.grpc.xds.VirtualHost.Route; import io.grpc.xds.VirtualHost.Route.RouteMatch; @@ -957,9 +955,11 @@ public void run() { new AtomicReference<>(routingConfig)).build()); when(serverCall.getAuthority()).thenReturn("not-match.google.com"); - Filter filter = mock(Filter.class); - when(filter.typeUrls()).thenReturn(new String[]{"filter-type-url"}); - filterRegistry.register(filter); + Filter.Provider filterProvider = mock(Filter.Provider.class); + when(filterProvider.typeUrls()).thenReturn(new String[]{"filter-type-url"}); + when(filterProvider.isServerFilter()).thenReturn(true); + filterRegistry.register(filterProvider); + ServerCallHandler next = mock(ServerCallHandler.class); interceptor.interceptCall(serverCall, new Metadata(), next); verify(next, never()).startCall(any(ServerCall.class), any(Metadata.class)); @@ -998,9 +998,11 @@ public void run() { when(serverCall.getMethodDescriptor()).thenReturn(createMethod("NotMatchMethod")); when(serverCall.getAuthority()).thenReturn("foo.google.com"); - Filter filter = mock(Filter.class); - when(filter.typeUrls()).thenReturn(new String[]{"filter-type-url"}); - filterRegistry.register(filter); + Filter.Provider filterProvider = mock(Filter.Provider.class); + when(filterProvider.typeUrls()).thenReturn(new String[]{"filter-type-url"}); + when(filterProvider.isServerFilter()).thenReturn(true); + filterRegistry.register(filterProvider); + ServerCallHandler next = mock(ServerCallHandler.class); interceptor.interceptCall(serverCall, new Metadata(), next); verify(next, never()).startCall(any(ServerCall.class), any(Metadata.class)); @@ -1044,9 +1046,11 @@ public void run() { when(serverCall.getMethodDescriptor()).thenReturn(createMethod("FooService/barMethod")); when(serverCall.getAuthority()).thenReturn("foo.google.com"); - Filter filter = mock(Filter.class); - when(filter.typeUrls()).thenReturn(new String[]{"filter-type-url"}); - filterRegistry.register(filter); + Filter.Provider filterProvider = mock(Filter.Provider.class); + when(filterProvider.typeUrls()).thenReturn(new String[]{"filter-type-url"}); + when(filterProvider.isServerFilter()).thenReturn(true); + filterRegistry.register(filterProvider); + ServerCallHandler next = mock(ServerCallHandler.class); interceptor.interceptCall(serverCall, new Metadata(), next); verify(next, never()).startCall(any(ServerCall.class), any(Metadata.class)); @@ -1113,10 +1117,14 @@ public void run() { RouteMatch.create( PathMatcher.fromPath("/FooService/barMethod", true), Collections.emptyList(), null); - Filter filter = mock(Filter.class, withSettings() - .extraInterfaces(ServerInterceptorBuilder.class)); - when(filter.typeUrls()).thenReturn(new String[]{"filter-type-url"}); - filterRegistry.register(filter); + + Filter filter = mock(Filter.class); + Filter.Provider filterProvider = mock(Filter.Provider.class); + when(filterProvider.typeUrls()).thenReturn(new String[]{"filter-type-url"}); + when(filterProvider.isServerFilter()).thenReturn(true); + when(filterProvider.newInstance()).thenReturn(filter); + filterRegistry.register(filterProvider); + FilterConfig f0 = mock(FilterConfig.class); FilterConfig f0Override = mock(FilterConfig.class); when(f0.typeUrl()).thenReturn("filter-type-url"); @@ -1137,10 +1145,8 @@ public ServerCall.Listener interceptCall(ServerCallof()); VirtualHost virtualHost = VirtualHost.create( @@ -1185,10 +1191,13 @@ public void run() { }); xdsClient.ldsResource.get(5, TimeUnit.SECONDS); - Filter filter = mock(Filter.class, withSettings() - .extraInterfaces(ServerInterceptorBuilder.class)); - when(filter.typeUrls()).thenReturn(new String[]{"filter-type-url"}); - filterRegistry.register(filter); + Filter filter = mock(Filter.class); + Filter.Provider filterProvider = mock(Filter.Provider.class); + when(filterProvider.typeUrls()).thenReturn(new String[]{"filter-type-url"}); + when(filterProvider.isServerFilter()).thenReturn(true); + when(filterProvider.newInstance()).thenReturn(filter); + filterRegistry.register(filterProvider); + FilterConfig f0 = mock(FilterConfig.class); FilterConfig f0Override = mock(FilterConfig.class); when(f0.typeUrl()).thenReturn("filter-type-url"); @@ -1209,10 +1218,8 @@ public ServerCall.Listener interceptCall(ServerCall Date: Wed, 19 Feb 2025 11:55:54 +0000 Subject: [PATCH 193/591] Upgrade gradle and gradle plugin versions. (#11906) Upgrading to Gradle 8.11. Gradle 8.12 requires newer versions of Windows (gradle/gradle#31939) that we can look into later. --- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72b8b9..94113f200e6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index 1d8600cbe8a..96cc173635d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,7 +25,7 @@ pluginManagement { // https://github.com/tbroyer/gradle-errorprone-plugin/releases id "net.ltgt.errorprone" version "4.1.0" // https://github.com/xvik/gradle-animalsniffer-plugin/releases - id "ru.vyarus.animalsniffer" version "1.7.1" + id "ru.vyarus.animalsniffer" version "2.0.0" } resolutionStrategy { eachPlugin { From 68d79b51307d371eeb76cd45a88558f72a7c7f4d Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:59:50 -0800 Subject: [PATCH 194/591] s2a: Use protos published under com.google.s2a.proto.v2. (#11908) --- MODULE.bazel | 1 + buildscripts/sync-protos.sh | 2 +- gradle/libs.versions.toml | 1 + repositories.bzl | 1 + s2a/BUILD.bazel | 73 +--- s2a/build.gradle | 1 + .../internal/handshaker/S2AServiceGrpc.java | 330 ---------------- .../GetAuthenticationMechanisms.java | 1 + .../s2a/internal/handshaker/ProtoUtil.java | 1 + .../s2a/internal/handshaker/S2AIdentity.java | 1 + .../handshaker/S2APrivateKeyMethod.java | 4 + .../S2AProtocolNegotiatorFactory.java | 1 + .../grpc/s2a/internal/handshaker/S2AStub.java | 3 + .../internal/handshaker/S2ATrustManager.java | 6 +- .../handshaker/SslContextFactory.java | 6 + s2a/src/main/proto/grpc/gcp/s2a/common.proto | 82 ---- s2a/src/main/proto/grpc/gcp/s2a/s2a.proto | 369 ------------------ .../main/proto/grpc/gcp/s2a/s2a_context.proto | 62 --- .../java/io/grpc/s2a/IntegrationTest.java | 2 +- .../internal/handshaker/FakeS2AServer.java | 3 + .../handshaker/FakeS2AServerTest.java | 12 +- .../s2a/internal/handshaker/FakeWriter.java | 16 +- .../GetAuthenticationMechanismsTest.java | 1 + .../internal/handshaker/ProtoUtilTest.java | 1 + .../handshaker/S2APrivateKeyMethodTest.java | 5 + .../S2AProtocolNegotiatorFactoryTest.java | 3 + .../s2a/internal/handshaker/S2AStubTest.java | 9 + 27 files changed, 78 insertions(+), 919 deletions(-) delete mode 100644 s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java delete mode 100644 s2a/src/main/proto/grpc/gcp/s2a/common.proto delete mode 100644 s2a/src/main/proto/grpc/gcp/s2a/s2a.proto delete mode 100644 s2a/src/main/proto/grpc/gcp/s2a/s2a_context.proto diff --git a/MODULE.bazel b/MODULE.bazel index 666fda73202..88f3a524060 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -19,6 +19,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:33.3.1-android", "com.google.re2j:re2j:1.8", + "com.google.s2a.proto.v2:s2a-proto:0.1.1", "com.google.truth:truth:1.4.2", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day diff --git a/buildscripts/sync-protos.sh b/buildscripts/sync-protos.sh index 628b1688d4c..5f01be2e5c9 100755 --- a/buildscripts/sync-protos.sh +++ b/buildscripts/sync-protos.sh @@ -8,7 +8,7 @@ curl -Ls https://github.com/grpc/grpc-proto/archive/master.tar.gz | tar xz -C "$ base="$tmpdir/grpc-proto-master" # Copy protos in 'src/main/proto' from grpc-proto for these projects -for project in alts grpclb services s2a rls interop-testing; do +for project in alts grpclb services rls interop-testing; do while read -r proto; do [ -f "$base/$proto" ] && cp "$base/$proto" "$project/src/main/proto/$proto" echo "$proto" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b6b2e5e0e45..2ea4c8b5fa1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -103,6 +103,7 @@ protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version. protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } re2j = "com.google.re2j:re2j:1.8" robolectric = "org.robolectric:robolectric:4.14.1" +s2a-proto = "com.google.s2a.proto.v2:s2a-proto:0.1.1" signature-android = "net.sf.androidscents.signature:android-api-level-21:5.0.1_r2" signature-java = "org.codehaus.mojo.signature:java18:1.0" # 11.0.0+ require Java 17+ diff --git a/repositories.bzl b/repositories.bzl index a4f5b0de1c6..d55ff07e7e6 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -23,6 +23,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:33.3.1-android", "com.google.re2j:re2j:1.8", + "com.google.s2a.proto.v2:s2a-proto:0.1.1", "com.google.truth:truth:1.4.2", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day diff --git a/s2a/BUILD.bazel b/s2a/BUILD.bazel index f8fb7f1df5e..807103bde4e 100644 --- a/s2a/BUILD.bazel +++ b/s2a/BUILD.bazel @@ -1,5 +1,3 @@ -load("@rules_proto//proto:defs.bzl", "proto_library") -load("//:java_grpc_library.bzl", "java_grpc_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( @@ -25,9 +23,9 @@ java_library( name = "s2a_identity", srcs = ["src/main/java/io/grpc/s2a/internal/handshaker/S2AIdentity.java"], deps = [ - ":common_java_proto", artifact("com.google.errorprone:error_prone_annotations"), artifact("com.google.guava:guava"), + artifact("com.google.s2a.proto.v2:s2a-proto"), ], ) @@ -58,11 +56,8 @@ java_library( ], deps = [ ":token_manager", - ":common_java_proto", ":s2a_channel_pool", ":s2a_identity", - ":s2a_java_proto", - ":s2a_java_grpc_proto", "//api", "//core:internal", "//netty", @@ -70,6 +65,7 @@ java_library( artifact("com.google.code.findbugs:jsr305"), artifact("com.google.errorprone:error_prone_annotations"), artifact("com.google.guava:guava"), + artifact("com.google.s2a.proto.v2:s2a-proto"), artifact("org.checkerframework:checker-qual"), "@com_google_protobuf//:protobuf_java", artifact("io.netty:netty-common"), @@ -94,67 +90,4 @@ java_library( artifact("com.google.guava:guava"), artifact("org.checkerframework:checker-qual"), ], -) - -# bazel only accepts proto import with absolute path. -genrule( - name = "protobuf_imports", - srcs = glob(["src/main/proto/grpc/gcp/s2a/*.proto"]), - outs = [ - "protobuf_out/grpc/gcp/s2a/s2a.proto", - "protobuf_out/grpc/gcp/s2a/s2a_context.proto", - "protobuf_out/grpc/gcp/s2a/common.proto", - ], - cmd = "for fname in $(SRCS); do " + - "sed 's,import \",import \"s2a/protobuf_out/,g' $$fname > " + - "$(@D)/protobuf_out/grpc/gcp/s2a/$$(basename $$fname); done", -) - -proto_library( - name = "common_proto", - srcs = [ - "protobuf_out/grpc/gcp/s2a/common.proto", - ], -) - -proto_library( - name = "s2a_context_proto", - srcs = [ - "protobuf_out/grpc/gcp/s2a/s2a_context.proto", - ], - deps = [ - ":common_proto", - ], -) - -proto_library( - name = "s2a_proto", - srcs = [ - "protobuf_out/grpc/gcp/s2a/s2a.proto", - ], - deps = [ - ":common_proto", - ":s2a_context_proto", - ], -) - -java_proto_library( - name = "s2a_java_proto", - deps = [":s2a_proto"], -) - -java_proto_library( - name = "s2a_context_java_proto", - deps = [":s2a_context_proto"], -) - -java_proto_library( - name = "common_java_proto", - deps = [":common_proto"], -) - -java_grpc_library( - name = "s2a_java_grpc_proto", - srcs = [":s2a_proto"], - deps = [":s2a_java_proto"], -) +) \ No newline at end of file diff --git a/s2a/build.gradle b/s2a/build.gradle index 6ac193938ca..1e48e2bb297 100644 --- a/s2a/build.gradle +++ b/s2a/build.gradle @@ -11,6 +11,7 @@ plugins { description = "gRPC: S2A" dependencies { + implementation libraries.s2a.proto api project(':grpc-api') implementation project(':grpc-stub'), diff --git a/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java b/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java deleted file mode 100644 index 95e217ac695..00000000000 --- a/s2a/src/generated/main/grpc/io/grpc/s2a/internal/handshaker/S2AServiceGrpc.java +++ /dev/null @@ -1,330 +0,0 @@ -package io.grpc.s2a.internal.handshaker; - -import static io.grpc.MethodDescriptor.generateFullMethodName; - -/** - */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/gcp/s2a/s2a.proto") -@io.grpc.stub.annotations.GrpcGenerated -public final class S2AServiceGrpc { - - private S2AServiceGrpc() {} - - public static final java.lang.String SERVICE_NAME = "grpc.gcp.s2a.S2AService"; - - // Static method descriptors that strictly reflect the proto. - private static volatile io.grpc.MethodDescriptor getSetUpSessionMethod; - - @io.grpc.stub.annotations.RpcMethod( - fullMethodName = SERVICE_NAME + '/' + "SetUpSession", - requestType = io.grpc.s2a.internal.handshaker.SessionReq.class, - responseType = io.grpc.s2a.internal.handshaker.SessionResp.class, - methodType = io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) - public static io.grpc.MethodDescriptor getSetUpSessionMethod() { - io.grpc.MethodDescriptor getSetUpSessionMethod; - if ((getSetUpSessionMethod = S2AServiceGrpc.getSetUpSessionMethod) == null) { - synchronized (S2AServiceGrpc.class) { - if ((getSetUpSessionMethod = S2AServiceGrpc.getSetUpSessionMethod) == null) { - S2AServiceGrpc.getSetUpSessionMethod = getSetUpSessionMethod = - io.grpc.MethodDescriptor.newBuilder() - .setType(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) - .setFullMethodName(generateFullMethodName(SERVICE_NAME, "SetUpSession")) - .setSampledToLocalTracing(true) - .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( - io.grpc.s2a.internal.handshaker.SessionReq.getDefaultInstance())) - .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( - io.grpc.s2a.internal.handshaker.SessionResp.getDefaultInstance())) - .setSchemaDescriptor(new S2AServiceMethodDescriptorSupplier("SetUpSession")) - .build(); - } - } - } - return getSetUpSessionMethod; - } - - /** - * Creates a new async stub that supports all call types for the service - */ - public static S2AServiceStub newStub(io.grpc.Channel channel) { - io.grpc.stub.AbstractStub.StubFactory factory = - new io.grpc.stub.AbstractStub.StubFactory() { - @java.lang.Override - public S2AServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { - return new S2AServiceStub(channel, callOptions); - } - }; - return S2AServiceStub.newStub(factory, channel); - } - - /** - * Creates a new blocking-style stub that supports all types of calls on the service - */ - public static S2AServiceBlockingV2Stub newBlockingV2Stub( - io.grpc.Channel channel) { - io.grpc.stub.AbstractStub.StubFactory factory = - new io.grpc.stub.AbstractStub.StubFactory() { - @java.lang.Override - public S2AServiceBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { - return new S2AServiceBlockingV2Stub(channel, callOptions); - } - }; - return S2AServiceBlockingV2Stub.newStub(factory, channel); - } - - /** - * Creates a new blocking-style stub that supports unary and streaming output calls on the service - */ - public static S2AServiceBlockingStub newBlockingStub( - io.grpc.Channel channel) { - io.grpc.stub.AbstractStub.StubFactory factory = - new io.grpc.stub.AbstractStub.StubFactory() { - @java.lang.Override - public S2AServiceBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { - return new S2AServiceBlockingStub(channel, callOptions); - } - }; - return S2AServiceBlockingStub.newStub(factory, channel); - } - - /** - * Creates a new ListenableFuture-style stub that supports unary calls on the service - */ - public static S2AServiceFutureStub newFutureStub( - io.grpc.Channel channel) { - io.grpc.stub.AbstractStub.StubFactory factory = - new io.grpc.stub.AbstractStub.StubFactory() { - @java.lang.Override - public S2AServiceFutureStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { - return new S2AServiceFutureStub(channel, callOptions); - } - }; - return S2AServiceFutureStub.newStub(factory, channel); - } - - /** - */ - public interface AsyncService { - - /** - *

-     * SetUpSession is a bidirectional stream used by applications to offload
-     * operations from the TLS handshake.
-     * 
- */ - default io.grpc.stub.StreamObserver setUpSession( - io.grpc.stub.StreamObserver responseObserver) { - return io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall(getSetUpSessionMethod(), responseObserver); - } - } - - /** - * Base class for the server implementation of the service S2AService. - */ - public static abstract class S2AServiceImplBase - implements io.grpc.BindableService, AsyncService { - - @java.lang.Override public final io.grpc.ServerServiceDefinition bindService() { - return S2AServiceGrpc.bindService(this); - } - } - - /** - * A stub to allow clients to do asynchronous rpc calls to service S2AService. - */ - public static final class S2AServiceStub - extends io.grpc.stub.AbstractAsyncStub { - private S2AServiceStub( - io.grpc.Channel channel, io.grpc.CallOptions callOptions) { - super(channel, callOptions); - } - - @java.lang.Override - protected S2AServiceStub build( - io.grpc.Channel channel, io.grpc.CallOptions callOptions) { - return new S2AServiceStub(channel, callOptions); - } - - /** - *
-     * SetUpSession is a bidirectional stream used by applications to offload
-     * operations from the TLS handshake.
-     * 
- */ - public io.grpc.stub.StreamObserver setUpSession( - io.grpc.stub.StreamObserver responseObserver) { - return io.grpc.stub.ClientCalls.asyncBidiStreamingCall( - getChannel().newCall(getSetUpSessionMethod(), getCallOptions()), responseObserver); - } - } - - /** - * A stub to allow clients to do synchronous rpc calls to service S2AService. - */ - public static final class S2AServiceBlockingV2Stub - extends io.grpc.stub.AbstractBlockingStub { - private S2AServiceBlockingV2Stub( - io.grpc.Channel channel, io.grpc.CallOptions callOptions) { - super(channel, callOptions); - } - - @java.lang.Override - protected S2AServiceBlockingV2Stub build( - io.grpc.Channel channel, io.grpc.CallOptions callOptions) { - return new S2AServiceBlockingV2Stub(channel, callOptions); - } - - /** - *
-     * SetUpSession is a bidirectional stream used by applications to offload
-     * operations from the TLS handshake.
-     * 
- */ - @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") - public io.grpc.stub.BlockingClientCall - setUpSession() { - return io.grpc.stub.ClientCalls.blockingBidiStreamingCall( - getChannel(), getSetUpSessionMethod(), getCallOptions()); - } - } - - /** - * A stub to allow clients to do limited synchronous rpc calls to service S2AService. - */ - public static final class S2AServiceBlockingStub - extends io.grpc.stub.AbstractBlockingStub { - private S2AServiceBlockingStub( - io.grpc.Channel channel, io.grpc.CallOptions callOptions) { - super(channel, callOptions); - } - - @java.lang.Override - protected S2AServiceBlockingStub build( - io.grpc.Channel channel, io.grpc.CallOptions callOptions) { - return new S2AServiceBlockingStub(channel, callOptions); - } - } - - /** - * A stub to allow clients to do ListenableFuture-style rpc calls to service S2AService. - */ - public static final class S2AServiceFutureStub - extends io.grpc.stub.AbstractFutureStub { - private S2AServiceFutureStub( - io.grpc.Channel channel, io.grpc.CallOptions callOptions) { - super(channel, callOptions); - } - - @java.lang.Override - protected S2AServiceFutureStub build( - io.grpc.Channel channel, io.grpc.CallOptions callOptions) { - return new S2AServiceFutureStub(channel, callOptions); - } - } - - private static final int METHODID_SET_UP_SESSION = 0; - - 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 AsyncService serviceImpl; - private final int methodId; - - MethodHandlers(AsyncService 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_SET_UP_SESSION: - return (io.grpc.stub.StreamObserver) serviceImpl.setUpSession( - (io.grpc.stub.StreamObserver) responseObserver); - default: - throw new AssertionError(); - } - } - } - - public static final io.grpc.ServerServiceDefinition bindService(AsyncService service) { - return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor()) - .addMethod( - getSetUpSessionMethod(), - io.grpc.stub.ServerCalls.asyncBidiStreamingCall( - new MethodHandlers< - io.grpc.s2a.internal.handshaker.SessionReq, - io.grpc.s2a.internal.handshaker.SessionResp>( - service, METHODID_SET_UP_SESSION))) - .build(); - } - - private static abstract class S2AServiceBaseDescriptorSupplier - implements io.grpc.protobuf.ProtoFileDescriptorSupplier, io.grpc.protobuf.ProtoServiceDescriptorSupplier { - S2AServiceBaseDescriptorSupplier() {} - - @java.lang.Override - public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() { - return io.grpc.s2a.internal.handshaker.S2AProto.getDescriptor(); - } - - @java.lang.Override - public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() { - return getFileDescriptor().findServiceByName("S2AService"); - } - } - - private static final class S2AServiceFileDescriptorSupplier - extends S2AServiceBaseDescriptorSupplier { - S2AServiceFileDescriptorSupplier() {} - } - - private static final class S2AServiceMethodDescriptorSupplier - extends S2AServiceBaseDescriptorSupplier - implements io.grpc.protobuf.ProtoMethodDescriptorSupplier { - private final java.lang.String methodName; - - S2AServiceMethodDescriptorSupplier(java.lang.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 (S2AServiceGrpc.class) { - result = serviceDescriptor; - if (result == null) { - serviceDescriptor = result = io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME) - .setSchemaDescriptor(new S2AServiceFileDescriptorSupplier()) - .addMethod(getSetUpSessionMethod()) - .build(); - } - } - } - return result; - } -} diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanisms.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanisms.java index 88dfd62675f..cf632418e66 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanisms.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanisms.java @@ -17,6 +17,7 @@ package io.grpc.s2a.internal.handshaker; import com.google.errorprone.annotations.Immutable; +import com.google.s2a.proto.v2.AuthenticationMechanism; import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.grpc.s2a.internal.handshaker.tokenmanager.AccessTokenManager; import java.util.Optional; diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/ProtoUtil.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/ProtoUtil.java index 1f24727a083..0526ec154f9 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/ProtoUtil.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/ProtoUtil.java @@ -18,6 +18,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; +import com.google.s2a.proto.v2.TLSVersion; /** Converts proto messages to Netty strings. */ final class ProtoUtil { diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AIdentity.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AIdentity.java index 0b691248e91..f4d6b88ce45 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AIdentity.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AIdentity.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.errorprone.annotations.ThreadSafe; +import com.google.s2a.proto.v2.Identity; /** * Stores an identity in such a way that it can be sent to the S2A handshaker service. The identity diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethod.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethod.java index c7262f70ef7..1a5c37eb989 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethod.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethod.java @@ -22,6 +22,10 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.protobuf.ByteString; +import com.google.s2a.proto.v2.OffloadPrivateKeyOperationReq; +import com.google.s2a.proto.v2.SessionReq; +import com.google.s2a.proto.v2.SessionResp; +import com.google.s2a.proto.v2.SignatureAlgorithm; import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.netty.handler.ssl.OpenSslPrivateKeyMethod; import java.io.IOException; diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java index 0822399aad6..03976cc7d7b 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java @@ -28,6 +28,7 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.errorprone.annotations.ThreadSafe; +import com.google.s2a.proto.v2.S2AServiceGrpc; import io.grpc.Channel; import io.grpc.internal.GrpcUtil; import io.grpc.internal.ObjectPool; diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java index 956ec485229..fe2ec388fe4 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AStub.java @@ -22,6 +22,9 @@ import static java.util.concurrent.TimeUnit.SECONDS; import com.google.common.annotations.VisibleForTesting; +import com.google.s2a.proto.v2.S2AServiceGrpc; +import com.google.s2a.proto.v2.SessionReq; +import com.google.s2a.proto.v2.SessionResp; import io.grpc.stub.StreamObserver; import java.io.IOException; import java.util.Optional; diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java index 406545b30bf..a7ffafd01f2 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2ATrustManager.java @@ -21,8 +21,12 @@ import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; +import com.google.s2a.proto.v2.SessionReq; +import com.google.s2a.proto.v2.SessionResp; +import com.google.s2a.proto.v2.ValidatePeerCertificateChainReq; +import com.google.s2a.proto.v2.ValidatePeerCertificateChainReq.VerificationMode; +import com.google.s2a.proto.v2.ValidatePeerCertificateChainResp; import io.grpc.s2a.internal.handshaker.S2AIdentity; -import io.grpc.s2a.internal.handshaker.ValidatePeerCertificateChainReq.VerificationMode; import java.io.IOException; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java index 153f4de6918..2dfde16cf2f 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java @@ -20,6 +20,12 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.ImmutableSet; +import com.google.s2a.proto.v2.AuthenticationMechanism; +import com.google.s2a.proto.v2.ConnectionSide; +import com.google.s2a.proto.v2.GetTlsConfigurationReq; +import com.google.s2a.proto.v2.GetTlsConfigurationResp; +import com.google.s2a.proto.v2.SessionReq; +import com.google.s2a.proto.v2.SessionResp; import io.grpc.netty.GrpcSslContexts; import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.netty.handler.ssl.OpenSslContextOption; diff --git a/s2a/src/main/proto/grpc/gcp/s2a/common.proto b/s2a/src/main/proto/grpc/gcp/s2a/common.proto deleted file mode 100644 index 1b999234669..00000000000 --- a/s2a/src/main/proto/grpc/gcp/s2a/common.proto +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2024 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// The canonical version of this proto can be found at -// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/s2a/common.proto - -syntax = "proto3"; - -package grpc.gcp.s2a; - -option java_multiple_files = true; -option java_outer_classname = "CommonProto"; -option java_package = "io.grpc.s2a.internal.handshaker"; - -// The TLS 1.0-1.2 ciphersuites that the application can negotiate when using -// S2A. -enum Ciphersuite { - CIPHERSUITE_UNSPECIFIED = 0; - CIPHERSUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 1; - CIPHERSUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 2; - CIPHERSUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 3; - CIPHERSUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 4; - CIPHERSUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 5; - CIPHERSUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 6; -} - -// The TLS versions supported by S2A's handshaker module. -enum TLSVersion { - TLS_VERSION_UNSPECIFIED = 0; - TLS_VERSION_1_0 = 1; - TLS_VERSION_1_1 = 2; - TLS_VERSION_1_2 = 3; - TLS_VERSION_1_3 = 4; -} - -// The side in the TLS connection. -enum ConnectionSide { - CONNECTION_SIDE_UNSPECIFIED = 0; - CONNECTION_SIDE_CLIENT = 1; - CONNECTION_SIDE_SERVER = 2; -} - -// The ALPN protocols that the application can negotiate during a TLS handshake. -enum AlpnProtocol { - ALPN_PROTOCOL_UNSPECIFIED = 0; - ALPN_PROTOCOL_GRPC = 1; - ALPN_PROTOCOL_HTTP2 = 2; - ALPN_PROTOCOL_HTTP1_1 = 3; -} - -message Identity { - oneof identity_oneof { - // The SPIFFE ID of a connection endpoint. - string spiffe_id = 1; - - // The hostname of a connection endpoint. - string hostname = 2; - - // The UID of a connection endpoint. - string uid = 4; - - // The username of a connection endpoint. - string username = 5; - - // The GCP ID of a connection endpoint. - string gcp_id = 6; - } - - // Additional identity-specific attributes. - map attributes = 3; -} diff --git a/s2a/src/main/proto/grpc/gcp/s2a/s2a.proto b/s2a/src/main/proto/grpc/gcp/s2a/s2a.proto deleted file mode 100644 index b3f153943db..00000000000 --- a/s2a/src/main/proto/grpc/gcp/s2a/s2a.proto +++ /dev/null @@ -1,369 +0,0 @@ -// Copyright 2024 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// The canonical version of this proto can be found at -// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/s2a/s2a.proto - -syntax = "proto3"; - -package grpc.gcp.s2a; - -import "grpc/gcp/s2a/common.proto"; -import "grpc/gcp/s2a/s2a_context.proto"; - -option java_multiple_files = true; -option java_outer_classname = "S2AProto"; -option java_package = "io.grpc.s2a.internal.handshaker"; - -enum SignatureAlgorithm { - S2A_SSL_SIGN_UNSPECIFIED = 0; - // RSA Public-Key Cryptography Standards #1. - S2A_SSL_SIGN_RSA_PKCS1_SHA256 = 1; - S2A_SSL_SIGN_RSA_PKCS1_SHA384 = 2; - S2A_SSL_SIGN_RSA_PKCS1_SHA512 = 3; - // ECDSA. - S2A_SSL_SIGN_ECDSA_SECP256R1_SHA256 = 4; - S2A_SSL_SIGN_ECDSA_SECP384R1_SHA384 = 5; - S2A_SSL_SIGN_ECDSA_SECP521R1_SHA512 = 6; - // RSA Probabilistic Signature Scheme. - S2A_SSL_SIGN_RSA_PSS_RSAE_SHA256 = 7; - S2A_SSL_SIGN_RSA_PSS_RSAE_SHA384 = 8; - S2A_SSL_SIGN_RSA_PSS_RSAE_SHA512 = 9; - // ED25519. - S2A_SSL_SIGN_ED25519 = 10; -} - -message AlpnPolicy { - // If true, the application MUST perform ALPN negotiation. - bool enable_alpn_negotiation = 1; - - // The ordered list of ALPN protocols that specify how the application SHOULD - // negotiate ALPN during the TLS handshake. - // - // The application MAY ignore any ALPN protocols in this list that are not - // supported by the application. - repeated AlpnProtocol alpn_protocols = 2; -} - -message AuthenticationMechanism { - // Applications may specify an identity associated to an authentication - // mechanism. Otherwise, S2A assumes that the authentication mechanism is - // associated with the default identity. If the default identity cannot be - // determined, the request is rejected. - Identity identity = 1; - - oneof mechanism_oneof { - // A token that the application uses to authenticate itself to S2A. - string token = 2; - } -} - -message Status { - // The status code that is specific to the application and the implementation - // of S2A, e.g., gRPC status code. - uint32 code = 1; - - // The status details. - string details = 2; -} - -message GetTlsConfigurationReq { - // The role of the application in the TLS connection. - ConnectionSide connection_side = 1; - - // The server name indication (SNI) extension, which MAY be populated when a - // server is offloading to S2A. The SNI is used to determine the server - // identity if the local identity in the request is empty. - string sni = 2; -} - -message GetTlsConfigurationResp { - // Next ID: 8 - message ClientTlsConfiguration { - reserved 4, 5; - - // The certificate chain that the client MUST use for the TLS handshake. - // It's a list of PEM-encoded certificates, ordered from leaf to root, - // excluding the root. - repeated string certificate_chain = 1; - - // The minimum TLS version number that the client MUST use for the TLS - // handshake. If this field is not provided, the client MUST use the default - // minimum version of the client's TLS library. - TLSVersion min_tls_version = 2; - - // The maximum TLS version number that the client MUST use for the TLS - // handshake. If this field is not provided, the client MUST use the default - // maximum version of the client's TLS library. - TLSVersion max_tls_version = 3; - - // The ordered list of TLS 1.0-1.2 ciphersuites that the client MAY offer to - // negotiate in the TLS handshake. - repeated Ciphersuite ciphersuites = 6; - - // The policy that dictates how the client negotiates ALPN during the TLS - // handshake. - AlpnPolicy alpn_policy = 7; - } - - // Next ID: 12 - message ServerTlsConfiguration { - reserved 4, 5; - - enum RequestClientCertificate { - UNSPECIFIED = 0; - DONT_REQUEST_CLIENT_CERTIFICATE = 1; - REQUEST_CLIENT_CERTIFICATE_BUT_DONT_VERIFY = 2; - REQUEST_CLIENT_CERTIFICATE_AND_VERIFY = 3; - REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_BUT_DONT_VERIFY = 4; - REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY = 5; - } - - // The certificate chain that the server MUST use for the TLS handshake. - // It's a list of PEM-encoded certificates, ordered from leaf to root, - // excluding the root. - repeated string certificate_chain = 1; - - // The minimum TLS version number that the server MUST use for the TLS - // handshake. If this field is not provided, the server MUST use the default - // minimum version of the server's TLS library. - TLSVersion min_tls_version = 2; - - // The maximum TLS version number that the server MUST use for the TLS - // handshake. If this field is not provided, the server MUST use the default - // maximum version of the server's TLS library. - TLSVersion max_tls_version = 3; - - // The ordered list of TLS 1.0-1.2 ciphersuites that the server MAY offer to - // negotiate in the TLS handshake. - repeated Ciphersuite ciphersuites = 10; - - // Whether to enable TLS resumption. - bool tls_resumption_enabled = 6; - - // Whether the server MUST request a client certificate (i.e. to negotiate - // TLS vs. mTLS). - RequestClientCertificate request_client_certificate = 7; - - // Returns the maximum number of extra bytes that - // |OffloadResumptionKeyOperation| can add to the number of unencrypted - // bytes to form the encrypted bytes. - uint32 max_overhead_of_ticket_aead = 9; - - // The policy that dictates how the server negotiates ALPN during the TLS - // handshake. - AlpnPolicy alpn_policy = 11; - } - - oneof tls_configuration { - ClientTlsConfiguration client_tls_configuration = 1; - ServerTlsConfiguration server_tls_configuration = 2; - } -} - -message OffloadPrivateKeyOperationReq { - enum PrivateKeyOperation { - UNSPECIFIED = 0; - // When performing a TLS 1.2 or 1.3 handshake, the (partial) transcript of - // the TLS handshake must be signed to prove possession of the private key. - // - // See https://www.rfc-editor.org/rfc/rfc8446.html#section-4.4.3. - SIGN = 1; - // When performing a TLS 1.2 handshake using an RSA algorithm, the key - // exchange algorithm involves the client generating a premaster secret, - // encrypting it using the server's public key, and sending this encrypted - // blob to the server in a ClientKeyExchange message. - // - // See https://www.rfc-editor.org/rfc/rfc4346#section-7.4.7.1. - DECRYPT = 2; - } - - // The operation the private key is used for. - PrivateKeyOperation operation = 1; - - // The signature algorithm to be used for signing operations. - SignatureAlgorithm signature_algorithm = 2; - - // The input bytes to be signed or decrypted. - oneof in_bytes { - // Raw bytes to be hashed and signed, or decrypted. - bytes raw_bytes = 4; - // A SHA256 hash to be signed. Must be 32 bytes. - bytes sha256_digest = 5; - // A SHA384 hash to be signed. Must be 48 bytes. - bytes sha384_digest = 6; - // A SHA512 hash to be signed. Must be 64 bytes. - bytes sha512_digest = 7; - } -} - -message OffloadPrivateKeyOperationResp { - // The signed or decrypted output bytes. - bytes out_bytes = 1; -} - -message OffloadResumptionKeyOperationReq { - enum ResumptionKeyOperation { - UNSPECIFIED = 0; - ENCRYPT = 1; - DECRYPT = 2; - } - - // The operation the resumption key is used for. - ResumptionKeyOperation operation = 1; - - // The bytes to be encrypted or decrypted. - bytes in_bytes = 2; -} - -message OffloadResumptionKeyOperationResp { - // The encrypted or decrypted bytes. - bytes out_bytes = 1; -} - -message ValidatePeerCertificateChainReq { - enum VerificationMode { - // The default verification mode supported by S2A. - UNSPECIFIED = 0; - // The SPIFFE verification mode selects the set of trusted certificates to - // use for path building based on the SPIFFE trust domain in the peer's leaf - // certificate. - SPIFFE = 1; - // The connect-to-Google verification mode uses the trust bundle for - // connecting to Google, e.g. *.mtls.googleapis.com endpoints. - CONNECT_TO_GOOGLE = 2; - } - - message ClientPeer { - // The certificate chain to be verified. The chain MUST be a list of - // DER-encoded certificates, ordered from leaf to root, excluding the root. - repeated bytes certificate_chain = 1; - } - - message ServerPeer { - // The certificate chain to be verified. The chain MUST be a list of - // DER-encoded certificates, ordered from leaf to root, excluding the root. - repeated bytes certificate_chain = 1; - - // The expected hostname of the server. - string server_hostname = 2; - - // The UnrestrictedClientPolicy specified by the user. - bytes serialized_unrestricted_client_policy = 3; - } - - // The verification mode that S2A MUST use to validate the peer certificate - // chain. - VerificationMode mode = 1; - - oneof peer_oneof { - ClientPeer client_peer = 2; - ServerPeer server_peer = 3; - } -} - -message ValidatePeerCertificateChainResp { - enum ValidationResult { - UNSPECIFIED = 0; - SUCCESS = 1; - FAILURE = 2; - } - - // The result of validating the peer certificate chain. - ValidationResult validation_result = 1; - - // The validation details. This field is only populated when the validation - // result is NOT SUCCESS. - string validation_details = 2; - - // The S2A context contains information from the peer certificate chain. - // - // The S2A context MAY be populated even if validation of the peer certificate - // chain fails. - S2AContext context = 3; -} - -message SessionReq { - // The identity corresponding to the TLS configurations that MUST be used for - // the TLS handshake. - // - // If a managed identity already exists, the local identity and authentication - // mechanisms are ignored. If a managed identity doesn't exist and the local - // identity is not populated, S2A will try to deduce the managed identity to - // use from the SNI extension. If that also fails, S2A uses the default - // identity (if one exists). - Identity local_identity = 1; - - // The authentication mechanisms that the application wishes to use to - // authenticate to S2A, ordered by preference. S2A will always use the first - // authentication mechanism that matches the managed identity. - repeated AuthenticationMechanism authentication_mechanisms = 2; - - oneof req_oneof { - // Requests the certificate chain and TLS configuration corresponding to the - // local identity, which the application MUST use to negotiate the TLS - // handshake. - GetTlsConfigurationReq get_tls_configuration_req = 3; - - // Signs or decrypts the input bytes using a private key corresponding to - // the local identity in the request. - // - // WARNING: More than one OffloadPrivateKeyOperationReq may be sent to the - // S2Av2 by a server during a TLS 1.2 handshake. - OffloadPrivateKeyOperationReq offload_private_key_operation_req = 4; - - // Encrypts or decrypts the input bytes using a resumption key corresponding - // to the local identity in the request. - OffloadResumptionKeyOperationReq offload_resumption_key_operation_req = 5; - - // Verifies the peer's certificate chain using - // (a) trust bundles corresponding to the local identity in the request, and - // (b) the verification mode in the request. - ValidatePeerCertificateChainReq validate_peer_certificate_chain_req = 6; - } -} - -message SessionResp { - // Status of the session response. - // - // The status field is populated so that if an error occurs when making an - // individual request, then communication with the S2A may continue. If an - // error is returned directly (e.g. at the gRPC layer), then it may result - // that the bidirectional stream being closed. - Status status = 1; - - oneof resp_oneof { - // Contains the certificate chain and TLS configurations corresponding to - // the local identity. - GetTlsConfigurationResp get_tls_configuration_resp = 2; - - // Contains the signed or encrypted output bytes using the private key - // corresponding to the local identity. - OffloadPrivateKeyOperationResp offload_private_key_operation_resp = 3; - - // Contains the encrypted or decrypted output bytes using the resumption key - // corresponding to the local identity. - OffloadResumptionKeyOperationResp offload_resumption_key_operation_resp = 4; - - // Contains the validation result, peer identity and fingerprints of peer - // certificates. - ValidatePeerCertificateChainResp validate_peer_certificate_chain_resp = 5; - } -} - -service S2AService { - // SetUpSession is a bidirectional stream used by applications to offload - // operations from the TLS handshake. - rpc SetUpSession(stream SessionReq) returns (stream SessionResp) {} -} diff --git a/s2a/src/main/proto/grpc/gcp/s2a/s2a_context.proto b/s2a/src/main/proto/grpc/gcp/s2a/s2a_context.proto deleted file mode 100644 index 745b4d267d6..00000000000 --- a/s2a/src/main/proto/grpc/gcp/s2a/s2a_context.proto +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2024 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// The canonical version of this proto can be found at -// https://github.com/grpc/grpc-proto/blob/master/grpc/gcp/s2a/s2a_context.proto - -syntax = "proto3"; - -package grpc.gcp.s2a; - -import "grpc/gcp/s2a/common.proto"; - -option java_multiple_files = true; -option java_outer_classname = "S2AContextProto"; -option java_package = "io.grpc.s2a.internal.handshaker"; - -message S2AContext { - // The SPIFFE ID from the peer leaf certificate, if present. - // - // This field is only populated if the leaf certificate is a valid SPIFFE - // SVID; in particular, there is a unique URI SAN and this URI SAN is a valid - // SPIFFE ID. - string leaf_cert_spiffe_id = 1; - - // The URIs that are present in the SubjectAltName extension of the peer leaf - // certificate. - // - // Note that the extracted URIs are not validated and may not be properly - // formatted. - repeated string leaf_cert_uris = 2; - - // The DNSNames that are present in the SubjectAltName extension of the peer - // leaf certificate. - repeated string leaf_cert_dnsnames = 3; - - // The (ordered) list of fingerprints in the certificate chain used to verify - // the given leaf certificate. The order MUST be from leaf certificate - // fingerprint to root certificate fingerprint. - // - // A fingerprint is the base-64 encoding of the SHA256 hash of the - // DER-encoding of a certificate. The list MAY be populated even if the peer - // certificate chain was NOT validated successfully. - repeated string peer_certificate_chain_fingerprints = 4; - - // The local identity used during session setup. - Identity local_identity = 5; - - // The SHA256 hash of the DER-encoding of the local leaf certificate used in - // the handshake. - bytes local_leaf_cert_fingerprint = 6; -} diff --git a/s2a/src/test/java/io/grpc/s2a/IntegrationTest.java b/s2a/src/test/java/io/grpc/s2a/IntegrationTest.java index d8d2fdd4d03..1d3568808c6 100644 --- a/s2a/src/test/java/io/grpc/s2a/IntegrationTest.java +++ b/s2a/src/test/java/io/grpc/s2a/IntegrationTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; +import com.google.s2a.proto.v2.S2AServiceGrpc; import io.grpc.Channel; import io.grpc.ChannelCredentials; import io.grpc.Grpc; @@ -37,7 +38,6 @@ import io.grpc.s2a.S2AChannelCredentials; import io.grpc.s2a.internal.channel.S2AHandshakerServiceChannel; import io.grpc.s2a.internal.handshaker.FakeS2AServer; -import io.grpc.s2a.internal.handshaker.S2AServiceGrpc; import io.grpc.s2a.internal.handshaker.S2AStub; import io.grpc.stub.StreamObserver; import io.grpc.testing.protobuf.SimpleRequest; diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServer.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServer.java index 2d19dd122ec..322397c93be 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServer.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServer.java @@ -16,6 +16,9 @@ package io.grpc.s2a.internal.handshaker; +import com.google.s2a.proto.v2.S2AServiceGrpc; +import com.google.s2a.proto.v2.SessionReq; +import com.google.s2a.proto.v2.SessionResp; import io.grpc.stub.StreamObserver; import java.io.IOException; import java.security.NoSuchAlgorithmException; diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java index fc3bbba9e39..d8374a8a382 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java @@ -22,12 +22,22 @@ import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.SettableFuture; import com.google.protobuf.ByteString; +import com.google.s2a.proto.v2.Ciphersuite; +import com.google.s2a.proto.v2.ConnectionSide; +import com.google.s2a.proto.v2.GetTlsConfigurationReq; +import com.google.s2a.proto.v2.GetTlsConfigurationResp; +import com.google.s2a.proto.v2.S2AServiceGrpc; +import com.google.s2a.proto.v2.SessionReq; +import com.google.s2a.proto.v2.SessionResp; +import com.google.s2a.proto.v2.TLSVersion; +import com.google.s2a.proto.v2.ValidatePeerCertificateChainReq; +import com.google.s2a.proto.v2.ValidatePeerCertificateChainReq.VerificationMode; +import com.google.s2a.proto.v2.ValidatePeerCertificateChainResp; import io.grpc.Grpc; import io.grpc.InsecureChannelCredentials; import io.grpc.ManagedChannel; import io.grpc.Server; import io.grpc.ServerBuilder; -import io.grpc.s2a.internal.handshaker.ValidatePeerCertificateChainReq.VerificationMode; import io.grpc.stub.StreamObserver; import java.io.IOException; import java.io.InputStream; diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java index 0b3ecff3f8e..0b398638f92 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeWriter.java @@ -16,13 +16,25 @@ package io.grpc.s2a.internal.handshaker; -import static io.grpc.s2a.internal.handshaker.TLSVersion.TLS_VERSION_1_2; -import static io.grpc.s2a.internal.handshaker.TLSVersion.TLS_VERSION_1_3; +import static com.google.s2a.proto.v2.TLSVersion.TLS_VERSION_1_2; +import static com.google.s2a.proto.v2.TLSVersion.TLS_VERSION_1_3; import com.google.common.collect.ImmutableMap; import com.google.common.io.CharStreams; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.ByteString; +import com.google.s2a.proto.v2.Ciphersuite; +import com.google.s2a.proto.v2.ConnectionSide; +import com.google.s2a.proto.v2.GetTlsConfigurationReq; +import com.google.s2a.proto.v2.GetTlsConfigurationResp; +import com.google.s2a.proto.v2.OffloadPrivateKeyOperationReq; +import com.google.s2a.proto.v2.OffloadPrivateKeyOperationResp; +import com.google.s2a.proto.v2.SessionReq; +import com.google.s2a.proto.v2.SessionResp; +import com.google.s2a.proto.v2.SignatureAlgorithm; +import com.google.s2a.proto.v2.Status; +import com.google.s2a.proto.v2.ValidatePeerCertificateChainReq; +import com.google.s2a.proto.v2.ValidatePeerCertificateChainResp; import io.grpc.stub.StreamObserver; import io.grpc.util.CertificateUtils; import java.io.FileNotFoundException; diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java index d69d84bf459..c1c629366aa 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/GetAuthenticationMechanismsTest.java @@ -17,6 +17,7 @@ package io.grpc.s2a.internal.handshaker; import com.google.common.truth.Expect; +import com.google.s2a.proto.v2.AuthenticationMechanism; import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.grpc.s2a.internal.handshaker.tokenmanager.AccessTokenManager; import io.grpc.s2a.internal.handshaker.tokenmanager.SingleTokenFetcher; diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/ProtoUtilTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/ProtoUtilTest.java index f60aa1a189b..28dbf0e4d88 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/ProtoUtilTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/ProtoUtilTest.java @@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.truth.Expect; +import com.google.s2a.proto.v2.TLSVersion; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethodTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethodTest.java index 1aceb9518c3..8f71496cab8 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethodTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2APrivateKeyMethodTest.java @@ -25,6 +25,11 @@ import com.google.common.truth.Expect; import com.google.protobuf.ByteString; +import com.google.s2a.proto.v2.OffloadPrivateKeyOperationReq; +import com.google.s2a.proto.v2.OffloadPrivateKeyOperationResp; +import com.google.s2a.proto.v2.SessionReq; +import com.google.s2a.proto.v2.SessionResp; +import com.google.s2a.proto.v2.SignatureAlgorithm; import io.grpc.netty.GrpcSslContexts; import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.netty.handler.ssl.OpenSslPrivateKeyMethod; diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java index e537687c287..7e776f16da2 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactoryTest.java @@ -22,6 +22,9 @@ import com.google.common.testing.NullPointerTester; import com.google.common.testing.NullPointerTester.Visibility; +import com.google.s2a.proto.v2.S2AServiceGrpc; +import com.google.s2a.proto.v2.SessionReq; +import com.google.s2a.proto.v2.SessionResp; import io.grpc.Channel; import io.grpc.InsecureChannelCredentials; import io.grpc.Server; diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java index c912faecd48..713984c361d 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/S2AStubTest.java @@ -21,6 +21,15 @@ import static org.junit.Assert.assertThrows; import com.google.common.truth.Expect; +import com.google.s2a.proto.v2.Ciphersuite; +import com.google.s2a.proto.v2.ConnectionSide; +import com.google.s2a.proto.v2.GetTlsConfigurationReq; +import com.google.s2a.proto.v2.GetTlsConfigurationResp; +import com.google.s2a.proto.v2.S2AServiceGrpc; +import com.google.s2a.proto.v2.SessionReq; +import com.google.s2a.proto.v2.SessionResp; +import com.google.s2a.proto.v2.Status; +import com.google.s2a.proto.v2.TLSVersion; import io.grpc.Channel; import io.grpc.InsecureChannelCredentials; import io.grpc.internal.ObjectPool; From 892144dcac18a1cedbdc4cddecefb41193a965fc Mon Sep 17 00:00:00 2001 From: Daniel Liu Date: Wed, 19 Feb 2025 20:25:33 -0800 Subject: [PATCH 195/591] xds: explicitly set request hash key for the ring hash LB policy Implements [gRFC A76: explicitly setting the request hash key for the ring hash LB policy][A76] * Explictly setting the request hash key is guarded by the `GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY` environment variable until API stabilized. Tested: * Verified end-to-end by spinning up multiple gRPC servers and a gRPC client that injects a custom service (load balancing) config with `ring_hash_experimental` and a custom `request_hash_header` (with NO associated value in the metadata headers) which generates a random hash for each request to the ring hash LB. Verified picks/RPCs are split evenly/uniformly across all backends. * Ran affected unit tests with thread sanitizer and 1000 iterations to prevent data races. [A76]: https://github.com/grpc/proposal/blob/master/A76-ring-hash-improvements.md#explicitly-setting-the-request-hash-key --- .../io/grpc/xds/RingHashLoadBalancer.java | 131 ++++++++--- .../xds/RingHashLoadBalancerProvider.java | 11 +- .../xds/ClusterResolverLoadBalancerTest.java | 2 +- .../xds/RingHashLoadBalancerProviderTest.java | 62 ++++++ .../io/grpc/xds/RingHashLoadBalancerTest.java | 206 ++++++++++++++++-- 5 files changed, 357 insertions(+), 55 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java index 3793482c86b..4f314e5391a 100644 --- a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java @@ -25,6 +25,8 @@ import static io.grpc.ConnectivityState.SHUTDOWN; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; @@ -34,9 +36,11 @@ import io.grpc.EquivalentAddressGroup; import io.grpc.InternalLogId; import io.grpc.LoadBalancer; +import io.grpc.Metadata; import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.util.MultiChildLoadBalancer; +import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl; import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger.XdsLogLevel; import java.net.SocketAddress; @@ -69,13 +73,21 @@ final class RingHashLoadBalancer extends MultiChildLoadBalancer { new LazyLoadBalancer.Factory(pickFirstLbProvider); private final XdsLogger logger; private final SynchronizationContext syncContext; + private final ThreadSafeRandom random; private List ring; + @Nullable private Metadata.Key requestHashHeaderKey; RingHashLoadBalancer(Helper helper) { + this(helper, ThreadSafeRandomImpl.instance); + } + + @VisibleForTesting + RingHashLoadBalancer(Helper helper, ThreadSafeRandom random) { super(helper); syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); logger = XdsLogger.withLogId(InternalLogId.allocate("ring_hash_lb", helper.getAuthority())); logger.log(XdsLogLevel.INFO, "Created"); + this.random = checkNotNull(random, "random"); } @Override @@ -92,6 +104,10 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { if (config == null) { throw new IllegalArgumentException("Missing RingHash configuration"); } + requestHashHeaderKey = + config.requestHashHeader.isEmpty() + ? null + : Metadata.Key.of(config.requestHashHeader, Metadata.ASCII_STRING_MARSHALLER); Map serverWeights = new HashMap<>(); long totalWeight = 0L; for (EquivalentAddressGroup eag : addrList) { @@ -197,7 +213,8 @@ protected void updateOverallBalancingState() { overallState = TRANSIENT_FAILURE; } - RingHashPicker picker = new RingHashPicker(syncContext, ring, getChildLbStates()); + RingHashPicker picker = + new RingHashPicker(syncContext, ring, getChildLbStates(), requestHashHeaderKey, random); getHelper().updateBalancingState(overallState, picker); this.currentConnectivityState = overallState; } @@ -325,21 +342,32 @@ private static final class RingHashPicker extends SubchannelPicker { // TODO(chengyuanzhang): can be more performance-friendly with // IdentityHashMap and RingEntry contains Subchannel. private final Map pickableSubchannels; // read-only + @Nullable private final Metadata.Key requestHashHeaderKey; + private final ThreadSafeRandom random; + private final boolean hasEndpointInConnectingState; private RingHashPicker( SynchronizationContext syncContext, List ring, - Collection children) { + Collection children, Metadata.Key requestHashHeaderKey, + ThreadSafeRandom random) { this.syncContext = syncContext; this.ring = ring; + this.requestHashHeaderKey = requestHashHeaderKey; + this.random = random; pickableSubchannels = new HashMap<>(children.size()); + boolean hasConnectingState = false; for (ChildLbState childLbState : children) { pickableSubchannels.put((Endpoint)childLbState.getKey(), new SubchannelView(childLbState, childLbState.getCurrentState())); + if (childLbState.getCurrentState() == CONNECTING) { + hasConnectingState = true; + } } + this.hasEndpointInConnectingState = hasConnectingState; } // Find the ring entry with hash next to (clockwise) the RPC's hash (binary search). - private int getTargetIndex(Long requestHash) { + private int getTargetIndex(long requestHash) { if (ring.size() <= 1) { return 0; } @@ -365,38 +393,77 @@ private int getTargetIndex(Long requestHash) { @Override public PickResult pickSubchannel(PickSubchannelArgs args) { - Long requestHash = args.getCallOptions().getOption(XdsNameResolver.RPC_HASH_KEY); - if (requestHash == null) { - return PickResult.withError(RPC_HASH_NOT_FOUND); + // Determine request hash. + boolean usingRandomHash = false; + long requestHash; + if (requestHashHeaderKey == null) { + // Set by the xDS config selector. + Long rpcHashFromCallOptions = args.getCallOptions().getOption(XdsNameResolver.RPC_HASH_KEY); + if (rpcHashFromCallOptions == null) { + return PickResult.withError(RPC_HASH_NOT_FOUND); + } + requestHash = rpcHashFromCallOptions; + } else { + Iterable headerValues = args.getHeaders().getAll(requestHashHeaderKey); + if (headerValues != null) { + requestHash = hashFunc.hashAsciiString(Joiner.on(",").join(headerValues)); + } else { + requestHash = random.nextLong(); + usingRandomHash = true; + } } int targetIndex = getTargetIndex(requestHash); - // Per gRFC A61, because of sticky-TF with PickFirst's auto reconnect on TF, we ignore - // all TF subchannels and find the first ring entry in READY, CONNECTING or IDLE. If - // CONNECTING or IDLE we return a pick with no results. Additionally, if that entry is in - // IDLE, we initiate a connection. - for (int i = 0; i < ring.size(); i++) { - int index = (targetIndex + i) % ring.size(); - SubchannelView subchannelView = pickableSubchannels.get(ring.get(index).addrKey); - ChildLbState childLbState = subchannelView.childLbState; - - if (subchannelView.connectivityState == READY) { - return childLbState.getCurrentPicker().pickSubchannel(args); + if (!usingRandomHash) { + // Per gRFC A61, because of sticky-TF with PickFirst's auto reconnect on TF, we ignore + // all TF subchannels and find the first ring entry in READY, CONNECTING or IDLE. If + // CONNECTING or IDLE we return a pick with no results. Additionally, if that entry is in + // IDLE, we initiate a connection. + for (int i = 0; i < ring.size(); i++) { + int index = (targetIndex + i) % ring.size(); + SubchannelView subchannelView = pickableSubchannels.get(ring.get(index).addrKey); + ChildLbState childLbState = subchannelView.childLbState; + + if (subchannelView.connectivityState == READY) { + return childLbState.getCurrentPicker().pickSubchannel(args); + } + + // RPCs can be buffered if the next subchannel is pending (per A62). Otherwise, RPCs + // are failed unless there is a READY connection. + if (subchannelView.connectivityState == CONNECTING) { + return PickResult.withNoResult(); + } + + if (subchannelView.connectivityState == IDLE) { + syncContext.execute(() -> { + childLbState.getLb().requestConnection(); + }); + + return PickResult.withNoResult(); // Indicates that this should be retried after backoff + } } - - // RPCs can be buffered if the next subchannel is pending (per A62). Otherwise, RPCs - // are failed unless there is a READY connection. - if (subchannelView.connectivityState == CONNECTING) { - return PickResult.withNoResult(); + } else { + // Using a random hash. Find and use the first READY ring entry, triggering at most one + // entry to attempt connection. + boolean requestedConnection = hasEndpointInConnectingState; + for (int i = 0; i < ring.size(); i++) { + int index = (targetIndex + i) % ring.size(); + SubchannelView subchannelView = pickableSubchannels.get(ring.get(index).addrKey); + ChildLbState childLbState = subchannelView.childLbState; + if (subchannelView.connectivityState == READY) { + return childLbState.getCurrentPicker().pickSubchannel(args); + } + if (!requestedConnection && subchannelView.connectivityState == IDLE) { + syncContext.execute( + () -> { + childLbState.getLb().requestConnection(); + }); + requestedConnection = true; + } } - - if (subchannelView.connectivityState == IDLE) { - syncContext.execute(() -> { - childLbState.getLb().requestConnection(); - }); - - return PickResult.withNoResult(); // Indicates that this should be retried after backoff + if (requestedConnection) { + return PickResult.withNoResult(); } } @@ -444,13 +511,16 @@ public int compareTo(RingEntry entry) { static final class RingHashConfig { final long minRingSize; final long maxRingSize; + final String requestHashHeader; - RingHashConfig(long minRingSize, long maxRingSize) { + RingHashConfig(long minRingSize, long maxRingSize, String requestHashHeader) { checkArgument(minRingSize > 0, "minRingSize <= 0"); checkArgument(maxRingSize > 0, "maxRingSize <= 0"); checkArgument(minRingSize <= maxRingSize, "minRingSize > maxRingSize"); + checkNotNull(requestHashHeader); this.minRingSize = minRingSize; this.maxRingSize = maxRingSize; + this.requestHashHeader = requestHashHeader; } @Override @@ -458,6 +528,7 @@ public String toString() { return MoreObjects.toStringHelper(this) .add("minRingSize", minRingSize) .add("maxRingSize", maxRingSize) + .add("requestHashHeader", requestHashHeader) .toString(); } } diff --git a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancerProvider.java index dad79384569..035ff76c585 100644 --- a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancerProvider.java @@ -24,6 +24,7 @@ import io.grpc.LoadBalancerProvider; import io.grpc.NameResolver.ConfigOrError; import io.grpc.Status; +import io.grpc.internal.GrpcUtil; import io.grpc.internal.JsonUtil; import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; import io.grpc.xds.RingHashOptions; @@ -81,6 +82,10 @@ private ConfigOrError parseLoadBalancingPolicyConfigInternal( Map rawLoadBalancingPolicyConfig) { Long minRingSize = JsonUtil.getNumberAsLong(rawLoadBalancingPolicyConfig, "minRingSize"); Long maxRingSize = JsonUtil.getNumberAsLong(rawLoadBalancingPolicyConfig, "maxRingSize"); + String requestHashHeader = ""; + if (GrpcUtil.getFlag("GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY", false)) { + requestHashHeader = JsonUtil.getString(rawLoadBalancingPolicyConfig, "requestHashHeader"); + } long maxRingSizeCap = RingHashOptions.getRingSizeCap(); if (minRingSize == null) { minRingSize = DEFAULT_MIN_RING_SIZE; @@ -88,6 +93,9 @@ private ConfigOrError parseLoadBalancingPolicyConfigInternal( if (maxRingSize == null) { maxRingSize = DEFAULT_MAX_RING_SIZE; } + if (requestHashHeader == null) { + requestHashHeader = ""; + } if (minRingSize > maxRingSizeCap) { minRingSize = maxRingSizeCap; } @@ -98,6 +106,7 @@ private ConfigOrError parseLoadBalancingPolicyConfigInternal( return ConfigOrError.fromError(Status.UNAVAILABLE.withDescription( "Invalid 'mingRingSize'/'maxRingSize'")); } - return ConfigOrError.fromConfig(new RingHashConfig(minRingSize, maxRingSize)); + return ConfigOrError.fromConfig( + new RingHashConfig(minRingSize, maxRingSize, requestHashHeader)); } } diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index 9243abba6d3..28898c0930f 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -163,7 +163,7 @@ public void uncaughtException(Thread t, Throwable e) { GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( new FakeLoadBalancerProvider("round_robin"), null))); private final Object ringHash = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - new FakeLoadBalancerProvider("ring_hash_experimental"), new RingHashConfig(10L, 100L)); + new FakeLoadBalancerProvider("ring_hash_experimental"), new RingHashConfig(10L, 100L, "")); private final Object leastRequest = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( new FakeLoadBalancerProvider("wrr_locality_experimental"), new WrrLocalityConfig( GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( diff --git a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerProviderTest.java b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerProviderTest.java index 87615a125c0..3036db5b09f 100644 --- a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerProviderTest.java @@ -42,6 +42,8 @@ @RunWith(JUnit4.class) public class RingHashLoadBalancerProviderTest { private static final String AUTHORITY = "foo.googleapis.com"; + private static final String GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY = + "GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY"; private final SynchronizationContext syncContext = new SynchronizationContext( new UncaughtExceptionHandler() { @@ -81,6 +83,7 @@ public void parseLoadBalancingConfig_valid() throws IOException { RingHashConfig config = (RingHashConfig) configOrError.getConfig(); assertThat(config.minRingSize).isEqualTo(10L); assertThat(config.maxRingSize).isEqualTo(100L); + assertThat(config.requestHashHeader).isEmpty(); } @Test @@ -92,6 +95,7 @@ public void parseLoadBalancingConfig_missingRingSize_useDefaults() throws IOExce RingHashConfig config = (RingHashConfig) configOrError.getConfig(); assertThat(config.minRingSize).isEqualTo(RingHashLoadBalancerProvider.DEFAULT_MIN_RING_SIZE); assertThat(config.maxRingSize).isEqualTo(RingHashLoadBalancerProvider.DEFAULT_MAX_RING_SIZE); + assertThat(config.requestHashHeader).isEmpty(); } @Test @@ -127,6 +131,7 @@ public void parseLoadBalancingConfig_ringTooLargeUsesCap() throws IOException { RingHashConfig config = (RingHashConfig) configOrError.getConfig(); assertThat(config.minRingSize).isEqualTo(10); assertThat(config.maxRingSize).isEqualTo(RingHashOptions.DEFAULT_RING_SIZE_CAP); + assertThat(config.requestHashHeader).isEmpty(); } @Test @@ -142,6 +147,7 @@ public void parseLoadBalancingConfig_ringCapCanBeRaised() throws IOException { RingHashConfig config = (RingHashConfig) configOrError.getConfig(); assertThat(config.minRingSize).isEqualTo(RingHashOptions.MAX_RING_SIZE_CAP); assertThat(config.maxRingSize).isEqualTo(RingHashOptions.MAX_RING_SIZE_CAP); + assertThat(config.requestHashHeader).isEmpty(); // Reset to avoid affecting subsequent test cases RingHashOptions.setRingSizeCap(RingHashOptions.DEFAULT_RING_SIZE_CAP); } @@ -159,6 +165,7 @@ public void parseLoadBalancingConfig_ringCapIsClampedTo8M() throws IOException { RingHashConfig config = (RingHashConfig) configOrError.getConfig(); assertThat(config.minRingSize).isEqualTo(RingHashOptions.MAX_RING_SIZE_CAP); assertThat(config.maxRingSize).isEqualTo(RingHashOptions.MAX_RING_SIZE_CAP); + assertThat(config.requestHashHeader).isEmpty(); // Reset to avoid affecting subsequent test cases RingHashOptions.setRingSizeCap(RingHashOptions.DEFAULT_RING_SIZE_CAP); } @@ -176,6 +183,7 @@ public void parseLoadBalancingConfig_ringCapCanBeLowered() throws IOException { RingHashConfig config = (RingHashConfig) configOrError.getConfig(); assertThat(config.minRingSize).isEqualTo(1); assertThat(config.maxRingSize).isEqualTo(1); + assertThat(config.requestHashHeader).isEmpty(); // Reset to avoid affecting subsequent test cases RingHashOptions.setRingSizeCap(RingHashOptions.DEFAULT_RING_SIZE_CAP); } @@ -193,6 +201,7 @@ public void parseLoadBalancingConfig_ringCapLowerLimitIs1() throws IOException { RingHashConfig config = (RingHashConfig) configOrError.getConfig(); assertThat(config.minRingSize).isEqualTo(1); assertThat(config.maxRingSize).isEqualTo(1); + assertThat(config.requestHashHeader).isEmpty(); // Reset to avoid affecting subsequent test cases RingHashOptions.setRingSizeCap(RingHashOptions.DEFAULT_RING_SIZE_CAP); } @@ -219,6 +228,59 @@ public void parseLoadBalancingConfig_minRingSizeGreaterThanMaxRingSize() throws .isEqualTo("Invalid 'mingRingSize'/'maxRingSize'"); } + @Test + public void parseLoadBalancingConfig_requestHashHeaderIgnoredWhenEnvVarNotSet() + throws IOException { + String lbConfig = + "{\"minRingSize\" : 10, \"maxRingSize\" : 100, \"requestHashHeader\" : \"dummy-hash\"}"; + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(lbConfig)); + assertThat(configOrError.getConfig()).isNotNull(); + RingHashConfig config = (RingHashConfig) configOrError.getConfig(); + assertThat(config.minRingSize).isEqualTo(10L); + assertThat(config.maxRingSize).isEqualTo(100L); + assertThat(config.requestHashHeader).isEmpty(); + } + + @Test + public void parseLoadBalancingConfig_requestHashHeaderSetWhenEnvVarSet() throws IOException { + System.setProperty(GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY, "true"); + try { + String lbConfig = + "{\"minRingSize\" : 10, \"maxRingSize\" : 100, \"requestHashHeader\" : \"dummy-hash\"}"; + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(lbConfig)); + assertThat(configOrError.getConfig()).isNotNull(); + RingHashConfig config = (RingHashConfig) configOrError.getConfig(); + assertThat(config.minRingSize).isEqualTo(10L); + assertThat(config.maxRingSize).isEqualTo(100L); + assertThat(config.requestHashHeader).isEqualTo("dummy-hash"); + assertThat(config.toString()).contains("minRingSize=10"); + assertThat(config.toString()).contains("maxRingSize=100"); + assertThat(config.toString()).contains("requestHashHeader=dummy-hash"); + } finally { + System.clearProperty(GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY); + } + } + + @Test + public void parseLoadBalancingConfig_requestHashHeaderUnsetWhenEnvVarSet_useDefaults() + throws IOException { + System.setProperty(GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY, "true"); + try { + String lbConfig = "{\"minRingSize\" : 10, \"maxRingSize\" : 100}"; + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(lbConfig)); + assertThat(configOrError.getConfig()).isNotNull(); + RingHashConfig config = (RingHashConfig) configOrError.getConfig(); + assertThat(config.minRingSize).isEqualTo(10L); + assertThat(config.maxRingSize).isEqualTo(100L); + assertThat(config.requestHashHeader).isEmpty(); + } finally { + System.clearProperty(GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY); + } + } + @SuppressWarnings("unchecked") private static Map parseJsonObject(String json) throws IOException { return (Map) JsonParser.parse(json); diff --git a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java index 65fc1527b0c..f27cb772d03 100644 --- a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java @@ -98,6 +98,9 @@ @RunWith(JUnit4.class) public class RingHashLoadBalancerTest { private static final String AUTHORITY = "foo.googleapis.com"; + private static final String CUSTOM_REQUEST_HASH_HEADER = "custom-request-hash-header"; + private static final Metadata.Key CUSTOM_METADATA_KEY = + Metadata.Key.of(CUSTOM_REQUEST_HASH_HEADER, Metadata.ASCII_STRING_MARSHALLER); private static final Attributes.Key CUSTOM_KEY = Attributes.Key.create("custom-key"); private static final ConnectivityStateInfo CSI_CONNECTING = ConnectivityStateInfo.forNonError(CONNECTING); @@ -142,7 +145,7 @@ public void tearDown() { @Test public void subchannelLazyConnectUntilPicked() { - RingHashConfig config = new RingHashConfig(10, 100); + RingHashConfig config = new RingHashConfig(10, 100, ""); List servers = createWeightedServerAddrs(1); // one server Status addressesAcceptanceStatus = loadBalancer.acceptResolvedAddresses( ResolvedAddresses.newBuilder() @@ -176,7 +179,7 @@ public void subchannelLazyConnectUntilPicked() { @Test public void subchannelNotAutoReconnectAfterReenteringIdle() { - RingHashConfig config = new RingHashConfig(10, 100); + RingHashConfig config = new RingHashConfig(10, 100, ""); List servers = createWeightedServerAddrs(1); // one server Status addressesAcceptanceStatus = loadBalancer.acceptResolvedAddresses( ResolvedAddresses.newBuilder() @@ -207,7 +210,7 @@ public void subchannelNotAutoReconnectAfterReenteringIdle() { @Test public void aggregateSubchannelStates_connectingReadyIdleFailure() { - RingHashConfig config = new RingHashConfig(10, 100); + RingHashConfig config = new RingHashConfig(10, 100, ""); List servers = createWeightedServerAddrs(1, 1); InOrder inOrder = Mockito.inOrder(helper); @@ -266,7 +269,7 @@ private void verifyConnection(int times) { @Test public void aggregateSubchannelStates_allSubchannelsInTransientFailure() { - RingHashConfig config = new RingHashConfig(10, 100); + RingHashConfig config = new RingHashConfig(10, 100, ""); List servers = createWeightedServerAddrs(1, 1, 1, 1); List subChannelList = initializeLbSubchannels(config, servers, STAY_IN_CONNECTING); @@ -324,7 +327,7 @@ private void refreshInvokedAndUpdateBS(InOrder inOrder, ConnectivityState state) @Test public void ignoreShutdownSubchannelStateChange() { - RingHashConfig config = new RingHashConfig(10, 100); + RingHashConfig config = new RingHashConfig(10, 100, ""); List servers = createWeightedServerAddrs(1, 1, 1); initializeLbSubchannels(config, servers); @@ -340,7 +343,7 @@ public void ignoreShutdownSubchannelStateChange() { @Test public void deterministicPickWithHostsPartiallyRemoved() { - RingHashConfig config = new RingHashConfig(10, 100); + RingHashConfig config = new RingHashConfig(10, 100, ""); List servers = createWeightedServerAddrs(1, 1, 1, 1, 1); initializeLbSubchannels(config, servers); InOrder inOrder = Mockito.inOrder(helper); @@ -380,7 +383,7 @@ public void deterministicPickWithHostsPartiallyRemoved() { @Test public void deterministicPickWithNewHostsAdded() { - RingHashConfig config = new RingHashConfig(10, 100); + RingHashConfig config = new RingHashConfig(10, 100, ""); List servers = createWeightedServerAddrs(1, 1); // server0 and server1 initializeLbSubchannels(config, servers, DO_NOT_VERIFY, DO_NOT_RESET_HELPER); @@ -412,6 +415,139 @@ public void deterministicPickWithNewHostsAdded() { inOrder.verifyNoMoreInteractions(); } + @Test + public void deterministicPickWithRequestHashHeader_oneHeaderValue() { + // Map each server address to exactly one ring entry. + RingHashConfig config = new RingHashConfig(3, 3, CUSTOM_REQUEST_HASH_HEADER); + List servers = createWeightedServerAddrs(1, 1, 1); + initializeLbSubchannels(config, servers); + InOrder inOrder = Mockito.inOrder(helper); + + // Bring all subchannels to READY. + for (Subchannel subchannel : subchannels.values()) { + deliverSubchannelState(subchannel, CSI_READY); + inOrder.verify(helper).updateBalancingState(eq(READY), pickerCaptor.capture()); + } + + // Pick subchannel with custom request hash header where the rpc hash hits server1. + Metadata headers = new Metadata(); + headers.put(CUSTOM_METADATA_KEY, "FakeSocketAddress-server1_0"); + PickSubchannelArgs args = + new PickSubchannelArgsImpl( + TestMethodDescriptors.voidMethod(), + headers, + CallOptions.DEFAULT, + new PickDetailsConsumer() {}); + SubchannelPicker picker = pickerCaptor.getValue(); + PickResult result = picker.pickSubchannel(args); + assertThat(result.getStatus().isOk()).isTrue(); + assertThat(result.getSubchannel().getAddresses()).isEqualTo(servers.get(1)); + } + + @Test + public void deterministicPickWithRequestHashHeader_multipleHeaderValues() { + // Map each server address to exactly one ring entry. + RingHashConfig config = new RingHashConfig(3, 3, CUSTOM_REQUEST_HASH_HEADER); + List servers = createWeightedServerAddrs(1, 1, 1); + initializeLbSubchannels(config, servers); + InOrder inOrder = Mockito.inOrder(helper); + + // Bring all subchannels to READY. + for (Subchannel subchannel : subchannels.values()) { + deliverSubchannelState(subchannel, CSI_READY); + inOrder.verify(helper).updateBalancingState(eq(READY), pickerCaptor.capture()); + } + + // Pick subchannel with custom request hash header with multiple values for the same key where + // the rpc hash hits server1. + Metadata headers = new Metadata(); + headers.put(CUSTOM_METADATA_KEY, "FakeSocketAddress-server0_0"); + headers.put(CUSTOM_METADATA_KEY, "FakeSocketAddress-server1_0"); + PickSubchannelArgs args = + new PickSubchannelArgsImpl( + TestMethodDescriptors.voidMethod(), + headers, + CallOptions.DEFAULT, + new PickDetailsConsumer() {}); + SubchannelPicker picker = pickerCaptor.getValue(); + PickResult result = picker.pickSubchannel(args); + assertThat(result.getStatus().isOk()).isTrue(); + assertThat(result.getSubchannel().getAddresses()).isEqualTo(servers.get(1)); + } + + @Test + public void pickWithRandomHash_allSubchannelsReady() { + loadBalancer = new RingHashLoadBalancer(helper, new FakeRandom()); + // Map each server address to exactly one ring entry. + RingHashConfig config = new RingHashConfig(2, 2, "dummy-random-hash"); + List servers = createWeightedServerAddrs(1, 1); + initializeLbSubchannels(config, servers); + InOrder inOrder = Mockito.inOrder(helper); + + // Bring all subchannels to READY. + Map pickCounts = new HashMap<>(); + for (Subchannel subchannel : subchannels.values()) { + deliverSubchannelState(subchannel, CSI_READY); + pickCounts.put(subchannel.getAddresses(), 0); + inOrder.verify(helper).updateBalancingState(eq(READY), pickerCaptor.capture()); + } + + // Pick subchannel 100 times with random hash. + SubchannelPicker picker = pickerCaptor.getValue(); + PickSubchannelArgs args = getDefaultPickSubchannelArgs(hashFunc.hashVoid()); + for (int i = 0; i < 100; ++i) { + Subchannel pickedSubchannel = picker.pickSubchannel(args).getSubchannel(); + EquivalentAddressGroup addr = pickedSubchannel.getAddresses(); + pickCounts.put(addr, pickCounts.get(addr) + 1); + } + + // Verify the distribution is uniform where server0 and server1 are exactly picked 50 times. + assertThat(pickCounts.get(servers.get(0))).isEqualTo(50); + assertThat(pickCounts.get(servers.get(1))).isEqualTo(50); + } + + @Test + public void pickWithRandomHash_atLeastOneSubchannelConnecting() { + // Map each server address to exactly one ring entry. + RingHashConfig config = new RingHashConfig(3, 3, "dummy-random-hash"); + List servers = createWeightedServerAddrs(1, 1, 1); + initializeLbSubchannels(config, servers); + + // Bring one subchannel to CONNECTING. + deliverSubchannelState(getSubChannel(servers.get(0)), CSI_CONNECTING); + verify(helper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); + + // Pick subchannel with random hash does not trigger connection. + SubchannelPicker picker = pickerCaptor.getValue(); + PickSubchannelArgs args = getDefaultPickSubchannelArgs(hashFunc.hashVoid()); + PickResult result = picker.pickSubchannel(args); + assertThat(result.getStatus().isOk()).isTrue(); + assertThat(result.getSubchannel()).isNull(); // buffer request + verifyConnection(0); + } + + @Test + public void pickWithRandomHash_firstSubchannelInTransientFailure_remainingSubchannelsIdle() { + // Map each server address to exactly one ring entry. + RingHashConfig config = new RingHashConfig(3, 3, "dummy-random-hash"); + List servers = createWeightedServerAddrs(1, 1, 1); + initializeLbSubchannels(config, servers); + + // Bring one subchannel to TRANSIENT_FAILURE. + deliverSubchannelUnreachable(getSubChannel(servers.get(0))); + verify(helper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); + verifyConnection(0); + + // Pick subchannel with random hash does trigger connection by walking the ring + // and choosing the first (at most one) IDLE subchannel along the way. + SubchannelPicker picker = pickerCaptor.getValue(); + PickSubchannelArgs args = getDefaultPickSubchannelArgs(hashFunc.hashVoid()); + PickResult result = picker.pickSubchannel(args); + assertThat(result.getStatus().isOk()).isTrue(); + assertThat(result.getSubchannel()).isNull(); // buffer request + verifyConnection(1); + } + private Subchannel getSubChannel(EquivalentAddressGroup eag) { return subchannels.get(Collections.singletonList(eag)); } @@ -419,7 +555,7 @@ private Subchannel getSubChannel(EquivalentAddressGroup eag) { @Test public void skipFailingHosts_pickNextNonFailingHost() { // Map each server address to exactly one ring entry. - RingHashConfig config = new RingHashConfig(3, 3); + RingHashConfig config = new RingHashConfig(3, 3, ""); List servers = createWeightedServerAddrs(1, 1, 1); Status addressesAcceptanceStatus = loadBalancer.acceptResolvedAddresses( @@ -489,7 +625,7 @@ private PickSubchannelArgs getDefaultPickSubchannelArgsForServer(int serverid) { @Test public void skipFailingHosts_firstTwoHostsFailed_pickNextFirstReady() { // Map each server address to exactly one ring entry. - RingHashConfig config = new RingHashConfig(3, 3); + RingHashConfig config = new RingHashConfig(3, 3, ""); List servers = createWeightedServerAddrs(1, 1, 1); initializeLbSubchannels(config, servers); @@ -555,7 +691,7 @@ public void skipFailingHosts_firstTwoHostsFailed_pickNextFirstReady() { @Test public void removingAddressShutdownSubchannel() { // Map each server address to exactly one ring entry. - RingHashConfig config = new RingHashConfig(3, 3); + RingHashConfig config = new RingHashConfig(3, 3, ""); List svs1 = createWeightedServerAddrs(1, 1, 1); List subchannels1 = initializeLbSubchannels(config, svs1, STAY_IN_CONNECTING); @@ -572,7 +708,7 @@ public void removingAddressShutdownSubchannel() { @Test public void allSubchannelsInTransientFailure() { // Map each server address to exactly one ring entry. - RingHashConfig config = new RingHashConfig(3, 3); + RingHashConfig config = new RingHashConfig(3, 3, ""); List servers = createWeightedServerAddrs(1, 1, 1); initializeLbSubchannels(config, servers); @@ -599,7 +735,7 @@ public void allSubchannelsInTransientFailure() { @Test public void firstSubchannelIdle() { // Map each server address to exactly one ring entry. - RingHashConfig config = new RingHashConfig(3, 3); + RingHashConfig config = new RingHashConfig(3, 3, ""); List servers = createWeightedServerAddrs(1, 1, 1); initializeLbSubchannels(config, servers); @@ -620,7 +756,7 @@ public void firstSubchannelIdle() { @Test public void firstSubchannelConnecting() { // Map each server address to exactly one ring entry. - RingHashConfig config = new RingHashConfig(3, 3); + RingHashConfig config = new RingHashConfig(3, 3, ""); List servers = createWeightedServerAddrs(1, 1, 1); initializeLbSubchannels(config, servers); @@ -644,7 +780,7 @@ private Subchannel getSubchannel(List servers, int serve @Test public void firstSubchannelFailure() { // Map each server address to exactly one ring entry. - RingHashConfig config = new RingHashConfig(3, 3); + RingHashConfig config = new RingHashConfig(3, 3, ""); List servers = createWeightedServerAddrs(1, 1, 1); List subchannelList = @@ -675,7 +811,7 @@ public void firstSubchannelFailure() { @Test public void secondSubchannelConnecting() { // Map each server address to exactly one ring entry. - RingHashConfig config = new RingHashConfig(3, 3); + RingHashConfig config = new RingHashConfig(3, 3, ""); List servers = createWeightedServerAddrs(1, 1, 1); initializeLbSubchannels(config, servers); @@ -706,7 +842,7 @@ public void secondSubchannelConnecting() { @Test public void secondSubchannelFailure() { // Map each server address to exactly one ring entry. - RingHashConfig config = new RingHashConfig(3, 3); + RingHashConfig config = new RingHashConfig(3, 3, ""); List servers = createWeightedServerAddrs(1, 1, 1); initializeLbSubchannels(config, servers); @@ -733,7 +869,7 @@ public void secondSubchannelFailure() { @Test public void thirdSubchannelConnecting() { // Map each server address to exactly one ring entry. - RingHashConfig config = new RingHashConfig(3, 3); + RingHashConfig config = new RingHashConfig(3, 3, ""); List servers = createWeightedServerAddrs(1, 1, 1); initializeLbSubchannels(config, servers); @@ -762,7 +898,7 @@ public void thirdSubchannelConnecting() { @Test public void stickyTransientFailure() { // Map each server address to exactly one ring entry. - RingHashConfig config = new RingHashConfig(3, 3); + RingHashConfig config = new RingHashConfig(3, 3, ""); List servers = createWeightedServerAddrs(1, 1, 1); initializeLbSubchannels(config, servers); @@ -791,7 +927,7 @@ public void stickyTransientFailure() { @Test public void largeWeights() { - RingHashConfig config = new RingHashConfig(10000, 100000); // large ring + RingHashConfig config = new RingHashConfig(10000, 100000, ""); // large ring List servers = createWeightedServerAddrs(Integer.MAX_VALUE, 10, 100); // MAX:10:100 @@ -829,7 +965,7 @@ public void largeWeights() { @Test public void hostSelectionProportionalToWeights() { - RingHashConfig config = new RingHashConfig(10000, 100000); // large ring + RingHashConfig config = new RingHashConfig(10000, 100000, ""); // large ring List servers = createWeightedServerAddrs(1, 10, 100); // 1:10:100 initializeLbSubchannels(config, servers); @@ -872,7 +1008,7 @@ public void nameResolutionErrorWithNoActiveSubchannels() { @Test public void nameResolutionErrorWithActiveSubchannels() { - RingHashConfig config = new RingHashConfig(10, 100); + RingHashConfig config = new RingHashConfig(10, 100, ""); List servers = createWeightedServerAddrs(1); initializeLbSubchannels(config, servers, DO_NOT_VERIFY, DO_NOT_RESET_HELPER); @@ -894,7 +1030,7 @@ public void nameResolutionErrorWithActiveSubchannels() { @Test public void duplicateAddresses() { - RingHashConfig config = new RingHashConfig(10, 100); + RingHashConfig config = new RingHashConfig(10, 100, ""); List servers = createRepeatedServerAddrs(1, 2, 3); initializeLbSubchannels(config, servers, DO_NOT_VERIFY); @@ -940,7 +1076,7 @@ protected Helper delegate() { InOrder inOrder = Mockito.inOrder(helper); List servers = createWeightedServerAddrs(1, 1); - initializeLbSubchannels(new RingHashConfig(10, 100), servers); + initializeLbSubchannels(new RingHashConfig(10, 100, ""), servers); Subchannel subchannel0 = subchannels.get(Collections.singletonList(servers.get(0))); Subchannel subchannel1 = subchannels.get(Collections.singletonList(servers.get(1))); @@ -1167,6 +1303,30 @@ public void requestConnection() { } } + private static final class FakeRandom implements ThreadSafeRandom { + int counter = 0; + + @Override + public int nextInt(int bound) { + throw new UnsupportedOperationException("Should not be called"); + } + + @Override + public long nextLong() { + ++counter; + if (counter % 2 == 0) { + return XxHash64.INSTANCE.hashAsciiString("FakeSocketAddress-server0_0"); + } else { + return XxHash64.INSTANCE.hashAsciiString("FakeSocketAddress-server1_0"); + } + } + + @Override + public long nextLong(long bound) { + throw new UnsupportedOperationException("Should not be called"); + } + } + enum InitializationFlags { DO_NOT_VERIFY, RESET_SUBCHANNEL_MOCKS, From f207be39a98e1bd773ec6613f6eb6014fcb254c5 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 20 Feb 2025 13:03:10 -0800 Subject: [PATCH 196/591] util: Remove GracefulSwitchLb.switchTo() It was deprecated in 85e0a01ec, so has been deprecated for six releases/over six months. --- .../grpc/util/GracefulSwitchLoadBalancer.java | 39 +- .../util/GracefulSwitchLoadBalancerTest.java | 447 ------------------ 2 files changed, 2 insertions(+), 484 deletions(-) diff --git a/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java b/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java index a63a641b037..72c41886ad4 100644 --- a/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java @@ -44,28 +44,14 @@ * generally created by calling {@link #parseLoadBalancingPolicyConfig(List)} from a * {@link io.grpc.LoadBalancerProvider#parseLoadBalancingPolicyConfig * provider's parseLoadBalancingPolicyConfig()} implementation. - * - *

Alternatively, the balancer may {@link #switchTo(LoadBalancer.Factory) switch to} a policy - * prior to {@link - * LoadBalancer#handleResolvedAddresses(ResolvedAddresses) handling resolved addresses} for the - * first time. This causes graceful switch to ignore the service config and pass through the - * resolved addresses directly to the child policy. */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/5999") @NotThreadSafe // Must be accessed in SynchronizationContext public final class GracefulSwitchLoadBalancer extends ForwardingLoadBalancer { private final LoadBalancer defaultBalancer = new LoadBalancer() { @Override - public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { - // Most LB policies using this class will receive child policy configuration within the - // service config, so they are naturally calling switchTo() just before - // handleResolvedAddresses(), within their own handleResolvedAddresses(). If switchTo() is - // not called immediately after construction that does open up potential for bugs in the - // parent policies, where they fail to call switchTo(). So we will use the exception to try - // to notice those bugs quickly, as it will fail very loudly. - throw new IllegalStateException( - "GracefulSwitchLoadBalancer must switch to a load balancing policy before handling" - + " ResolvedAddresses"); + public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + throw new AssertionError("real LB is called instead"); } @Override @@ -104,7 +90,6 @@ public String toString() { private LoadBalancer pendingLb = defaultBalancer; private ConnectivityState pendingState; private SubchannelPicker pendingPicker; - private boolean switchToCalled; private boolean currentLbIsReady; @@ -114,10 +99,6 @@ public GracefulSwitchLoadBalancer(Helper helper) { @Override public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { - if (switchToCalled) { - delegate().handleResolvedAddresses(resolvedAddresses); - return; - } Config config = (Config) resolvedAddresses.getLoadBalancingPolicyConfig(); switchToInternal(config.childFactory); delegate().handleResolvedAddresses( @@ -128,9 +109,6 @@ public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { @Override public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - if (switchToCalled) { - return delegate().acceptResolvedAddresses(resolvedAddresses); - } Config config = (Config) resolvedAddresses.getLoadBalancingPolicyConfig(); switchToInternal(config.childFactory); return delegate().acceptResolvedAddresses( @@ -139,19 +117,6 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { .build()); } - /** - * Gracefully switch to a new policy defined by the given factory, if the given factory isn't - * equal to the current one. - * - * @deprecated Use {@code parseLoadBalancingPolicyConfig()} and pass the configuration to - * {@link io.grpc.LoadBalancer.ResolvedAddresses.Builder#setLoadBalancingPolicyConfig} - */ - @Deprecated - public void switchTo(LoadBalancer.Factory newBalancerFactory) { - switchToCalled = true; - switchToInternal(newBalancerFactory); - } - private void switchToInternal(LoadBalancer.Factory newBalancerFactory) { checkNotNull(newBalancerFactory, "newBalancerFactory"); diff --git a/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java b/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java index f31443ace7b..9a4f569c144 100644 --- a/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java @@ -83,453 +83,6 @@ public class GracefulSwitchLoadBalancerTest { new FakeLoadBalancerProvider("lb_policy_3"), }; - // OLD TESTS - - @Test - @Deprecated - public void switchTo_canHandleEmptyAddressListFromNameResolutionForwardedToLatestPolicy() { - gracefulSwitchLb.switchTo(lbPolicies[0]); - LoadBalancer lb0 = balancers.get(lbPolicies[0]); - Helper helper0 = helpers.get(lb0); - SubchannelPicker picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(READY, picker); - - assertThat(gracefulSwitchLb.canHandleEmptyAddressListFromNameResolution()).isFalse(); - when(lb0.canHandleEmptyAddressListFromNameResolution()).thenReturn(true); - assertThat(gracefulSwitchLb.canHandleEmptyAddressListFromNameResolution()).isTrue(); - - gracefulSwitchLb.switchTo(lbPolicies[1]); - LoadBalancer lb1 = balancers.get(lbPolicies[1]); - - assertThat(gracefulSwitchLb.canHandleEmptyAddressListFromNameResolution()).isFalse(); - - when(lb1.canHandleEmptyAddressListFromNameResolution()).thenReturn(true); - assertThat(gracefulSwitchLb.canHandleEmptyAddressListFromNameResolution()).isTrue(); - - gracefulSwitchLb.switchTo(lbPolicies[2]); - LoadBalancer lb2 = balancers.get(lbPolicies[2]); - - assertThat(gracefulSwitchLb.canHandleEmptyAddressListFromNameResolution()).isFalse(); - - when(lb2.canHandleEmptyAddressListFromNameResolution()).thenReturn(true); - assertThat(gracefulSwitchLb.canHandleEmptyAddressListFromNameResolution()).isTrue(); - } - - @Test - @Deprecated - public void switchTo_handleResolvedAddressesAndNameResolutionErrorForwardedToLatestPolicy() { - gracefulSwitchLb.switchTo(lbPolicies[0]); - LoadBalancer lb0 = balancers.get(lbPolicies[0]); - Helper helper0 = helpers.get(lb0); - SubchannelPicker picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(READY, picker); - - ResolvedAddresses addresses = newFakeAddresses(); - gracefulSwitchLb.handleResolvedAddresses(addresses); - verify(lb0).handleResolvedAddresses(addresses); - gracefulSwitchLb.handleNameResolutionError(Status.DATA_LOSS); - verify(lb0).handleNameResolutionError(Status.DATA_LOSS); - - gracefulSwitchLb.switchTo(lbPolicies[1]); - LoadBalancer lb1 = balancers.get(lbPolicies[1]); - addresses = newFakeAddresses(); - gracefulSwitchLb.handleResolvedAddresses(addresses); - verify(lb0, never()).handleResolvedAddresses(addresses); - verify(lb1).handleResolvedAddresses(addresses); - gracefulSwitchLb.handleNameResolutionError(Status.ALREADY_EXISTS); - verify(lb0, never()).handleNameResolutionError(Status.ALREADY_EXISTS); - verify(lb1).handleNameResolutionError(Status.ALREADY_EXISTS); - - gracefulSwitchLb.switchTo(lbPolicies[2]); - verify(lb1).shutdown(); - LoadBalancer lb2 = balancers.get(lbPolicies[2]); - addresses = newFakeAddresses(); - gracefulSwitchLb.handleResolvedAddresses(addresses); - verify(lb0, never()).handleResolvedAddresses(addresses); - verify(lb1, never()).handleResolvedAddresses(addresses); - verify(lb2).handleResolvedAddresses(addresses); - gracefulSwitchLb.handleNameResolutionError(Status.CANCELLED); - verify(lb0, never()).handleNameResolutionError(Status.CANCELLED); - verify(lb1, never()).handleNameResolutionError(Status.CANCELLED); - verify(lb2).handleNameResolutionError(Status.CANCELLED); - - verifyNoMoreInteractions(lb0, lb1, lb2); - } - - @Test - @Deprecated - public void switchTo_acceptResolvedAddressesAndNameResolutionErrorForwardedToLatestPolicy() { - gracefulSwitchLb.switchTo(lbPolicies[0]); - LoadBalancer lb0 = balancers.get(lbPolicies[0]); - Helper helper0 = helpers.get(lb0); - SubchannelPicker picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(READY, picker); - - ResolvedAddresses addresses = newFakeAddresses(); - gracefulSwitchLb.acceptResolvedAddresses(addresses); - verify(lb0).acceptResolvedAddresses(addresses); - gracefulSwitchLb.handleNameResolutionError(Status.DATA_LOSS); - verify(lb0).handleNameResolutionError(Status.DATA_LOSS); - - gracefulSwitchLb.switchTo(lbPolicies[1]); - LoadBalancer lb1 = balancers.get(lbPolicies[1]); - addresses = newFakeAddresses(); - gracefulSwitchLb.acceptResolvedAddresses(addresses); - verify(lb0, never()).acceptResolvedAddresses(addresses); - verify(lb1).acceptResolvedAddresses(addresses); - gracefulSwitchLb.handleNameResolutionError(Status.ALREADY_EXISTS); - verify(lb0, never()).handleNameResolutionError(Status.ALREADY_EXISTS); - verify(lb1).handleNameResolutionError(Status.ALREADY_EXISTS); - - gracefulSwitchLb.switchTo(lbPolicies[2]); - verify(lb1).shutdown(); - LoadBalancer lb2 = balancers.get(lbPolicies[2]); - addresses = newFakeAddresses(); - gracefulSwitchLb.acceptResolvedAddresses(addresses); - verify(lb0, never()).acceptResolvedAddresses(addresses); - verify(lb1, never()).acceptResolvedAddresses(addresses); - verify(lb2).acceptResolvedAddresses(addresses); - gracefulSwitchLb.handleNameResolutionError(Status.CANCELLED); - verify(lb0, never()).handleNameResolutionError(Status.CANCELLED); - verify(lb1, never()).handleNameResolutionError(Status.CANCELLED); - verify(lb2).handleNameResolutionError(Status.CANCELLED); - - verifyNoMoreInteractions(lb0, lb1, lb2); - } - - @Test - @Deprecated - public void switchTo_shutdownTriggeredWhenSwitchAndForwardedWhenSwitchLbShutdown() { - gracefulSwitchLb.switchTo(lbPolicies[0]); - LoadBalancer lb0 = balancers.get(lbPolicies[0]); - Helper helper0 = helpers.get(lb0); - SubchannelPicker picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(READY, picker); - - gracefulSwitchLb.switchTo(lbPolicies[1]); - LoadBalancer lb1 = balancers.get(lbPolicies[1]); - verify(lb1, never()).shutdown(); - - gracefulSwitchLb.switchTo(lbPolicies[2]); - verify(lb1).shutdown(); - LoadBalancer lb2 = balancers.get(lbPolicies[2]); - verify(lb0, never()).shutdown(); - helpers.get(lb2).updateBalancingState(READY, mock(SubchannelPicker.class)); - verify(lb0).shutdown(); - - gracefulSwitchLb.switchTo(lbPolicies[3]); - LoadBalancer lb3 = balancers.get(lbPolicies[3]); - verify(lb2, never()).shutdown(); - verify(lb3, never()).shutdown(); - - gracefulSwitchLb.shutdown(); - verify(lb2).shutdown(); - verify(lb3).shutdown(); - - verifyNoMoreInteractions(lb0, lb1, lb2, lb3); - } - - @Test - @Deprecated - public void switchTo_requestConnectionForwardedToLatestPolicies() { - gracefulSwitchLb.switchTo(lbPolicies[0]); - LoadBalancer lb0 = balancers.get(lbPolicies[0]); - Helper helper0 = helpers.get(lb0); - SubchannelPicker picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(READY, picker); - - gracefulSwitchLb.requestConnection(); - verify(lb0).requestConnection(); - - gracefulSwitchLb.switchTo(lbPolicies[1]); - LoadBalancer lb1 = balancers.get(lbPolicies[1]); - gracefulSwitchLb.requestConnection(); - verify(lb1).requestConnection(); - - gracefulSwitchLb.switchTo(lbPolicies[2]); - verify(lb1).shutdown(); - LoadBalancer lb2 = balancers.get(lbPolicies[2]); - gracefulSwitchLb.requestConnection(); - verify(lb2).requestConnection(); - - // lb2 reports READY - helpers.get(lb2).updateBalancingState(READY, mock(SubchannelPicker.class)); - verify(lb0).shutdown(); - - gracefulSwitchLb.requestConnection(); - verify(lb2, times(2)).requestConnection(); - - gracefulSwitchLb.switchTo(lbPolicies[3]); - LoadBalancer lb3 = balancers.get(lbPolicies[3]); - gracefulSwitchLb.requestConnection(); - verify(lb3).requestConnection(); - - verifyNoMoreInteractions(lb0, lb1, lb2, lb3); - } - - @Test - @Deprecated - public void switchTo_createSubchannelForwarded() { - gracefulSwitchLb.switchTo(lbPolicies[0]); - LoadBalancer lb0 = balancers.get(lbPolicies[0]); - Helper helper0 = helpers.get(lb0); - SubchannelPicker picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(READY, picker); - - CreateSubchannelArgs createSubchannelArgs = newFakeCreateSubchannelArgs(); - helper0.createSubchannel(createSubchannelArgs); - verify(mockHelper).createSubchannel(createSubchannelArgs); - - gracefulSwitchLb.switchTo(lbPolicies[1]); - LoadBalancer lb1 = balancers.get(lbPolicies[1]); - Helper helper1 = helpers.get(lb1); - createSubchannelArgs = newFakeCreateSubchannelArgs(); - helper1.createSubchannel(createSubchannelArgs); - verify(mockHelper).createSubchannel(createSubchannelArgs); - - createSubchannelArgs = newFakeCreateSubchannelArgs(); - helper0.createSubchannel(createSubchannelArgs); - verify(mockHelper).createSubchannel(createSubchannelArgs); - - verifyNoMoreInteractions(lb0, lb1); - } - - @Test - @Deprecated - public void switchTo_updateBalancingStateIsGraceful() { - gracefulSwitchLb.switchTo(lbPolicies[0]); - LoadBalancer lb0 = balancers.get(lbPolicies[0]); - Helper helper0 = helpers.get(lb0); - SubchannelPicker picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(READY, picker); - verify(mockHelper).updateBalancingState(READY, picker); - - gracefulSwitchLb.switchTo(lbPolicies[1]); - LoadBalancer lb1 = balancers.get(lbPolicies[1]); - Helper helper1 = helpers.get(lb1); - picker = mock(SubchannelPicker.class); - helper1.updateBalancingState(CONNECTING, picker); - verify(mockHelper, never()).updateBalancingState(CONNECTING, picker); - - gracefulSwitchLb.switchTo(lbPolicies[2]); - verify(lb1).shutdown(); - LoadBalancer lb2 = balancers.get(lbPolicies[2]); - Helper helper2 = helpers.get(lb2); - picker = mock(SubchannelPicker.class); - helper2.updateBalancingState(CONNECTING, picker); - verify(mockHelper, never()).updateBalancingState(CONNECTING, picker); - - // lb2 reports READY - SubchannelPicker picker2 = mock(SubchannelPicker.class); - helper2.updateBalancingState(READY, picker2); - verify(lb0).shutdown(); - verify(mockHelper).updateBalancingState(READY, picker2); - - gracefulSwitchLb.switchTo(lbPolicies[3]); - LoadBalancer lb3 = balancers.get(lbPolicies[3]); - Helper helper3 = helpers.get(lb3); - SubchannelPicker picker3 = mock(SubchannelPicker.class); - helper3.updateBalancingState(CONNECTING, picker3); - verify(mockHelper, never()).updateBalancingState(CONNECTING, picker3); - - // lb2 out of READY - picker2 = mock(SubchannelPicker.class); - helper2.updateBalancingState(CONNECTING, picker2); - verify(mockHelper, never()).updateBalancingState(CONNECTING, picker2); - verify(mockHelper).updateBalancingState(CONNECTING, picker3); - verify(lb2).shutdown(); - - picker3 = mock(SubchannelPicker.class); - helper3.updateBalancingState(CONNECTING, picker3); - verify(mockHelper).updateBalancingState(CONNECTING, picker3); - - verifyNoMoreInteractions(lb0, lb1, lb2, lb3); - } - - @Test - @Deprecated - public void switchTo_switchWhileOldPolicyIsNotReady() { - gracefulSwitchLb.switchTo(lbPolicies[0]); - LoadBalancer lb0 = balancers.get(lbPolicies[0]); - Helper helper0 = helpers.get(lb0); - SubchannelPicker picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(READY, picker); - picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(CONNECTING, picker); - - verify(lb0, never()).shutdown(); - gracefulSwitchLb.switchTo(lbPolicies[1]); - verify(lb0).shutdown(); - LoadBalancer lb1 = balancers.get(lbPolicies[1]); - - Helper helper1 = helpers.get(lb1); - picker = mock(SubchannelPicker.class); - helper1.updateBalancingState(CONNECTING, picker); - verify(mockHelper).updateBalancingState(CONNECTING, picker); - - verify(lb1, never()).shutdown(); - gracefulSwitchLb.switchTo(lbPolicies[2]); - verify(lb1).shutdown(); - LoadBalancer lb2 = balancers.get(lbPolicies[2]); - - verifyNoMoreInteractions(lb0, lb1, lb2); - } - - @Test - @Deprecated - public void switchTo_switchWhileOldPolicyGoesFromReadyToNotReady() { - gracefulSwitchLb.switchTo(lbPolicies[0]); - LoadBalancer lb0 = balancers.get(lbPolicies[0]); - Helper helper0 = helpers.get(lb0); - SubchannelPicker picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(READY, picker); - - gracefulSwitchLb.switchTo(lbPolicies[1]); - verify(lb0, never()).shutdown(); - - LoadBalancer lb1 = balancers.get(lbPolicies[1]); - Helper helper1 = helpers.get(lb1); - SubchannelPicker picker1 = mock(SubchannelPicker.class); - helper1.updateBalancingState(CONNECTING, picker1); - verify(mockHelper, never()).updateBalancingState(CONNECTING, picker1); - - picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(CONNECTING, picker); - verify(lb0).shutdown(); - verify(mockHelper, never()).updateBalancingState(CONNECTING, picker); - verify(mockHelper).updateBalancingState(CONNECTING, picker1); - - picker1 = mock(SubchannelPicker.class); - helper1.updateBalancingState(READY, picker1); - verify(mockHelper).updateBalancingState(READY, picker1); - - verifyNoMoreInteractions(lb0, lb1); - } - - @Test - @Deprecated - public void switchTo_switchWhileOldPolicyGoesFromReadyToNotReadyWhileNewPolicyStillIdle() { - gracefulSwitchLb.switchTo(lbPolicies[0]); - LoadBalancer lb0 = balancers.get(lbPolicies[0]); - InOrder inOrder = inOrder(lb0, mockHelper); - Helper helper0 = helpers.get(lb0); - SubchannelPicker picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(READY, picker); - - gracefulSwitchLb.switchTo(lbPolicies[1]); - verify(lb0, never()).shutdown(); - - LoadBalancer lb1 = balancers.get(lbPolicies[1]); - Helper helper1 = helpers.get(lb1); - - picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(CONNECTING, picker); - - verify(mockHelper, never()).updateBalancingState(CONNECTING, picker); - inOrder.verify(mockHelper).updateBalancingState(CONNECTING, BUFFER_PICKER); - inOrder.verify(lb0).shutdown(); // shutdown after update - - picker = mock(SubchannelPicker.class); - helper1.updateBalancingState(CONNECTING, picker); - inOrder.verify(mockHelper).updateBalancingState(CONNECTING, picker); - - inOrder.verifyNoMoreInteractions(); - verifyNoMoreInteractions(lb1); - } - - @Test - @Deprecated - public void switchTo_newPolicyNameTheSameAsPendingPolicy_shouldHaveNoEffect() { - gracefulSwitchLb.switchTo(lbPolicies[0]); - LoadBalancer lb0 = balancers.get(lbPolicies[0]); - Helper helper0 = helpers.get(lb0); - SubchannelPicker picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(READY, picker); - - gracefulSwitchLb.switchTo(lbPolicies[1]); - LoadBalancer lb1 = balancers.get(lbPolicies[1]); - - gracefulSwitchLb.switchTo(lbPolicies[1]); - assertThat(balancers.get(lbPolicies[1])).isSameInstanceAs(lb1); - - verifyNoMoreInteractions(lb0, lb1); - } - - @Test - @Deprecated - public void switchTo_newPolicyNameTheSameAsCurrentPolicy_shouldShutdownPendingLb() { - gracefulSwitchLb.switchTo(lbPolicies[0]); - LoadBalancer lb0 = balancers.get(lbPolicies[0]); - - gracefulSwitchLb.switchTo(lbPolicies[0]); - assertThat(balancers.get(lbPolicies[0])).isSameInstanceAs(lb0); - - Helper helper0 = helpers.get(lb0); - SubchannelPicker picker = mock(SubchannelPicker.class); - helper0.updateBalancingState(READY, picker); - - gracefulSwitchLb.switchTo(lbPolicies[1]); - LoadBalancer lb1 = balancers.get(lbPolicies[1]); - - gracefulSwitchLb.switchTo(lbPolicies[0]); - verify(lb1).shutdown(); - assertThat(balancers.get(lbPolicies[0])).isSameInstanceAs(lb0); - - verifyNoMoreInteractions(lb0, lb1); - } - - - @Test - @Deprecated - public void switchTo_newLbFactoryEqualToOldOneShouldHaveNoEffect() { - final List balancers = new ArrayList<>(); - - final class LoadBalancerFactoryWithId extends LoadBalancer.Factory { - final int id; - - LoadBalancerFactoryWithId(int id) { - this.id = id; - } - - @Override - public LoadBalancer newLoadBalancer(Helper helper) { - LoadBalancer balancer = mock(LoadBalancer.class); - balancers.add(balancer); - return balancer; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof LoadBalancerFactoryWithId)) { - return false; - } - LoadBalancerFactoryWithId that = (LoadBalancerFactoryWithId) o; - return id == that.id; - } - - @Override - public int hashCode() { - return id; - } - } - - gracefulSwitchLb.switchTo(new LoadBalancerFactoryWithId(0)); - assertThat(balancers).hasSize(1); - LoadBalancer lb0 = balancers.get(0); - - gracefulSwitchLb.switchTo(new LoadBalancerFactoryWithId(0)); - assertThat(balancers).hasSize(1); - - gracefulSwitchLb.switchTo(new LoadBalancerFactoryWithId(1)); - assertThat(balancers).hasSize(2); - LoadBalancer lb1 = balancers.get(1); - verify(lb0).shutdown(); - - verifyNoMoreInteractions(lb0, lb1); - } - - // END OF OLD TESTS - @Test public void transientFailureOnInitialResolutionError() { gracefulSwitchLb.handleNameResolutionError(Status.DATA_LOSS); From 110c1ff0d66b9420f735ca8d738ed60483e0aa01 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 20 Feb 2025 15:13:53 -0800 Subject: [PATCH 197/591] xds: Use acceptResolvedAddresses() for PriorityLb children PriorityLb should propagate config problems up to the name resolver so it can refresh. --- .../io/grpc/xds/PriorityLoadBalancer.java | 41 ++-- .../io/grpc/xds/PriorityLoadBalancerTest.java | 199 ++++++++++++++++-- 2 files changed, 207 insertions(+), 33 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java b/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java index 259ae7406c1..845c167a643 100644 --- a/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java @@ -91,6 +91,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { checkNotNull(config, "missing priority lb config"); priorityNames = config.priorities; priorityConfigs = config.childConfigs; + Status status = Status.OK; Set prioritySet = new HashSet<>(config.priorities); ArrayList childKeys = new ArrayList<>(children.keySet()); for (String priority : childKeys) { @@ -105,12 +106,18 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { for (String priority : priorityNames) { ChildLbState childLbState = children.get(priority); if (childLbState != null) { - childLbState.updateResolvedAddresses(); + Status newStatus = childLbState.updateResolvedAddresses(); + if (!newStatus.isOk()) { + status = newStatus; + } } } handlingResolvedAddresses = false; - tryNextPriority(); - return Status.OK; + Status newStatus = tryNextPriority(); + if (!newStatus.isOk()) { + status = newStatus; + } + return status; } @Override @@ -140,7 +147,7 @@ public void shutdown() { children.clear(); } - private void tryNextPriority() { + private Status tryNextPriority() { for (int i = 0; i < priorityNames.size(); i++) { String priority = priorityNames.get(i); if (!children.containsKey(priority)) { @@ -151,8 +158,7 @@ private void tryNextPriority() { // Calling the child's updateResolvedAddresses() can result in tryNextPriority() being // called recursively. We need to be sure to be done with processing here before it is // called. - child.updateResolvedAddresses(); - return; // Give priority i time to connect. + return child.updateResolvedAddresses(); // Give priority i time to connect. } ChildLbState child = children.get(priority); child.reactivate(); @@ -165,16 +171,16 @@ private void tryNextPriority() { children.get(p).deactivate(); } } - return; + return Status.OK; } if (child.failOverTimer != null && child.failOverTimer.isPending()) { updateOverallState(priority, child.connectivityState, child.picker); - return; // Give priority i time to connect. + return Status.OK; // Give priority i time to connect. } if (priority.equals(currentPriority) && child.connectivityState != TRANSIENT_FAILURE) { // If the current priority is not changed into TRANSIENT_FAILURE, keep using it. updateOverallState(priority, child.connectivityState, child.picker); - return; + return Status.OK; } } // TODO(zdapeng): Include error details of each priority. @@ -182,6 +188,7 @@ private void tryNextPriority() { String lastPriority = priorityNames.get(priorityNames.size() - 1); SubchannelPicker errorPicker = children.get(lastPriority).picker; updateOverallState(lastPriority, TRANSIENT_FAILURE, errorPicker); + return Status.OK; } private void updateOverallState( @@ -228,7 +235,11 @@ public void run() { Status.UNAVAILABLE.withDescription("Connection timeout for priority " + priority))); logger.log(XdsLogLevel.DEBUG, "Priority {0} failed over to next", priority); currentPriority = null; // reset currentPriority to guarantee failover happen - tryNextPriority(); + Status status = tryNextPriority(); + if (!status.isOk()) { + // A child had a problem with the addresses/config. Request it to be refreshed + helper.refreshNameResolution(); + } } } @@ -279,10 +290,10 @@ void tearDown() { * resolvedAddresses}, or when priority lb receives a new resolved addresses while the child * already exists. */ - void updateResolvedAddresses() { + Status updateResolvedAddresses() { PriorityLbConfig config = (PriorityLbConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); - lb.handleResolvedAddresses( + return lb.acceptResolvedAddresses( resolvedAddresses.toBuilder() .setAddresses(AddressFilter.filter(resolvedAddresses.getAddresses(), priority)) .setLoadBalancingPolicyConfig(config.childConfigs.get(priority).childConfig) @@ -331,7 +342,11 @@ public void updateBalancingState(final ConnectivityState newState, // If we are currently handling newly resolved addresses, let's not try to reconfigure as // the address handling process will take care of that to provide an atomic config update. if (!handlingResolvedAddresses) { - tryNextPriority(); + Status status = tryNextPriority(); + if (!status.isOk()) { + // A child had a problem with the addresses/config. Request it to be refreshed + helper.refreshNameResolution(); + } } } diff --git a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java index fafcd4d674a..9823501dcd9 100644 --- a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java @@ -33,6 +33,7 @@ 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.ImmutableMap; @@ -97,6 +98,8 @@ public void uncaughtException(Thread t, Throwable e) { public LoadBalancer newLoadBalancer(Helper helper) { fooHelpers.add(helper); LoadBalancer childBalancer = mock(LoadBalancer.class); + when(childBalancer.acceptResolvedAddresses(any(ResolvedAddresses.class))) + .thenReturn(Status.OK); fooBalancers.add(childBalancer); return childBalancer; } @@ -107,6 +110,8 @@ public LoadBalancer newLoadBalancer(Helper helper) { @Override public LoadBalancer newLoadBalancer(Helper helper) { LoadBalancer childBalancer = mock(LoadBalancer.class); + when(childBalancer.acceptResolvedAddresses(any(ResolvedAddresses.class))) + .thenReturn(Status.OK); barBalancers.add(childBalancer); return childBalancer; } @@ -141,7 +146,7 @@ public void tearDown() { } @Test - public void handleResolvedAddresses() { + public void acceptResolvedAddresses() { SocketAddress socketAddress = new InetSocketAddress(8080); EquivalentAddressGroup eag = new EquivalentAddressGroup(socketAddress); eag = AddressFilter.setPathFilter(eag, ImmutableList.of("p1")); @@ -162,16 +167,17 @@ public void handleResolvedAddresses() { ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1, "p2", priorityChildConfig2), ImmutableList.of("p0", "p1", "p2")); - priorityLb.handleResolvedAddresses( + Status status = priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(addresses) .setAttributes(attributes) .setLoadBalancingPolicyConfig(priorityLbConfig) .build()); + assertThat(status.getCode()).isEqualTo(Status.Code.OK); assertThat(fooBalancers).hasSize(1); assertThat(barBalancers).isEmpty(); LoadBalancer fooBalancer0 = Iterables.getOnlyElement(fooBalancers); - verify(fooBalancer0).handleResolvedAddresses(resolvedAddressesCaptor.capture()); + verify(fooBalancer0).acceptResolvedAddresses(resolvedAddressesCaptor.capture()); ResolvedAddresses addressesReceived = resolvedAddressesCaptor.getValue(); assertThat(addressesReceived.getAddresses()).isEmpty(); assertThat(addressesReceived.getAttributes()).isEqualTo(attributes); @@ -182,7 +188,7 @@ public void handleResolvedAddresses() { assertThat(fooBalancers).hasSize(1); assertThat(barBalancers).hasSize(1); LoadBalancer barBalancer0 = Iterables.getOnlyElement(barBalancers); - verify(barBalancer0).handleResolvedAddresses(resolvedAddressesCaptor.capture()); + verify(barBalancer0).acceptResolvedAddresses(resolvedAddressesCaptor.capture()); addressesReceived = resolvedAddressesCaptor.getValue(); assertThat(Iterables.getOnlyElement(addressesReceived.getAddresses()).getAddresses()) .containsExactly(socketAddress); @@ -194,7 +200,7 @@ public void handleResolvedAddresses() { assertThat(fooBalancers).hasSize(2); assertThat(barBalancers).hasSize(1); LoadBalancer fooBalancer1 = Iterables.getLast(fooBalancers); - verify(fooBalancer1).handleResolvedAddresses(resolvedAddressesCaptor.capture()); + verify(fooBalancer1).acceptResolvedAddresses(resolvedAddressesCaptor.capture()); addressesReceived = resolvedAddressesCaptor.getValue(); assertThat(addressesReceived.getAddresses()).isEmpty(); assertThat(addressesReceived.getAttributes()).isEqualTo(attributes); @@ -211,14 +217,15 @@ public void handleResolvedAddresses() { ImmutableMap.of("p1", new PriorityChildConfig(newChildConfig(barLbProvider, newBarConfig), true)), ImmutableList.of("p1")); - priorityLb.handleResolvedAddresses( + status = priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(newAddresses) .setLoadBalancingPolicyConfig(newPriorityLbConfig) .build()); + assertThat(status.getCode()).isEqualTo(Status.Code.OK); assertThat(fooBalancers).hasSize(2); assertThat(barBalancers).hasSize(1); - verify(barBalancer0, times(2)).handleResolvedAddresses(resolvedAddressesCaptor.capture()); + verify(barBalancer0, times(2)).acceptResolvedAddresses(resolvedAddressesCaptor.capture()); addressesReceived = resolvedAddressesCaptor.getValue(); assertThat(Iterables.getOnlyElement(addressesReceived.getAddresses()).getAddresses()) .containsExactly(newSocketAddress); @@ -232,6 +239,60 @@ public void handleResolvedAddresses() { verify(barBalancer0, never()).shutdown(); } + @Test + public void acceptResolvedAddresses_propagatesChildFailures() { + LoadBalancerProvider lbProvider = new CannedLoadBalancer.Provider(); + CannedLoadBalancer.Config internalTf = new CannedLoadBalancer.Config( + Status.INTERNAL, TRANSIENT_FAILURE); + CannedLoadBalancer.Config okTf = new CannedLoadBalancer.Config(Status.OK, TRANSIENT_FAILURE); + ResolvedAddresses resolvedAddresses = ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.of()) + .setAttributes(Attributes.EMPTY) + .build(); + + // tryNewPriority() propagates status + Status status = priorityLb.acceptResolvedAddresses( + resolvedAddresses.toBuilder() + .setLoadBalancingPolicyConfig(new PriorityLbConfig( + ImmutableMap.of( + "p0", newPriorityChildConfig(lbProvider, internalTf, true)), + ImmutableList.of("p0"))) + .build()); + assertThat(status.getCode()).isNotEqualTo(Status.Code.OK); + + // Updating a child propagates status + status = priorityLb.acceptResolvedAddresses( + resolvedAddresses.toBuilder() + .setLoadBalancingPolicyConfig(new PriorityLbConfig( + ImmutableMap.of( + "p0", newPriorityChildConfig(lbProvider, internalTf, true)), + ImmutableList.of("p0"))) + .build()); + assertThat(status.getCode()).isNotEqualTo(Status.Code.OK); + + // A single pre-existing child failure propagates + status = priorityLb.acceptResolvedAddresses( + resolvedAddresses.toBuilder() + .setLoadBalancingPolicyConfig(new PriorityLbConfig( + ImmutableMap.of( + "p0", newPriorityChildConfig(lbProvider, okTf, true), + "p1", newPriorityChildConfig(lbProvider, okTf, true), + "p2", newPriorityChildConfig(lbProvider, okTf, true)), + ImmutableList.of("p0", "p1", "p2"))) + .build()); + assertThat(status.getCode()).isEqualTo(Status.Code.OK); + status = priorityLb.acceptResolvedAddresses( + resolvedAddresses.toBuilder() + .setLoadBalancingPolicyConfig(new PriorityLbConfig( + ImmutableMap.of( + "p0", newPriorityChildConfig(lbProvider, okTf, true), + "p1", newPriorityChildConfig(lbProvider, internalTf, true), + "p2", newPriorityChildConfig(lbProvider, okTf, true)), + ImmutableList.of("p0", "p1", "p2"))) + .build()); + assertThat(status.getCode()).isNotEqualTo(Status.Code.OK); + } + @Test public void handleNameResolutionError() { Object fooConfig0 = new Object(); @@ -243,7 +304,7 @@ public void handleNameResolutionError() { PriorityLbConfig priorityLbConfig = new PriorityLbConfig(ImmutableMap.of("p0", priorityChildConfig0), ImmutableList.of("p0")); - priorityLb.handleResolvedAddresses( + priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) @@ -255,7 +316,7 @@ public void handleNameResolutionError() { priorityLbConfig = new PriorityLbConfig(ImmutableMap.of("p1", priorityChildConfig1), ImmutableList.of("p1")); - priorityLb.handleResolvedAddresses( + priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) @@ -286,7 +347,7 @@ public void typicalPriorityFailOverFlow() { ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1, "p2", priorityChildConfig2, "p3", priorityChildConfig3), ImmutableList.of("p0", "p1", "p2", "p3")); - priorityLb.handleResolvedAddresses( + priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) @@ -419,7 +480,7 @@ public void idleToConnectingDoesNotTriggerFailOver() { new PriorityLbConfig( ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1), ImmutableList.of("p0", "p1")); - priorityLb.handleResolvedAddresses( + priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) @@ -455,7 +516,7 @@ public void connectingResetFailOverIfSeenReadyOrIdleSinceTransientFailure() { new PriorityLbConfig( ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1), ImmutableList.of("p0", "p1")); - priorityLb.handleResolvedAddresses( + priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) @@ -497,7 +558,7 @@ public void readyToConnectDoesNotFailOverButUpdatesPicker() { new PriorityLbConfig( ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1), ImmutableList.of("p0", "p1")); - priorityLb.handleResolvedAddresses( + priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) @@ -530,7 +591,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { // resolution update without priority change does not trigger failover Attributes.Key fooKey = Attributes.Key.create("fooKey"); - priorityLb.handleResolvedAddresses( + priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) @@ -559,7 +620,7 @@ public void typicalPriorityFailOverFlowWithIdleUpdate() { ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1, "p2", priorityChildConfig2, "p3", priorityChildConfig3), ImmutableList.of("p0", "p1", "p2", "p3")); - priorityLb.handleResolvedAddresses( + priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) @@ -652,6 +713,55 @@ public void typicalPriorityFailOverFlowWithIdleUpdate() { verify(balancer3).shutdown(); } + @Test + public void failover_propagatesChildFailures() { + LoadBalancerProvider lbProvider = new CannedLoadBalancer.Provider(); + ResolvedAddresses resolvedAddresses = ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.of()) + .setAttributes(Attributes.EMPTY) + .build(); + + Status status = priorityLb.acceptResolvedAddresses( + resolvedAddresses.toBuilder() + .setLoadBalancingPolicyConfig(new PriorityLbConfig( + ImmutableMap.of( + "p0", newPriorityChildConfig( + lbProvider, new CannedLoadBalancer.Config(Status.OK, TRANSIENT_FAILURE), true), + "p1", newPriorityChildConfig( + lbProvider, new CannedLoadBalancer.Config(Status.INTERNAL, CONNECTING), true)), + ImmutableList.of("p0", "p1"))) + .build()); + // Since P1's activation wasn't noticed by the result status, it triggered name resolution + assertThat(status.getCode()).isEqualTo(Status.Code.OK); + verify(helper).refreshNameResolution(); + } + + @Test + public void failoverTimer_propagatesChildFailures() { + LoadBalancerProvider lbProvider = new CannedLoadBalancer.Provider(); + ResolvedAddresses resolvedAddresses = ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.of()) + .setAttributes(Attributes.EMPTY) + .build(); + + Status status = priorityLb.acceptResolvedAddresses( + resolvedAddresses.toBuilder() + .setLoadBalancingPolicyConfig(new PriorityLbConfig( + ImmutableMap.of( + "p0", newPriorityChildConfig( + lbProvider, new CannedLoadBalancer.Config(Status.OK, CONNECTING), true), + "p1", newPriorityChildConfig( + lbProvider, new CannedLoadBalancer.Config(Status.INTERNAL, CONNECTING), true)), + ImmutableList.of("p0", "p1"))) + .build()); + assertThat(status.getCode()).isEqualTo(Status.Code.OK); + + // P1's activation will refresh name resolution + verify(helper, never()).refreshNameResolution(); + fakeClock.forwardTime(10, TimeUnit.SECONDS); + verify(helper).refreshNameResolution(); + } + @Test public void bypassReresolutionRequestsIfConfiged() { PriorityChildConfig priorityChildConfig0 = @@ -662,7 +772,7 @@ public void bypassReresolutionRequestsIfConfiged() { new PriorityLbConfig( ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1), ImmutableList.of("p0", "p1")); - priorityLb.handleResolvedAddresses( + priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) @@ -690,7 +800,7 @@ public void raceBetweenShutdownAndChildLbBalancingStateUpdate() { new PriorityLbConfig( ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1), ImmutableList.of("p0", "p1")); - priorityLb.handleResolvedAddresses( + priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) @@ -717,7 +827,7 @@ public void noDuplicateOverallBalancingStateUpdate() { new PriorityLbConfig( ImmutableMap.of("p0", priorityChildConfig0), ImmutableList.of("p0")); - priorityLb.handleResolvedAddresses( + priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) @@ -727,7 +837,7 @@ public void noDuplicateOverallBalancingStateUpdate() { new PriorityLbConfig( ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1), ImmutableList.of("p0", "p1")); - priorityLb.handleResolvedAddresses( + priorityLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) @@ -769,6 +879,11 @@ private Object newChildConfig(LoadBalancerProvider provider, Object config) { return GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig(provider, config); } + private PriorityChildConfig newPriorityChildConfig( + LoadBalancerProvider provider, Object config, boolean ignoreRefresh) { + return new PriorityChildConfig(newChildConfig(provider, config), ignoreRefresh); + } + private static class FakeLoadBalancerProvider extends LoadBalancerProvider { @Override @@ -801,9 +916,10 @@ static class FakeLoadBalancer extends LoadBalancer { } @Override - public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { + public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { helper.updateBalancingState( TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(Status.INTERNAL))); + return Status.OK; } @Override @@ -814,4 +930,47 @@ public void handleNameResolutionError(Status error) { public void shutdown() { } } + + static final class CannedLoadBalancer extends LoadBalancer { + private final Helper helper; + + private CannedLoadBalancer(Helper helper) { + this.helper = helper; + } + + @Override + public Status acceptResolvedAddresses(ResolvedAddresses addresses) { + Config config = (Config) addresses.getLoadBalancingPolicyConfig(); + helper.updateBalancingState( + config.state, new FixedResultPicker(PickResult.withError(Status.INTERNAL))); + return config.resolvedAddressesResult; + } + + @Override + public void handleNameResolutionError(Status status) {} + + @Override + public void shutdown() {} + + static final class Provider extends StandardLoadBalancerProvider { + public Provider() { + super("echo"); + } + + @Override + public LoadBalancer newLoadBalancer(Helper helper) { + return new CannedLoadBalancer(helper); + } + } + + static final class Config { + final Status resolvedAddressesResult; + final ConnectivityState state; + + public Config(Status resolvedAddressesResult, ConnectivityState state) { + this.resolvedAddressesResult = resolvedAddressesResult; + this.state = state; + } + } + } } From 57124d6b29d180de2ab7f21baeb4a2246e490c1a Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 20 Feb 2025 15:48:12 -0800 Subject: [PATCH 198/591] Use acceptResolvedAddresses() in easy cases We want to move away from handleResolvedAddresses(). These are "easy" in that they need no logic. LBs extending ForwardingLoadBalancer had the method duplicated from handleResolvedAddresses() and swapped away from `super` because ForwardingLoadBalancer only forwards handleResolvedAddresses() reliably today. Duplicating small methods was less bug-prone than dealing with ForwardingLoadBalancer. --- .../RpcBehaviorLoadBalancerProvider.java | 7 ++ .../RpcBehaviorLoadBalancerProviderTest.java | 9 ++ .../HealthCheckingLoadBalancerFactory.java | 13 ++- ...HealthCheckingLoadBalancerFactoryTest.java | 97 ++++++++++--------- .../util/OutlierDetectionLoadBalancer.java | 3 +- .../OutlierDetectionLoadBalancerTest.java | 2 +- .../io/grpc/xds/WrrLocalityLoadBalancer.java | 4 +- .../java/io/grpc/xds/orca/OrcaOobUtil.java | 2 +- .../xds/ClusterManagerLoadBalancerTest.java | 2 +- .../xds/MetadataLoadBalancerProvider.java | 8 ++ .../grpc/xds/WrrLocalityLoadBalancerTest.java | 10 +- 11 files changed, 95 insertions(+), 62 deletions(-) diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/RpcBehaviorLoadBalancerProvider.java b/interop-testing/src/main/java/io/grpc/testing/integration/RpcBehaviorLoadBalancerProvider.java index 83c416765ec..65f1c46892b 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/RpcBehaviorLoadBalancerProvider.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/RpcBehaviorLoadBalancerProvider.java @@ -116,6 +116,13 @@ public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { ((RpcBehaviorConfig) resolvedAddresses.getLoadBalancingPolicyConfig()).rpcBehavior); delegateLb.handleResolvedAddresses(resolvedAddresses); } + + @Override + public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + helper.setRpcBehavior( + ((RpcBehaviorConfig) resolvedAddresses.getLoadBalancingPolicyConfig()).rpcBehavior); + return delegateLb.acceptResolvedAddresses(resolvedAddresses); + } } /** diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/RpcBehaviorLoadBalancerProviderTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/RpcBehaviorLoadBalancerProviderTest.java index 02ede46bcdd..f17ea059211 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/RpcBehaviorLoadBalancerProviderTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/RpcBehaviorLoadBalancerProviderTest.java @@ -87,6 +87,15 @@ public void handleResolvedAddressesDelegated() { verify(mockDelegateLb).handleResolvedAddresses(resolvedAddresses); } + @Test + public void acceptResolvedAddressesDelegated() { + RpcBehaviorLoadBalancer lb = new RpcBehaviorLoadBalancer(new RpcBehaviorHelper(mockHelper), + mockDelegateLb); + ResolvedAddresses resolvedAddresses = buildResolvedAddresses(buildConfig()); + lb.acceptResolvedAddresses(resolvedAddresses); + verify(mockDelegateLb).acceptResolvedAddresses(resolvedAddresses); + } + @Test public void helperWrapsPicker() { RpcBehaviorHelper helper = new RpcBehaviorHelper(mockHelper); diff --git a/services/src/main/java/io/grpc/protobuf/services/HealthCheckingLoadBalancerFactory.java b/services/src/main/java/io/grpc/protobuf/services/HealthCheckingLoadBalancerFactory.java index cac522caf9e..8cf1458f5dc 100644 --- a/services/src/main/java/io/grpc/protobuf/services/HealthCheckingLoadBalancerFactory.java +++ b/services/src/main/java/io/grpc/protobuf/services/HealthCheckingLoadBalancerFactory.java @@ -194,7 +194,18 @@ public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { .get(LoadBalancer.ATTR_HEALTH_CHECKING_CONFIG); String serviceName = ServiceConfigUtil.getHealthCheckedServiceName(healthCheckingConfig); helper.setHealthCheckedService(serviceName); - super.handleResolvedAddresses(resolvedAddresses); + delegate.handleResolvedAddresses(resolvedAddresses); + } + + @Override + public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + Map healthCheckingConfig = + resolvedAddresses + .getAttributes() + .get(LoadBalancer.ATTR_HEALTH_CHECKING_CONFIG); + String serviceName = ServiceConfigUtil.getHealthCheckedServiceName(healthCheckingConfig); + helper.setHealthCheckedService(serviceName); + return delegate.acceptResolvedAddresses(resolvedAddresses); } @Override diff --git a/services/src/test/java/io/grpc/protobuf/services/HealthCheckingLoadBalancerFactoryTest.java b/services/src/test/java/io/grpc/protobuf/services/HealthCheckingLoadBalancerFactoryTest.java index 08a33106fb9..a49c426f7e1 100644 --- a/services/src/test/java/io/grpc/protobuf/services/HealthCheckingLoadBalancerFactoryTest.java +++ b/services/src/test/java/io/grpc/protobuf/services/HealthCheckingLoadBalancerFactoryTest.java @@ -206,15 +206,16 @@ public void setup() throws Exception { boolean shutdown; @Override - public void handleResolvedAddresses(final ResolvedAddresses resolvedAddresses) { + public Status acceptResolvedAddresses(final ResolvedAddresses resolvedAddresses) { syncContext.execute(new Runnable() { @Override public void run() { if (!shutdown) { - hcLb.handleResolvedAddresses(resolvedAddresses); + hcLb.acceptResolvedAddresses(resolvedAddresses); } } }); + return Status.OK; } @Override @@ -264,9 +265,9 @@ public void typicalWorkflow() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result); + hcLbEventDelivery.acceptResolvedAddresses(result); - verify(origLb).handleResolvedAddresses(result); + verify(origLb).acceptResolvedAddresses(result); verify(origHelper, atLeast(0)).getSynchronizationContext(); verify(origHelper, atLeast(0)).getScheduledExecutorService(); verifyNoMoreInteractions(origHelper); @@ -404,9 +405,9 @@ public void healthCheckDisabledWhenServiceNotImplemented() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result); + hcLbEventDelivery.acceptResolvedAddresses(result); - verify(origLb).handleResolvedAddresses(result); + verify(origLb).acceptResolvedAddresses(result); verifyNoMoreInteractions(origLb); // We create 2 Subchannels. One of them connects to a server that doesn't implement health check @@ -489,9 +490,9 @@ public void backoffRetriesWhenServerErroneouslyClosesRpcBeforeAnyResponse() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result); + hcLbEventDelivery.acceptResolvedAddresses(result); - verify(origLb).handleResolvedAddresses(result); + verify(origLb).acceptResolvedAddresses(result); verifyNoMoreInteractions(origLb); SubchannelStateListener mockHealthListener = mockHealthListeners[0]; @@ -567,9 +568,9 @@ public void serverRespondResetsBackoff() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result); + hcLbEventDelivery.acceptResolvedAddresses(result); - verify(origLb).handleResolvedAddresses(result); + verify(origLb).acceptResolvedAddresses(result); verifyNoMoreInteractions(origLb); SubchannelStateListener mockStateListener = mockStateListeners[0]; @@ -667,9 +668,9 @@ public void serviceConfigHasNoHealthCheckingInitiallyButDoesLater() { .setAddresses(resolvedAddressList) .setAttributes(Attributes.EMPTY) .build(); - hcLbEventDelivery.handleResolvedAddresses(result1); + hcLbEventDelivery.acceptResolvedAddresses(result1); - verify(origLb).handleResolvedAddresses(result1); + verify(origLb).acceptResolvedAddresses(result1); verifyNoMoreInteractions(origLb); // First, create Subchannels 0 @@ -688,8 +689,8 @@ public void serviceConfigHasNoHealthCheckingInitiallyButDoesLater() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result2); - verify(origLb).handleResolvedAddresses(result2); + hcLbEventDelivery.acceptResolvedAddresses(result2); + verify(origLb).acceptResolvedAddresses(result2); // Health check started on existing Subchannel assertThat(healthImpls[0].calls).hasSize(1); @@ -711,9 +712,9 @@ public void serviceConfigDisablesHealthCheckWhenRpcActive() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result1); + hcLbEventDelivery.acceptResolvedAddresses(result1); - verify(origLb).handleResolvedAddresses(result1); + verify(origLb).acceptResolvedAddresses(result1); verifyNoMoreInteractions(origLb); Subchannel subchannel = createSubchannel(0, Attributes.EMPTY, maybeGetMockListener()); @@ -738,7 +739,7 @@ public void serviceConfigDisablesHealthCheckWhenRpcActive() { .setAddresses(resolvedAddressList) .setAttributes(Attributes.EMPTY) .build(); - hcLbEventDelivery.handleResolvedAddresses(result2); + hcLbEventDelivery.acceptResolvedAddresses(result2); // Health check RPC cancelled. assertThat(serverCall.cancelled).isTrue(); @@ -746,7 +747,7 @@ public void serviceConfigDisablesHealthCheckWhenRpcActive() { inOrder.verify(getMockListener()).onSubchannelState( eq(ConnectivityStateInfo.forNonError(READY))); - inOrder.verify(origLb).handleResolvedAddresses(result2); + inOrder.verify(origLb).acceptResolvedAddresses(result2); verifyNoMoreInteractions(origLb, mockStateListeners[0]); assertThat(healthImpl.calls).isEmpty(); @@ -759,9 +760,9 @@ public void serviceConfigDisablesHealthCheckWhenRetryPending() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result); + hcLbEventDelivery.acceptResolvedAddresses(result); - verify(origLb).handleResolvedAddresses(result); + verify(origLb).acceptResolvedAddresses(result); verifyNoMoreInteractions(origLb); SubchannelStateListener mockHealthListener = mockHealthListeners[0]; @@ -793,7 +794,7 @@ public void serviceConfigDisablesHealthCheckWhenRetryPending() { .setAddresses(resolvedAddressList) .setAttributes(Attributes.EMPTY) .build(); - hcLbEventDelivery.handleResolvedAddresses(result2); + hcLbEventDelivery.acceptResolvedAddresses(result2); // Retry timer is cancelled assertThat(clock.getPendingTasks()).isEmpty(); @@ -805,7 +806,7 @@ public void serviceConfigDisablesHealthCheckWhenRetryPending() { inOrder.verify(getMockListener()).onSubchannelState( eq(ConnectivityStateInfo.forNonError(READY))); - inOrder.verify(origLb).handleResolvedAddresses(result2); + inOrder.verify(origLb).acceptResolvedAddresses(result2); verifyNoMoreInteractions(origLb, mockStateListeners[0]); } @@ -817,9 +818,9 @@ public void serviceConfigDisablesHealthCheckWhenRpcInactive() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result1); + hcLbEventDelivery.acceptResolvedAddresses(result1); - verify(origLb).handleResolvedAddresses(result1); + verify(origLb).acceptResolvedAddresses(result1); verifyNoMoreInteractions(origLb); Subchannel subchannel = createSubchannel(0, Attributes.EMPTY, maybeGetMockListener()); @@ -842,9 +843,9 @@ public void serviceConfigDisablesHealthCheckWhenRpcInactive() { .setAddresses(resolvedAddressList) .setAttributes(Attributes.EMPTY) .build(); - hcLbEventDelivery.handleResolvedAddresses(result2); + hcLbEventDelivery.acceptResolvedAddresses(result2); - inOrder.verify(origLb).handleResolvedAddresses(result2); + inOrder.verify(origLb).acceptResolvedAddresses(result2); // Underlying subchannel is now ready deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY)); @@ -870,9 +871,9 @@ public void serviceConfigChangesServiceNameWhenRpcActive() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result1); + hcLbEventDelivery.acceptResolvedAddresses(result1); - verify(origLb).handleResolvedAddresses(result1); + verify(origLb).acceptResolvedAddresses(result1); verifyNoMoreInteractions(origLb); SubchannelStateListener mockHealthListener = mockHealthListeners[0]; @@ -900,9 +901,9 @@ public void serviceConfigChangesServiceNameWhenRpcActive() { eq(ConnectivityStateInfo.forNonError(READY))); // Service config returns with the same health check name. - hcLbEventDelivery.handleResolvedAddresses(result1); + hcLbEventDelivery.acceptResolvedAddresses(result1); // It's delivered to origLb, but nothing else happens - inOrder.verify(origLb).handleResolvedAddresses(result1); + inOrder.verify(origLb).acceptResolvedAddresses(result1); verifyNoMoreInteractions(origLb, mockListener); // Service config returns a different health check name. @@ -911,8 +912,8 @@ public void serviceConfigChangesServiceNameWhenRpcActive() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result2); - inOrder.verify(origLb).handleResolvedAddresses(result2); + hcLbEventDelivery.acceptResolvedAddresses(result2); + inOrder.verify(origLb).acceptResolvedAddresses(result2); // Current health check RPC cancelled. assertThat(serverCall.cancelled).isTrue(); @@ -934,9 +935,9 @@ public void serviceConfigChangesServiceNameWhenRetryPending() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result1); + hcLbEventDelivery.acceptResolvedAddresses(result1); - verify(origLb).handleResolvedAddresses(result1); + verify(origLb).acceptResolvedAddresses(result1); verifyNoMoreInteractions(origLb); SubchannelStateListener mockHealthListener = mockHealthListeners[0]; @@ -969,9 +970,9 @@ public void serviceConfigChangesServiceNameWhenRetryPending() { // Service config returns with the same health check name. - hcLbEventDelivery.handleResolvedAddresses(result1); + hcLbEventDelivery.acceptResolvedAddresses(result1); // It's delivered to origLb, but nothing else happens - inOrder.verify(origLb).handleResolvedAddresses(result1); + inOrder.verify(origLb).acceptResolvedAddresses(result1); verifyNoMoreInteractions(origLb, mockListener); assertThat(clock.getPendingTasks()).hasSize(1); assertThat(healthImpl.calls).isEmpty(); @@ -982,12 +983,12 @@ public void serviceConfigChangesServiceNameWhenRetryPending() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result2); + hcLbEventDelivery.acceptResolvedAddresses(result2); // Concluded CONNECTING state inOrder.verify(getMockListener()).onSubchannelState( eq(ConnectivityStateInfo.forNonError(CONNECTING))); - inOrder.verify(origLb).handleResolvedAddresses(result2); + inOrder.verify(origLb).acceptResolvedAddresses(result2); // Current retry timer cancelled assertThat(clock.getPendingTasks()).isEmpty(); @@ -1008,9 +1009,9 @@ public void serviceConfigChangesServiceNameWhenRpcInactive() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result1); + hcLbEventDelivery.acceptResolvedAddresses(result1); - verify(origLb).handleResolvedAddresses(result1); + verify(origLb).acceptResolvedAddresses(result1); verifyNoMoreInteractions(origLb); Subchannel subchannel = createSubchannel(0, Attributes.EMPTY, maybeGetMockListener()); @@ -1031,9 +1032,9 @@ public void serviceConfigChangesServiceNameWhenRpcInactive() { inOrder.verifyNoMoreInteractions(); // Service config returns with the same health check name. - hcLbEventDelivery.handleResolvedAddresses(result1); + hcLbEventDelivery.acceptResolvedAddresses(result1); // It's delivered to origLb, but nothing else happens - inOrder.verify(origLb).handleResolvedAddresses(result1); + inOrder.verify(origLb).acceptResolvedAddresses(result1); assertThat(healthImpl.calls).isEmpty(); verifyNoMoreInteractions(origLb); @@ -1043,9 +1044,9 @@ public void serviceConfigChangesServiceNameWhenRpcInactive() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result2); + hcLbEventDelivery.acceptResolvedAddresses(result2); - inOrder.verify(origLb).handleResolvedAddresses(result2); + inOrder.verify(origLb).acceptResolvedAddresses(result2); // Underlying subchannel is now ready deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY)); @@ -1092,9 +1093,9 @@ public void balancerShutdown() { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result); + hcLbEventDelivery.acceptResolvedAddresses(result); - verify(origLb).handleResolvedAddresses(result); + verify(origLb).acceptResolvedAddresses(result); verifyNoMoreInteractions(origLb); ServerSideCall[] serverCalls = new ServerSideCall[NUM_SUBCHANNELS]; @@ -1172,8 +1173,8 @@ public LoadBalancer newLoadBalancer(Helper helper) { .setAddresses(resolvedAddressList) .setAttributes(resolutionAttrs) .build(); - hcLbEventDelivery.handleResolvedAddresses(result); - verify(origLb).handleResolvedAddresses(result); + hcLbEventDelivery.acceptResolvedAddresses(result); + verify(origLb).acceptResolvedAddresses(result); createSubchannel(0, Attributes.EMPTY); assertThat(healthImpls[0].calls).isEmpty(); deliverSubchannelState(0, ConnectivityStateInfo.forNonError(READY)); diff --git a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java index 928592e5534..59b2d38ecd9 100644 --- a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java @@ -171,9 +171,8 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { endpointTrackerMap.cancelTracking(); } - switchLb.handleResolvedAddresses( + return switchLb.acceptResolvedAddresses( resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(config.childConfig).build()); - return Status.OK; } @Override diff --git a/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java b/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java index 1b0139affef..d81740e116a 100644 --- a/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java @@ -280,7 +280,7 @@ public void acceptResolvedAddresses() { loadBalancer.acceptResolvedAddresses(resolvedAddresses); // Handling of resolved addresses is delegated - verify(mockChildLb).handleResolvedAddresses( + verify(mockChildLb).acceptResolvedAddresses( resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(childConfig).build()); // There is a single pending task to run the outlier detection algorithm diff --git a/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java index 3e58efa7c23..ab1abb1da15 100644 --- a/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java @@ -113,12 +113,10 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { Object switchConfig = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( lbRegistry.getProvider(WEIGHTED_TARGET_POLICY_NAME), new WeightedTargetConfig(weightedPolicySelections)); - switchLb.handleResolvedAddresses( + return switchLb.acceptResolvedAddresses( resolvedAddresses.toBuilder() .setLoadBalancingPolicyConfig(switchConfig) .build()); - - return Status.OK; } @Override diff --git a/xds/src/main/java/io/grpc/xds/orca/OrcaOobUtil.java b/xds/src/main/java/io/grpc/xds/orca/OrcaOobUtil.java index ba03140d627..9ac06d362fc 100644 --- a/xds/src/main/java/io/grpc/xds/orca/OrcaOobUtil.java +++ b/xds/src/main/java/io/grpc/xds/orca/OrcaOobUtil.java @@ -83,7 +83,7 @@ private OrcaOobUtil() {} * class WrrLoadbalancer extends LoadBalancer { * private final Helper originHelper; // the original Helper * - * public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { + * public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { * // listener implements the logic for WRR's usage of backend metrics. * OrcaReportingHelper orcaHelper = * OrcaOobUtil.newOrcaReportingHelper(originHelper); diff --git a/xds/src/test/java/io/grpc/xds/ClusterManagerLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterManagerLoadBalancerTest.java index aa0e205dd8f..8856efd685f 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterManagerLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterManagerLoadBalancerTest.java @@ -118,7 +118,7 @@ public void tearDown() { } @Test - public void handleResolvedAddressesUpdatesChannelPicker() { + public void acceptResolvedAddressesUpdatesChannelPicker() { deliverResolvedAddresses(ImmutableMap.of("childA", "policy_a", "childB", "policy_b")); verify(helper, atLeastOnce()).updateBalancingState( diff --git a/xds/src/test/java/io/grpc/xds/MetadataLoadBalancerProvider.java b/xds/src/test/java/io/grpc/xds/MetadataLoadBalancerProvider.java index ecc0112a2e0..7b56f59451c 100644 --- a/xds/src/test/java/io/grpc/xds/MetadataLoadBalancerProvider.java +++ b/xds/src/test/java/io/grpc/xds/MetadataLoadBalancerProvider.java @@ -114,6 +114,14 @@ public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { helper.setMetadata(config.metadataKey, config.metadataValue); delegateLb.handleResolvedAddresses(resolvedAddresses); } + + @Override + public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + MetadataLoadBalancerConfig config + = (MetadataLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); + helper.setMetadata(config.metadataKey, config.metadataValue); + return delegateLb.acceptResolvedAddresses(resolvedAddresses); + } } /** diff --git a/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java index a87d881563c..b6a5d8dbf73 100644 --- a/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java @@ -108,7 +108,7 @@ public void setUp() { } @Test - public void handleResolvedAddresses() { + public void acceptResolvedAddresses() { // A two locality cluster with a mock child LB policy. String localityOne = "localityOne"; String localityTwo = "localityTwo"; @@ -124,7 +124,7 @@ public void handleResolvedAddresses() { // Assert that the child policy and the locality weights were correctly mapped to a // WeightedTargetConfig. - verify(mockWeightedTargetLb).handleResolvedAddresses(resolvedAddressesCaptor.capture()); + verify(mockWeightedTargetLb).acceptResolvedAddresses(resolvedAddressesCaptor.capture()); Object config = resolvedAddressesCaptor.getValue().getLoadBalancingPolicyConfig(); assertThat(config).isInstanceOf(WeightedTargetConfig.class); WeightedTargetConfig wtConfig = (WeightedTargetConfig) config; @@ -136,7 +136,7 @@ public void handleResolvedAddresses() { } @Test - public void handleResolvedAddresses_noLocalityWeights() { + public void acceptResolvedAddresses_noLocalityWeights() { // A two locality cluster with a mock child LB policy. Object childPolicy = newChildConfig(mockChildProvider, null); @@ -182,7 +182,7 @@ public void localityWeightAttributeNotPropagated() { // Assert that the child policy and the locality weights were correctly mapped to a // WeightedTargetConfig. - verify(mockWeightedTargetLb).handleResolvedAddresses(resolvedAddressesCaptor.capture()); + verify(mockWeightedTargetLb).acceptResolvedAddresses(resolvedAddressesCaptor.capture()); //assertThat(resolvedAddressesCaptor.getValue().getAttributes() // .get(XdsAttributes.ATTR_LOCALITY_WEIGHTS)).isNull(); @@ -213,7 +213,7 @@ private Object newChildConfig(LoadBalancerProvider provider, Object config) { } private void deliverAddresses(WrrLocalityConfig config, List addresses) { - loadBalancer.handleResolvedAddresses( + loadBalancer.acceptResolvedAddresses( ResolvedAddresses.newBuilder().setAddresses(addresses).setLoadBalancingPolicyConfig(config) .build()); } From cdab410b816f65f0aec683dda5b96f37f9fd567e Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 24 Feb 2025 14:58:11 +0000 Subject: [PATCH 199/591] netty: Per-rpc authority verification against peer cert subject names (#11724) Per-rpc verification of authority specified via call options or set by the LB API against peer cert's subject names. --- .../io/grpc/internal/AuthorityVerifier.java | 24 ++ .../io/grpc/internal/CertificateUtils.java | 66 ++++ .../java/io/grpc/internal/GrpcAttributes.java | 3 + .../java/io/grpc/internal/NoopSslSession.java | 132 ++++++++ .../grpc/netty/GrpcHttp2OutboundHeaders.java | 10 + .../netty/InternalProtocolNegotiators.java | 4 +- .../io/grpc/netty/NettyChannelBuilder.java | 2 +- .../io/grpc/netty/NettyClientHandler.java | 62 ++++ .../io/grpc/netty/NettyClientTransport.java | 1 + .../NettySslContextChannelCredentials.java | 2 +- .../java/io/grpc/netty/NoopSslEngine.java | 151 +++++++++ .../io/grpc/netty/ProtocolNegotiator.java | 1 + .../io/grpc/netty/ProtocolNegotiators.java | 194 +++++++++--- .../io/grpc/netty/X509AuthorityVerifier.java | 108 +++++++ .../io/grpc/netty/NettyClientHandlerTest.java | 163 +++++++++- .../grpc/netty/NettyClientTransportTest.java | 292 +++++++++++++++++- .../grpc/netty/ProtocolNegotiatorsTest.java | 67 +++- .../io/grpc/okhttp/OkHttpChannelBuilder.java | 24 +- .../grpc/okhttp/OkHttpChannelBuilderTest.java | 5 +- 19 files changed, 1228 insertions(+), 83 deletions(-) create mode 100644 core/src/main/java/io/grpc/internal/AuthorityVerifier.java create mode 100644 core/src/main/java/io/grpc/internal/CertificateUtils.java create mode 100644 core/src/main/java/io/grpc/internal/NoopSslSession.java create mode 100644 netty/src/main/java/io/grpc/netty/NoopSslEngine.java create mode 100644 netty/src/main/java/io/grpc/netty/X509AuthorityVerifier.java diff --git a/core/src/main/java/io/grpc/internal/AuthorityVerifier.java b/core/src/main/java/io/grpc/internal/AuthorityVerifier.java new file mode 100644 index 00000000000..e6164a7dc4d --- /dev/null +++ b/core/src/main/java/io/grpc/internal/AuthorityVerifier.java @@ -0,0 +1,24 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import io.grpc.Status; + +/** Verifier for the outgoing authority pseudo-header against peer cert. */ +public interface AuthorityVerifier { + Status verifyAuthority(String authority); +} diff --git a/core/src/main/java/io/grpc/internal/CertificateUtils.java b/core/src/main/java/io/grpc/internal/CertificateUtils.java new file mode 100644 index 00000000000..cc26cedb274 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/CertificateUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collection; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.security.auth.x500.X500Principal; + +/** + * Contains certificate/key PEM file utility method(s) for internal usage. + */ +public final class CertificateUtils { + /** + * Creates X509TrustManagers using the provided CA certs. + */ + public static TrustManager[] createTrustManager(InputStream rootCerts) + throws GeneralSecurityException { + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + try { + ks.load(null, null); + } catch (IOException ex) { + // Shouldn't really happen, as we're not loading any data. + throw new GeneralSecurityException(ex); + } + X509Certificate[] certs = CertificateUtils.getX509Certificates(rootCerts); + for (X509Certificate cert : certs) { + X500Principal principal = cert.getSubjectX500Principal(); + ks.setCertificateEntry(principal.getName("RFC2253"), cert); + } + + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(ks); + return trustManagerFactory.getTrustManagers(); + } + + private static X509Certificate[] getX509Certificates(InputStream inputStream) + throws CertificateException { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + Collection certs = factory.generateCertificates(inputStream); + return certs.toArray(new X509Certificate[0]); + } +} diff --git a/core/src/main/java/io/grpc/internal/GrpcAttributes.java b/core/src/main/java/io/grpc/internal/GrpcAttributes.java index da43ae14800..f95f9b9dab8 100644 --- a/core/src/main/java/io/grpc/internal/GrpcAttributes.java +++ b/core/src/main/java/io/grpc/internal/GrpcAttributes.java @@ -42,5 +42,8 @@ public final class GrpcAttributes { public static final Attributes.Key ATTR_CLIENT_EAG_ATTRS = Attributes.Key.create("io.grpc.internal.GrpcAttributes.clientEagAttrs"); + public static final Attributes.Key ATTR_AUTHORITY_VERIFIER = + Attributes.Key.create("io.grpc.internal.GrpcAttributes.authorityVerifier"); + private GrpcAttributes() {} } diff --git a/core/src/main/java/io/grpc/internal/NoopSslSession.java b/core/src/main/java/io/grpc/internal/NoopSslSession.java new file mode 100644 index 00000000000..9a79d281ad5 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/NoopSslSession.java @@ -0,0 +1,132 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import java.security.Principal; +import java.security.cert.Certificate; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionContext; + +/** A no-op ssl session, to facilitate overriding only the required methods in specific + * implementations. + */ +public class NoopSslSession implements SSLSession { + @Override + public byte[] getId() { + return new byte[0]; + } + + @Override + public SSLSessionContext getSessionContext() { + return null; + } + + @Override + @SuppressWarnings("deprecation") + public javax.security.cert.X509Certificate[] getPeerCertificateChain() { + throw new UnsupportedOperationException("This method is deprecated and marked for removal. " + + "Use the getPeerCertificates() method instead."); + } + + @Override + public long getCreationTime() { + return 0; + } + + @Override + public long getLastAccessedTime() { + return 0; + } + + @Override + public void invalidate() { + } + + @Override + public boolean isValid() { + return false; + } + + @Override + public void putValue(String s, Object o) { + } + + @Override + public Object getValue(String s) { + return null; + } + + @Override + public void removeValue(String s) { + } + + @Override + public String[] getValueNames() { + return new String[0]; + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + return new Certificate[0]; + } + + @Override + public Certificate[] getLocalCertificates() { + return new Certificate[0]; + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + return null; + } + + @Override + public Principal getLocalPrincipal() { + return null; + } + + @Override + public String getCipherSuite() { + return null; + } + + @Override + public String getProtocol() { + return null; + } + + @Override + public String getPeerHost() { + return null; + } + + @Override + public int getPeerPort() { + return 0; + } + + @Override + public int getPacketBufferSize() { + return 0; + } + + @Override + public int getApplicationBufferSize() { + return 0; + } +} diff --git a/netty/src/main/java/io/grpc/netty/GrpcHttp2OutboundHeaders.java b/netty/src/main/java/io/grpc/netty/GrpcHttp2OutboundHeaders.java index 0489e135813..aabcd4fbaaa 100644 --- a/netty/src/main/java/io/grpc/netty/GrpcHttp2OutboundHeaders.java +++ b/netty/src/main/java/io/grpc/netty/GrpcHttp2OutboundHeaders.java @@ -66,6 +66,16 @@ private GrpcHttp2OutboundHeaders(AsciiString[] preHeaders, byte[][] serializedMe this.preHeaders = preHeaders; } + @Override + public CharSequence authority() { + for (int i = 0; i < preHeaders.length / 2; i++) { + if (preHeaders[i * 2].equals(Http2Headers.PseudoHeaderName.AUTHORITY.value())) { + return preHeaders[i * 2 + 1]; + } + } + return null; + } + @Override @SuppressWarnings("ReferenceEquality") // STATUS.value() never changes. public CharSequence status() { diff --git a/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java index 3d1aa83d9ff..039ea6c4f24 100644 --- a/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java @@ -44,7 +44,7 @@ public static InternalProtocolNegotiator.ProtocolNegotiator tls(SslContext sslCo ObjectPool executorPool, Optional handshakeCompleteRunnable) { final io.grpc.netty.ProtocolNegotiator negotiator = ProtocolNegotiators.tls(sslContext, - executorPool, handshakeCompleteRunnable); + executorPool, handshakeCompleteRunnable, null); final class TlsNegotiator implements InternalProtocolNegotiator.ProtocolNegotiator { @Override @@ -170,7 +170,7 @@ public static ChannelHandler clientTlsHandler( ChannelHandler next, SslContext sslContext, String authority, ChannelLogger negotiationLogger) { return new ClientTlsHandler(next, sslContext, authority, null, negotiationLogger, - Optional.absent()); + Optional.absent(), null, null); } public static class ProtocolNegotiationHandler diff --git a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java index 8b80f7b4e46..46566eaca1a 100644 --- a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java +++ b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java @@ -652,7 +652,7 @@ static ProtocolNegotiator createProtocolNegotiatorByType( case PLAINTEXT_UPGRADE: return ProtocolNegotiators.plaintextUpgrade(); case TLS: - return ProtocolNegotiators.tls(sslContext, executorPool, Optional.absent()); + return ProtocolNegotiators.tls(sslContext, executorPool, Optional.absent(), null); default: throw new IllegalArgumentException("Unsupported negotiationType: " + negotiationType); } diff --git a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java index 194decb1120..5a93dbc982b 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java @@ -28,6 +28,7 @@ import io.grpc.Attributes; import io.grpc.ChannelLogger; import io.grpc.InternalChannelz; +import io.grpc.InternalStatus; import io.grpc.Metadata; import io.grpc.Status; import io.grpc.StatusException; @@ -83,6 +84,8 @@ import io.perfmark.Tag; import io.perfmark.TaskCloseable; import java.nio.channels.ClosedChannelException; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.concurrent.Executor; import java.util.logging.Level; import java.util.logging.Logger; @@ -94,6 +97,8 @@ */ class NettyClientHandler extends AbstractNettyHandler { private static final Logger logger = Logger.getLogger(NettyClientHandler.class.getName()); + static boolean enablePerRpcAuthorityCheck = + GrpcUtil.getFlag("GRPC_ENABLE_PER_RPC_AUTHORITY_CHECK", false); /** * A message that simply passes through the channel without any real processing. It is useful to @@ -128,6 +133,13 @@ protected void handleNotInUse() { lifecycleManager.notifyInUse(false); } }; + private final Map peerVerificationResults = + new LinkedHashMap() { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 100; + } + }; private WriteQueue clientWriteQueue; private Http2Ping ping; @@ -591,6 +603,56 @@ private void createStream(CreateStreamCommand command, ChannelPromise promise) return; } + CharSequence authorityHeader = command.headers().authority(); + if (authorityHeader == null) { + Status authorityVerificationStatus = Status.UNAVAILABLE.withDescription( + "Missing authority header"); + command.stream().setNonExistent(); + command.stream().transportReportStatus( + Status.UNAVAILABLE, RpcProgress.PROCESSED, true, new Metadata()); + promise.setFailure(InternalStatus.asRuntimeExceptionWithoutStacktrace( + authorityVerificationStatus, null)); + return; + } + // No need to verify authority for the rpc outgoing header if it is same as the authority + // for the transport + if (!authority.contentEquals(authorityHeader)) { + Status authorityVerificationStatus = peerVerificationResults.get( + authorityHeader.toString()); + if (authorityVerificationStatus == null) { + if (attributes.get(GrpcAttributes.ATTR_AUTHORITY_VERIFIER) == null) { + authorityVerificationStatus = Status.UNAVAILABLE.withDescription( + "Authority verifier not found to verify authority"); + command.stream().setNonExistent(); + command.stream().transportReportStatus( + authorityVerificationStatus, RpcProgress.PROCESSED, true, new Metadata()); + promise.setFailure(InternalStatus.asRuntimeExceptionWithoutStacktrace( + authorityVerificationStatus, null)); + return; + } + authorityVerificationStatus = attributes.get(GrpcAttributes.ATTR_AUTHORITY_VERIFIER) + .verifyAuthority(authorityHeader.toString()); + peerVerificationResults.put(authorityHeader.toString(), authorityVerificationStatus); + if (!authorityVerificationStatus.isOk() && !enablePerRpcAuthorityCheck) { + logger.log(Level.WARNING, String.format("%s.%s", + authorityVerificationStatus.getDescription(), + enablePerRpcAuthorityCheck + ? "" : " This will be an error in the future."), + InternalStatus.asRuntimeExceptionWithoutStacktrace( + authorityVerificationStatus, null)); + } + } + if (!authorityVerificationStatus.isOk()) { + if (enablePerRpcAuthorityCheck) { + command.stream().setNonExistent(); + command.stream().transportReportStatus( + authorityVerificationStatus, RpcProgress.PROCESSED, true, new Metadata()); + promise.setFailure(InternalStatus.asRuntimeExceptionWithoutStacktrace( + authorityVerificationStatus, null)); + return; + } + } + } // Get the stream ID for the new stream. int streamId; try { diff --git a/netty/src/main/java/io/grpc/netty/NettyClientTransport.java b/netty/src/main/java/io/grpc/netty/NettyClientTransport.java index 54d1641a7ed..86d8991ba95 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientTransport.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientTransport.java @@ -106,6 +106,7 @@ class NettyClientTransport implements ConnectionClientTransport { private final boolean useGetForSafeMethods; private final Ticker ticker; + NettyClientTransport( SocketAddress address, ChannelFactory channelFactory, diff --git a/netty/src/main/java/io/grpc/netty/NettySslContextChannelCredentials.java b/netty/src/main/java/io/grpc/netty/NettySslContextChannelCredentials.java index ede511b68f6..3d3fdc67e8e 100644 --- a/netty/src/main/java/io/grpc/netty/NettySslContextChannelCredentials.java +++ b/netty/src/main/java/io/grpc/netty/NettySslContextChannelCredentials.java @@ -34,6 +34,6 @@ public static ChannelCredentials create(SslContext sslContext) { Preconditions.checkArgument(sslContext.isClient(), "Server SSL context can not be used for client channel"); GrpcSslContexts.ensureAlpnAndH2Enabled(sslContext.applicationProtocolNegotiator()); - return NettyChannelCredentials.create(ProtocolNegotiators.tlsClientFactory(sslContext)); + return NettyChannelCredentials.create(ProtocolNegotiators.tlsClientFactory(sslContext, null)); } } diff --git a/netty/src/main/java/io/grpc/netty/NoopSslEngine.java b/netty/src/main/java/io/grpc/netty/NoopSslEngine.java new file mode 100644 index 00000000000..7e14dbf0e79 --- /dev/null +++ b/netty/src/main/java/io/grpc/netty/NoopSslEngine.java @@ -0,0 +1,151 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.netty; + +import java.nio.ByteBuffer; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; + +/** + * A no-op implementation of SslEngine, to facilitate overriding only the required methods in + * specific implementations. + */ +class NoopSslEngine extends SSLEngine { + @Override + public SSLEngineResult wrap(ByteBuffer[] srcs, int offset, int length, ByteBuffer dst) + throws SSLException { + return null; + } + + @Override + public SSLEngineResult unwrap(ByteBuffer src, ByteBuffer[] dsts, int offset, int length) + throws SSLException { + return null; + } + + @Override + public Runnable getDelegatedTask() { + return null; + } + + @Override + public void closeInbound() throws SSLException { + + } + + @Override + public boolean isInboundDone() { + return false; + } + + @Override + public void closeOutbound() { + + } + + @Override + public boolean isOutboundDone() { + return false; + } + + @Override + public String[] getSupportedCipherSuites() { + return new String[0]; + } + + @Override + public String[] getEnabledCipherSuites() { + return new String[0]; + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + + } + + @Override + public String[] getSupportedProtocols() { + return new String[0]; + } + + @Override + public String[] getEnabledProtocols() { + return new String[0]; + } + + @Override + public void setEnabledProtocols(String[] protocols) { + + } + + @Override + public SSLSession getSession() { + return null; + } + + @Override + public void beginHandshake() throws SSLException { + + } + + @Override + public SSLEngineResult.HandshakeStatus getHandshakeStatus() { + return null; + } + + @Override + public void setUseClientMode(boolean mode) { + + } + + @Override + public boolean getUseClientMode() { + return false; + } + + @Override + public void setNeedClientAuth(boolean need) { + + } + + @Override + public boolean getNeedClientAuth() { + return false; + } + + @Override + public void setWantClientAuth(boolean want) { + + } + + @Override + public boolean getWantClientAuth() { + return false; + } + + @Override + public void setEnableSessionCreation(boolean flag) { + + } + + @Override + public boolean getEnableSessionCreation() { + return false; + } +} diff --git a/netty/src/main/java/io/grpc/netty/ProtocolNegotiator.java b/netty/src/main/java/io/grpc/netty/ProtocolNegotiator.java index 8a2c6f104b2..4332fdf2919 100644 --- a/netty/src/main/java/io/grpc/netty/ProtocolNegotiator.java +++ b/netty/src/main/java/io/grpc/netty/ProtocolNegotiator.java @@ -63,4 +63,5 @@ interface ServerFactory { */ ProtocolNegotiator newNegotiator(ObjectPool offloadExecutorPool); } + } diff --git a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java index 3f4d59bb334..77308c76ace 100644 --- a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java @@ -16,7 +16,6 @@ package io.grpc.netty; -import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.common.annotations.VisibleForTesting; @@ -42,8 +41,10 @@ import io.grpc.Status; import io.grpc.TlsChannelCredentials; import io.grpc.TlsServerCredentials; +import io.grpc.internal.CertificateUtils; import io.grpc.internal.GrpcAttributes; import io.grpc.internal.GrpcUtil; +import io.grpc.internal.NoopSslSession; import io.grpc.internal.ObjectPool; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFutureListener; @@ -71,8 +72,11 @@ import java.net.SocketAddress; import java.net.URI; import java.nio.channels.ClosedChannelException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; import java.util.Arrays; import java.util.EnumSet; +import java.util.List; import java.util.Set; import java.util.concurrent.Executor; import java.util.logging.Level; @@ -82,6 +86,9 @@ import javax.net.ssl.SSLException; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; /** @@ -95,7 +102,15 @@ final class ProtocolNegotiators { private static final EnumSet understoodServerTlsFeatures = EnumSet.of( TlsServerCredentials.Feature.MTLS, TlsServerCredentials.Feature.CUSTOM_MANAGERS); + private static Class x509ExtendedTrustManagerClass; + static { + try { + x509ExtendedTrustManagerClass = Class.forName("javax.net.ssl.X509ExtendedTrustManager"); + } catch (ClassNotFoundException e) { + // Will disallow per-rpc authority override via call option. + } + } private ProtocolNegotiators() { } @@ -118,14 +133,32 @@ public static FromChannelCredentialsResult from(ChannelCredentials creds) { new ByteArrayInputStream(tlsCreds.getPrivateKey()), tlsCreds.getPrivateKeyPassword()); } - if (tlsCreds.getTrustManagers() != null) { - builder.trustManager(new FixedTrustManagerFactory(tlsCreds.getTrustManagers())); - } else if (tlsCreds.getRootCertificates() != null) { - builder.trustManager(new ByteArrayInputStream(tlsCreds.getRootCertificates())); - } // else use system default try { - return FromChannelCredentialsResult.negotiator(tlsClientFactory(builder.build())); - } catch (SSLException ex) { + List trustManagers; + if (tlsCreds.getTrustManagers() != null) { + trustManagers = tlsCreds.getTrustManagers(); + } else if (tlsCreds.getRootCertificates() != null) { + trustManagers = Arrays.asList(CertificateUtils.createTrustManager( + new ByteArrayInputStream(tlsCreds.getRootCertificates()))); + } else { // else use system default + TrustManagerFactory tmf = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + trustManagers = Arrays.asList(tmf.getTrustManagers()); + } + builder.trustManager(new FixedTrustManagerFactory(trustManagers)); + TrustManager x509ExtendedTrustManager = null; + if (x509ExtendedTrustManagerClass != null) { + for (TrustManager trustManager : trustManagers) { + if (x509ExtendedTrustManagerClass.isInstance(trustManager)) { + x509ExtendedTrustManager = trustManager; + break; + } + } + } + return FromChannelCredentialsResult.negotiator(tlsClientFactory(builder.build(), + (X509TrustManager) x509ExtendedTrustManager)); + } catch (SSLException | GeneralSecurityException ex) { log.log(Level.FINE, "Exception building SslContext", ex); return FromChannelCredentialsResult.error( "Unable to create SslContext: " + ex.getMessage()); @@ -411,8 +444,8 @@ static final class ServerTlsHandler extends ChannelInboundHandlerAdapter { ServerTlsHandler(ChannelHandler next, SslContext sslContext, final ObjectPool executorPool) { - this.sslContext = checkNotNull(sslContext, "sslContext"); - this.next = checkNotNull(next, "next"); + this.sslContext = Preconditions.checkNotNull(sslContext, "sslContext"); + this.next = Preconditions.checkNotNull(next, "next"); if (executorPool != null) { this.executor = executorPool.getObject(); } @@ -469,8 +502,8 @@ private void fireProtocolNegotiationEvent(ChannelHandlerContext ctx, SSLSession public static ProtocolNegotiator httpProxy(final SocketAddress proxyAddress, final @Nullable String proxyUsername, final @Nullable String proxyPassword, final ProtocolNegotiator negotiator) { - checkNotNull(negotiator, "negotiator"); - checkNotNull(proxyAddress, "proxyAddress"); + Preconditions.checkNotNull(negotiator, "negotiator"); + Preconditions.checkNotNull(proxyAddress, "proxyAddress"); final AsciiString scheme = negotiator.scheme(); class ProxyNegotiator implements ProtocolNegotiator { @Override @@ -516,7 +549,7 @@ public ProxyProtocolNegotiationHandler( ChannelHandler next, ChannelLogger negotiationLogger) { super(next, negotiationLogger); - this.address = checkNotNull(address, "address"); + this.address = Preconditions.checkNotNull(address, "address"); this.userName = userName; this.password = password; } @@ -545,18 +578,21 @@ protected void userEventTriggered0(ChannelHandlerContext ctx, Object evt) throws static final class ClientTlsProtocolNegotiator implements ProtocolNegotiator { public ClientTlsProtocolNegotiator(SslContext sslContext, - ObjectPool executorPool, Optional handshakeCompleteRunnable) { - this.sslContext = checkNotNull(sslContext, "sslContext"); + ObjectPool executorPool, Optional handshakeCompleteRunnable, + X509TrustManager x509ExtendedTrustManager) { + this.sslContext = Preconditions.checkNotNull(sslContext, "sslContext"); this.executorPool = executorPool; if (this.executorPool != null) { this.executor = this.executorPool.getObject(); } this.handshakeCompleteRunnable = handshakeCompleteRunnable; + this.x509ExtendedTrustManager = x509ExtendedTrustManager; } private final SslContext sslContext; private final ObjectPool executorPool; private final Optional handshakeCompleteRunnable; + private final X509TrustManager x509ExtendedTrustManager; private Executor executor; @Override @@ -569,7 +605,8 @@ public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { ChannelHandler gnh = new GrpcNegotiationHandler(grpcHandler); ChannelLogger negotiationLogger = grpcHandler.getNegotiationLogger(); ChannelHandler cth = new ClientTlsHandler(gnh, sslContext, grpcHandler.getAuthority(), - this.executor, negotiationLogger, handshakeCompleteRunnable); + this.executor, negotiationLogger, handshakeCompleteRunnable, this, + x509ExtendedTrustManager); return new WaitUntilActiveHandler(cth, negotiationLogger); } @@ -579,6 +616,11 @@ public void close() { this.executorPool.returnObject(this.executor); } } + + @VisibleForTesting + boolean hasX509ExtendedTrustManager() { + return x509ExtendedTrustManager != null; + } } static final class ClientTlsHandler extends ProtocolNegotiationHandler { @@ -588,23 +630,28 @@ static final class ClientTlsHandler extends ProtocolNegotiationHandler { private final int port; private Executor executor; private final Optional handshakeCompleteRunnable; + private final X509TrustManager x509ExtendedTrustManager; + private SSLEngine sslEngine; ClientTlsHandler(ChannelHandler next, SslContext sslContext, String authority, Executor executor, ChannelLogger negotiationLogger, - Optional handshakeCompleteRunnable) { + Optional handshakeCompleteRunnable, + ClientTlsProtocolNegotiator clientTlsProtocolNegotiator, + X509TrustManager x509ExtendedTrustManager) { super(next, negotiationLogger); - this.sslContext = checkNotNull(sslContext, "sslContext"); + this.sslContext = Preconditions.checkNotNull(sslContext, "sslContext"); HostPort hostPort = parseAuthority(authority); this.host = hostPort.host; this.port = hostPort.port; this.executor = executor; this.handshakeCompleteRunnable = handshakeCompleteRunnable; + this.x509ExtendedTrustManager = x509ExtendedTrustManager; } @Override @IgnoreJRERequirement protected void handlerAdded0(ChannelHandlerContext ctx) { - SSLEngine sslEngine = sslContext.newEngine(ctx.alloc(), host, port); + sslEngine = sslContext.newEngine(ctx.alloc(), host, port); SSLParameters sslParams = sslEngine.getSSLParameters(); sslParams.setEndpointIdentificationAlgorithm("HTTPS"); sslEngine.setSSLParameters(sslParams); @@ -661,6 +708,8 @@ private void propagateTlsComplete(ChannelHandlerContext ctx, SSLSession session) Attributes attrs = existingPne.getAttributes().toBuilder() .set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.PRIVACY_AND_INTEGRITY) .set(Grpc.TRANSPORT_ATTR_SSL_SESSION, session) + .set(GrpcAttributes.ATTR_AUTHORITY_VERIFIER, new X509AuthorityVerifier( + sslEngine, x509ExtendedTrustManager)) .build(); replaceProtocolNegotiationEvent(existingPne.withAttributes(attrs).withSecurity(security)); if (handshakeCompleteRunnable.isPresent()) { @@ -700,8 +749,10 @@ static HostPort parseAuthority(String authority) { * @param executorPool a dedicated {@link Executor} pool for time-consuming TLS tasks */ public static ProtocolNegotiator tls(SslContext sslContext, - ObjectPool executorPool, Optional handshakeCompleteRunnable) { - return new ClientTlsProtocolNegotiator(sslContext, executorPool, handshakeCompleteRunnable); + ObjectPool executorPool, Optional handshakeCompleteRunnable, + X509TrustManager x509ExtendedTrustManager) { + return new ClientTlsProtocolNegotiator(sslContext, executorPool, handshakeCompleteRunnable, + x509ExtendedTrustManager); } /** @@ -709,25 +760,30 @@ public static ProtocolNegotiator tls(SslContext sslContext, * be negotiated, the {@code handler} is added and writes to the {@link io.netty.channel.Channel} * may happen immediately, even before the TLS Handshake is complete. */ - public static ProtocolNegotiator tls(SslContext sslContext) { - return tls(sslContext, null, Optional.absent()); + public static ProtocolNegotiator tls(SslContext sslContext, + X509TrustManager x509ExtendedTrustManager) { + return tls(sslContext, null, Optional.absent(), x509ExtendedTrustManager); } - public static ProtocolNegotiator.ClientFactory tlsClientFactory(SslContext sslContext) { - return new TlsProtocolNegotiatorClientFactory(sslContext); + public static ProtocolNegotiator.ClientFactory tlsClientFactory(SslContext sslContext, + X509TrustManager x509ExtendedTrustManager) { + return new TlsProtocolNegotiatorClientFactory(sslContext, x509ExtendedTrustManager); } @VisibleForTesting static final class TlsProtocolNegotiatorClientFactory implements ProtocolNegotiator.ClientFactory { private final SslContext sslContext; + private final X509TrustManager x509ExtendedTrustManager; - public TlsProtocolNegotiatorClientFactory(SslContext sslContext) { + public TlsProtocolNegotiatorClientFactory(SslContext sslContext, + X509TrustManager x509ExtendedTrustManager) { this.sslContext = Preconditions.checkNotNull(sslContext, "sslContext"); + this.x509ExtendedTrustManager = x509ExtendedTrustManager; } @Override public ProtocolNegotiator newNegotiator() { - return tls(sslContext); + return tls(sslContext, x509ExtendedTrustManager); } @Override public int getDefaultPort() { @@ -780,7 +836,9 @@ public AsciiString scheme() { public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { ChannelHandler upgradeHandler = new Http2UpgradeAndGrpcHandler(grpcHandler.getAuthority(), grpcHandler); - return new WaitUntilActiveHandler(upgradeHandler, grpcHandler.getNegotiationLogger()); + ChannelHandler plaintextHandler = + new PlaintextHandler(upgradeHandler, grpcHandler.getNegotiationLogger()); + return new WaitUntilActiveHandler(plaintextHandler, grpcHandler.getNegotiationLogger()); } @Override @@ -801,8 +859,8 @@ static final class Http2UpgradeAndGrpcHandler extends ChannelInboundHandlerAdapt private ProtocolNegotiationEvent pne; Http2UpgradeAndGrpcHandler(String authority, GrpcHttp2ConnectionHandler next) { - this.authority = checkNotNull(authority, "authority"); - this.next = checkNotNull(next, "next"); + this.authority = Preconditions.checkNotNull(authority, "authority"); + this.next = Preconditions.checkNotNull(next, "next"); this.negotiationLogger = next.getNegotiationLogger(); } @@ -846,9 +904,9 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc } /** - * Returns a {@link ChannelHandler} that ensures that the {@code handler} is added to the - * pipeline writes to the {@link io.netty.channel.Channel} may happen immediately, even before it - * is active. + * Returns a {@link io.netty.channel.ChannelHandler} that ensures that the {@code handler} is + * added to the pipeline writes to the {@link io.netty.channel.Channel} may happen immediately, + * even before it is active. */ public static ProtocolNegotiator plaintext() { return new PlaintextProtocolNegotiator(); @@ -926,7 +984,7 @@ static final class GrpcNegotiationHandler extends ChannelInboundHandlerAdapter { private final GrpcHttp2ConnectionHandler next; public GrpcNegotiationHandler(GrpcHttp2ConnectionHandler next) { - this.next = checkNotNull(next, "next"); + this.next = Preconditions.checkNotNull(next, "next"); } @Override @@ -977,7 +1035,9 @@ static final class PlaintextProtocolNegotiator implements ProtocolNegotiator { @Override public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { ChannelHandler grpcNegotiationHandler = new GrpcNegotiationHandler(grpcHandler); - ChannelHandler activeHandler = new WaitUntilActiveHandler(grpcNegotiationHandler, + ChannelHandler plaintextHandler = + new PlaintextHandler(grpcNegotiationHandler, grpcHandler.getNegotiationLogger()); + ChannelHandler activeHandler = new WaitUntilActiveHandler(plaintextHandler, grpcHandler.getNegotiationLogger()); return activeHandler; } @@ -991,6 +1051,22 @@ public AsciiString scheme() { } } + static final class PlaintextHandler extends ProtocolNegotiationHandler { + PlaintextHandler(ChannelHandler next, ChannelLogger negotiationLogger) { + super(next, negotiationLogger); + } + + @Override + protected void protocolNegotiationEventTriggered(ChannelHandlerContext ctx) { + ProtocolNegotiationEvent existingPne = getProtocolNegotiationEvent(); + Attributes attrs = existingPne.getAttributes().toBuilder() + .set(GrpcAttributes.ATTR_AUTHORITY_VERIFIER, (authority) -> Status.OK) + .build(); + replaceProtocolNegotiationEvent(existingPne.withAttributes(attrs)); + fireProtocolNegotiationEvent(ctx); + } + } + /** * Waits for the channel to be active, and then installs the next Handler. Using this allows * subsequent handlers to assume the channel is active and ready to send. Additionally, this a @@ -1048,15 +1124,15 @@ static class ProtocolNegotiationHandler extends ChannelDuplexHandler { protected ProtocolNegotiationHandler(ChannelHandler next, String negotiatorName, ChannelLogger negotiationLogger) { - this.next = checkNotNull(next, "next"); + this.next = Preconditions.checkNotNull(next, "next"); this.negotiatorName = negotiatorName; - this.negotiationLogger = checkNotNull(negotiationLogger, "negotiationLogger"); + this.negotiationLogger = Preconditions.checkNotNull(negotiationLogger, "negotiationLogger"); } protected ProtocolNegotiationHandler(ChannelHandler next, ChannelLogger negotiationLogger) { - this.next = checkNotNull(next, "next"); + this.next = Preconditions.checkNotNull(next, "next"); this.negotiatorName = getClass().getSimpleName().replace("Handler", ""); - this.negotiationLogger = checkNotNull(negotiationLogger, "negotiationLogger"); + this.negotiationLogger = Preconditions.checkNotNull(negotiationLogger, "negotiationLogger"); } @Override @@ -1097,7 +1173,7 @@ protected final ProtocolNegotiationEvent getProtocolNegotiationEvent() { protected final void replaceProtocolNegotiationEvent(ProtocolNegotiationEvent pne) { checkState(this.pne != null, "previous protocol negotiation event hasn't triggered"); - this.pne = checkNotNull(pne); + this.pne = Preconditions.checkNotNull(pne); } protected final void fireProtocolNegotiationEvent(ChannelHandlerContext ctx) { @@ -1107,4 +1183,42 @@ protected final void fireProtocolNegotiationEvent(ChannelHandlerContext ctx) { ctx.fireUserEventTriggered(pne); } } + + static final class SslEngineWrapper extends NoopSslEngine { + private final SSLEngine sslEngine; + private final String peerHost; + + SslEngineWrapper(SSLEngine sslEngine, String peerHost) { + this.sslEngine = sslEngine; + this.peerHost = peerHost; + } + + @Override + public String getPeerHost() { + return peerHost; + } + + @Override + public SSLSession getHandshakeSession() { + return new FakeSslSession(peerHost); + } + + @Override + public SSLParameters getSSLParameters() { + return sslEngine.getSSLParameters(); + } + } + + static final class FakeSslSession extends NoopSslSession { + private final String peerHost; + + FakeSslSession(String peerHost) { + this.peerHost = peerHost; + } + + @Override + public String getPeerHost() { + return peerHost; + } + } } diff --git a/netty/src/main/java/io/grpc/netty/X509AuthorityVerifier.java b/netty/src/main/java/io/grpc/netty/X509AuthorityVerifier.java new file mode 100644 index 00000000000..8a8d426662f --- /dev/null +++ b/netty/src/main/java/io/grpc/netty/X509AuthorityVerifier.java @@ -0,0 +1,108 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.netty; + +import static com.google.common.base.Preconditions.checkNotNull; + +import io.grpc.Status; +import io.grpc.internal.AuthorityVerifier; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.annotation.Nonnull; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.X509TrustManager; + +final class X509AuthorityVerifier implements AuthorityVerifier { + private final SSLEngine sslEngine; + private final X509TrustManager x509ExtendedTrustManager; + + private static final Method checkServerTrustedMethod; + + static { + Method method = null; + try { + Class x509ExtendedTrustManagerClass = + Class.forName("javax.net.ssl.X509ExtendedTrustManager"); + method = x509ExtendedTrustManagerClass.getMethod("checkServerTrusted", + X509Certificate[].class, String.class, SSLEngine.class); + } catch (ClassNotFoundException e) { + // Per-rpc authority overriding via call options will be disallowed. + } catch (NoSuchMethodException e) { + // Should never happen since X509ExtendedTrustManager was introduced in Android API level 24 + // along with checkServerTrusted. + } + checkServerTrustedMethod = method; + } + + public X509AuthorityVerifier(SSLEngine sslEngine, X509TrustManager x509ExtendedTrustManager) { + this.sslEngine = checkNotNull(sslEngine, "sslEngine"); + this.x509ExtendedTrustManager = x509ExtendedTrustManager; + } + + @Override + public Status verifyAuthority(@Nonnull String authority) { + if (x509ExtendedTrustManager == null) { + return Status.UNAVAILABLE.withDescription( + "Can't allow authority override in rpc when X509ExtendedTrustManager" + + " is not available"); + } + Status peerVerificationStatus; + try { + // Because the authority pseudo-header can contain a port number: + // https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2.3 + verifyAuthorityAllowedForPeerCert(removeAnyPortNumber(authority)); + peerVerificationStatus = Status.OK; + } catch (SSLPeerUnverifiedException | CertificateException | InvocationTargetException + | IllegalAccessException | IllegalStateException e) { + peerVerificationStatus = Status.UNAVAILABLE.withDescription( + String.format("Peer hostname verification during rpc failed for authority '%s'", + authority)).withCause(e); + } + return peerVerificationStatus; + } + + private String removeAnyPortNumber(String authority) { + int closingSquareBracketIndex = authority.lastIndexOf(']'); + int portNumberSeperatorColonIndex = authority.lastIndexOf(':'); + if (portNumberSeperatorColonIndex > closingSquareBracketIndex) { + return authority.substring(0, portNumberSeperatorColonIndex); + } + return authority; + } + + private void verifyAuthorityAllowedForPeerCert(String authority) + throws SSLPeerUnverifiedException, CertificateException, InvocationTargetException, + IllegalAccessException { + SSLEngine sslEngineWrapper = new ProtocolNegotiators.SslEngineWrapper(sslEngine, authority); + // The typecasting of Certificate to X509Certificate should work because this method will only + // be called when using TLS and thus X509. + Certificate[] peerCertificates = sslEngine.getSession().getPeerCertificates(); + X509Certificate[] x509PeerCertificates = new X509Certificate[peerCertificates.length]; + for (int i = 0; i < peerCertificates.length; i++) { + x509PeerCertificates[i] = (X509Certificate) peerCertificates[i]; + } + if (checkServerTrustedMethod == null) { + throw new IllegalStateException("checkServerTrustedMethod not found"); + } + checkServerTrustedMethod.invoke( + x509ExtendedTrustManager, x509PeerCertificates, "RSA", sslEngineWrapper); + } +} diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java index e5c97e9efd9..945c6c1267a 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java @@ -36,6 +36,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; @@ -64,6 +65,7 @@ import io.grpc.internal.ClientStreamListener.RpcProgress; import io.grpc.internal.ClientTransport; import io.grpc.internal.ClientTransport.PingCallback; +import io.grpc.internal.GrpcAttributes; import io.grpc.internal.GrpcUtil; import io.grpc.internal.KeepAliveManager; import io.grpc.internal.ManagedClientTransport; @@ -90,10 +92,12 @@ import io.netty.handler.codec.http2.Http2Stream; import io.netty.util.AsciiString; import java.io.InputStream; +import java.security.cert.CertificateException; import java.text.MessageFormat; import java.util.LinkedList; import java.util.List; import java.util.Queue; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Handler; @@ -189,7 +193,11 @@ public Void answer(InvocationOnMock invocation) throws Throwable { }) .when(streamListener) .messagesAvailable(ArgumentMatchers.any()); - + doAnswer((attributes) -> Attributes.newBuilder().set( + GrpcAttributes.ATTR_AUTHORITY_VERIFIER, + (authority) -> Status.OK).build()) + .when(listener) + .filterTransport(ArgumentMatchers.any(Attributes.class)); lifecycleManager = new ClientTransportLifecycleManager(listener); // This mocks the keepalive manager only for there's in which we verify it. For other tests // it'll be null which will be testing if we behave correctly when it's not present. @@ -919,6 +927,159 @@ public void exceptionCaughtShouldCloseConnection() throws Exception { assertFalse(channel().isOpen()); } + @Test + public void missingAuthorityHeader_streamCreationShouldFail() throws Exception { + Http2Headers grpcHeadersWithoutAuthority = new DefaultHttp2Headers() + .scheme(HTTPS) + .path(as("/fakemethod")) + .method(HTTP_METHOD) + .add(as("auth"), as("sometoken")) + .add(CONTENT_TYPE_HEADER, CONTENT_TYPE_GRPC) + .add(TE_HEADER, TE_TRAILERS); + ChannelFuture channelFuture = enqueue(newCreateStreamCommand( + grpcHeadersWithoutAuthority, streamTransportState)); + try { + channelFuture.get(); + fail("Expected stream creation failure"); + } catch (ExecutionException e) { + assertThat(e.getCause().getMessage()).isEqualTo("UNAVAILABLE: Missing authority header"); + } + } + + @Test + public void missingAuthorityVerifierInAttributes_streamCreationShouldFail() throws Exception { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + StreamListener.MessageProducer producer = + (StreamListener.MessageProducer) invocation.getArguments()[0]; + InputStream message; + while ((message = producer.next()) != null) { + streamListenerMessageQueue.add(message); + } + return null; + } + }) + .when(streamListener) + .messagesAvailable(ArgumentMatchers.any()); + doAnswer((attributes) -> Attributes.EMPTY) + .when(listener) + .filterTransport(ArgumentMatchers.any(Attributes.class)); + lifecycleManager = new ClientTransportLifecycleManager(listener); + // This mocks the keepalive manager only for there's in which we verify it. For other tests + // it'll be null which will be testing if we behave correctly when it's not present. + if (setKeepaliveManagerFor.contains(testNameRule.getMethodName())) { + mockKeepAliveManager = mock(KeepAliveManager.class); + } + + initChannel(new GrpcHttp2ClientHeadersDecoder(GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE)); + streamTransportState = new TransportStateImpl( + handler(), + channel().eventLoop(), + DEFAULT_MAX_MESSAGE_SIZE, + transportTracer); + streamTransportState.setListener(streamListener); + + grpcHeaders = new DefaultHttp2Headers() + .scheme(HTTPS) + .authority(as("www.fake.com")) + .path(as("/fakemethod")) + .method(HTTP_METHOD) + .add(as("auth"), as("sometoken")) + .add(CONTENT_TYPE_HEADER, CONTENT_TYPE_GRPC) + .add(TE_HEADER, TE_TRAILERS); + + // Simulate receipt of initial remote settings. + ByteBuf serializedSettings = serializeSettings(new Http2Settings()); + channelRead(serializedSettings); + channel().releaseOutbound(); + + ChannelFuture channelFuture = createStream(); + try { + channelFuture.get(); + fail("Expected stream creation failure"); + } catch (ExecutionException e) { + assertThat(e.getCause().getMessage()).isEqualTo( + "UNAVAILABLE: Authority verifier not found to verify authority"); + } + } + + @Test + public void authorityVerificationSuccess_streamCreationSucceeds() throws Exception { + NettyClientHandler.enablePerRpcAuthorityCheck = true; + try { + ChannelFuture channelFuture = createStream(); + channelFuture.get(); + } finally { + NettyClientHandler.enablePerRpcAuthorityCheck = false; + } + } + + @Test + public void authorityVerificationFailure_streamCreationFails() throws Exception { + NettyClientHandler.enablePerRpcAuthorityCheck = true; + try { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + StreamListener.MessageProducer producer = + (StreamListener.MessageProducer) invocation.getArguments()[0]; + InputStream message; + while ((message = producer.next()) != null) { + streamListenerMessageQueue.add(message); + } + return null; + } + }) + .when(streamListener) + .messagesAvailable(ArgumentMatchers.any()); + doAnswer((attributes) -> Attributes.newBuilder().set( + GrpcAttributes.ATTR_AUTHORITY_VERIFIER, + (authority) -> Status.UNAVAILABLE.withCause( + new CertificateException("Peer verification failed"))).build()) + .when(listener) + .filterTransport(ArgumentMatchers.any(Attributes.class)); + lifecycleManager = new ClientTransportLifecycleManager(listener); + // This mocks the keepalive manager only for there's in which we verify it. For other tests + // it'll be null which will be testing if we behave correctly when it's not present. + if (setKeepaliveManagerFor.contains(testNameRule.getMethodName())) { + mockKeepAliveManager = mock(KeepAliveManager.class); + } + + initChannel(new GrpcHttp2ClientHeadersDecoder(GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE)); + streamTransportState = new TransportStateImpl( + handler(), + channel().eventLoop(), + DEFAULT_MAX_MESSAGE_SIZE, + transportTracer); + streamTransportState.setListener(streamListener); + + grpcHeaders = new DefaultHttp2Headers() + .scheme(HTTPS) + .authority(as("www.fake.com")) + .path(as("/fakemethod")) + .method(HTTP_METHOD) + .add(as("auth"), as("sometoken")) + .add(CONTENT_TYPE_HEADER, CONTENT_TYPE_GRPC) + .add(TE_HEADER, TE_TRAILERS); + + // Simulate receipt of initial remote settings. + ByteBuf serializedSettings = serializeSettings(new Http2Settings()); + channelRead(serializedSettings); + channel().releaseOutbound(); + + ChannelFuture channelFuture = createStream(); + try { + channelFuture.get(); + fail("Expected stream creation failure"); + } catch (ExecutionException e) { + assertThat(e.getMessage()).isEqualTo("io.grpc.InternalStatusRuntimeException: UNAVAILABLE"); + } + } finally { + NettyClientHandler.enablePerRpcAuthorityCheck = false; + } + } + @Override protected void makeStream() throws Exception { createStream(); diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index d0a6456c430..9b3b2e386d3 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -59,6 +59,7 @@ import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.StatusException; +import io.grpc.TlsChannelCredentials; import io.grpc.internal.ClientStream; import io.grpc.internal.ClientStreamListener; import io.grpc.internal.ClientTransport; @@ -76,6 +77,7 @@ import io.grpc.netty.NettyChannelBuilder.LocalSocketPicker; import io.grpc.netty.NettyTestUtil.TrackingObjectPoolForTest; import io.grpc.testing.TlsTesting; +import io.grpc.util.CertificateUtils; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.ChannelConfig; @@ -101,9 +103,14 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -115,8 +122,15 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -131,6 +145,7 @@ * Tests for {@link NettyClientTransport}. */ @RunWith(JUnit4.class) +@IgnoreJRERequirement public class NettyClientTransportTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @@ -203,7 +218,7 @@ public void addDefaultUserAgent() throws Exception { } @Test - public void setSoLingerChannelOption() throws IOException { + public void setSoLingerChannelOption() throws IOException, GeneralSecurityException { startServer(); Map, Object> channelOptions = new HashMap<>(); // set SO_LINGER option @@ -354,7 +369,7 @@ public void tlsNegotiationFailurePropagatesToStatus() throws Exception { .trustManager(caCert) .keyManager(clientCert, clientKey) .build(); - ProtocolNegotiator negotiator = ProtocolNegotiators.tls(clientContext); + ProtocolNegotiator negotiator = ProtocolNegotiators.tls(clientContext, null); final NettyClientTransport transport = newTransport(negotiator); callMeMaybe(transport.start(clientTransportListener)); verify(clientTransportListener, timeout(5000)).transportTerminated(); @@ -821,7 +836,7 @@ public void tlsNegotiationServerExecutorShouldSucceed() throws Exception { .keyManager(clientCert, clientKey) .build(); ProtocolNegotiator negotiator = ProtocolNegotiators.tls(clientContext, clientExecutorPool, - Optional.absent()); + Optional.absent(), null); // after starting the client, the Executor in the client pool should be used assertEquals(true, clientExecutorPool.isInUse()); final NettyClientTransport transport = newTransport(negotiator); @@ -836,6 +851,179 @@ public void tlsNegotiationServerExecutorShouldSucceed() throws Exception { assertEquals(false, serverExecutorPool.isInUse()); } + /** + * This test tests the case of TlsCredentials passed to ProtocolNegotiators not having an instance + * of X509ExtendedTrustManager (this is not testable in ProtocolNegotiatorsTest without creating + * accessors for the internal state of negotiator whether it has a X509ExtendedTrustManager, + * hence the need to test it in this class instead). To establish a successful handshake we create + * a fake X509TrustManager not implementing X509ExtendedTrustManager but wraps the real + * X509ExtendedTrustManager. + */ + @Test + public void authorityOverrideInCallOptions_noX509ExtendedTrustManager_newStreamCreationFails() + throws IOException, InterruptedException, GeneralSecurityException, ExecutionException, + TimeoutException { + NettyClientHandler.enablePerRpcAuthorityCheck = true; + try { + startServer(); + InputStream caCert = TlsTesting.loadCert("ca.pem"); + X509TrustManager x509ExtendedTrustManager = + (X509TrustManager) getX509ExtendedTrustManager(caCert); + ProtocolNegotiators.FromChannelCredentialsResult result = + ProtocolNegotiators.from(TlsChannelCredentials.newBuilder() + .trustManager(new FakeTrustManager(x509ExtendedTrustManager)).build()); + NettyClientTransport transport = newTransport(result.negotiator.newNegotiator()); + SettableFuture connected = SettableFuture.create(); + FakeClientTransportListener fakeClientTransportListener = + new FakeClientTransportListener(connected); + callMeMaybe(transport.start(fakeClientTransportListener)); + connected.get(10, TimeUnit.SECONDS); + assertThat(fakeClientTransportListener.isConnected()).isTrue(); + + Rpc rpc = new Rpc(transport, new Metadata(), "foo.test.google.in"); + try { + rpc.waitForClose(); + fail("Expected exception in starting stream"); + } catch (ExecutionException ex) { + Status status = ((StatusException) ex.getCause()).getStatus(); + assertThat(status.getDescription()).isEqualTo("Can't allow authority override in rpc " + + "when X509ExtendedTrustManager is not available"); + assertThat(status.getCode()).isEqualTo(Code.UNAVAILABLE); + } + } finally { + NettyClientHandler.enablePerRpcAuthorityCheck = false; + } + } + + @Test + public void authorityOverrideInCallOptions_doesntMatchServerPeerHost_newStreamCreationFails() + throws IOException, InterruptedException, GeneralSecurityException, ExecutionException, + TimeoutException { + NettyClientHandler.enablePerRpcAuthorityCheck = true; + try { + startServer(); + NettyClientTransport transport = newTransport(newNegotiator()); + SettableFuture connected = SettableFuture.create(); + FakeClientTransportListener fakeClientTransportListener = + new FakeClientTransportListener(connected); + callMeMaybe(transport.start(fakeClientTransportListener)); + connected.get(10, TimeUnit.SECONDS); + assertThat(fakeClientTransportListener.isConnected()).isTrue(); + + Rpc rpc = new Rpc(transport, new Metadata(), "foo.test.google.in"); + try { + rpc.waitForClose(); + fail("Expected exception in starting stream"); + } catch (ExecutionException ex) { + Status status = ((StatusException) ex.getCause()).getStatus(); + assertThat(status.getDescription()).isEqualTo("Peer hostname verification during rpc " + + "failed for authority 'foo.test.google.in'"); + assertThat(status.getCode()).isEqualTo(Code.UNAVAILABLE); + assertThat(((InvocationTargetException) ex.getCause().getCause()).getTargetException()) + .isInstanceOf(CertificateException.class); + assertThat(((InvocationTargetException) ex.getCause().getCause()).getTargetException() + .getMessage()).isEqualTo( + "No subject alternative DNS name matching foo.test.google.in found."); + } + } finally { + NettyClientHandler.enablePerRpcAuthorityCheck = false; + } + } + + @Test + public void authorityOverrideInCallOptions_matchesServerPeerHost_newStreamCreationSucceeds() + throws IOException, InterruptedException, GeneralSecurityException, ExecutionException, + TimeoutException { + NettyClientHandler.enablePerRpcAuthorityCheck = true; + try { + startServer(); + NettyClientTransport transport = newTransport(newNegotiator()); + SettableFuture connected = SettableFuture.create(); + FakeClientTransportListener fakeClientTransportListener = + new FakeClientTransportListener(connected); + callMeMaybe(transport.start(fakeClientTransportListener)); + connected.get(10, TimeUnit.SECONDS); + assertThat(fakeClientTransportListener.isConnected()).isTrue(); + + new Rpc(transport, new Metadata(), "foo.test.google.fr").waitForResponse(); + } finally { + NettyClientHandler.enablePerRpcAuthorityCheck = false;; + } + } + + // Without removing the port number part that {@link X509AuthorityVerifier} does, there will be a + // java.security.cert.CertificateException: Illegal given domain name: foo.test.google.fr:12345 + @Test + public void authorityOverrideInCallOptions_portNumberInAuthority_isStrippedForPeerVerification() + throws IOException, InterruptedException, GeneralSecurityException, ExecutionException, + TimeoutException { + NettyClientHandler.enablePerRpcAuthorityCheck = true; + try { + startServer(); + NettyClientTransport transport = newTransport(newNegotiator()); + SettableFuture connected = SettableFuture.create(); + FakeClientTransportListener fakeClientTransportListener = + new FakeClientTransportListener(connected); + callMeMaybe(transport.start(fakeClientTransportListener)); + connected.get(10, TimeUnit.SECONDS); + assertThat(fakeClientTransportListener.isConnected()).isTrue(); + + new Rpc(transport, new Metadata(), "foo.test.google.fr:12345").waitForResponse(); + } finally { + NettyClientHandler.enablePerRpcAuthorityCheck = false;; + } + } + + @Test + public void authorityOverrideInCallOptions_portNumberAndIpv6_isStrippedForPeerVerification() + throws IOException, InterruptedException, GeneralSecurityException, ExecutionException, + TimeoutException { + NettyClientHandler.enablePerRpcAuthorityCheck = true; + try { + startServer(); + NettyClientTransport transport = newTransport(newNegotiator()); + SettableFuture connected = SettableFuture.create(); + FakeClientTransportListener fakeClientTransportListener = + new FakeClientTransportListener(connected); + callMeMaybe(transport.start(fakeClientTransportListener)); + connected.get(10, TimeUnit.SECONDS); + assertThat(fakeClientTransportListener.isConnected()).isTrue(); + + new Rpc(transport, new Metadata(), "[2001:db8:3333:4444:5555:6666:1.2.3.4]:12345") + .waitForResponse(); + } catch (ExecutionException ex) { + Status status = ((StatusException) ex.getCause()).getStatus(); + assertThat(status.getDescription()).isEqualTo("Peer hostname verification during rpc " + + "failed for authority '[2001:db8:3333:4444:5555:6666:1.2.3.4]:12345'"); + assertThat(status.getCode()).isEqualTo(Code.UNAVAILABLE); + assertThat(((InvocationTargetException) ex.getCause().getCause()).getTargetException()) + .isInstanceOf(CertificateException.class); + // Port number is removed by {@link X509AuthorityVerifier}. + assertThat(((InvocationTargetException) ex.getCause().getCause()).getTargetException() + .getMessage()).isEqualTo( + "No subject alternative names matching IP address 2001:db8:3333:4444:5555:6666:1.2.3.4 " + + "found"); + } finally { + NettyClientHandler.enablePerRpcAuthorityCheck = false;; + } + } + + @Test + public void authorityOverrideInCallOptions_notMatches_flagDisabled_createsStream() + throws IOException, InterruptedException, GeneralSecurityException, ExecutionException, + TimeoutException { + startServer(); + NettyClientTransport transport = newTransport(newNegotiator()); + SettableFuture connected = SettableFuture.create(); + FakeClientTransportListener fakeClientTransportListener = + new FakeClientTransportListener(connected); + callMeMaybe(transport.start(fakeClientTransportListener)); + connected.get(10, TimeUnit.SECONDS); + assertThat(fakeClientTransportListener.isConnected()).isTrue(); + + new Rpc(transport, new Metadata(), "foo.test.google.in").waitForResponse(); + } + private Throwable getRootCause(Throwable t) { if (t.getCause() == null) { return t; @@ -843,10 +1031,37 @@ private Throwable getRootCause(Throwable t) { return getRootCause(t.getCause()); } - private ProtocolNegotiator newNegotiator() throws IOException { + private ProtocolNegotiator newNegotiator() throws IOException, GeneralSecurityException { InputStream caCert = TlsTesting.loadCert("ca.pem"); SslContext clientContext = GrpcSslContexts.forClient().trustManager(caCert).build(); - return ProtocolNegotiators.tls(clientContext); + return ProtocolNegotiators.tls(clientContext, + (X509TrustManager) getX509ExtendedTrustManager(TlsTesting.loadCert("ca.pem"))); + } + + private static TrustManager getX509ExtendedTrustManager(InputStream rootCerts) + throws GeneralSecurityException { + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + try { + ks.load(null, null); + } catch (IOException ex) { + // Shouldn't really happen, as we're not loading any data. + throw new GeneralSecurityException(ex); + } + X509Certificate[] certs = CertificateUtils.getX509Certificates(rootCerts); + for (X509Certificate cert : certs) { + X500Principal principal = cert.getSubjectX500Principal(); + ks.setCertificateEntry(principal.getName("RFC2253"), cert); + } + + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(ks); + for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) { + if (trustManager instanceof X509ExtendedTrustManager) { + return trustManager; + } + } + return null; } private NettyClientTransport newTransport(ProtocolNegotiator negotiator) { @@ -965,13 +1180,20 @@ private static class Rpc { final TestClientStreamListener listener = new TestClientStreamListener(); Rpc(NettyClientTransport transport) { - this(transport, new Metadata()); + this(transport, new Metadata(), null); } Rpc(NettyClientTransport transport, Metadata headers) { + this(transport, headers, null); + } + + Rpc(NettyClientTransport transport, Metadata headers, String authorityOverride) { stream = transport.newStream( METHOD, headers, CallOptions.DEFAULT, new ClientStreamTracer[]{ new ClientStreamTracer() {} }); + if (authorityOverride != null) { + stream.setAuthority(authorityOverride); + } stream.start(listener); stream.request(1); stream.writeMessage(new ByteArrayInputStream(MESSAGE.getBytes(UTF_8))); @@ -1169,4 +1391,62 @@ public void log(ChannelLogLevel level, String message) {} @Override public void log(ChannelLogLevel level, String messageFormat, Object... args) {} } + + static class FakeClientTransportListener implements ManagedClientTransport.Listener { + private final SettableFuture connected; + + @GuardedBy("this") + private boolean isConnected = false; + + public FakeClientTransportListener(SettableFuture connected) { + this.connected = connected; + } + + @Override + public void transportShutdown(Status s) {} + + @Override + public void transportTerminated() {} + + @Override + public void transportReady() { + synchronized (this) { + isConnected = true; + } + connected.set(null); + } + + synchronized boolean isConnected() { + return isConnected; + } + + @Override + public void transportInUse(boolean inUse) {} + } + + private static class FakeTrustManager implements X509TrustManager { + + private final X509TrustManager delegate; + + public FakeTrustManager(X509TrustManager x509ExtendedTrustManager) { + this.delegate = x509ExtendedTrustManager; + } + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException { + delegate.checkClientTrusted(x509Certificates, s); + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException { + delegate.checkServerTrusted(x509Certificates, s); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return delegate.getAcceptedIssuers(); + } + } } diff --git a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java index 6dff3de2b2a..4829bcc7419 100644 --- a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java +++ b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java @@ -112,10 +112,14 @@ import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandshakeCompletionEvent; import java.io.File; +import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayDeque; import java.util.Arrays; @@ -222,13 +226,52 @@ public ChannelCredentials withoutBearerTokens() { } @Test - public void fromClient_tls() { + public void fromClient_tls_trustManager() + throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException { + KeyStore certStore = KeyStore.getInstance(KeyStore.getDefaultType()); + certStore.load(null); + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + try (InputStream ca = TlsTesting.loadCert("ca.pem")) { + for (X509Certificate cert : CertificateUtils.getX509Certificates(ca)) { + certStore.setCertificateEntry(cert.getSubjectX500Principal().getName("RFC2253"), cert); + } + } + trustManagerFactory.init(certStore); + ProtocolNegotiators.FromChannelCredentialsResult result = + ProtocolNegotiators.from(TlsChannelCredentials.newBuilder() + .trustManager(trustManagerFactory.getTrustManagers()).build()); + assertThat(result.error).isNull(); + assertThat(result.callCredentials).isNull(); + assertThat(result.negotiator) + .isInstanceOf(ProtocolNegotiators.TlsProtocolNegotiatorClientFactory.class); + assertThat(((ClientTlsProtocolNegotiator) result.negotiator.newNegotiator()) + .hasX509ExtendedTrustManager()).isTrue(); + } + + @Test + public void fromClient_tls_CaCertsInputStream() throws IOException { + ProtocolNegotiators.FromChannelCredentialsResult result = + ProtocolNegotiators.from(TlsChannelCredentials.newBuilder() + .trustManager(TlsTesting.loadCert("ca.pem")).build()); + assertThat(result.error).isNull(); + assertThat(result.callCredentials).isNull(); + assertThat(result.negotiator) + .isInstanceOf(ProtocolNegotiators.TlsProtocolNegotiatorClientFactory.class); + assertThat(((ClientTlsProtocolNegotiator) result.negotiator.newNegotiator()) + .hasX509ExtendedTrustManager()).isTrue(); + } + + @Test + public void fromClient_tls_systemDefault() { ProtocolNegotiators.FromChannelCredentialsResult result = ProtocolNegotiators.from(TlsChannelCredentials.create()); assertThat(result.error).isNull(); assertThat(result.callCredentials).isNull(); assertThat(result.negotiator) .isInstanceOf(ProtocolNegotiators.TlsProtocolNegotiatorClientFactory.class); + assertThat(((ClientTlsProtocolNegotiator) result.negotiator.newNegotiator()) + .hasX509ExtendedTrustManager()).isTrue(); } @Test @@ -877,7 +920,8 @@ public String applicationProtocol() { DefaultEventLoopGroup elg = new DefaultEventLoopGroup(1); ClientTlsHandler handler = new ClientTlsHandler(grpcHandler, sslContext, - "authority", elg, noopLogger, Optional.absent()); + "authority", elg, noopLogger, Optional.absent(), + getClientTlsProtocolNegotiator(), null); pipeline.addLast(handler); pipeline.replace(SslHandler.class, null, goodSslHandler); pipeline.fireUserEventTriggered(ProtocolNegotiationEvent.DEFAULT); @@ -915,7 +959,8 @@ public String applicationProtocol() { .applicationProtocolConfig(apn).build(); ClientTlsHandler handler = new ClientTlsHandler(grpcHandler, sslContext, - "authority", elg, noopLogger, Optional.absent()); + "authority", elg, noopLogger, Optional.absent(), + getClientTlsProtocolNegotiator(), null); pipeline.addLast(handler); pipeline.replace(SslHandler.class, null, goodSslHandler); pipeline.fireUserEventTriggered(ProtocolNegotiationEvent.DEFAULT); @@ -939,7 +984,8 @@ public String applicationProtocol() { DefaultEventLoopGroup elg = new DefaultEventLoopGroup(1); ClientTlsHandler handler = new ClientTlsHandler(grpcHandler, sslContext, - "authority", elg, noopLogger, Optional.absent()); + "authority", elg, noopLogger, Optional.absent(), + getClientTlsProtocolNegotiator(), null); pipeline.addLast(handler); final AtomicReference error = new AtomicReference<>(); @@ -967,7 +1013,8 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { @Test public void clientTlsHandler_closeDuringNegotiation() throws Exception { ClientTlsHandler handler = new ClientTlsHandler(grpcHandler, sslContext, - "authority", null, noopLogger, Optional.absent()); + "authority", null, noopLogger, Optional.absent(), + getClientTlsProtocolNegotiator(), null); pipeline.addLast(new WriteBufferingAndExceptionHandler(handler)); ChannelFuture pendingWrite = channel.writeAndFlush(NettyClientHandler.NOOP_MESSAGE); @@ -979,6 +1026,12 @@ public void clientTlsHandler_closeDuringNegotiation() throws Exception { .isEqualTo(Status.Code.UNAVAILABLE); } + private ClientTlsProtocolNegotiator getClientTlsProtocolNegotiator() throws SSLException { + return new ClientTlsProtocolNegotiator(GrpcSslContexts.forClient().trustManager( + TlsTesting.loadCert("ca.pem")).build(), + null, Optional.absent(), null); + } + @Test public void engineLog() { ChannelHandler handler = new ServerTlsHandler(grpcHandler, sslContext, null); @@ -1007,7 +1060,7 @@ public boolean isLoggable(LogRecord record) { public void tls_failsOnNullSslContext() { thrown.expect(NullPointerException.class); - Object unused = ProtocolNegotiators.tls(null); + Object unused = ProtocolNegotiators.tls(null, null); } @Test @@ -1230,7 +1283,7 @@ public void clientTlsHandler_firesNegotiation() throws Exception { } FakeGrpcHttp2ConnectionHandler gh = FakeGrpcHttp2ConnectionHandler.newHandler(); ClientTlsProtocolNegotiator pn = new ClientTlsProtocolNegotiator(clientSslContext, - null, Optional.absent()); + null, Optional.absent(), null); WriteBufferingAndExceptionHandler clientWbaeh = new WriteBufferingAndExceptionHandler(pn.newHandler(gh)); diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java index f42cb9fb16d..7eaaa6fd763 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java @@ -81,8 +81,6 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.security.auth.x500.X500Principal; /** Convenience class for building channels with the OkHttp transport. */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1785") @@ -705,32 +703,12 @@ static KeyManager[] createKeyManager(InputStream certChain, InputStream privateK static TrustManager[] createTrustManager(byte[] rootCerts) throws GeneralSecurityException { InputStream rootCertsStream = new ByteArrayInputStream(rootCerts); try { - return createTrustManager(rootCertsStream); + return io.grpc.internal.CertificateUtils.createTrustManager(rootCertsStream); } finally { GrpcUtil.closeQuietly(rootCertsStream); } } - static TrustManager[] createTrustManager(InputStream rootCerts) throws GeneralSecurityException { - KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); - try { - ks.load(null, null); - } catch (IOException ex) { - // Shouldn't really happen, as we're not loading any data. - throw new GeneralSecurityException(ex); - } - X509Certificate[] certs = CertificateUtils.getX509Certificates(rootCerts); - for (X509Certificate cert : certs) { - X500Principal principal = cert.getSubjectX500Principal(); - ks.setCertificateEntry(principal.getName("RFC2253"), cert); - } - - TrustManagerFactory trustManagerFactory = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(ks); - return trustManagerFactory.getTrustManagers(); - } - static Collection> getSupportedSocketAddressTypes() { return Collections.singleton(InetSocketAddress.class); } diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpChannelBuilderTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpChannelBuilderTest.java index 3670cd057c1..c86e80656e3 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpChannelBuilderTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpChannelBuilderTest.java @@ -34,6 +34,7 @@ import io.grpc.InsecureChannelCredentials; import io.grpc.ManagedChannel; import io.grpc.TlsChannelCredentials; +import io.grpc.internal.CertificateUtils; import io.grpc.internal.ClientTransportFactory; import io.grpc.internal.ClientTransportFactory.SwapChannelCredentialsResult; import io.grpc.internal.FakeClock; @@ -212,7 +213,7 @@ public void sslSocketFactoryFrom_tls_mtls() throws Exception { TrustManager[] trustManagers; try (InputStream ca = TlsTesting.loadCert("ca.pem")) { - trustManagers = OkHttpChannelBuilder.createTrustManager(ca); + trustManagers = CertificateUtils.createTrustManager(ca); } SSLContext serverContext = SSLContext.getInstance("TLS"); @@ -257,7 +258,7 @@ public void sslSocketFactoryFrom_tls_mtls_keyFile() throws Exception { InputStream ca = TlsTesting.loadCert("ca.pem")) { serverContext.init( OkHttpChannelBuilder.createKeyManager(server1Chain, server1Key), - OkHttpChannelBuilder.createTrustManager(ca), + CertificateUtils.createTrustManager(ca), null); } final SSLServerSocket serverListenSocket = From 1a2285b52786ee190e4157cf0b005aabb7316f96 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Mon, 3 Mar 2025 17:28:36 -0500 Subject: [PATCH 200/591] xds: ensure server interceptors are created in a sync context (#11930) `XdsServerWrapper#generatePerRouteInterceptors` was always intended to be executed within a sync context. This PR ensures that by calling `syncContext.throwIfNotInThisSynchronizationContext()`. This change is needed for upcoming xDS filter state retention because the new tests in XdsServerWrapperTest flake with this NPE: > `Cannot invoke "io.grpc.xds.client.XdsClient$ResourceWatcher.onChanged(io.grpc.xds.client.XdsClient$ResourceUpdate)" because "this.ldsWatcher" is null` --- .../java/io/grpc/xds/XdsServerWrapper.java | 4 +- .../java/io/grpc/xds/XdsServerTestHelper.java | 83 +++++++++++++++---- .../io/grpc/xds/XdsServerWrapperTest.java | 28 +++---- 3 files changed, 79 insertions(+), 36 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index bbb17d9b616..e5b25ae458b 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -524,9 +524,7 @@ private AtomicReference generateRoutingConfig(FilterChain f private ImmutableMap generatePerRouteInterceptors( @Nullable List filterConfigs, List virtualHosts) { - // This should always be called from the sync context. - // Ideally we'd want to throw otherwise, but this breaks the tests now. - // syncContext.throwIfNotInThisSynchronizationContext(); + syncContext.throwIfNotInThisSynchronizationContext(); ImmutableMap.Builder perRouteInterceptors = new ImmutableMap.Builder<>(); diff --git a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java index a27c2917712..0508b11c205 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java @@ -38,6 +38,7 @@ import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsInitializationException; import io.grpc.xds.client.XdsResourceType; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -45,7 +46,10 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; /** @@ -174,12 +178,18 @@ public List getTargets() { } } + // Implementation details: + // 1. Use `synchronized` in methods where XdsClientImpl uses its own `syncContext`. + // 2. Use `serverExecutor` via `execute()` in methods where XdsClientImpl uses watcher's executor. static final class FakeXdsClient extends XdsClient { - boolean shutdown; - SettableFuture ldsResource = SettableFuture.create(); - ResourceWatcher ldsWatcher; - CountDownLatch rdsCount = new CountDownLatch(1); + public static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5); + + private boolean shutdown; + @Nullable SettableFuture ldsResource = SettableFuture.create(); + @Nullable ResourceWatcher ldsWatcher; + private CountDownLatch rdsCount = new CountDownLatch(1); final Map> rdsWatchers = new HashMap<>(); + @Nullable private volatile Executor serverExecutor; @Override public TlsContextManager getSecurityConfig() { @@ -193,14 +203,20 @@ public BootstrapInfo getBootstrapInfo() { @Override @SuppressWarnings("unchecked") - public void watchXdsResource(XdsResourceType resourceType, - String resourceName, - ResourceWatcher watcher, - Executor syncContext) { + public synchronized void watchXdsResource( + XdsResourceType resourceType, + String resourceName, + ResourceWatcher watcher, + Executor executor) { + if (serverExecutor != null) { + assertThat(executor).isEqualTo(serverExecutor); + } + switch (resourceType.typeName()) { case "LDS": assertThat(ldsWatcher).isNull(); ldsWatcher = (ResourceWatcher) watcher; + serverExecutor = executor; ldsResource.set(resourceName); break; case "RDS": @@ -213,14 +229,14 @@ public void watchXdsResource(XdsResourceType resou } @Override - public void cancelXdsResourceWatch(XdsResourceType type, - String resourceName, - ResourceWatcher watcher) { + public synchronized void cancelXdsResourceWatch( + XdsResourceType type, String resourceName, ResourceWatcher watcher) { switch (type.typeName()) { case "LDS": assertThat(ldsWatcher).isNotNull(); ldsResource = null; ldsWatcher = null; + serverExecutor = null; break; case "RDS": rdsWatchers.remove(resourceName); @@ -230,27 +246,58 @@ public void cancelXdsResourceWatch(XdsResourceType } @Override - public void shutdown() { + public synchronized void shutdown() { shutdown = true; } @Override - public boolean isShutDown() { + public synchronized boolean isShutDown() { return shutdown; } + public void awaitRds(Duration timeout) throws InterruptedException, TimeoutException { + if (!rdsCount.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Timeout " + timeout + " waiting for RDSs"); + } + } + + public void setExpectedRdsCount(int count) { + rdsCount = new CountDownLatch(count); + } + + private void execute(Runnable action) { + // This method ensures that all watcher updates: + // - Happen after the server started watching LDS. + // - Are executed within the sync context of the server. + // + // Note that this doesn't guarantee that any of the RDS watchers are created. + // Tests should use setExpectedRdsCount(int) and awaitRds() for that. + if (ldsResource == null) { + throw new IllegalStateException("xDS resource update after watcher cancel"); + } + try { + ldsResource.get(DEFAULT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + } catch (ExecutionException | TimeoutException e) { + throw new RuntimeException("Can't resolve LDS resource name in " + DEFAULT_TIMEOUT, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + serverExecutor.execute(action); + } + void deliverLdsUpdate(List filterChains, FilterChain defaultFilterChain) { - ldsWatcher.onChanged(LdsUpdate.forTcpListener(Listener.create( - "listener", "0.0.0.0:1", ImmutableList.copyOf(filterChains), defaultFilterChain))); + deliverLdsUpdate(LdsUpdate.forTcpListener(Listener.create( + "listener", "0.0.0.0:1", ImmutableList.copyOf(filterChains), defaultFilterChain))); } void deliverLdsUpdate(LdsUpdate ldsUpdate) { - ldsWatcher.onChanged(ldsUpdate); + execute(() -> ldsWatcher.onChanged(ldsUpdate)); } - void deliverRdsUpdate(String rdsName, List virtualHosts) { - rdsWatchers.get(rdsName).onChanged(new RdsUpdate(virtualHosts)); + void deliverRdsUpdate(String resourceName, List virtualHosts) { + execute(() -> rdsWatchers.get(resourceName).onChanged(new RdsUpdate(virtualHosts))); } } } diff --git a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java index 41f005ba583..388052a3dc8 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java @@ -74,7 +74,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -252,7 +251,7 @@ public void run() { FilterChain f0 = createFilterChain("filter-chain-0", hcm_virtual); FilterChain f1 = createFilterChain("filter-chain-1", createRds("rds")); xdsClient.deliverLdsUpdate(Collections.singletonList(f0), f1); - xdsClient.rdsCount.await(5, TimeUnit.SECONDS); + xdsClient.awaitRds(FakeXdsClient.DEFAULT_TIMEOUT); xdsClient.deliverRdsUpdate("rds", Collections.singletonList(createVirtualHost("virtual-host-1"))); verify(listener, timeout(5000)).onServing(); @@ -261,7 +260,7 @@ public void run() { xdsServerWrapper.shutdown(); assertThat(xdsServerWrapper.isShutdown()).isTrue(); assertThat(xdsClient.ldsResource).isNull(); - assertThat(xdsClient.shutdown).isTrue(); + assertThat(xdsClient.isShutDown()).isTrue(); verify(mockServer).shutdown(); assertThat(f0.sslContextProviderSupplier().isShutdown()).isTrue(); assertThat(f1.sslContextProviderSupplier().isShutdown()).isTrue(); @@ -303,7 +302,7 @@ public void run() { verify(mockServer, never()).start(); assertThat(xdsServerWrapper.isShutdown()).isTrue(); assertThat(xdsClient.ldsResource).isNull(); - assertThat(xdsClient.shutdown).isTrue(); + assertThat(xdsClient.isShutDown()).isTrue(); verify(mockServer).shutdown(); assertThat(f0.sslContextProviderSupplier().isShutdown()).isTrue(); assertThat(f1.sslContextProviderSupplier().isShutdown()).isTrue(); @@ -342,7 +341,7 @@ public void run() { xdsServerWrapper.shutdown(); assertThat(xdsServerWrapper.isShutdown()).isTrue(); assertThat(xdsClient.ldsResource).isNull(); - assertThat(xdsClient.shutdown).isTrue(); + assertThat(xdsClient.isShutDown()).isTrue(); verify(mockBuilder, times(1)).build(); verify(mockServer, times(1)).shutdown(); xdsServerWrapper.awaitTermination(1, TimeUnit.SECONDS); @@ -367,7 +366,7 @@ public void run() { FilterChain filterChain = createFilterChain("filter-chain-1", createRds("rds")); SslContextProviderSupplier sslSupplier = filterChain.sslContextProviderSupplier(); xdsClient.deliverLdsUpdate(Collections.singletonList(filterChain), null); - xdsClient.rdsCount.await(5, TimeUnit.SECONDS); + xdsClient.awaitRds(FakeXdsClient.DEFAULT_TIMEOUT); xdsClient.deliverRdsUpdate("rds", Collections.singletonList(createVirtualHost("virtual-host-1"))); try { @@ -434,7 +433,7 @@ public void run() { xdsClient.ldsResource.get(5, TimeUnit.SECONDS); FilterChain filterChain = createFilterChain("filter-chain-1", createRds("rds")); xdsClient.deliverLdsUpdate(Collections.singletonList(filterChain), null); - xdsClient.rdsCount.await(5, TimeUnit.SECONDS); + xdsClient.awaitRds(FakeXdsClient.DEFAULT_TIMEOUT); xdsClient.deliverRdsUpdate("rds", Collections.singletonList(createVirtualHost("virtual-host-1"))); try { @@ -544,7 +543,7 @@ public void run() { 0L, Collections.singletonList(virtualHost), new ArrayList()); EnvoyServerProtoData.FilterChain f0 = createFilterChain("filter-chain-0", hcmVirtual); EnvoyServerProtoData.FilterChain f1 = createFilterChain("filter-chain-1", createRds("r0")); - xdsClient.rdsCount = new CountDownLatch(3); + xdsClient.setExpectedRdsCount(3); xdsClient.deliverLdsUpdate(Arrays.asList(f0, f1), null); assertThat(start.isDone()).isFalse(); assertThat(selectorManager.getSelectorToUpdateSelector()).isNull(); @@ -556,7 +555,7 @@ public void run() { xdsClient.deliverLdsUpdate(Arrays.asList(f0, f2), f3); verify(mockServer, never()).start(); verify(listener, never()).onServing(); - xdsClient.rdsCount.await(5, TimeUnit.SECONDS); + xdsClient.awaitRds(FakeXdsClient.DEFAULT_TIMEOUT); xdsClient.deliverRdsUpdate("r1", Collections.singletonList(createVirtualHost("virtual-host-1"))); @@ -602,12 +601,11 @@ public void run() { EnvoyServerProtoData.FilterChain f1 = createFilterChain("filter-chain-1", createRds("r0")); EnvoyServerProtoData.FilterChain f2 = createFilterChain("filter-chain-2", createRds("r0")); - xdsClient.rdsCount = new CountDownLatch(1); xdsClient.deliverLdsUpdate(Arrays.asList(f0, f1), f2); assertThat(start.isDone()).isFalse(); assertThat(selectorManager.getSelectorToUpdateSelector()).isNull(); - xdsClient.rdsCount.await(5, TimeUnit.SECONDS); + xdsClient.awaitRds(FakeXdsClient.DEFAULT_TIMEOUT); xdsClient.deliverRdsUpdate("r0", Collections.singletonList(createVirtualHost("virtual-host-0"))); start.get(5000, TimeUnit.MILLISECONDS); @@ -633,9 +631,9 @@ public void run() { EnvoyServerProtoData.FilterChain f3 = createFilterChain("filter-chain-3", createRds("r0")); EnvoyServerProtoData.FilterChain f4 = createFilterChain("filter-chain-4", createRds("r1")); EnvoyServerProtoData.FilterChain f5 = createFilterChain("filter-chain-4", createRds("r1")); - xdsClient.rdsCount = new CountDownLatch(1); + xdsClient.setExpectedRdsCount(1); xdsClient.deliverLdsUpdate(Arrays.asList(f5, f3), f4); - xdsClient.rdsCount.await(5, TimeUnit.SECONDS); + xdsClient.awaitRds(FakeXdsClient.DEFAULT_TIMEOUT); xdsClient.deliverRdsUpdate("r1", Collections.singletonList(createVirtualHost("virtual-host-1"))); xdsClient.deliverRdsUpdate("r0", @@ -688,7 +686,7 @@ public void run() { EnvoyServerProtoData.FilterChain f0 = createFilterChain("filter-chain-0", hcmVirtual); EnvoyServerProtoData.FilterChain f1 = createFilterChain("filter-chain-1", createRds("r0")); xdsClient.deliverLdsUpdate(Arrays.asList(f0, f1), null); - xdsClient.rdsCount.await(); + xdsClient.awaitRds(FakeXdsClient.DEFAULT_TIMEOUT); xdsClient.rdsWatchers.get("r0").onError(Status.CANCELLED); start.get(5000, TimeUnit.MILLISECONDS); assertThat(selectorManager.getSelectorToUpdateSelector().getRoutingConfigs().size()) @@ -1235,7 +1233,7 @@ public ServerCall.Listener interceptCall(ServerCall Date: Tue, 4 Mar 2025 04:32:03 +0000 Subject: [PATCH 201/591] rls: allow maxAge to exceed 5m if staleAge is set (#11931) --- .../java/io/grpc/rls/RlsProtoConverters.java | 9 ++- .../io/grpc/rls/RlsProtoConvertersTest.java | 59 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/rls/src/main/java/io/grpc/rls/RlsProtoConverters.java b/rls/src/main/java/io/grpc/rls/RlsProtoConverters.java index cd164f5e2a7..aa5147449c4 100644 --- a/rls/src/main/java/io/grpc/rls/RlsProtoConverters.java +++ b/rls/src/main/java/io/grpc/rls/RlsProtoConverters.java @@ -152,10 +152,15 @@ protected RouteLookupConfig doForward(Map json) { checkArgument(staleAge == null, "to specify staleAge, must have maxAge"); maxAge = MAX_AGE_NANOS; } - if (staleAge == null) { + // If staleAge is not set, clamp maxAge to <= 5. + if (staleAge == null && maxAge > MAX_AGE_NANOS) { + maxAge = MAX_AGE_NANOS; + } + // Clamp staleAge to <= 5 + if (staleAge == null || staleAge > MAX_AGE_NANOS) { staleAge = MAX_AGE_NANOS; } - maxAge = Math.min(maxAge, MAX_AGE_NANOS); + // Ignore staleAge if greater than maxAge. staleAge = Math.min(staleAge, maxAge); long cacheSize = orDefault(JsonUtil.getNumberAsLong(json, "cacheSizeBytes"), MAX_CACHE_SIZE); checkArgument(cacheSize > 0, "cacheSize must be positive"); diff --git a/rls/src/test/java/io/grpc/rls/RlsProtoConvertersTest.java b/rls/src/test/java/io/grpc/rls/RlsProtoConvertersTest.java index 98b7101fd5e..fc5fdb59f21 100644 --- a/rls/src/test/java/io/grpc/rls/RlsProtoConvertersTest.java +++ b/rls/src/test/java/io/grpc/rls/RlsProtoConvertersTest.java @@ -469,6 +469,65 @@ public void convert_jsonRlsConfig_staleAgeGivenWithoutMaxAge() throws IOExceptio } } + @Test + public void convert_jsonRlsConfig_doNotClampMaxAgeIfStaleAgeIsSet() throws IOException { + String jsonStr = "{\n" + + " \"grpcKeybuilders\": [\n" + + " {\n" + + " \"names\": [\n" + + " {\n" + + " \"service\": \"service1\",\n" + + " \"method\": \"create\"\n" + + " }\n" + + " ],\n" + + " \"headers\": [\n" + + " {\n" + + " \"key\": \"user\"," + + " \"names\": [\"User\", \"Parent\"],\n" + + " \"optional\": true\n" + + " },\n" + + " {\n" + + " \"key\": \"id\"," + + " \"names\": [\"X-Google-Id\"],\n" + + " \"optional\": true\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"lookupService\": \"service1\",\n" + + " \"lookupServiceTimeout\": \"2s\",\n" + + " \"maxAge\": \"350s\",\n" + + " \"staleAge\": \"310s\",\n" + + " \"validTargets\": [\"a valid target\"]," + + " \"cacheSizeBytes\": \"1000\",\n" + + " \"defaultTarget\": \"us_east_1.cloudbigtable.googleapis.com\"\n" + + "}"; + + RouteLookupConfig expectedConfig = + RouteLookupConfig.builder() + .grpcKeybuilders(ImmutableList.of( + GrpcKeyBuilder.create( + ImmutableList.of(Name.create("service1", "create")), + ImmutableList.of( + NameMatcher.create("user", ImmutableList.of("User", "Parent")), + NameMatcher.create("id", ImmutableList.of("X-Google-Id"))), + ExtraKeys.DEFAULT, + ImmutableMap.of()))) + .lookupService("service1") + .lookupServiceTimeoutInNanos(TimeUnit.SECONDS.toNanos(2)) + .maxAgeInNanos(TimeUnit.SECONDS.toNanos(350)) // Should not be clamped + .staleAgeInNanos(TimeUnit.SECONDS.toNanos(300)) // Should be clamped to max 300s + .cacheSizeBytes(1000) + .defaultTarget("us_east_1.cloudbigtable.googleapis.com") + .build(); + + RouteLookupConfigConverter converter = new RouteLookupConfigConverter(); + @SuppressWarnings("unchecked") + Map parsedJson = (Map) JsonParser.parse(jsonStr); + RouteLookupConfig converted = converter.convert(parsedJson); + assertThat(converted).isEqualTo(expectedConfig); + } + @Test public void convert_jsonRlsConfig_keyBuilderWithoutName() throws IOException { String jsonStr = "{\n" From 12197065fec471eccff8f8f83093b91f698acc47 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Thu, 6 Mar 2025 08:10:18 +0000 Subject: [PATCH 202/591] xds: xDS-based HTTP CONNECT configuration (#11861) --- xds/BUILD.bazel | 1 + .../java/io/grpc/xds/CdsLoadBalancer2.java | 4 +- .../grpc/xds/ClusterResolverLoadBalancer.java | 46 ++- .../ClusterResolverLoadBalancerProvider.java | 9 +- xds/src/main/java/io/grpc/xds/Endpoints.java | 20 +- .../io/grpc/xds/GcpAuthenticationFilter.java | 12 +- .../java/io/grpc/xds/MetadataRegistry.java | 60 +++- .../java/io/grpc/xds/XdsClusterResource.java | 126 ++++---- .../java/io/grpc/xds/XdsEndpointResource.java | 74 ++++- .../io/grpc/xds/CdsLoadBalancer2Test.java | 45 +-- .../xds/ClusterResolverLoadBalancerTest.java | 305 ++++++++++++------ .../grpc/xds/GrpcXdsClientImplDataTest.java | 120 ++++++- .../grpc/xds/GrpcXdsClientImplTestBase.java | 16 +- .../test/java/io/grpc/xds/XdsTestUtils.java | 8 +- xds/third_party/envoy/import.sh | 1 + .../v3/upstream_http_11_connect.proto | 38 +++ 16 files changed, 665 insertions(+), 220 deletions(-) create mode 100644 xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/http_11_proxy/v3/upstream_http_11_connect.proto diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index b235a79c526..53fac28b2da 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -85,6 +85,7 @@ java_proto_library( "@envoy_api//envoy/extensions/load_balancing_policies/ring_hash/v3:pkg", "@envoy_api//envoy/extensions/load_balancing_policies/round_robin/v3:pkg", "@envoy_api//envoy/extensions/load_balancing_policies/wrr_locality/v3:pkg", + "@envoy_api//envoy/extensions/transport_sockets/http_11_proxy/v3:pkg", "@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg", "@envoy_api//envoy/service/discovery/v3:pkg", "@envoy_api//envoy/service/load_stats/v3:pkg", diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java index 04b7663fd35..bb44071a484 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java @@ -243,7 +243,9 @@ private void handleClusterDiscovered() { } ClusterResolverConfig config = new ClusterResolverConfig( - Collections.unmodifiableList(instances), configOrError.getConfig()); + Collections.unmodifiableList(instances), + configOrError.getConfig(), + root.result.isHttp11ProxyAvailable()); if (childLb == null) { childLb = lbRegistry.getProvider(CLUSTER_RESOLVER_POLICY_NAME).newLoadBalancer(helper); } diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index 4e08ddc5973..c92f592ebc8 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -25,6 +25,7 @@ import com.google.protobuf.Struct; import io.grpc.Attributes; import io.grpc.EquivalentAddressGroup; +import io.grpc.HttpConnectProxiedSocketAddress; import io.grpc.InternalLogId; import io.grpc.LoadBalancer; import io.grpc.LoadBalancerProvider; @@ -59,6 +60,8 @@ import io.grpc.xds.client.XdsClient.ResourceWatcher; import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger.XdsLogLevel; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -430,8 +433,18 @@ public void run() { .set(XdsAttributes.ATTR_SERVER_WEIGHT, weight) .set(XdsAttributes.ATTR_ADDRESS_NAME, endpoint.hostname()) .build(); - EquivalentAddressGroup eag = new EquivalentAddressGroup( - endpoint.eag().getAddresses(), attr); + + EquivalentAddressGroup eag; + if (config.isHttp11ProxyAvailable()) { + List rewrittenAddresses = new ArrayList<>(); + for (SocketAddress addr : endpoint.eag().getAddresses()) { + rewrittenAddresses.add(rewriteAddress( + addr, endpoint.endpointMetadata(), localityLbInfo.localityMetadata())); + } + eag = new EquivalentAddressGroup(rewrittenAddresses, attr); + } else { + eag = new EquivalentAddressGroup(endpoint.eag().getAddresses(), attr); + } eag = AddressFilter.setPathFilter(eag, Arrays.asList(priorityName, localityName)); addresses.add(eag); } @@ -469,6 +482,35 @@ public void run() { new EndpointsUpdated().run(); } + private SocketAddress rewriteAddress(SocketAddress addr, + ImmutableMap endpointMetadata, + ImmutableMap localityMetadata) { + if (!(addr instanceof InetSocketAddress)) { + return addr; + } + + SocketAddress proxyAddress; + try { + proxyAddress = (SocketAddress) endpointMetadata.get( + "envoy.http11_proxy_transport_socket.proxy_address"); + if (proxyAddress == null) { + proxyAddress = (SocketAddress) localityMetadata.get( + "envoy.http11_proxy_transport_socket.proxy_address"); + } + } catch (ClassCastException e) { + return addr; + } + + if (proxyAddress == null) { + return addr; + } + + return HttpConnectProxiedSocketAddress.newBuilder() + .setTargetAddress((InetSocketAddress) addr) + .setProxyAddress(proxyAddress) + .build(); + } + private List generatePriorityNames(String name, Map localityLbEndpoints) { TreeMap> todo = new TreeMap<>(); diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java index 2301cb670e0..b5dcb271368 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java @@ -74,10 +74,17 @@ static final class ClusterResolverConfig { final List discoveryMechanisms; // GracefulSwitch configuration final Object lbConfig; + private final boolean isHttp11ProxyAvailable; - ClusterResolverConfig(List discoveryMechanisms, Object lbConfig) { + ClusterResolverConfig(List discoveryMechanisms, Object lbConfig, + boolean isHttp11ProxyAvailable) { this.discoveryMechanisms = checkNotNull(discoveryMechanisms, "discoveryMechanisms"); this.lbConfig = checkNotNull(lbConfig, "lbConfig"); + this.isHttp11ProxyAvailable = isHttp11ProxyAvailable; + } + + boolean isHttp11ProxyAvailable() { + return isHttp11ProxyAvailable; } @Override diff --git a/xds/src/main/java/io/grpc/xds/Endpoints.java b/xds/src/main/java/io/grpc/xds/Endpoints.java index 7d7aa3e386d..b0d97d42c11 100644 --- a/xds/src/main/java/io/grpc/xds/Endpoints.java +++ b/xds/src/main/java/io/grpc/xds/Endpoints.java @@ -21,6 +21,7 @@ import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import io.grpc.EquivalentAddressGroup; import java.net.InetSocketAddress; import java.util.List; @@ -41,11 +42,13 @@ abstract static class LocalityLbEndpoints { // Locality's priority level. abstract int priority(); + abstract ImmutableMap localityMetadata(); + static LocalityLbEndpoints create(List endpoints, int localityWeight, - int priority) { + int priority, ImmutableMap localityMetadata) { checkArgument(localityWeight > 0, "localityWeight must be greater than 0"); return new AutoValue_Endpoints_LocalityLbEndpoints( - ImmutableList.copyOf(endpoints), localityWeight, priority); + ImmutableList.copyOf(endpoints), localityWeight, priority, localityMetadata); } } @@ -63,17 +66,20 @@ abstract static class LbEndpoint { abstract String hostname(); + abstract ImmutableMap endpointMetadata(); + static LbEndpoint create(EquivalentAddressGroup eag, int loadBalancingWeight, - boolean isHealthy, String hostname) { - return new AutoValue_Endpoints_LbEndpoint(eag, loadBalancingWeight, isHealthy, hostname); + boolean isHealthy, String hostname, ImmutableMap endpointMetadata) { + return new AutoValue_Endpoints_LbEndpoint( + eag, loadBalancingWeight, isHealthy, hostname, endpointMetadata); } // Only for testing. @VisibleForTesting - static LbEndpoint create( - String address, int port, int loadBalancingWeight, boolean isHealthy, String hostname) { + static LbEndpoint create(String address, int port, int loadBalancingWeight, boolean isHealthy, + String hostname, ImmutableMap endpointMetadata) { return LbEndpoint.create(new EquivalentAddressGroup(new InetSocketAddress(address, port)), - loadBalancingWeight, isHealthy, hostname); + loadBalancingWeight, isHealthy, hostname, endpointMetadata); } } diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java index 7ed617c9843..41687817c47 100644 --- a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java +++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java @@ -36,6 +36,7 @@ import io.grpc.Status; import io.grpc.auth.MoreCallCredentials; import io.grpc.xds.MetadataRegistry.MetadataValueParser; +import io.grpc.xds.client.XdsResourceType.ResourceInvalidException; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; @@ -240,11 +241,16 @@ public String getTypeUrl() { } @Override - public String parse(Any any) throws InvalidProtocolBufferException { - Audience audience = any.unpack(Audience.class); + public String parse(Any any) throws ResourceInvalidException { + Audience audience; + try { + audience = any.unpack(Audience.class); + } catch (InvalidProtocolBufferException ex) { + throw new ResourceInvalidException("Invalid Resource in address proto", ex); + } String url = audience.getUrl(); if (url.isEmpty()) { - throw new InvalidProtocolBufferException( + throw new ResourceInvalidException( "Audience URL is empty. Metadata value must contain a valid URL."); } return url; diff --git a/xds/src/main/java/io/grpc/xds/MetadataRegistry.java b/xds/src/main/java/io/grpc/xds/MetadataRegistry.java index 8243b6a6f0f..b79a61a261a 100644 --- a/xds/src/main/java/io/grpc/xds/MetadataRegistry.java +++ b/xds/src/main/java/io/grpc/xds/MetadataRegistry.java @@ -17,9 +17,14 @@ package io.grpc.xds; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; import com.google.protobuf.Any; -import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Struct; +import io.envoyproxy.envoy.config.core.v3.Metadata; import io.grpc.xds.GcpAuthenticationFilter.AudienceMetadataParser; +import io.grpc.xds.XdsEndpointResource.AddressMetadataParser; +import io.grpc.xds.client.XdsResourceType.ResourceInvalidException; +import io.grpc.xds.internal.ProtobufJsonConverter; import java.util.HashMap; import java.util.Map; @@ -36,6 +41,7 @@ final class MetadataRegistry { private MetadataRegistry() { registerParser(new AudienceMetadataParser()); + registerParser(new AddressMetadataParser()); } static MetadataRegistry getInstance() { @@ -55,6 +61,54 @@ void removeParser(MetadataValueParser parser) { supportedParsers.remove(parser.getTypeUrl()); } + /** + * Parses cluster metadata into a structured map. + * + *

Values in {@code typed_filter_metadata} take precedence over + * {@code filter_metadata} when keys overlap, following Envoy API behavior. See + * + * Envoy metadata documentation for details. + * + * @param metadata the {@link Metadata} containing the fields to parse. + * @return an immutable map of parsed metadata. + * @throws ResourceInvalidException if parsing {@code typed_filter_metadata} fails. + */ + public ImmutableMap parseMetadata(Metadata metadata) + throws ResourceInvalidException { + ImmutableMap.Builder parsedMetadata = ImmutableMap.builder(); + + // Process typed_filter_metadata + for (Map.Entry entry : metadata.getTypedFilterMetadataMap().entrySet()) { + String key = entry.getKey(); + Any value = entry.getValue(); + MetadataValueParser parser = findParser(value.getTypeUrl()); + if (parser != null) { + try { + Object parsedValue = parser.parse(value); + parsedMetadata.put(key, parsedValue); + } catch (ResourceInvalidException e) { + throw new ResourceInvalidException( + String.format("Failed to parse metadata key: %s, type: %s. Error: %s", + key, value.getTypeUrl(), e.getMessage()), e); + } + } + } + // building once to reuse in the next loop + ImmutableMap intermediateParsedMetadata = parsedMetadata.build(); + + // Process filter_metadata for remaining keys + for (Map.Entry entry : metadata.getFilterMetadataMap().entrySet()) { + String key = entry.getKey(); + if (!intermediateParsedMetadata.containsKey(key)) { + Struct structValue = entry.getValue(); + Object jsonValue = ProtobufJsonConverter.convertToJson(structValue); + parsedMetadata.put(key, jsonValue); + } + } + + return parsedMetadata.build(); + } + interface MetadataValueParser { String getTypeUrl(); @@ -64,8 +118,8 @@ interface MetadataValueParser { * * @param any the {@link Any} object to parse. * @return the parsed metadata value. - * @throws InvalidProtocolBufferException if the parsing fails. + * @throws ResourceInvalidException if the parsing fails. */ - Object parse(Any any) throws InvalidProtocolBufferException; + Object parse(Any any) throws ResourceInvalidException; } } diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index 626d61c1f55..cfc74f3ca70 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -25,7 +25,6 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.protobuf.Any; import com.google.protobuf.Duration; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; @@ -33,10 +32,11 @@ import com.google.protobuf.util.Durations; import io.envoyproxy.envoy.config.cluster.v3.CircuitBreakers.Thresholds; import io.envoyproxy.envoy.config.cluster.v3.Cluster; -import io.envoyproxy.envoy.config.core.v3.Metadata; import io.envoyproxy.envoy.config.core.v3.RoutingPriority; import io.envoyproxy.envoy.config.core.v3.SocketAddress; +import io.envoyproxy.envoy.config.core.v3.TransportSocket; import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; +import io.envoyproxy.envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; import io.grpc.LoadBalancerRegistry; @@ -46,15 +46,12 @@ import io.grpc.internal.ServiceConfigUtil.LbConfig; import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; -import io.grpc.xds.MetadataRegistry.MetadataValueParser; import io.grpc.xds.XdsClusterResource.CdsUpdate; import io.grpc.xds.client.XdsClient.ResourceUpdate; import io.grpc.xds.client.XdsResourceType; -import io.grpc.xds.internal.ProtobufJsonConverter; import io.grpc.xds.internal.security.CommonTlsContextUtil; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Set; import javax.annotation.Nullable; @@ -67,6 +64,8 @@ class XdsClusterResource extends XdsResourceType { @VisibleForTesting public static boolean enableSystemRootCerts = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_SYSTEM_ROOT_CERTS", false); + static boolean isEnabledXdsHttpConnect = + GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT", false); @VisibleForTesting static final String AGGREGATE_CLUSTER_TYPE_NAME = "envoy.clusters.aggregate"; @@ -78,6 +77,9 @@ class XdsClusterResource extends XdsResourceType { "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext"; private static final String TYPE_URL_UPSTREAM_TLS_CONTEXT_V2 = "type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext"; + static final String TRANSPORT_SOCKET_NAME_HTTP11_PROXY = + "type.googleapis.com/envoy.extensions.transport_sockets.http_11_proxy.v3" + + ".Http11ProxyUpstreamTransport"; private final LoadBalancerRegistry loadBalancerRegistry = LoadBalancerRegistry.getDefaultRegistry(); @@ -177,10 +179,11 @@ static CdsUpdate processCluster(Cluster cluster, ImmutableMap.copyOf(cluster.getMetadata().getFilterMetadataMap())); try { + MetadataRegistry registry = MetadataRegistry.getInstance(); ImmutableMap parsedFilterMetadata = - parseClusterMetadata(cluster.getMetadata()); + registry.parseMetadata(cluster.getMetadata()); updateBuilder.parsedMetadata(parsedFilterMetadata); - } catch (InvalidProtocolBufferException e) { + } catch (ResourceInvalidException e) { throw new ResourceInvalidException( "Failed to parse xDS filter metadata for cluster '" + cluster.getName() + "': " + e.getMessage(), e); @@ -189,49 +192,6 @@ static CdsUpdate processCluster(Cluster cluster, return updateBuilder.build(); } - /** - * Parses cluster metadata into a structured map. - * - *

Values in {@code typed_filter_metadata} take precedence over - * {@code filter_metadata} when keys overlap, following Envoy API behavior. See - * - * Envoy metadata documentation for details. - * - * @param metadata the {@link Metadata} containing the fields to parse. - * @return an immutable map of parsed metadata. - * @throws InvalidProtocolBufferException if parsing {@code typed_filter_metadata} fails. - */ - private static ImmutableMap parseClusterMetadata(Metadata metadata) - throws InvalidProtocolBufferException { - ImmutableMap.Builder parsedMetadata = ImmutableMap.builder(); - - MetadataRegistry registry = MetadataRegistry.getInstance(); - // Process typed_filter_metadata - for (Map.Entry entry : metadata.getTypedFilterMetadataMap().entrySet()) { - String key = entry.getKey(); - Any value = entry.getValue(); - MetadataValueParser parser = registry.findParser(value.getTypeUrl()); - if (parser != null) { - Object parsedValue = parser.parse(value); - parsedMetadata.put(key, parsedValue); - } - } - // building once to reuse in the next loop - ImmutableMap intermediateParsedMetadata = parsedMetadata.build(); - - // Process filter_metadata for remaining keys - for (Map.Entry entry : metadata.getFilterMetadataMap().entrySet()) { - String key = entry.getKey(); - if (!intermediateParsedMetadata.containsKey(key)) { - Struct structValue = entry.getValue(); - Object jsonValue = ProtobufJsonConverter.convertToJson(structValue); - parsedMetadata.put(key, jsonValue); - } - } - - return parsedMetadata.build(); - } - private static StructOrError parseAggregateCluster(Cluster cluster) { String clusterName = cluster.getName(); Cluster.CustomClusterType customType = cluster.getClusterType(); @@ -259,6 +219,7 @@ private static StructOrError parseNonAggregateCluster( Long maxConcurrentRequests = null; UpstreamTlsContext upstreamTlsContext = null; OutlierDetection outlierDetection = null; + boolean isHttp11ProxyAvailable = false; if (cluster.hasLrsServer()) { if (!cluster.getLrsServer().hasSelf()) { return StructOrError.fromError( @@ -281,17 +242,43 @@ private static StructOrError parseNonAggregateCluster( return StructOrError.fromError("Cluster " + clusterName + ": transport-socket-matches not supported."); } - if (cluster.hasTransportSocket()) { - if (!TRANSPORT_SOCKET_NAME_TLS.equals(cluster.getTransportSocket().getName())) { - return StructOrError.fromError("transport-socket with name " - + cluster.getTransportSocket().getName() + " not supported."); + boolean hasTransportSocket = cluster.hasTransportSocket(); + TransportSocket transportSocket = cluster.getTransportSocket(); + + if (hasTransportSocket && !TRANSPORT_SOCKET_NAME_TLS.equals(transportSocket.getName()) + && !(isEnabledXdsHttpConnect + && TRANSPORT_SOCKET_NAME_HTTP11_PROXY.equals(transportSocket.getName()))) { + return StructOrError.fromError( + "transport-socket with name " + transportSocket.getName() + " not supported."); + } + + if (hasTransportSocket && isEnabledXdsHttpConnect + && TRANSPORT_SOCKET_NAME_HTTP11_PROXY.equals(transportSocket.getName())) { + isHttp11ProxyAvailable = true; + try { + Http11ProxyUpstreamTransport wrappedTransportSocket = transportSocket + .getTypedConfig().unpack(io.envoyproxy.envoy.extensions.transport_sockets + .http_11_proxy.v3.Http11ProxyUpstreamTransport.class); + hasTransportSocket = wrappedTransportSocket.hasTransportSocket(); + transportSocket = wrappedTransportSocket.getTransportSocket(); + } catch (InvalidProtocolBufferException e) { + return StructOrError.fromError( + "Cluster " + clusterName + ": malformed Http11ProxyUpstreamTransport: " + e); + } catch (ClassCastException e) { + return StructOrError.fromError( + "Cluster " + clusterName + + ": invalid transport_socket type in Http11ProxyUpstreamTransport"); } + } + + if (hasTransportSocket && TRANSPORT_SOCKET_NAME_TLS.equals(transportSocket.getName())) { try { upstreamTlsContext = UpstreamTlsContext.fromEnvoyProtoUpstreamTlsContext( validateUpstreamTlsContext( - unpackCompatibleType(cluster.getTransportSocket().getTypedConfig(), - io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext.class, - TYPE_URL_UPSTREAM_TLS_CONTEXT, TYPE_URL_UPSTREAM_TLS_CONTEXT_V2), + unpackCompatibleType(transportSocket.getTypedConfig(), + io.envoyproxy.envoy.extensions + .transport_sockets.tls.v3.UpstreamTlsContext.class, + TYPE_URL_UPSTREAM_TLS_CONTEXT, TYPE_URL_UPSTREAM_TLS_CONTEXT_V2), certProviderInstances)); } catch (InvalidProtocolBufferException | ResourceInvalidException e) { return StructOrError.fromError( @@ -329,9 +316,10 @@ private static StructOrError parseNonAggregateCluster( return StructOrError.fromError( "EDS service_name must be set when Cluster resource has an xdstp name"); } + return StructOrError.fromStruct(CdsUpdate.forEds( clusterName, edsServiceName, lrsServerInfo, maxConcurrentRequests, upstreamTlsContext, - outlierDetection)); + outlierDetection, isHttp11ProxyAvailable)); } else if (type.equals(Cluster.DiscoveryType.LOGICAL_DNS)) { if (!cluster.hasLoadAssignment()) { return StructOrError.fromError( @@ -366,7 +354,8 @@ private static StructOrError parseNonAggregateCluster( String dnsHostName = String.format( Locale.US, "%s:%d", socketAddress.getAddress(), socketAddress.getPortValue()); return StructOrError.fromStruct(CdsUpdate.forLogicalDns( - clusterName, dnsHostName, lrsServerInfo, maxConcurrentRequests, upstreamTlsContext)); + clusterName, dnsHostName, lrsServerInfo, maxConcurrentRequests, + upstreamTlsContext, isHttp11ProxyAvailable)); } return StructOrError.fromError( "Cluster " + clusterName + ": unsupported built-in discovery type: " + type); @@ -620,6 +609,8 @@ abstract static class CdsUpdate implements ResourceUpdate { @Nullable abstract UpstreamTlsContext upstreamTlsContext(); + abstract boolean isHttp11ProxyAvailable(); + // List of underlying clusters making of this aggregate cluster. // Only valid for AGGREGATE cluster. @Nullable @@ -640,7 +631,8 @@ private static Builder newBuilder(String clusterName) { .maxRingSize(0) .choiceCount(0) .filterMetadata(ImmutableMap.of()) - .parsedMetadata(ImmutableMap.of()); + .parsedMetadata(ImmutableMap.of()) + .isHttp11ProxyAvailable(false); } static Builder forAggregate(String clusterName, List prioritizedClusterNames) { @@ -653,26 +645,30 @@ static Builder forAggregate(String clusterName, List prioritizedClusterN static Builder forEds(String clusterName, @Nullable String edsServiceName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext upstreamTlsContext, - @Nullable OutlierDetection outlierDetection) { + @Nullable OutlierDetection outlierDetection, + boolean isHttp11ProxyAvailable) { return newBuilder(clusterName) .clusterType(ClusterType.EDS) .edsServiceName(edsServiceName) .lrsServerInfo(lrsServerInfo) .maxConcurrentRequests(maxConcurrentRequests) .upstreamTlsContext(upstreamTlsContext) - .outlierDetection(outlierDetection); + .outlierDetection(outlierDetection) + .isHttp11ProxyAvailable(isHttp11ProxyAvailable); } static Builder forLogicalDns(String clusterName, String dnsHostName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, - @Nullable UpstreamTlsContext upstreamTlsContext) { + @Nullable UpstreamTlsContext upstreamTlsContext, + boolean isHttp11ProxyAvailable) { return newBuilder(clusterName) .clusterType(ClusterType.LOGICAL_DNS) .dnsHostName(dnsHostName) .lrsServerInfo(lrsServerInfo) .maxConcurrentRequests(maxConcurrentRequests) - .upstreamTlsContext(upstreamTlsContext); + .upstreamTlsContext(upstreamTlsContext) + .isHttp11ProxyAvailable(isHttp11ProxyAvailable); } enum ClusterType { @@ -749,6 +745,8 @@ Builder leastRequestLbPolicy(Integer choiceCount) { // Private, use one of the static factory methods instead. protected abstract Builder maxConcurrentRequests(Long maxConcurrentRequests); + protected abstract Builder isHttp11ProxyAvailable(boolean isHttp11ProxyAvailable); + // Private, use one of the static factory methods instead. protected abstract Builder upstreamTlsContext(UpstreamTlsContext upstreamTlsContext); diff --git a/xds/src/main/java/io/grpc/xds/XdsEndpointResource.java b/xds/src/main/java/io/grpc/xds/XdsEndpointResource.java index 6a3cd35bd59..11111fa51ca 100644 --- a/xds/src/main/java/io/grpc/xds/XdsEndpointResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsEndpointResource.java @@ -20,9 +20,14 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; +import com.google.common.net.InetAddresses; +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; import io.envoyproxy.envoy.config.core.v3.Address; import io.envoyproxy.envoy.config.core.v3.HealthStatus; +import io.envoyproxy.envoy.config.core.v3.SocketAddress; import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; import io.envoyproxy.envoy.config.endpoint.v3.Endpoint; import io.envoyproxy.envoy.type.v3.FractionalPercent; @@ -30,6 +35,7 @@ import io.grpc.internal.GrpcUtil; import io.grpc.xds.Endpoints.DropOverload; import io.grpc.xds.Endpoints.LocalityLbEndpoints; +import io.grpc.xds.MetadataRegistry.MetadataValueParser; import io.grpc.xds.XdsEndpointResource.EdsUpdate; import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsClient.ResourceUpdate; @@ -185,7 +191,8 @@ private static int getRatePerMillion(FractionalPercent percent) { @VisibleForTesting @Nullable static StructOrError parseLocalityLbEndpoints( - io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto) { + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto) + throws ResourceInvalidException { // Filter out localities without or with 0 weight. if (!proto.hasLoadBalancingWeight() || proto.getLoadBalancingWeight().getValue() < 1) { return null; @@ -193,6 +200,15 @@ static StructOrError parseLocalityLbEndpoints( if (proto.getPriority() < 0) { return StructOrError.fromError("negative priority"); } + + ImmutableMap localityMetadata; + MetadataRegistry registry = MetadataRegistry.getInstance(); + try { + localityMetadata = registry.parseMetadata(proto.getMetadata()); + } catch (ResourceInvalidException e) { + throw new ResourceInvalidException("Failed to parse Locality Endpoint metadata: " + + e.getMessage(), e); + } List endpoints = new ArrayList<>(proto.getLbEndpointsCount()); for (io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint endpoint : proto.getLbEndpointsList()) { // The endpoint field of each lb_endpoints must be set. @@ -200,6 +216,13 @@ static StructOrError parseLocalityLbEndpoints( if (!endpoint.hasEndpoint() || !endpoint.getEndpoint().hasAddress()) { return StructOrError.fromError("LbEndpoint with no endpoint/address"); } + ImmutableMap endpointMetadata; + try { + endpointMetadata = registry.parseMetadata(endpoint.getMetadata()); + } catch (ResourceInvalidException e) { + throw new ResourceInvalidException("Failed to parse Endpoint metadata: " + + e.getMessage(), e); + } List addresses = new ArrayList<>(); addresses.add(getInetSocketAddress(endpoint.getEndpoint().getAddress())); @@ -214,10 +237,12 @@ static StructOrError parseLocalityLbEndpoints( endpoints.add(Endpoints.LbEndpoint.create( new EquivalentAddressGroup(addresses), endpoint.getLoadBalancingWeight().getValue(), isHealthy, - endpoint.getEndpoint().getHostname())); + endpoint.getEndpoint().getHostname(), + endpointMetadata)); } return StructOrError.fromStruct(Endpoints.LocalityLbEndpoints.create( - endpoints, proto.getLoadBalancingWeight().getValue(), proto.getPriority())); + endpoints, proto.getLoadBalancingWeight().getValue(), + proto.getPriority(), localityMetadata)); } private static InetSocketAddress getInetSocketAddress(Address address) { @@ -270,4 +295,47 @@ public String toString() { .toString(); } } + + public static class AddressMetadataParser implements MetadataValueParser { + + @Override + public String getTypeUrl() { + return "type.googleapis.com/envoy.config.core.v3.Address"; + } + + @Override + public java.net.SocketAddress parse(Any any) throws ResourceInvalidException { + SocketAddress socketAddress; + try { + socketAddress = any.unpack(Address.class).getSocketAddress(); + } catch (InvalidProtocolBufferException ex) { + throw new ResourceInvalidException("Invalid Resource in address proto", ex); + } + validateAddress(socketAddress); + + String ip = socketAddress.getAddress(); + int port = socketAddress.getPortValue(); + + try { + return new InetSocketAddress(InetAddresses.forString(ip), port); + } catch (IllegalArgumentException e) { + throw createException("Invalid IP address or port: " + ip + ":" + port); + } + } + + private void validateAddress(SocketAddress socketAddress) throws ResourceInvalidException { + if (socketAddress.getAddress().isEmpty()) { + throw createException("Address field is empty or invalid."); + } + long port = Integer.toUnsignedLong(socketAddress.getPortValue()); + if (port > 65535) { + throw createException(String.format("Port value %d out of range 1-65535.", port)); + } + } + + private ResourceInvalidException createException(String message) { + return new ResourceInvalidException( + "Failed to parse envoy.config.core.v3.Address: " + message); + } + } } diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java index 82a61e79abf..479bde76ce5 100644 --- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java +++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java @@ -179,7 +179,7 @@ public void tearDown() { public void discoverTopLevelEdsCluster() { CdsUpdate update = CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, - outlierDetection) + outlierDetection, false) .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(CLUSTER, update); assertThat(childBalancers).hasSize(1); @@ -198,7 +198,8 @@ public void discoverTopLevelEdsCluster() { @Test public void discoverTopLevelLogicalDnsCluster() { CdsUpdate update = - CdsUpdate.forLogicalDns(CLUSTER, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext) + CdsUpdate.forLogicalDns(CLUSTER, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, + false) .leastRequestLbPolicy(3).build(); xdsClient.deliverCdsUpdate(CLUSTER, update); assertThat(childBalancers).hasSize(1); @@ -232,7 +233,7 @@ public void nonAggregateCluster_resourceNotExist_returnErrorPicker() { @Test public void nonAggregateCluster_resourceUpdate() { CdsUpdate update = - CdsUpdate.forEds(CLUSTER, null, null, 100L, upstreamTlsContext, outlierDetection) + CdsUpdate.forEds(CLUSTER, null, null, 100L, upstreamTlsContext, outlierDetection, false) .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(CLUSTER, update); assertThat(childBalancers).hasSize(1); @@ -243,7 +244,7 @@ public void nonAggregateCluster_resourceUpdate() { 100L, upstreamTlsContext, outlierDetection); update = CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, null, - outlierDetection).roundRobinLbPolicy().build(); + outlierDetection, false).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(CLUSTER, update); childLbConfig = (ClusterResolverConfig) childBalancer.config; instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); @@ -254,7 +255,8 @@ public void nonAggregateCluster_resourceUpdate() { @Test public void nonAggregateCluster_resourceRevoked() { CdsUpdate update = - CdsUpdate.forLogicalDns(CLUSTER, DNS_HOST_NAME, null, 100L, upstreamTlsContext) + CdsUpdate.forLogicalDns(CLUSTER, DNS_HOST_NAME, null, 100L, upstreamTlsContext, + false) .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(CLUSTER, update); assertThat(childBalancers).hasSize(1); @@ -298,16 +300,16 @@ public void discoverAggregateCluster() { CLUSTER, cluster1, cluster2, cluster3, cluster4); assertThat(childBalancers).isEmpty(); CdsUpdate update3 = CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, - upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); + upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster3, update3); assertThat(childBalancers).isEmpty(); CdsUpdate update2 = - CdsUpdate.forLogicalDns(cluster2, DNS_HOST_NAME, null, 100L, null) + CdsUpdate.forLogicalDns(cluster2, DNS_HOST_NAME, null, 100L, null, false) .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster2, update2); assertThat(childBalancers).isEmpty(); CdsUpdate update4 = - CdsUpdate.forEds(cluster4, null, LRS_SERVER_INFO, 300L, null, outlierDetection) + CdsUpdate.forEds(cluster4, null, LRS_SERVER_INFO, 300L, null, outlierDetection, false) .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster4, update4); assertThat(childBalancers).hasSize(1); // all non-aggregate clusters discovered @@ -362,10 +364,11 @@ public void aggregateCluster_descendantClustersRevoked() { xdsClient.deliverCdsUpdate(CLUSTER, update); assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1, cluster2); CdsUpdate update1 = CdsUpdate.forEds(cluster1, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, - upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); + upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster1, update1); CdsUpdate update2 = - CdsUpdate.forLogicalDns(cluster2, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null) + CdsUpdate.forLogicalDns(cluster2, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null, + false) .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster2, update2); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); @@ -412,10 +415,11 @@ public void aggregateCluster_rootClusterRevoked() { xdsClient.deliverCdsUpdate(CLUSTER, update); assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1, cluster2); CdsUpdate update1 = CdsUpdate.forEds(cluster1, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, - upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); + upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster1, update1); CdsUpdate update2 = - CdsUpdate.forLogicalDns(cluster2, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null) + CdsUpdate.forLogicalDns(cluster2, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null, + false) .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster2, update2); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); @@ -467,7 +471,7 @@ public void aggregateCluster_intermediateClusterChanges() { xdsClient.deliverCdsUpdate(cluster2, update2); assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster2, cluster3); CdsUpdate update3 = CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, - upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); + upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster3, update3); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; @@ -518,7 +522,7 @@ public void aggregateCluster_withLoops() { reset(helper); CdsUpdate update3 = CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, - upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); + upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster3, update3); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); @@ -553,7 +557,7 @@ public void aggregateCluster_withLoops_afterEds() { .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster2, update2); CdsUpdate update3 = CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, - upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); + upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster3, update3); // cluster2 (aggr.) -> [cluster3 (EDS)] @@ -602,7 +606,7 @@ public void aggregateCluster_duplicateChildren() { // Define EDS cluster CdsUpdate update3 = CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, - upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); + upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster3, update3); // cluster4 (agg) -> [cluster3 (EDS)] with dups (3 copies) @@ -649,7 +653,8 @@ public void aggregateCluster_discoveryErrorAfterChildLbCreated_propagateToChildL .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(CLUSTER, update); CdsUpdate update1 = - CdsUpdate.forLogicalDns(cluster1, DNS_HOST_NAME, LRS_SERVER_INFO, 200L, null) + CdsUpdate.forLogicalDns(cluster1, DNS_HOST_NAME, LRS_SERVER_INFO, 200L, null, + false) .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster1, update1); FakeLoadBalancer childLb = Iterables.getOnlyElement(childBalancers); @@ -676,7 +681,7 @@ public void handleNameResolutionErrorFromUpstream_beforeChildLbCreated_returnErr @Test public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_fallThrough() { CdsUpdate update = CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, - upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); + upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(CLUSTER, update); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.shutdown).isFalse(); @@ -692,7 +697,7 @@ public void unknownLbProvider() { try { xdsClient.deliverCdsUpdate(CLUSTER, CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, - outlierDetection) + outlierDetection, false) .lbPolicyConfig(ImmutableMap.of("unknownLb", ImmutableMap.of("foo", "bar"))).build()); } catch (Exception e) { assertThat(e).hasMessageThat().contains("unknownLb"); @@ -706,7 +711,7 @@ public void invalidLbConfig() { try { xdsClient.deliverCdsUpdate(CLUSTER, CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, - outlierDetection).lbPolicyConfig( + outlierDetection, false).lbPolicyConfig( ImmutableMap.of("ring_hash_experimental", ImmutableMap.of("minRingSize", "-1"))) .build()); } catch (Exception e) { diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index 28898c0930f..d0176d7aa38 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -36,6 +36,7 @@ import io.grpc.ChannelLogger; import io.grpc.ConnectivityState; import io.grpc.EquivalentAddressGroup; +import io.grpc.HttpConnectProxiedSocketAddress; import io.grpc.InsecureChannelCredentials; import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; @@ -83,6 +84,7 @@ import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsResourceType; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; +import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; @@ -240,7 +242,7 @@ public void tearDown() { @Test public void edsClustersWithRingHashEndpointLbPolicy() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanism1), ringHash); + Collections.singletonList(edsDiscoveryMechanism1), ringHash, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertThat(childBalancers).isEmpty(); @@ -252,14 +254,18 @@ public void edsClustersWithRingHashEndpointLbPolicy() { LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( Arrays.asList( - LbEndpoint.create(endpoint1, 0 /* loadBalancingWeight */, true, "hostname1"), - LbEndpoint.create(endpoint2, 0 /* loadBalancingWeight */, true, "hostname2")), - 10 /* localityWeight */, 1 /* priority */); + LbEndpoint.create(endpoint1, 0 /* loadBalancingWeight */, + true, "hostname1", ImmutableMap.of()), + LbEndpoint.create(endpoint2, 0 /* loadBalancingWeight */, + true, "hostname2", ImmutableMap.of())), + 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); LocalityLbEndpoints localityLbEndpoints2 = LocalityLbEndpoints.create( Collections.singletonList( - LbEndpoint.create(endpoint3, 60 /* loadBalancingWeight */, true, "hostname3")), - 50 /* localityWeight */, 1 /* priority */); + LbEndpoint.create( + endpoint3, 60 /* loadBalancingWeight */, true, + "hostname3", ImmutableMap.of())), + 50 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, ImmutableMap.of(locality1, localityLbEndpoints1, locality2, localityLbEndpoints2)); @@ -302,7 +308,7 @@ public void edsClustersWithRingHashEndpointLbPolicy() { @Test public void edsClustersWithLeastRequestEndpointLbPolicy() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanism1), leastRequest); + Collections.singletonList(edsDiscoveryMechanism1), leastRequest, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertThat(childBalancers).isEmpty(); @@ -312,8 +318,9 @@ public void edsClustersWithLeastRequestEndpointLbPolicy() { LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( Arrays.asList( - LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, "hostname1")), - 100 /* localityWeight */, 1 /* priority */); + LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, + "hostname1", ImmutableMap.of())), + 100 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, ImmutableMap.of(locality1, localityLbEndpoints)); @@ -348,7 +355,7 @@ public void edsClustersWithLeastRequestEndpointLbPolicy() { @Test public void edsClustersEndpointHostname_addedToAddressAttribute() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection), leastRequest); + Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection), leastRequest, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertThat(childBalancers).isEmpty(); @@ -358,8 +365,9 @@ public void edsClustersEndpointHostname_addedToAddressAttribute() { LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( Arrays.asList( - LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, "hostname1")), - 100 /* localityWeight */, 1 /* priority */); + LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, + "hostname1", ImmutableMap.of())), + 100 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, ImmutableMap.of(locality1, localityLbEndpoints)); @@ -371,11 +379,104 @@ public void edsClustersEndpointHostname_addedToAddressAttribute() { .get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo("hostname1"); } + @Test + public void endpointAddressRewritten_whenProxyMetadataIsInEndpointMetadata() { + ClusterResolverConfig config = new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection), leastRequest, true); + deliverLbConfig(config); + assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); + assertThat(childBalancers).isEmpty(); + + EquivalentAddressGroup endpoint = + new EquivalentAddressGroup(InetSocketAddress.createUnresolved("127.0.0.1", 8080)); + + // Proxy address in endpointMetadata (use FakeSocketAddress directly) + SocketAddress proxyAddress = new FakeSocketAddress("127.0.0.2"); + ImmutableMap endpointMetadata = + ImmutableMap.of("envoy.http11_proxy_transport_socket.proxy_address", proxyAddress); + + // No proxy in locality metadata + ImmutableMap localityMetadata = ImmutableMap.of(); + + LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( + Arrays.asList( + LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, + "hostname1", endpointMetadata)), + 100 /* localityWeight */, 1 /* priority */, localityMetadata); + + xdsClient.deliverClusterLoadAssignment( + EDS_SERVICE_NAME1, + ImmutableMap.of(locality1, localityLbEndpoints)); + + assertThat(childBalancers).hasSize(1); + FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); + + // Get the rewritten address + SocketAddress rewrittenAddress = + childBalancer.addresses.get(0).getAddresses().get(0); + assertThat(rewrittenAddress).isInstanceOf(HttpConnectProxiedSocketAddress.class); + HttpConnectProxiedSocketAddress proxiedSocket = + (HttpConnectProxiedSocketAddress) rewrittenAddress; + + // Assert that the target address is the original address + assertThat(proxiedSocket.getTargetAddress()) + .isEqualTo(endpoint.getAddresses().get(0)); + + // Assert that the proxy address is correctly set + assertThat(proxiedSocket.getProxyAddress()).isEqualTo(proxyAddress); + } + + @Test + public void endpointAddressRewritten_whenProxyMetadataIsInLocalityMetadata() { + ClusterResolverConfig config = new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection), leastRequest, true); + deliverLbConfig(config); + assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); + assertThat(childBalancers).isEmpty(); + + EquivalentAddressGroup endpoint = + new EquivalentAddressGroup(InetSocketAddress.createUnresolved("127.0.0.2", 8080)); + + // No proxy in endpointMetadata + ImmutableMap endpointMetadata = ImmutableMap.of(); + + // Proxy address is now in localityMetadata + SocketAddress proxyAddress = new FakeSocketAddress("proxy-addr"); + ImmutableMap localityMetadata = + ImmutableMap.of("envoy.http11_proxy_transport_socket.proxy_address", proxyAddress); + + LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( + Arrays.asList( + LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, + "hostname2", endpointMetadata)), + 100 /* localityWeight */, 1 /* priority */, localityMetadata); + + xdsClient.deliverClusterLoadAssignment( + EDS_SERVICE_NAME1, + ImmutableMap.of(locality1, localityLbEndpoints)); + + assertThat(childBalancers).hasSize(1); + FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); + + // Get the rewritten address + SocketAddress rewrittenAddress = childBalancer.addresses.get(0).getAddresses().get(0); + + // Assert that the address was rewritten + assertThat(rewrittenAddress).isInstanceOf(HttpConnectProxiedSocketAddress.class); + HttpConnectProxiedSocketAddress proxiedSocket = + (HttpConnectProxiedSocketAddress) rewrittenAddress; + + // Assert that the target address is the original address + assertThat(proxiedSocket.getTargetAddress()).isEqualTo(endpoint.getAddresses().get(0)); + + // Assert that the proxy address is correctly set from locality metadata + assertThat(proxiedSocket.getProxyAddress()).isEqualTo(proxyAddress); + } @Test public void onlyEdsClusters_receivedEndpoints() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), roundRobin); + Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1, EDS_SERVICE_NAME2); assertThat(childBalancers).isEmpty(); @@ -389,17 +490,21 @@ public void onlyEdsClusters_receivedEndpoints() { LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( Arrays.asList( - LbEndpoint.create(endpoint1, 100, true, "hostname1"), - LbEndpoint.create(endpoint2, 100, true, "hostname1")), - 70 /* localityWeight */, 1 /* priority */); + LbEndpoint.create(endpoint1, 100, + true, "hostname1", ImmutableMap.of()), + LbEndpoint.create(endpoint2, 100, + true, "hostname1", ImmutableMap.of())), + 70 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); LocalityLbEndpoints localityLbEndpoints2 = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint3, 100, true, "hostname2")), - 10 /* localityWeight */, 1 /* priority */); + Collections.singletonList(LbEndpoint.create(endpoint3, 100, true, + "hostname2", ImmutableMap.of())), + 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); LocalityLbEndpoints localityLbEndpoints3 = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint4, 100, true, "hostname3")), - 20 /* localityWeight */, 2 /* priority */); + Collections.singletonList(LbEndpoint.create(endpoint4, 100, true, + "hostname3", ImmutableMap.of())), + 20 /* localityWeight */, 2 /* priority */, ImmutableMap.of()); String priority1 = CLUSTER2 + "[child1]"; String priority2 = CLUSTER2 + "[child2]"; String priority3 = CLUSTER1 + "[child1]"; @@ -487,7 +592,7 @@ public void onlyEdsClusters_receivedEndpoints() { private void verifyEdsPriorityNames(List want, Map... updates) { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism2), roundRobin); + Arrays.asList(edsDiscoveryMechanism2), roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME2); assertThat(childBalancers).isEmpty(); @@ -553,15 +658,17 @@ locality2, createEndpoints(1) private LocalityLbEndpoints createEndpoints(int priority) { return LocalityLbEndpoints.create( Arrays.asList( - LbEndpoint.create(makeAddress("endpoint-addr-1"), 100, true, "hostname1"), - LbEndpoint.create(makeAddress("endpoint-addr-2"), 100, true, "hostname2")), - 70 /* localityWeight */, priority /* priority */); + LbEndpoint.create(makeAddress("endpoint-addr-1"), 100, + true, "hostname1", ImmutableMap.of()), + LbEndpoint.create(makeAddress("endpoint-addr-2"), 100, + true, "hostname2", ImmutableMap.of())), + 70 /* localityWeight */, priority /* priority */, ImmutableMap.of()); } @Test public void onlyEdsClusters_resourceNeverExist_returnErrorPicker() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), roundRobin); + Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1, EDS_SERVICE_NAME2); assertThat(childBalancers).isEmpty(); @@ -583,7 +690,7 @@ public void onlyEdsClusters_resourceNeverExist_returnErrorPicker() { @Test public void onlyEdsClusters_allResourcesRevoked_shutDownChildLbPolicy() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), roundRobin); + Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1, EDS_SERVICE_NAME2); assertThat(childBalancers).isEmpty(); @@ -592,12 +699,14 @@ public void onlyEdsClusters_allResourcesRevoked_shutDownChildLbPolicy() { EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint1, 100, true, "hostname1")), - 10 /* localityWeight */, 1 /* priority */); + Collections.singletonList(LbEndpoint.create(endpoint1, 100, true, + "hostname1", ImmutableMap.of())), + 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); LocalityLbEndpoints localityLbEndpoints2 = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint2, 100, true, "hostname2")), - 20 /* localityWeight */, 2 /* priority */); + Collections.singletonList(LbEndpoint.create(endpoint2, 100, true, + "hostname2", ImmutableMap.of())), + 20 /* localityWeight */, 2 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints1)); xdsClient.deliverClusterLoadAssignment( @@ -618,17 +727,19 @@ public void onlyEdsClusters_allResourcesRevoked_shutDownChildLbPolicy() { @Test public void handleEdsResource_ignoreUnhealthyEndpoints() { - ClusterResolverConfig config = - new ClusterResolverConfig(Collections.singletonList(edsDiscoveryMechanism1), roundRobin); + ClusterResolverConfig config = new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanism1), roundRobin, false); deliverLbConfig(config); EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( Arrays.asList( - LbEndpoint.create(endpoint1, 100, false /* isHealthy */, "hostname1"), - LbEndpoint.create(endpoint2, 100, true /* isHealthy */, "hostname2")), - 10 /* localityWeight */, 1 /* priority */); + LbEndpoint.create(endpoint1, 100, false /* isHealthy */, + "hostname1", ImmutableMap.of()), + LbEndpoint.create(endpoint2, 100, true /* isHealthy */, + "hostname2", ImmutableMap.of())), + 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); @@ -638,21 +749,21 @@ public void handleEdsResource_ignoreUnhealthyEndpoints() { @Test public void handleEdsResource_ignoreLocalitiesWithNoHealthyEndpoints() { - ClusterResolverConfig config = - new ClusterResolverConfig(Collections.singletonList(edsDiscoveryMechanism1), roundRobin); + ClusterResolverConfig config = new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanism1), roundRobin, false); deliverLbConfig(config); EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint1, 100, false /* isHealthy */, - "hostname1")), - 10 /* localityWeight */, 1 /* priority */); + "hostname1", ImmutableMap.of())), + 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); LocalityLbEndpoints localityLbEndpoints2 = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint2, 100, true /* isHealthy */, - "hostname2")), - 10 /* localityWeight */, 1 /* priority */); + "hostname2", ImmutableMap.of())), + 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, ImmutableMap.of(locality1, localityLbEndpoints1, locality2, localityLbEndpoints2)); @@ -665,21 +776,21 @@ public void handleEdsResource_ignoreLocalitiesWithNoHealthyEndpoints() { @Test public void handleEdsResource_ignorePrioritiesWithNoHealthyEndpoints() { - ClusterResolverConfig config = - new ClusterResolverConfig(Collections.singletonList(edsDiscoveryMechanism1), roundRobin); + ClusterResolverConfig config = new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanism1), roundRobin, false); deliverLbConfig(config); EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint1, 100, false /* isHealthy */, - "hostname1")), - 10 /* localityWeight */, 1 /* priority */); + "hostname1", ImmutableMap.of())), + 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); LocalityLbEndpoints localityLbEndpoints2 = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint2, 200, true /* isHealthy */, - "hostname2")), - 10 /* localityWeight */, 2 /* priority */); + "hostname2", ImmutableMap.of())), + 10 /* localityWeight */, 2 /* priority */, ImmutableMap.of()); String priority2 = CLUSTER1 + "[child2]"; xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, @@ -691,15 +802,15 @@ public void handleEdsResource_ignorePrioritiesWithNoHealthyEndpoints() { @Test public void handleEdsResource_noHealthyEndpoint() { - ClusterResolverConfig config = - new ClusterResolverConfig(Collections.singletonList(edsDiscoveryMechanism1), roundRobin); + ClusterResolverConfig config = new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanism1), roundRobin, false); deliverLbConfig(config); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint, 100, false /* isHealthy */, - "hostname1")), - 10 /* localityWeight */, 1 /* priority */); + "hostname1", ImmutableMap.of())), + 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment(EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); // single endpoint, unhealthy @@ -716,7 +827,7 @@ public void handleEdsResource_noHealthyEndpoint() { @Test public void onlyLogicalDnsCluster_endpointsResolved() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin); + Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); @@ -749,7 +860,7 @@ public void onlyLogicalDnsCluster_endpointsResolved() { @Test public void onlyLogicalDnsCluster_handleRefreshNameResolution() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin); + Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); @@ -767,7 +878,7 @@ public void onlyLogicalDnsCluster_resolutionError_backoffAndRefresh() { InOrder inOrder = Mockito.inOrder(helper, backoffPolicyProvider, backoffPolicy1, backoffPolicy2); ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin); + Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); @@ -813,7 +924,7 @@ public void onlyLogicalDnsCluster_resolutionError_backoffAndRefresh() { public void onlyLogicalDnsCluster_refreshNameResolutionRaceWithResolutionError() { InOrder inOrder = Mockito.inOrder(backoffPolicyProvider, backoffPolicy1, backoffPolicy2); ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin); + Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); @@ -851,7 +962,7 @@ public void onlyLogicalDnsCluster_refreshNameResolutionRaceWithResolutionError() @Test public void edsClustersAndLogicalDnsCluster_receivedEndpoints() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin); + Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); @@ -862,8 +973,9 @@ public void edsClustersAndLogicalDnsCluster_receivedEndpoints() { resolver.deliverEndpointAddresses(Arrays.asList(endpoint1, endpoint2)); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint3, 100, true, "hostname3")), - 10 /* localityWeight */, 1 /* priority */); + Collections.singletonList(LbEndpoint.create(endpoint3, 100, true, + "hostname3", ImmutableMap.of())), + 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); @@ -886,7 +998,7 @@ public void edsClustersAndLogicalDnsCluster_receivedEndpoints() { @Test public void noEdsResourceExists_useDnsResolutionResults() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin); + Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); @@ -910,7 +1022,7 @@ public void noEdsResourceExists_useDnsResolutionResults() { @Test public void edsResourceRevoked_dnsResolutionError_shutDownChildLbPolicyAndReturnErrorPicker() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin); + Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); @@ -919,8 +1031,9 @@ public void edsResourceRevoked_dnsResolutionError_shutDownChildLbPolicyAndReturn EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint, 100, true, "hostname1")), - 10 /* localityWeight */, 1 /* priority */); + Collections.singletonList(LbEndpoint.create(endpoint, 100, true, + "hostname1", ImmutableMap.of())), + 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); resolver.deliverError(Status.UNKNOWN.withDescription("I am lost")); @@ -941,7 +1054,7 @@ public void edsResourceRevoked_dnsResolutionError_shutDownChildLbPolicyAndReturn @Test public void resolutionErrorAfterChildLbCreated_propagateErrorIfAllClustersEncounterError() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin); + Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); @@ -950,8 +1063,9 @@ public void resolutionErrorAfterChildLbCreated_propagateErrorIfAllClustersEncoun EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint, 100, true, "hostname1")), - 10 /* localityWeight */, 1 /* priority */); + Collections.singletonList(LbEndpoint.create(endpoint, 100, true, + "hostname1", ImmutableMap.of())), + 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); assertThat(childBalancers).isEmpty(); // not created until all clusters resolved. @@ -976,7 +1090,7 @@ public void resolutionErrorAfterChildLbCreated_propagateErrorIfAllClustersEncoun @Test public void resolutionErrorBeforeChildLbCreated_returnErrorPickerIfAllClustersEncounterError() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin); + Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); @@ -999,7 +1113,7 @@ public void resolutionErrorBeforeChildLbCreated_returnErrorPickerIfAllClustersEn @Test public void resolutionErrorBeforeChildLbCreated_edsOnly_returnErrorPicker() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1), roundRobin); + Arrays.asList(edsDiscoveryMechanism1), roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertThat(childBalancers).isEmpty(); @@ -1017,7 +1131,7 @@ public void resolutionErrorBeforeChildLbCreated_edsOnly_returnErrorPicker() { @Test public void handleNameResolutionErrorFromUpstream_beforeChildLbCreated_returnErrorPicker() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin); + Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertResolverCreated("/" + DNS_HOST_NAME); @@ -1033,7 +1147,7 @@ public void handleNameResolutionErrorFromUpstream_beforeChildLbCreated_returnErr @Test public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_fallThrough() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin); + Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); @@ -1043,8 +1157,9 @@ public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_fallThroug EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint1, 100, true, "hostname1")), - 10 /* localityWeight */, 1 /* priority */); + Collections.singletonList(LbEndpoint.create(endpoint1, 100, true, + "hostname1", ImmutableMap.of())), + 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); resolver.deliverEndpointAddresses(Collections.singletonList(endpoint2)); @@ -1118,37 +1233,37 @@ private static void assertAddressesEqual( } private static EquivalentAddressGroup makeAddress(final String name) { - class FakeSocketAddress extends SocketAddress { - private final String name; + return new EquivalentAddressGroup(new FakeSocketAddress(name)); + } - private FakeSocketAddress(String name) { - this.name = name; - } + static class FakeSocketAddress extends SocketAddress { + private final String name; - @Override - public int hashCode() { - return Objects.hash(name); - } + private FakeSocketAddress(String name) { + this.name = name; + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof FakeSocketAddress)) { - return false; - } - FakeSocketAddress that = (FakeSocketAddress) o; - return Objects.equals(name, that.name); - } + @Override + public int hashCode() { + return Objects.hash(name); + } - @Override - public String toString() { - return name; + @Override + public boolean equals(Object o) { + if (this == o) { + return true; } + if (!(o instanceof FakeSocketAddress)) { + return false; + } + FakeSocketAddress that = (FakeSocketAddress) o; + return Objects.equals(name, that.name); } - return new EquivalentAddressGroup(new FakeSocketAddress(name)); + @Override + public String toString() { + return name; + } } private static final class FakeXdsClient extends XdsClient { diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 610d147ccf9..7fac666f983 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static io.envoyproxy.envoy.config.route.v3.RouteAction.ClusterSpecifierCase.CLUSTER_SPECIFIER_PLUGIN; +import static io.grpc.xds.XdsClusterResource.TRANSPORT_SOCKET_NAME_HTTP11_PROXY; import static io.grpc.xds.XdsEndpointResource.GRPC_EXPERIMENTAL_XDS_DUALSTACK_ENDPOINTS; import static org.junit.Assert.fail; @@ -93,6 +94,7 @@ import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds; import io.envoyproxy.envoy.extensions.load_balancing_policies.client_side_weighted_round_robin.v3.ClientSideWeightedRoundRobin; import io.envoyproxy.envoy.extensions.load_balancing_policies.wrr_locality.v3.WrrLocality; +import io.envoyproxy.envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateProviderPluginInstance; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; @@ -1055,7 +1057,7 @@ public void parseClusterWeight() { } @Test - public void parseLocalityLbEndpoints_withHealthyEndpoints() { + public void parseLocalityLbEndpoints_withHealthyEndpoints() throws ResourceInvalidException { io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto = io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints.newBuilder() .setLocality(Locality.newBuilder() @@ -1075,12 +1077,14 @@ public void parseLocalityLbEndpoints_withHealthyEndpoints() { assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct()).isEqualTo( LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, true, "")), - 100, 1)); + Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, + 20, true, "", ImmutableMap.of())), + 100, 1, ImmutableMap.of())); } @Test - public void parseLocalityLbEndpoints_treatUnknownHealthAsHealthy() { + public void parseLocalityLbEndpoints_treatUnknownHealthAsHealthy() + throws ResourceInvalidException { io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto = io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints.newBuilder() .setLocality(Locality.newBuilder() @@ -1100,12 +1104,13 @@ public void parseLocalityLbEndpoints_treatUnknownHealthAsHealthy() { assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct()).isEqualTo( LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, true, "")), 100, - 1)); + Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, + 20, true, "", ImmutableMap.of())), + 100, 1, ImmutableMap.of())); } @Test - public void parseLocalityLbEndpoints_withUnHealthyEndpoints() { + public void parseLocalityLbEndpoints_withUnHealthyEndpoints() throws ResourceInvalidException { io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto = io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints.newBuilder() .setLocality(Locality.newBuilder() @@ -1125,12 +1130,13 @@ public void parseLocalityLbEndpoints_withUnHealthyEndpoints() { assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct()).isEqualTo( LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, false, "")), 100, - 1)); + Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, + false, "", ImmutableMap.of())), + 100, 1, ImmutableMap.of())); } @Test - public void parseLocalityLbEndpoints_ignorZeroWeightLocality() { + public void parseLocalityLbEndpoints_ignorZeroWeightLocality() throws ResourceInvalidException { io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto = io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints.newBuilder() .setLocality(Locality.newBuilder() @@ -1187,7 +1193,10 @@ public void parseLocalityLbEndpoints_withDualStackEndpoints() { EquivalentAddressGroup expectedEag = new EquivalentAddressGroup(socketAddressList); assertThat(struct.getStruct()).isEqualTo( LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(expectedEag, 20, true, "")), 100, 1)); + Collections.singletonList(LbEndpoint.create( + expectedEag, 20, true, "", ImmutableMap.of())), 100, 1, ImmutableMap.of())); + } catch (ResourceInvalidException e) { + throw new RuntimeException(e); } finally { if (originalDualStackProp != null) { System.setProperty(GRPC_EXPERIMENTAL_XDS_DUALSTACK_ENDPOINTS, originalDualStackProp); @@ -1198,7 +1207,7 @@ public void parseLocalityLbEndpoints_withDualStackEndpoints() { } @Test - public void parseLocalityLbEndpoints_invalidPriority() { + public void parseLocalityLbEndpoints_invalidPriority() throws ResourceInvalidException { io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto = io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints.newBuilder() .setLocality(Locality.newBuilder() @@ -2456,6 +2465,59 @@ public void processCluster_parsesAudienceMetadata() assertThat(update.parsedMetadata()).isEqualTo(expectedParsedMetadata); } + @Test + public void processCluster_parsesAddressMetadata() throws Exception { + + // Create an Address message + Address address = Address.newBuilder() + .setSocketAddress(SocketAddress.newBuilder() + .setAddress("192.168.1.1") + .setPortValue(8080) + .build()) + .build(); + + // Wrap the Address in Any + Any addressMetadata = Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.config.core.v3.Address") + .setValue(address.toByteString()) + .build(); + + Struct filterMetadata = Struct.newBuilder() + .putFields("key1", Value.newBuilder().setStringValue("value1").build()) + .putFields("key2", Value.newBuilder().setNumberValue(42).build()) + .build(); + + Metadata metadata = Metadata.newBuilder() + .putTypedFilterMetadata("ADDRESS_METADATA", addressMetadata) + .putFilterMetadata("FILTER_METADATA", filterMetadata) + .build(); + + Cluster cluster = Cluster.newBuilder() + .setName("cluster-foo.googleapis.com") + .setType(DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.getDefaultInstance())) + .setServiceName("service-foo.googleapis.com")) + .setLbPolicy(LbPolicy.ROUND_ROBIN) + .setMetadata(metadata) + .build(); + + CdsUpdate update = XdsClusterResource.processCluster( + cluster, null, LRS_SERVER_INFO, + LoadBalancerRegistry.getDefaultRegistry()); + + ImmutableMap expectedParsedMetadata = ImmutableMap.of( + "ADDRESS_METADATA", new InetSocketAddress("192.168.1.1", 8080), + "FILTER_METADATA", ImmutableMap.of( + "key1", "value1", + "key2", 42.0)); + + assertThat(update.parsedMetadata()).isEqualTo(expectedParsedMetadata); + } + @Test public void processCluster_metadataKeyCollision_resolvesToTypedMetadata() throws ResourceInvalidException, InvalidProtocolBufferException { @@ -2512,6 +2574,40 @@ public Object parse(Any value) { metadataRegistry.removeParser(testParser); } + @Test + public void parseNonAggregateCluster_withHttp11ProxyTransportSocket() + throws ResourceInvalidException, InvalidProtocolBufferException { + XdsClusterResource.isEnabledXdsHttpConnect = true; + + Http11ProxyUpstreamTransport http11ProxyUpstreamTransport = + Http11ProxyUpstreamTransport.newBuilder() + .setTransportSocket(TransportSocket.getDefaultInstance()) + .build(); + + TransportSocket transportSocket = TransportSocket.newBuilder() + .setName(TRANSPORT_SOCKET_NAME_HTTP11_PROXY) + .setTypedConfig(Any.pack(http11ProxyUpstreamTransport)) + .build(); + + Cluster cluster = Cluster.newBuilder() + .setName("cluster-http11-proxy.googleapis.com") + .setType(DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) + .setServiceName("service-http11-proxy.googleapis.com")) + .setLbPolicy(LbPolicy.ROUND_ROBIN) + .setTransportSocket(transportSocket) + .build(); + + CdsUpdate result = + XdsClusterResource.processCluster(cluster, null, LRS_SERVER_INFO, + LoadBalancerRegistry.getDefaultRegistry()); + + assertThat(result).isNotNull(); + assertThat(result.isHttp11ProxyAvailable()).isTrue(); + } @Test public void parseServerSideListener_invalidTrafficDirection() throws ResourceInvalidException { diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 00fbfe669af..51c07cb3537 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -607,9 +607,9 @@ private void validateGoldenClusterLoadAssignment(EdsUpdate edsUpdate) { Locality.create("region1", "zone1", "subzone1"), LocalityLbEndpoints.create( ImmutableList.of(LbEndpoint.create("192.168.0.1", 8080, 2, true, - "endpoint-host-name")), 1, 0), + "endpoint-host-name", ImmutableMap.of())), 1, 0, ImmutableMap.of()), Locality.create("region3", "zone3", "subzone3"), - LocalityLbEndpoints.create(ImmutableList.of(), 2, 1)); + LocalityLbEndpoints.create(ImmutableList.of(), 2, 1, ImmutableMap.of())); } /** @@ -3246,7 +3246,9 @@ public void edsResourceUpdated() { Locality.create("region2", "zone2", "subzone2"), LocalityLbEndpoints.create( ImmutableList.of( - LbEndpoint.create("172.44.2.2", 8000, 3, true, "endpoint-host-name")), 2, 0)); + LbEndpoint.create("172.44.2.2", 8000, 3, + true, "endpoint-host-name", ImmutableMap.of())), + 2, 0, ImmutableMap.of())); verifyResourceMetadataAcked(EDS, EDS_RESOURCE, updatedClusterLoadAssignment, VERSION_2, TIME_INCREMENT * 2); verifySubscribedResourcesMetadataSizes(0, 0, 0, 1); @@ -3416,7 +3418,9 @@ public void multipleEdsWatchers() { Locality.create("region2", "zone2", "subzone2"), LocalityLbEndpoints.create( ImmutableList.of( - LbEndpoint.create("172.44.2.2", 8000, 3, true, "endpoint-host-name")), 2, 0)); + LbEndpoint.create("172.44.2.2", 8000, 3, + true, "endpoint-host-name", ImmutableMap.of())), + 2, 0, ImmutableMap.of())); verify(watcher2).onChanged(edsUpdateCaptor.capture()); edsUpdate = edsUpdateCaptor.getValue(); assertThat(edsUpdate.clusterName).isEqualTo(edsResourceTwo); @@ -3426,7 +3430,9 @@ public void multipleEdsWatchers() { Locality.create("region2", "zone2", "subzone2"), LocalityLbEndpoints.create( ImmutableList.of( - LbEndpoint.create("172.44.2.2", 8000, 3, true, "endpoint-host-name")), 2, 0)); + LbEndpoint.create("172.44.2.2", 8000, 3, + true, "endpoint-host-name", ImmutableMap.of())), + 2, 0, ImmutableMap.of())); verifyNoMoreInteractions(edsResourceWatcher); verifyResourceMetadataAcked( EDS, edsResourceTwo, clusterLoadAssignmentTwo, VERSION_2, TIME_INCREMENT * 2); diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java index ea28734ec6a..d0580ae2667 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -257,17 +257,17 @@ static XdsConfig getDefaultXdsConfig(String serverHostName) // Need to create endpoints to create locality endpoints map to create edsUpdate Map lbEndpointsMap = new HashMap<>(); - LbEndpoint lbEndpoint = - LbEndpoint.create(serverHostName, ENDPOINT_PORT, 0, true, ENDPOINT_HOSTNAME); + LbEndpoint lbEndpoint = LbEndpoint.create( + serverHostName, ENDPOINT_PORT, 0, true, ENDPOINT_HOSTNAME, ImmutableMap.of()); lbEndpointsMap.put( Locality.create("", "", ""), - LocalityLbEndpoints.create(ImmutableList.of(lbEndpoint), 10, 0)); + LocalityLbEndpoints.create(ImmutableList.of(lbEndpoint), 10, 0, ImmutableMap.of())); // Need to create EdsUpdate to create CdsUpdate to create XdsClusterConfig for builder XdsEndpointResource.EdsUpdate edsUpdate = new XdsEndpointResource.EdsUpdate( EDS_NAME, lbEndpointsMap, Collections.emptyList()); XdsClusterResource.CdsUpdate cdsUpdate = XdsClusterResource.CdsUpdate.forEds( - CLUSTER_NAME, EDS_NAME, serverInfo, null, null, null) + CLUSTER_NAME, EDS_NAME, serverInfo, null, null, null, false) .lbPolicyConfig(getWrrLbConfigAsMap()).build(); XdsConfig.XdsClusterConfig clusterConfig = new XdsConfig.XdsClusterConfig( CLUSTER_NAME, cdsUpdate, new EndpointConfig(StatusOr.fromValue(edsUpdate))); diff --git a/xds/third_party/envoy/import.sh b/xds/third_party/envoy/import.sh index dbe6f81b1a8..7a6b33871b3 100755 --- a/xds/third_party/envoy/import.sh +++ b/xds/third_party/envoy/import.sh @@ -86,6 +86,7 @@ envoy/extensions/load_balancing_policies/pick_first/v3/pick_first.proto envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto envoy/extensions/load_balancing_policies/round_robin/v3/round_robin.proto envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto +envoy/extensions/transport_sockets/http_11_proxy/v3/upstream_http_11_connect.proto envoy/extensions/transport_sockets/tls/v3/cert.proto envoy/extensions/transport_sockets/tls/v3/common.proto envoy/extensions/transport_sockets/tls/v3/secret.proto diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/http_11_proxy/v3/upstream_http_11_connect.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/http_11_proxy/v3/upstream_http_11_connect.proto new file mode 100644 index 00000000000..2c9b5333f41 --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/http_11_proxy/v3/upstream_http_11_connect.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package envoy.extensions.transport_sockets.http_11_proxy.v3; + +import "envoy/config/core/v3/base.proto"; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.transport_sockets.http_11_proxy.v3"; +option java_outer_classname = "UpstreamHttp11ConnectProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/http_11_proxy/v3;http_11_proxyv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Upstream HTTP/1.1 Proxy] +// [#extension: envoy.transport_sockets.http_11_proxy] + +// HTTP/1.1 proxy transport socket establishes an upstream connection to a proxy address +// instead of the target host's address. This behavior is triggered when the transport +// socket is configured and proxy information is provided. +// +// Behavior when proxying: +// ======================= +// When an upstream connection is established, instead of connecting directly to the endpoint +// address, the client will connect to the specified proxy address, send an HTTP/1.1 ``CONNECT`` request +// indicating the endpoint address, and process the response. If the response has HTTP status 200, +// the connection will be passed down to the underlying transport socket. +// +// Configuring proxy information: +// ============================== +// Set ``typed_filter_metadata`` in :ref:`LbEndpoint.Metadata ` or :ref:`LocalityLbEndpoints.Metadata `. +// using the key ``envoy.http11_proxy_transport_socket.proxy_address`` and the +// proxy address in ``config::core::v3::Address`` format. +// +message Http11ProxyUpstreamTransport { + // The underlying transport socket being wrapped. Defaults to plaintext (raw_buffer) if unset. + config.core.v3.TransportSocket transport_socket = 1; +} From 602aece0810fce6f298eec8d6433777b43f4b30c Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Thu, 6 Mar 2025 10:34:53 +0000 Subject: [PATCH 203/591] xds: avoid unnecessary dns lookup (#11932) --- .../io/grpc/xds/EnvoyServerProtoData.java | 5 +- .../java/io/grpc/xds/XdsListenerResource.java | 14 ++- ...rChainMatchingProtocolNegotiatorsTest.java | 112 +++++++++++------- 3 files changed, 82 insertions(+), 49 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java index 4a6213277e7..8c37e90855b 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java @@ -27,7 +27,6 @@ import io.grpc.xds.client.EnvoyProtoData; import io.grpc.xds.internal.security.SslContextProviderSupplier; import java.net.InetAddress; -import java.net.UnknownHostException; import java.util.Objects; import javax.annotation.Nullable; @@ -150,9 +149,9 @@ abstract static class CidrRange { abstract int prefixLen(); - static CidrRange create(String addressPrefix, int prefixLen) throws UnknownHostException { + static CidrRange create(InetAddress addressPrefix, int prefixLen) { return new AutoValue_EnvoyServerProtoData_CidrRange( - InetAddress.getByName(addressPrefix), prefixLen); + addressPrefix, prefixLen); } } diff --git a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java index 4b554be1743..2d9743143fc 100644 --- a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java @@ -25,6 +25,7 @@ import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.common.net.InetAddresses; import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; @@ -43,7 +44,6 @@ import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.XdsListenerResource.LdsUpdate; import io.grpc.xds.client.XdsResourceType; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -446,16 +446,18 @@ private static FilterChainMatch parseFilterChainMatch( try { for (io.envoyproxy.envoy.config.core.v3.CidrRange range : proto.getPrefixRangesList()) { prefixRanges.add( - CidrRange.create(range.getAddressPrefix(), range.getPrefixLen().getValue())); + CidrRange.create(InetAddresses.forString(range.getAddressPrefix()), + range.getPrefixLen().getValue())); } for (io.envoyproxy.envoy.config.core.v3.CidrRange range : proto.getSourcePrefixRangesList()) { - sourcePrefixRanges.add( - CidrRange.create(range.getAddressPrefix(), range.getPrefixLen().getValue())); + sourcePrefixRanges.add(CidrRange.create( + InetAddresses.forString(range.getAddressPrefix()), range.getPrefixLen().getValue())); } - } catch (UnknownHostException e) { - throw new ResourceInvalidException("Failed to create CidrRange", e); + } catch (IllegalArgumentException ex) { + throw new ResourceInvalidException("Failed to create CidrRange", ex); } + ConnectionSourceType sourceType; switch (proto.getSourceType()) { case ANY: diff --git a/xds/src/test/java/io/grpc/xds/FilterChainMatchingProtocolNegotiatorsTest.java b/xds/src/test/java/io/grpc/xds/FilterChainMatchingProtocolNegotiatorsTest.java index 685102477cc..c3d006a6003 100644 --- a/xds/src/test/java/io/grpc/xds/FilterChainMatchingProtocolNegotiatorsTest.java +++ b/xds/src/test/java/io/grpc/xds/FilterChainMatchingProtocolNegotiatorsTest.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.net.InetAddresses; import com.google.common.util.concurrent.SettableFuture; import io.grpc.ServerInterceptor; import io.grpc.internal.TestUtils.NoopChannelLogger; @@ -58,7 +59,6 @@ import io.netty.handler.codec.http2.Http2Settings; import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -318,7 +318,8 @@ public void destPrefixRangeMatch() throws Exception { EnvoyServerProtoData.FilterChainMatch filterChainMatchWithMatch = EnvoyServerProtoData.FilterChainMatch.create( 0, - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.1.2.0", 24)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.2.0"), 24)), ImmutableList.of(), ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.ANY, @@ -360,7 +361,8 @@ public void destPrefixRangeMismatch_returnDefaultFilterChain() EnvoyServerProtoData.FilterChainMatch filterChainMatchWithMismatch = EnvoyServerProtoData.FilterChainMatch.create( 0, - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.2.2.0", 24)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.2.2.0"), 24)), ImmutableList.of(), ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.ANY, @@ -403,7 +405,8 @@ public void dest0LengthPrefixRange() EnvoyServerProtoData.FilterChainMatch filterChainMatch0Length = EnvoyServerProtoData.FilterChainMatch.create( 0, - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.2.2.0", 0)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.2.2.0"), 0)), ImmutableList.of(), ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.ANY, @@ -444,7 +447,8 @@ public void destPrefixRange_moreSpecificWins() EnvoyServerProtoData.FilterChainMatch filterChainMatchLessSpecific = EnvoyServerProtoData.FilterChainMatch.create( 0, - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.1.2.0", 24)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.2.0"), 24)), ImmutableList.of(), ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.ANY, @@ -461,7 +465,8 @@ public void destPrefixRange_moreSpecificWins() EnvoyServerProtoData.FilterChainMatch filterChainMatchMoreSpecific = EnvoyServerProtoData.FilterChainMatch.create( 0, - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.1.2.2", 31)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.2.2"), 31)), ImmutableList.of(), ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.ANY, @@ -519,7 +524,8 @@ public void destPrefixRange_emptyListLessSpecific() EnvoyServerProtoData.FilterChainMatch filterChainMatchMoreSpecific = EnvoyServerProtoData.FilterChainMatch.create( 0, - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("8.0.0.0", 5)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("8.0.0.0"), 5)), ImmutableList.of(), ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.ANY, @@ -559,7 +565,8 @@ public void destPrefixRangeIpv6_moreSpecificWins() EnvoyServerProtoData.FilterChainMatch filterChainMatchLessSpecific = EnvoyServerProtoData.FilterChainMatch.create( 0, - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("FE80:0:0:0:0:0:0:0", 60)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("FE80:0:0:0:0:0:0:0"), 60)), ImmutableList.of(), ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.ANY, @@ -577,7 +584,8 @@ public void destPrefixRangeIpv6_moreSpecificWins() EnvoyServerProtoData.FilterChainMatch.create( 0, ImmutableList.of( - EnvoyServerProtoData.CidrRange.create("FE80:0000:0000:0000:0202:0:0:0", 80)), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("FE80:0000:0000:0000:0202:0:0:0"), 80)), ImmutableList.of(), ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.ANY, @@ -620,8 +628,10 @@ public void destPrefixRange_moreSpecificWith2Wins() EnvoyServerProtoData.FilterChainMatch.create( 0, ImmutableList.of( - EnvoyServerProtoData.CidrRange.create("10.1.2.0", 24), - EnvoyServerProtoData.CidrRange.create(LOCAL_IP, 32)), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.2.0"), 24), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString(LOCAL_IP), 32)), ImmutableList.of(), ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.ANY, @@ -638,7 +648,8 @@ public void destPrefixRange_moreSpecificWith2Wins() EnvoyServerProtoData.FilterChainMatch filterChainMatchLessSpecific = EnvoyServerProtoData.FilterChainMatch.create( 0, - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.1.2.2", 31)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.2.2"), 31)), ImmutableList.of(), ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.ANY, @@ -763,8 +774,10 @@ public void sourcePrefixRange_moreSpecificWith2Wins() ImmutableList.of(), ImmutableList.of(), ImmutableList.of( - EnvoyServerProtoData.CidrRange.create("10.4.2.0", 24), - EnvoyServerProtoData.CidrRange.create(REMOTE_IP, 32)), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.4.2.0"), 24), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString(REMOTE_IP), 32)), EnvoyServerProtoData.ConnectionSourceType.ANY, ImmutableList.of(), ImmutableList.of(), @@ -781,7 +794,8 @@ public void sourcePrefixRange_moreSpecificWith2Wins() 0, ImmutableList.of(), ImmutableList.of(), - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.4.2.2", 31)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.4.2.2"), 31)), EnvoyServerProtoData.ConnectionSourceType.ANY, ImmutableList.of(), ImmutableList.of(), @@ -811,8 +825,7 @@ filterChainLessSpecific, randomConfig("no-match")), } @Test - public void sourcePrefixRange_2Matchers_expectException() - throws UnknownHostException { + public void sourcePrefixRange_2Matchers_expectException() { ChannelHandler next = new ChannelInboundHandlerAdapter() { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { @@ -831,8 +844,10 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { ImmutableList.of(), ImmutableList.of(), ImmutableList.of( - EnvoyServerProtoData.CidrRange.create("10.4.2.0", 24), - EnvoyServerProtoData.CidrRange.create("192.168.10.2", 32)), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.4.2.0"), 24), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("192.168.10.2"), 32)), EnvoyServerProtoData.ConnectionSourceType.ANY, ImmutableList.of(), ImmutableList.of(), @@ -848,7 +863,8 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { 0, ImmutableList.of(), ImmutableList.of(), - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.4.2.0", 24)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.4.2.0"), 24)), EnvoyServerProtoData.ConnectionSourceType.ANY, ImmutableList.of(), ImmutableList.of(), @@ -890,8 +906,10 @@ public void sourcePortMatch_exactMatchWinsOverEmptyList() throws Exception { ImmutableList.of(), ImmutableList.of(), ImmutableList.of( - EnvoyServerProtoData.CidrRange.create("10.4.2.0", 24), - EnvoyServerProtoData.CidrRange.create("10.4.2.2", 31)), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.4.2.0"), 24), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.4.2.2"), 31)), EnvoyServerProtoData.ConnectionSourceType.ANY, ImmutableList.of(), ImmutableList.of(), @@ -908,7 +926,8 @@ public void sourcePortMatch_exactMatchWinsOverEmptyList() throws Exception { 0, ImmutableList.of(), ImmutableList.of(), - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.4.2.2", 31)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.4.2.2"), 31)), EnvoyServerProtoData.ConnectionSourceType.ANY, ImmutableList.of(7000, 15000), ImmutableList.of(), @@ -966,7 +985,8 @@ public void filterChain_5stepMatch() throws Exception { PORT, ImmutableList.of(), ImmutableList.of(), - ImmutableList.of(EnvoyServerProtoData.CidrRange.create(REMOTE_IP, 32)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString(REMOTE_IP), 32)), EnvoyServerProtoData.ConnectionSourceType.ANY, ImmutableList.of(), ImmutableList.of(), @@ -981,9 +1001,11 @@ public void filterChain_5stepMatch() throws Exception { EnvoyServerProtoData.FilterChainMatch filterChainMatch2 = EnvoyServerProtoData.FilterChainMatch.create( 0, - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.1.2.0", 30)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.2.0"), 30)), ImmutableList.of(), - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.4.0.0", 16)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.4.0.0"), 16)), EnvoyServerProtoData.ConnectionSourceType.ANY, ImmutableList.of(), ImmutableList.of(), @@ -997,8 +1019,10 @@ public void filterChain_5stepMatch() throws Exception { EnvoyServerProtoData.FilterChainMatch.create( 0, ImmutableList.of( - EnvoyServerProtoData.CidrRange.create("192.168.2.0", 24), - EnvoyServerProtoData.CidrRange.create("10.1.2.0", 30)), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("192.168.2.0"), 24), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.2.0"), 30)), ImmutableList.of(), ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.SAME_IP_OR_LOOPBACK, @@ -1015,10 +1039,13 @@ public void filterChain_5stepMatch() throws Exception { EnvoyServerProtoData.FilterChainMatch.create( 0, ImmutableList.of( - EnvoyServerProtoData.CidrRange.create("10.1.0.0", 16), - EnvoyServerProtoData.CidrRange.create("10.1.2.0", 30)), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.0.0"), 16), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.2.0"), 30)), ImmutableList.of(), - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.4.2.0", 24)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.4.2.0"), 24)), EnvoyServerProtoData.ConnectionSourceType.EXTERNAL, ImmutableList.of(16000, 9000), ImmutableList.of(), @@ -1034,12 +1061,16 @@ public void filterChain_5stepMatch() throws Exception { EnvoyServerProtoData.FilterChainMatch.create( 0, ImmutableList.of( - EnvoyServerProtoData.CidrRange.create("10.1.0.0", 16), - EnvoyServerProtoData.CidrRange.create("10.1.2.0", 30)), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.0.0"), 16), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.2.0"), 30)), ImmutableList.of(), ImmutableList.of( - EnvoyServerProtoData.CidrRange.create("10.4.2.0", 24), - EnvoyServerProtoData.CidrRange.create("192.168.2.0", 24)), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.4.2.0"), 24), + EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("192.168.2.0"), 24)), EnvoyServerProtoData.ConnectionSourceType.ANY, ImmutableList.of(15000, 8000), ImmutableList.of(), @@ -1053,7 +1084,8 @@ public void filterChain_5stepMatch() throws Exception { EnvoyServerProtoData.FilterChainMatch filterChainMatch6 = EnvoyServerProtoData.FilterChainMatch.create( 0, - ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.1.2.0", 29)), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.2.0"), 29)), ImmutableList.of(), ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.ANY, @@ -1105,8 +1137,8 @@ public void filterChainMatch_unsupportedMatchers() throws Exception { EnvoyServerProtoData.FilterChainMatch filterChainMatch1 = EnvoyServerProtoData.FilterChainMatch.create( 0 /* destinationPort */, - ImmutableList.of( - EnvoyServerProtoData.CidrRange.create("10.1.0.0", 16)) /* prefixRange */, + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.1.0.0"), 16)) /* prefixRange */, ImmutableList.of("managed-mtls", "h2") /* applicationProtocol */, ImmutableList.of() /* sourcePrefixRanges */, EnvoyServerProtoData.ConnectionSourceType.ANY /* sourceType */, @@ -1117,8 +1149,8 @@ public void filterChainMatch_unsupportedMatchers() throws Exception { EnvoyServerProtoData.FilterChainMatch filterChainMatch2 = EnvoyServerProtoData.FilterChainMatch.create( 0 /* destinationPort */, - ImmutableList.of( - EnvoyServerProtoData.CidrRange.create("10.0.0.0", 8)) /* prefixRange */, + ImmutableList.of(EnvoyServerProtoData.CidrRange.create( + InetAddresses.forString("10.0.0.0"), 8)) /* prefixRange */, ImmutableList.of() /* applicationProtocol */, ImmutableList.of() /* sourcePrefixRanges */, EnvoyServerProtoData.ConnectionSourceType.ANY /* sourceType */, From a6a041e4159bf2a8e631877232ca1c4bfacd6b56 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Thu, 6 Mar 2025 13:32:08 -0500 Subject: [PATCH 204/591] xds: Support filter state retention This PR adds support filter state retention in Java. The mechanism will be similar to the one described in [A83] (https://github.com/grpc/proposal/blob/master/A83-xds-gcp-authn-filter.md#filter-call-credentials-cache) for C-core, and will serve the same purpose. However, the implementation details are very different due to the different nature of xDS HTTP filter support in C-core and Java. ### Filter instance lifecycle #### xDS gRPC clients New filter instances are created per combination of: 1. `XdsNameResolver` instance, 2. Filter name+typeUrl as configured in HttpConnectionManager (HCM) http_filters. Existing client-side filter instances are shutdown: - A single a filter instance is shutdown when an LDS update contains HCM that is missing filter configuration for name+typeUrl combination of this instance. - All filter instances when watched LDS resource is missing from an LDS update. - All filter instances name resolver shutdown. #### xDS-enabled gRPC servers New filter instances are created per combination of: 1. Server instance, 2. FilterChain name, 3. Filter name+typeUrl as configured in FilterChain's HCM.http_filters Filter instances of Default Filter Chain is tracked separately per: 1. Server instance, 2. Filter name+typeUrl in default_filter_chain's HCM.http_filters. Existing server-side filter instances are shutdown: - A single a filter instance is shutdown when an LDS update contains FilterChain with HCM.http_filters that is missing configuration for filter name+typeUrl. - All filter instances associated with the FilterChain when an LDS update no longer contains FilterChain's name. - All filter instances when watched LDS resource is missing from an LDS update. - All filter instances on server shutdown. ### Related - Part 1: #11883 --- .../io/grpc/xds/EnvoyServerProtoData.java | 2 +- xds/src/main/java/io/grpc/xds/Filter.java | 28 +- ...ilterChainMatchingProtocolNegotiators.java | 4 + .../java/io/grpc/xds/XdsListenerResource.java | 5 + .../java/io/grpc/xds/XdsNameResolver.java | 71 ++- .../java/io/grpc/xds/XdsServerWrapper.java | 159 +++-- .../grpc/xds/GrpcXdsClientImplDataTest.java | 39 ++ .../test/java/io/grpc/xds/StatefulFilter.java | 174 ++++++ .../java/io/grpc/xds/XdsNameResolverTest.java | 350 ++++++++++- .../java/io/grpc/xds/XdsServerTestHelper.java | 35 +- .../io/grpc/xds/XdsServerWrapperTest.java | 556 +++++++++++++++++- 11 files changed, 1345 insertions(+), 78 deletions(-) create mode 100644 xds/src/test/java/io/grpc/xds/StatefulFilter.java diff --git a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java index 8c37e90855b..ae5b3c5b1c9 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java @@ -206,7 +206,7 @@ public static FilterChainMatch create(int destinationPort, @AutoValue abstract static class FilterChain { - // possibly empty + // Must be unique per server instance (except the default chain). abstract String name(); // TODO(sanjaypujare): flatten structure by moving FilterChainMatch class members here. diff --git a/xds/src/main/java/io/grpc/xds/Filter.java b/xds/src/main/java/io/grpc/xds/Filter.java index ab61ba2b570..aa326b55ad7 100644 --- a/xds/src/main/java/io/grpc/xds/Filter.java +++ b/xds/src/main/java/io/grpc/xds/Filter.java @@ -20,6 +20,7 @@ import com.google.protobuf.Message; import io.grpc.ClientInterceptor; import io.grpc.ServerInterceptor; +import java.io.Closeable; import java.util.Objects; import java.util.concurrent.ScheduledExecutorService; import javax.annotation.Nullable; @@ -32,7 +33,7 @@ * {@link Provider#isClientFilter()}, {@link Provider#isServerFilter()} to indicate that the filter * is capable of working on the client side or server side or both, respectively. */ -interface Filter { +interface Filter extends Closeable { /** Represents an opaque data structure holding configuration for a filter. */ interface FilterConfig { @@ -72,6 +73,19 @@ default boolean isServerFilter() { * *

Returns a filter instance registered with the same typeUrls as the provider, * capable of working with the same FilterConfig type returned by provider's parse functions. + * + *

For xDS gRPC clients, new filter instances are created per combination of: + *

    + *
  1. XdsNameResolver instance,
  2. + *
  3. Filter name+typeUrl in HttpConnectionManager (HCM) http_filters.
  4. + *
+ * + *

For xDS-enabled gRPC servers, new filter instances are created per combination of: + *

    + *
  1. Server instance,
  2. + *
  3. FilterChain name,
  4. + *
  5. Filter name+typeUrl in FilterChain's HCM.http_filters.
  6. + *
*/ Filter newInstance(); @@ -103,6 +117,14 @@ default ServerInterceptor buildServerInterceptor( return null; } + /** + * Releases filter resources like shared resources and remote connections. + * + *

See {@link Provider#newInstance()} for details on filter instance creation. + */ + @Override + default void close() {} + /** Filter config with instance name. */ final class NamedFilterConfig { // filter instance name @@ -114,6 +136,10 @@ final class NamedFilterConfig { this.filterConfig = filterConfig; } + String filterStateKey() { + return name + "_" + filterConfig.typeUrl(); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java b/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java index aaf8d69d2c1..77a66495614 100644 --- a/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java +++ b/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java @@ -151,6 +151,10 @@ static final class FilterChainSelector { this.defaultRoutingConfig = checkNotNull(defaultRoutingConfig, "defaultRoutingConfig"); } + FilterChainSelector(Map> routingConfigs) { + this(routingConfigs, null, new AtomicReference<>()); + } + @VisibleForTesting Map> getRoutingConfigs() { return routingConfigs; diff --git a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java index 2d9743143fc..ec0cbbf243f 100644 --- a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java @@ -178,6 +178,7 @@ static EnvoyServerProtoData.Listener parseServerSideListener( } ImmutableList.Builder filterChains = ImmutableList.builder(); + Set filterChainNames = new HashSet<>(); Set filterChainMatchSet = new HashSet<>(); int i = 0; for (io.envoyproxy.envoy.config.listener.v3.FilterChain fc : proto.getFilterChainsList()) { @@ -187,6 +188,10 @@ static EnvoyServerProtoData.Listener parseServerSideListener( // Generate a name, so we can identify it in the logs. filterChainName = "chain_" + i; } + if (!filterChainNames.add(filterChainName)) { + throw new ResourceInvalidException("Filter chain names must be unique. " + + "Found duplicate: " + filterChainName); + } filterChains.add( parseFilterChain(fc, filterChainName, tlsContextManager, filterRegistry, filterChainMatchSet, certProviderInstances, args)); diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index b7b1ed0bdba..507808d718b 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -127,6 +127,10 @@ final class XdsNameResolver extends NameResolver { private final ConfigSelector configSelector = new ConfigSelector(); private final long randomChannelId; private final MetricRecorder metricRecorder; + // Must be accessed in syncContext. + // Filter instances are unique per channel, and per filter (name+typeUrl). + // NamedFilterConfig.filterStateKey -> filter_instance. + private final HashMap activeFilters = new HashMap<>(); private volatile RoutingConfig routingConfig = RoutingConfig.EMPTY; private Listener2 listener; @@ -658,18 +662,23 @@ public void onChanged(final XdsListenerResource.LdsUpdate update) { HttpConnectionManager httpConnectionManager = update.httpConnectionManager(); List virtualHosts = httpConnectionManager.virtualHosts(); String rdsName = httpConnectionManager.rdsName(); + ImmutableList filterConfigs = httpConnectionManager.httpFilterConfigs(); + long streamDurationNano = httpConnectionManager.httpMaxStreamDurationNano(); + + // Create/update HCM-bound state. cleanUpRouteDiscoveryState(); + updateActiveFilters(filterConfigs); + + // Routes specified directly in LDS. if (virtualHosts != null) { - updateRoutes(virtualHosts, httpConnectionManager.httpMaxStreamDurationNano(), - httpConnectionManager.httpFilterConfigs()); - } else { - routeDiscoveryState = new RouteDiscoveryState( - rdsName, httpConnectionManager.httpMaxStreamDurationNano(), - httpConnectionManager.httpFilterConfigs()); - logger.log(XdsLogLevel.INFO, "Start watching RDS resource {0}", rdsName); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), - rdsName, routeDiscoveryState, syncContext); + updateRoutes(virtualHosts, streamDurationNano, filterConfigs); + return; } + // Routes provided by RDS. + routeDiscoveryState = new RouteDiscoveryState(rdsName, streamDurationNano, filterConfigs); + logger.log(XdsLogLevel.INFO, "Start watching RDS resource {0}", rdsName); + xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), + rdsName, routeDiscoveryState, syncContext); } @Override @@ -690,6 +699,7 @@ public void onResourceDoesNotExist(final String resourceName) { String error = "LDS resource does not exist: " + resourceName; logger.log(XdsLogLevel.INFO, error); cleanUpRouteDiscoveryState(); + updateActiveFilters(null); cleanUpRoutes(error); } @@ -703,9 +713,35 @@ private void stop() { logger.log(XdsLogLevel.INFO, "Stop watching LDS resource {0}", ldsResourceName); stopped = true; cleanUpRouteDiscoveryState(); + updateActiveFilters(null); xdsClient.cancelXdsResourceWatch(XdsListenerResource.getInstance(), ldsResourceName, this); } + // called in syncContext + private void updateActiveFilters(@Nullable List filterConfigs) { + if (filterConfigs == null) { + filterConfigs = ImmutableList.of(); + } + Set filtersToShutdown = new HashSet<>(activeFilters.keySet()); + for (NamedFilterConfig namedFilter : filterConfigs) { + String typeUrl = namedFilter.filterConfig.typeUrl(); + String filterKey = namedFilter.filterStateKey(); + + Filter.Provider provider = filterRegistry.get(typeUrl); + checkNotNull(provider, "provider %s", typeUrl); + Filter filter = activeFilters.computeIfAbsent(filterKey, k -> provider.newInstance()); + checkNotNull(filter, "filter %s", filterKey); + filtersToShutdown.remove(filterKey); + } + + // Shutdown filters not present in current HCM. + for (String filterKey : filtersToShutdown) { + Filter filterToShutdown = activeFilters.remove(filterKey); + checkNotNull(filterToShutdown, "filterToShutdown %s", filterKey); + filterToShutdown.close(); + } + } + // called in syncContext private void updateRoutes(List virtualHosts, long httpMaxStreamDurationNano, @Nullable List filterConfigs) { @@ -836,19 +872,16 @@ private ClientInterceptor createFilters( ImmutableList.Builder filterInterceptors = ImmutableList.builder(); for (NamedFilterConfig namedFilter : filterConfigs) { - FilterConfig config = namedFilter.filterConfig; String name = namedFilter.name; - String typeUrl = config.typeUrl(); - - Filter.Provider provider = filterRegistry.get(typeUrl); - if (provider == null || !provider.isClientFilter()) { - continue; - } - - Filter filter = provider.newInstance(); + FilterConfig config = namedFilter.filterConfig; + FilterConfig overrideConfig = selectedOverrideConfigs.get(name); + String filterKey = namedFilter.filterStateKey(); + Filter filter = activeFilters.get(filterKey); + checkNotNull(filter, "activeFilters.get(%s)", filterKey); ClientInterceptor interceptor = - filter.buildClientInterceptor(config, selectedOverrideConfigs.get(name), scheduler); + filter.buildClientInterceptor(config, overrideConfig, scheduler); + if (interceptor != null) { filterInterceptors.add(interceptor); } diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index e5b25ae458b..e0185974861 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -113,6 +113,14 @@ public void uncaughtException(Thread t, Throwable e) { private DiscoveryState discoveryState; private volatile Server delegate; + // Must be accessed in syncContext. + // Filter instances are unique per Server, per FilterChain, and per filter's name+typeUrl. + // FilterChain.name -> filter_instance>. + private final HashMap> activeFilters = new HashMap<>(); + // Default filter chain Filter instances are unique per Server, and per filter's name+typeUrl. + // NamedFilterConfig.filterStateKey -> filter_instance. + private final HashMap activeFiltersDefaultChain = new HashMap<>(); + XdsServerWrapper( String listenerAddress, ServerBuilder delegateBuilder, @@ -382,13 +390,18 @@ public void onChanged(final LdsUpdate update) { releaseSuppliersInFlight(); pendingRds.clear(); } + filterChains = update.listener().filterChains(); defaultFilterChain = update.listener().defaultFilterChain(); + // Filters are loaded even if the server isn't serving yet. + updateActiveFilters(); + List allFilterChains = filterChains; if (defaultFilterChain != null) { allFilterChains = new ArrayList<>(filterChains); allFilterChains.add(defaultFilterChain); } + Set allRds = new HashSet<>(); for (FilterChain filterChain : allFilterChains) { HttpConnectionManager hcm = filterChain.httpConnectionManager(); @@ -406,6 +419,7 @@ public void onChanged(final LdsUpdate update) { allRds.add(hcm.rdsName()); } } + for (Map.Entry entry: routeDiscoveryStates.entrySet()) { if (!allRds.contains(entry.getKey())) { xdsClient.cancelXdsResourceWatch(XdsRouteConfigureResource.getInstance(), @@ -448,6 +462,7 @@ private void shutdown() { cleanUpRouteDiscoveryStates(); logger.log(Level.FINE, "Stop watching LDS resource {0}", resourceName); xdsClient.cancelXdsResourceWatch(XdsListenerResource.getInstance(), resourceName, this); + shutdownActiveFilters(); List toRelease = getSuppliersInUse(); filterChainSelectorManager.updateSelector(FilterChainSelector.NO_FILTER_CHAIN); for (SslContextProviderSupplier s: toRelease) { @@ -464,7 +479,8 @@ private void updateSelector() { ImmutableMap.Builder> routingConfigs = ImmutableMap.builder(); for (FilterChain filterChain: filterChains) { - routingConfigs.put(filterChain, generateRoutingConfig(filterChain)); + HashMap chainFilters = activeFilters.get(filterChain.name()); + routingConfigs.put(filterChain, generateRoutingConfig(filterChain, chainFilters)); } // Prepare the new selector. @@ -473,9 +489,9 @@ private void updateSelector() { selector = new FilterChainSelector( routingConfigs.build(), defaultFilterChain.sslContextProviderSupplier(), - generateRoutingConfig(defaultFilterChain)); + generateRoutingConfig(defaultFilterChain, activeFiltersDefaultChain)); } else { - selector = new FilterChainSelector(routingConfigs.build(), null, new AtomicReference<>()); + selector = new FilterChainSelector(routingConfigs.build()); } // Prepare the list of current selector's resources to close later. @@ -494,38 +510,105 @@ private void updateSelector() { startDelegateServer(); } - private AtomicReference generateRoutingConfig(FilterChain filterChain) { + // called in syncContext + private void updateActiveFilters() { + Set removedChains = new HashSet<>(activeFilters.keySet()); + for (FilterChain filterChain: filterChains) { + removedChains.remove(filterChain.name()); + updateActiveFiltersForChain( + activeFilters.computeIfAbsent(filterChain.name(), k -> new HashMap<>()), + filterChain.httpConnectionManager().httpFilterConfigs()); + } + + // Shutdown all filters of chains missing from the LDS. + for (String chainToShutdown : removedChains) { + HashMap filtersToShutdown = activeFilters.get(chainToShutdown); + checkNotNull(filtersToShutdown, "filtersToShutdown of chain %s", chainToShutdown); + updateActiveFiltersForChain(filtersToShutdown, null); + activeFilters.remove(chainToShutdown); + } + + // Default chain. + ImmutableList defaultChainConfigs = null; + if (defaultFilterChain != null) { + defaultChainConfigs = defaultFilterChain.httpConnectionManager().httpFilterConfigs(); + } + updateActiveFiltersForChain(activeFiltersDefaultChain, defaultChainConfigs); + } + + // called in syncContext + private void shutdownActiveFilters() { + for (HashMap chainFilters : activeFilters.values()) { + checkNotNull(chainFilters, "chainFilters"); + updateActiveFiltersForChain(chainFilters, null); + } + activeFilters.clear(); + updateActiveFiltersForChain(activeFiltersDefaultChain, null); + } + + // called in syncContext + private void updateActiveFiltersForChain( + Map chainFilters, @Nullable List filterConfigs) { + if (filterConfigs == null) { + filterConfigs = ImmutableList.of(); + } + + Set filtersToShutdown = new HashSet<>(chainFilters.keySet()); + for (NamedFilterConfig namedFilter : filterConfigs) { + String typeUrl = namedFilter.filterConfig.typeUrl(); + String filterKey = namedFilter.filterStateKey(); + + Filter.Provider provider = filterRegistry.get(typeUrl); + checkNotNull(provider, "provider %s", typeUrl); + Filter filter = chainFilters.computeIfAbsent(filterKey, k -> provider.newInstance()); + checkNotNull(filter, "filter %s", filterKey); + filtersToShutdown.remove(filterKey); + } + + // Shutdown filters not present in current HCM. + for (String filterKey : filtersToShutdown) { + Filter filterToShutdown = chainFilters.remove(filterKey); + checkNotNull(filterToShutdown, "filterToShutdown %s", filterKey); + filterToShutdown.close(); + } + } + + private AtomicReference generateRoutingConfig( + FilterChain filterChain, Map chainFilters) { HttpConnectionManager hcm = filterChain.httpConnectionManager(); - ImmutableMap interceptors; + ServerRoutingConfig routingConfig; // Inlined routes. - if (hcm.virtualHosts() != null) { - interceptors = generatePerRouteInterceptors(hcm.httpFilterConfigs(), hcm.virtualHosts()); - return new AtomicReference<>(ServerRoutingConfig.create(hcm.virtualHosts(), interceptors)); + ImmutableList vhosts = hcm.virtualHosts(); + if (vhosts != null) { + routingConfig = ServerRoutingConfig.create(vhosts, + generatePerRouteInterceptors(hcm.httpFilterConfigs(), vhosts, chainFilters)); + return new AtomicReference<>(routingConfig); } // Routes from RDS. RouteDiscoveryState rds = routeDiscoveryStates.get(hcm.rdsName()); checkNotNull(rds, "rds"); - ServerRoutingConfig routingConfig; ImmutableList savedVhosts = rds.savedVirtualHosts; if (savedVhosts != null) { - interceptors = generatePerRouteInterceptors(hcm.httpFilterConfigs(), savedVhosts); - routingConfig = ServerRoutingConfig.create(savedVhosts, interceptors); + routingConfig = ServerRoutingConfig.create(savedVhosts, + generatePerRouteInterceptors(hcm.httpFilterConfigs(), savedVhosts, chainFilters)); } else { routingConfig = ServerRoutingConfig.FAILING_ROUTING_CONFIG; } - AtomicReference routingConfigRef = new AtomicReference<>(routingConfig); savedRdsRoutingConfigRef.put(filterChain, routingConfigRef); return routingConfigRef; } private ImmutableMap generatePerRouteInterceptors( - @Nullable List filterConfigs, List virtualHosts) { + @Nullable List filterConfigs, + List virtualHosts, + Map chainFilters) { syncContext.throwIfNotInThisSynchronizationContext(); + checkNotNull(chainFilters, "chainFilters"); ImmutableMap.Builder perRouteInterceptors = new ImmutableMap.Builder<>(); @@ -545,21 +628,16 @@ private ImmutableMap generatePerRouteInterceptors( // Interceptors for this vhost/route combo. List interceptors = new ArrayList<>(filterConfigs.size()); - for (NamedFilterConfig namedFilter : filterConfigs) { - FilterConfig config = namedFilter.filterConfig; String name = namedFilter.name; - String typeUrl = config.typeUrl(); + FilterConfig config = namedFilter.filterConfig; + FilterConfig overrideConfig = perRouteOverrides.get(name); + String filterKey = namedFilter.filterStateKey(); - Filter.Provider provider = filterRegistry.get(typeUrl); - if (provider == null || !provider.isServerFilter()) { - logger.warning("HttpFilter[" + name + "]: not supported on server-side: " + typeUrl); - continue; - } + Filter filter = chainFilters.get(filterKey); + checkNotNull(filter, "chainFilters.get(%s)", filterKey); + ServerInterceptor interceptor = filter.buildServerInterceptor(config, overrideConfig); - Filter filter = provider.newInstance(); - ServerInterceptor interceptor = - filter.buildServerInterceptor(config, perRouteOverrides.get(name)); if (interceptor != null) { interceptors.add(interceptor); } @@ -597,6 +675,7 @@ public Listener interceptCall(ServerCall call, private void handleConfigNotFound(StatusException exception) { cleanUpRouteDiscoveryStates(); + shutdownActiveFilters(); List toRelease = getSuppliersInUse(); filterChainSelectorManager.updateSelector(FilterChainSelector.NO_FILTER_CHAIN); for (SslContextProviderSupplier s: toRelease) { @@ -718,22 +797,24 @@ public void run() { private void updateRdsRoutingConfig() { for (FilterChain filterChain : savedRdsRoutingConfigRef.keySet()) { - if (resourceName.equals(filterChain.httpConnectionManager().rdsName())) { - ServerRoutingConfig updatedRoutingConfig; - if (savedVirtualHosts == null) { - updatedRoutingConfig = ServerRoutingConfig.FAILING_ROUTING_CONFIG; - } else { - ImmutableMap updatedInterceptors = - generatePerRouteInterceptors( - filterChain.httpConnectionManager().httpFilterConfigs(), - savedVirtualHosts); - updatedRoutingConfig = ServerRoutingConfig.create(savedVirtualHosts, - updatedInterceptors); - } - logger.log(Level.FINEST, "Updating filter chain {0} rds routing config: {1}", - new Object[]{filterChain.name(), updatedRoutingConfig}); - savedRdsRoutingConfigRef.get(filterChain).set(updatedRoutingConfig); + HttpConnectionManager hcm = filterChain.httpConnectionManager(); + if (!resourceName.equals(hcm.rdsName())) { + continue; + } + + ServerRoutingConfig updatedRoutingConfig; + if (savedVirtualHosts == null) { + updatedRoutingConfig = ServerRoutingConfig.FAILING_ROUTING_CONFIG; + } else { + HashMap chainFilters = activeFilters.get(filterChain.name()); + ImmutableMap interceptors = generatePerRouteInterceptors( + hcm.httpFilterConfigs(), savedVirtualHosts, chainFilters); + updatedRoutingConfig = ServerRoutingConfig.create(savedVirtualHosts, interceptors); } + + logger.log(Level.FINEST, "Updating filter chain {0} rds routing config: {1}", + new Object[]{filterChain.name(), updatedRoutingConfig}); + savedRdsRoutingConfigRef.get(filterChain).set(updatedRoutingConfig); } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 7fac666f983..90b83320d63 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -2924,6 +2924,45 @@ public void parseFilterChain_noName() throws ResourceInvalidException { assertThat(listener.defaultFilterChain().name()).isEqualTo("chain_default"); } + @Test + public void parseFilterChain_duplicateName() throws ResourceInvalidException { + FilterChain filterChain0 = + FilterChain.newBuilder() + .setName("filter_chain") + .setFilterChainMatch(FilterChainMatch.getDefaultInstance()) + .addFilters(buildHttpConnectionManagerFilter( + HttpFilter.newBuilder() + .setName("http-filter-foo") + .setIsOptional(true) + .setTypedConfig(Any.pack(Router.newBuilder().build())) + .build())) + .build(); + + FilterChain filterChain1 = + FilterChain.newBuilder() + .setName("filter_chain") + .setFilterChainMatch( + FilterChainMatch.newBuilder().addAllSourcePorts(Arrays.asList(443, 8080))) + .addFilters(buildHttpConnectionManagerFilter( + HttpFilter.newBuilder() + .setName("http-filter-bar") + .setTypedConfig(Any.pack(Router.newBuilder().build())) + .setIsOptional(true) + .build())) + .build(); + + Listener listenerProto = + Listener.newBuilder() + .setName("listener1") + .setTrafficDirection(TrafficDirection.INBOUND) + .addAllFilterChains(Arrays.asList(filterChain0, filterChain1)) + .build(); + thrown.expect(ResourceInvalidException.class); + thrown.expectMessage("Filter chain names must be unique. Found duplicate: filter_chain"); + XdsListenerResource.parseServerSideListener( + listenerProto, null, filterRegistry, null, getXdsResourceTypeArgs(true)); + } + @Test public void validateCommonTlsContext_tlsParams() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() diff --git a/xds/src/test/java/io/grpc/xds/StatefulFilter.java b/xds/src/test/java/io/grpc/xds/StatefulFilter.java new file mode 100644 index 00000000000..162dd380daf --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/StatefulFilter.java @@ -0,0 +1,174 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.common.collect.ImmutableList; +import com.google.protobuf.Message; +import io.grpc.ServerInterceptor; +import java.util.ConcurrentModificationException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.IntStream; +import javax.annotation.Nullable; + +/** + * Unlike most singleton-based filters, each StatefulFilter object has a distinct identity. + */ +class StatefulFilter implements Filter { + + static final String DEFAULT_TYPE_URL = "type.googleapis.com/grpc.test.StatefulFilter"; + private final AtomicBoolean shutdown = new AtomicBoolean(); + + final int idx; + @Nullable volatile String lastCfg = null; + + public StatefulFilter(int idx) { + this.idx = idx; + } + + public boolean isShutdown() { + return shutdown.get(); + } + + @Override + public void close() { + if (!shutdown.compareAndSet(false, true)) { + throw new ConcurrentModificationException( + "Unexpected: StatefulFilter#close called multiple times"); + } + } + + @Nullable + @Override + public ServerInterceptor buildServerInterceptor( + FilterConfig config, + @Nullable FilterConfig overrideConfig) { + Config cfg = (Config) config; + // TODO(sergiitk): to be replaced when name argument passed to the constructor. + lastCfg = cfg.getConfig(); + return null; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder().append("StatefulFilter{") + .append("idx=").append(idx); + if (lastCfg != null) { + sb.append(", name=").append(lastCfg); + } + return sb.append("}").toString(); + } + + static final class Provider implements Filter.Provider { + + private final String typeUrl; + private final ConcurrentMap instances = new ConcurrentHashMap<>(); + + volatile int counter; + + Provider() { + this(DEFAULT_TYPE_URL); + } + + Provider(String typeUrl) { + this.typeUrl = typeUrl; + } + + @Override + public String[] typeUrls() { + return new String[]{ typeUrl }; + } + + @Override + public boolean isClientFilter() { + return true; + } + + @Override + public boolean isServerFilter() { + return true; + } + + @Override + public synchronized StatefulFilter newInstance() { + StatefulFilter filter = new StatefulFilter(counter++); + instances.put(filter.idx, filter); + return filter; + } + + public synchronized StatefulFilter getInstance(int idx) { + return instances.get(idx); + } + + public synchronized ImmutableList getAllInstances() { + return IntStream.range(0, counter).mapToObj(this::getInstance).collect(toImmutableList()); + } + + @SuppressWarnings("UnusedMethod") + public synchronized int getCount() { + return counter; + } + + @Override + public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + return ConfigOrError.fromConfig(Config.fromProto(rawProtoMessage, typeUrl)); + } + + @Override + public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { + return ConfigOrError.fromConfig(Config.fromProto(rawProtoMessage, typeUrl)); + } + } + + + static final class Config implements FilterConfig { + + private final String typeUrl; + private final String config; + + public Config(String config, String typeUrl) { + this.config = config; + this.typeUrl = typeUrl; + } + + public Config(String config) { + this(config, DEFAULT_TYPE_URL); + } + + public Config() { + this("", DEFAULT_TYPE_URL); + } + + public static Config fromProto(Message rawProtoMessage, String typeUrl) { + checkNotNull(rawProtoMessage, "rawProtoMessage"); + return new Config(rawProtoMessage.toString(), typeUrl); + } + + public String getConfig() { + return config; + } + + @Override + public String typeUrl() { + return typeUrl; + } + } +} diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index f7309051f92..877fa6f88dc 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static io.grpc.xds.FaultFilter.HEADER_ABORT_GRPC_STATUS_KEY; import static io.grpc.xds.FaultFilter.HEADER_ABORT_HTTP_STATUS_KEY; import static io.grpc.xds.FaultFilter.HEADER_ABORT_PERCENTAGE_KEY; @@ -135,6 +136,14 @@ public class XdsNameResolverTest { private static final FaultFilter.Provider FAULT_FILTER_PROVIDER = new FaultFilter.Provider(); private static final RouterFilter.Provider ROUTER_FILTER_PROVIDER = new RouterFilter.Provider(); + // Readability: makes it simpler to distinguish resource parameters. + private static final ImmutableMap NO_FILTER_OVERRIDES = ImmutableMap.of(); + private static final ImmutableList NO_HASH_POLICIES = ImmutableList.of(); + + // Stateful instance filter names. + private static final String STATEFUL_1 = "test.stateful.filter.1"; + private static final String STATEFUL_2 = "test.stateful.filter.2"; + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); private final SynchronizationContext syncContext = new SynchronizationContext( @@ -210,6 +219,10 @@ public void setUp() { @After public void tearDown() { XdsNameResolver.enableTimeout = originalEnableTimeout; + if (resolver == null) { + // Allow tests to test shutdown. + return; + } FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); resolver.shutdown(); if (xdsClient != null) { @@ -1274,6 +1287,316 @@ public void resolved_simpleCallSucceeds_routeToRls() { call1, configSelector2, "rls-plugin-foo", 30.0); } + // Begin filter state tests. + + /** + * Verifies the lifecycle of HCM filter instances across LDS updates. + * + *

Filter instances: + * 1. Must have one unique instance per HCM filter name. + * 2. Must be reused when an LDS update with HCM contains a filter with the same name. + * 3. Must be shutdown (closed) when an HCM in a LDS update doesn't a filter with the same name. + */ + @Test + public void filterState_survivesLds() { + StatefulFilter.Provider statefulFilterProvider = filterStateTestSetupResolver(); + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + VirtualHost vhost = filterStateTestVhost(); + + // LDS 1. + xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + assertClusterResolutionResult(call1, cluster1); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + // Verify that StatefulFilter with different filter names result in different Filter instances. + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); + // Naming: ldsFilter + StatefulFilter lds1Filter1 = lds1Snapshot.get(0); + StatefulFilter lds1Filter2 = lds1Snapshot.get(1); + assertThat(lds1Filter1).isNotSameInstanceAs(lds1Filter2); + // Redundant check just in case StatefulFilter synchronization is broken. + assertThat(lds1Filter1.idx).isEqualTo(0); + assertThat(lds1Filter2.idx).isEqualTo(1); + + // LDS 2: filter configs with the same names. + xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + assertClusterResolutionResult(call1, cluster1); + ImmutableList lds2Snapshot = statefulFilterProvider.getAllInstances(); + // Filter names hasn't changed, so expecting no new StatefulFilter instances. + assertWithMessage("LDS 2: Expected Filter instances to be reused across LDS updates") + .that(lds2Snapshot).isEqualTo(lds1Snapshot); + + // LDS 3: Filter "STATEFUL_2" removed. + xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1)); + assertClusterResolutionResult(call1, cluster1); + ImmutableList lds3Snapshot = statefulFilterProvider.getAllInstances(); + // Again, no new StatefulFilter instances should be created. + assertWithMessage("LDS 3: Expected Filter instances to be reused across LDS updates") + .that(lds3Snapshot).isEqualTo(lds1Snapshot); + // Verify the shutdown state. + assertThat(lds1Filter1.isShutdown()).isFalse(); + assertWithMessage("LDS 3: Expected %s to be shut down", lds1Filter2) + .that(lds1Filter2.isShutdown()).isTrue(); + + // LDS 4: Filter "STATEFUL_2" added back. + xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + assertClusterResolutionResult(call1, cluster1); + ImmutableList lds4Snapshot = statefulFilterProvider.getAllInstances(); + // Filter "STATEFUL_2" should be treated as any other new filter name in an LDS update: + // a new instance should be created. + assertWithMessage("LDS 4: Expected a new filter instance for %s", STATEFUL_2) + .that(lds4Snapshot).hasSize(3); + StatefulFilter lds4Filter2 = lds4Snapshot.get(2); + assertThat(lds4Filter2.idx).isEqualTo(2); + assertThat(lds4Filter2).isNotSameInstanceAs(lds1Filter2); + assertThat(lds4Snapshot).containsAtLeastElementsIn(lds1Snapshot); + // Verify the shutdown state. + assertThat(lds1Filter1.isShutdown()).isFalse(); + assertThat(lds1Filter2.isShutdown()).isTrue(); + assertThat(lds4Filter2.isShutdown()).isFalse(); + } + + /** + * Verifies the lifecycle of HCM filter instances across RDS updates. + * + *

Filter instances: + * 1. Must have instantiated by the initial LDS. + * 2. Must be reused by all subsequent RDS updates. + * 3. Must be not shutdown (closed) by valid RDS updates. + */ + @Test + public void filterState_survivesRds() { + StatefulFilter.Provider statefulFilterProvider = filterStateTestSetupResolver(); + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + + // LDS 1. + xdsClient.deliverLdsUpdateForRdsNameWithFilters(RDS_RESOURCE_NAME, + filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + // Verify that StatefulFilter with different filter names result in different Filter instances. + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); + // Naming: ldsFilter + StatefulFilter lds1Filter1 = lds1Snapshot.get(0); + StatefulFilter lds1Filter2 = lds1Snapshot.get(1); + assertThat(lds1Filter1).isNotSameInstanceAs(lds1Filter2); + + // RDS 1. + VirtualHost vhost1 = filterStateTestVhost(); + xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, vhost1); + assertClusterResolutionResult(call1, cluster1); + // Initial RDS update should not generate Filter instances. + ImmutableList rds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("RDS 1: Expected Filter instances to be reused across RDS route updates") + .that(rds1Snapshot).isEqualTo(lds1Snapshot); + + // RDS 2: exactly the same as RDS 1. + xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, vhost1); + assertClusterResolutionResult(call1, cluster1); + ImmutableList rds2Snapshot = statefulFilterProvider.getAllInstances(); + // Neither should any subsequent RDS updates. + assertWithMessage("RDS 2: Expected Filter instances to be reused across RDS route updates") + .that(rds2Snapshot).isEqualTo(lds1Snapshot); + + // RDS 3: Contains a per-route override for STATEFUL_1. + VirtualHost vhost3 = filterStateTestVhost(ImmutableMap.of( + STATEFUL_1, new StatefulFilter.Config("RDS3") + )); + xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, vhost3); + assertClusterResolutionResult(call1, cluster1); + ImmutableList rds3Snapshot = statefulFilterProvider.getAllInstances(); + // As with any other Route update, typed_per_filter_config overrides should not result in + // creating new filter instances. + assertWithMessage("RDS 3: Expected Filter instances to be reused on per-route filter overrides") + .that(rds3Snapshot).isEqualTo(lds1Snapshot); + } + + /** + * Verifies a special case where an existing filter is has a different typeUrl in a subsequent + * LDS update. + * + *

Expectations: + * 1. The old filter instance must be shutdown. + * 2. A new filter instance must be created for the new filter with different typeUrl. + */ + @Test + public void filterState_specialCase_sameNameDifferentTypeUrl() { + // Prepare filter registry with StatefulFilter of different typeUrl. + StatefulFilter.Provider statefulFilterProvider = new StatefulFilter.Provider(); + String altTypeUrl = "type.googleapis.com/grpc.test.AltStatefulFilter"; + StatefulFilter.Provider altStatefulFilterProvider = new StatefulFilter.Provider(altTypeUrl); + FilterRegistry filterRegistry = FilterRegistry.newRegistry() + .register(statefulFilterProvider, altStatefulFilterProvider, ROUTER_FILTER_PROVIDER); + resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, + syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, null, + metricRecorder); + resolver.start(mockListener); + + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + VirtualHost vhost = filterStateTestVhost(); + + // LDS 1. + xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + assertClusterResolutionResult(call1, cluster1); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + ImmutableList lds1SnapshotAlt = altStatefulFilterProvider.getAllInstances(); + // Verify that StatefulFilter with different filter names result in different Filter instances. + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); + // Naming: ldsFilter + StatefulFilter lds1Filter1 = lds1Snapshot.get(0); + StatefulFilter lds1Filter2 = lds1Snapshot.get(1); + assertThat(lds1Filter1).isNotSameInstanceAs(lds1Filter2); + // Nothing in the alternative provider. + assertThat(lds1SnapshotAlt).isEmpty(); + + // LDS 2: Filter STATEFUL_2 present, but with a different typeUrl: altTypeUrl. + ImmutableList filterConfigs = ImmutableList.of( + new NamedFilterConfig(STATEFUL_1, new StatefulFilter.Config(STATEFUL_1)), + new NamedFilterConfig(STATEFUL_2, new StatefulFilter.Config(STATEFUL_2, altTypeUrl)), + new NamedFilterConfig(ROUTER_FILTER_INSTANCE_NAME, RouterFilter.ROUTER_CONFIG) + ); + xdsClient.deliverLdsUpdateWithFilters(vhost, filterConfigs); + assertClusterResolutionResult(call1, cluster1); + ImmutableList lds2Snapshot = statefulFilterProvider.getAllInstances(); + ImmutableList lds2SnapshotAlt = altStatefulFilterProvider.getAllInstances(); + // Filter "STATEFUL_2" has different typeUrl, and should be treated as a new filter. + // No changes in the snapshot of normal stateful filters. + assertWithMessage("LDS 2: expected a new filter instance of different type") + .that(lds2Snapshot).isEqualTo(lds1Snapshot); + // A new filter instance is created by altStatefulFilterProvider. + assertWithMessage("LDS 2: expected a new filter instance for type %s", altTypeUrl) + .that(lds2SnapshotAlt).hasSize(1); + StatefulFilter lds2Filter2Alt = lds2SnapshotAlt.get(0); + assertThat(lds2Filter2Alt).isNotSameInstanceAs(lds1Filter2); + // Verify the shutdown state. + assertThat(lds1Filter1.isShutdown()).isFalse(); + assertThat(lds1Filter2.isShutdown()).isTrue(); + assertThat(lds2Filter2Alt.isShutdown()).isFalse(); + } + + /** + * Verifies that all filter instances are shutdown (closed) on LDS resource not found. + */ + @Test + public void filterState_shutdown_onLdsNotFound() { + StatefulFilter.Provider statefulFilterProvider = filterStateTestSetupResolver(); + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + VirtualHost vhost = filterStateTestVhost(); + + // LDS 1. + xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + assertClusterResolutionResult(call1, cluster1); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); + // Naming: ldsFilter + StatefulFilter lds1Filter1 = lds1Snapshot.get(0); + StatefulFilter lds1Filter2 = lds1Snapshot.get(1); + + // LDS 2: resource not found. + reset(mockListener); + xdsClient.deliverLdsResourceNotFound(); + assertEmptyResolutionResult(expectedLdsResourceName); + // Verify shutdown. + assertThat(lds1Filter1.isShutdown()).isTrue(); + assertThat(lds1Filter2.isShutdown()).isTrue(); + } + + /** + * Verifies that all filter instances are shutdown (closed) on LDS ResourceWatcher shutdown. + */ + @Test + public void filterState_shutdown_onResolverShutdown() { + StatefulFilter.Provider statefulFilterProvider = filterStateTestSetupResolver(); + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + VirtualHost vhost = filterStateTestVhost(); + + // LDS 1. + xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + assertClusterResolutionResult(call1, cluster1); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); + // Naming: ldsFilter + StatefulFilter lds1Filter1 = lds1Snapshot.get(0); + StatefulFilter lds1Filter2 = lds1Snapshot.get(1); + + // Shutdown. + resolver.shutdown(); + resolver = null; // no need to shutdown again in the teardown. + // Verify shutdown. + assertThat(lds1Filter1.isShutdown()).isTrue(); + assertThat(lds1Filter2.isShutdown()).isTrue(); + } + + /** + * Verifies that filter instances are NOT shutdown on RDS_RESOURCE_NAME not found. + */ + @Test + public void filterState_shutdown_noShutdownOnRdsNotFound() { + StatefulFilter.Provider statefulFilterProvider = filterStateTestSetupResolver(); + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + + // LDS 1. + xdsClient.deliverLdsUpdateForRdsNameWithFilters(RDS_RESOURCE_NAME, + filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); + // Naming: ldsFilter + StatefulFilter lds1Filter1 = lds1Snapshot.get(0); + StatefulFilter lds1Filter2 = lds1Snapshot.get(1); + + // RDS 1: Standard vhost with a route. + xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, filterStateTestVhost()); + assertClusterResolutionResult(call1, cluster1); + assertThat(statefulFilterProvider.getAllInstances()).isEqualTo(lds1Snapshot); + + // RDS 2: RDS_RESOURCE_NAME not found. + reset(mockListener); + xdsClient.deliverRdsResourceNotFound(RDS_RESOURCE_NAME); + assertEmptyResolutionResult(RDS_RESOURCE_NAME); + assertThat(lds1Filter1.isShutdown()).isFalse(); + assertThat(lds1Filter2.isShutdown()).isFalse(); + } + + private StatefulFilter.Provider filterStateTestSetupResolver() { + StatefulFilter.Provider statefulFilterProvider = new StatefulFilter.Provider(); + FilterRegistry filterRegistry = FilterRegistry.newRegistry() + .register(statefulFilterProvider, ROUTER_FILTER_PROVIDER); + resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, + syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, null, + metricRecorder); + resolver.start(mockListener); + return statefulFilterProvider; + } + + private ImmutableList filterStateTestConfigs(String... names) { + ImmutableList.Builder result = ImmutableList.builder(); + for (String name : names) { + result.add(new NamedFilterConfig(name, new StatefulFilter.Config(name))); + } + result.add(new NamedFilterConfig(ROUTER_FILTER_INSTANCE_NAME, RouterFilter.ROUTER_CONFIG)); + return result.build(); + } + + private Route filterStateTestRoute(ImmutableMap perRouteOverrides) { + // Standard basic route for filterState tests. + return Route.forAction( + RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), + RouteAction.forCluster(cluster1, NO_HASH_POLICIES, null, null, true), + perRouteOverrides); + } + + private VirtualHost filterStateTestVhost() { + return filterStateTestVhost(NO_FILTER_OVERRIDES); + } + + private VirtualHost filterStateTestVhost(ImmutableMap perRouteOverrides) { + return VirtualHost.create( + "stateful-vhost", + ImmutableList.of(expectedLdsResourceName), + ImmutableList.of(filterStateTestRoute(perRouteOverrides)), + NO_FILTER_OVERRIDES); + } + + // End filter state tests. + @SuppressWarnings("unchecked") private void assertEmptyResolutionResult(String resource) { verify(mockListener).onResult(resolutionResultCaptor.capture()); @@ -1287,6 +1610,13 @@ private void assertEmptyResolutionResult(String resource) { assertThat(configResult.getStatus().getDescription()).contains(resource); } + private void assertClusterResolutionResult(CallInfo call, String expectedCluster) { + verify(mockListener).onResult(resolutionResultCaptor.capture()); + ResolutionResult result = resolutionResultCaptor.getValue(); + InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); + assertCallSelectClusterResult(call, configSelector, expectedCluster, null); + } + private void assertCallSelectClusterResult( CallInfo call, InternalConfigSelector configSelector, String expectedCluster, @Nullable Double expectedTimeoutSec) { @@ -1528,7 +1858,6 @@ public void generateServiceConfig_forPerMethodConfig() throws IOException { assertThat(XdsNameResolver.generateServiceConfigWithMethodConfig(null, retryPolicy)) .isEqualTo(expectedServiceConfig); - // timeout and retry expectedServiceConfigJson = "{\n" + " \"methodConfig\": [{\n" @@ -2127,6 +2456,13 @@ void deliverLdsUpdate(final List routes) { }); } + void deliverLdsUpdateWithFilters(VirtualHost vhost, List filterConfigs) { + syncContext.execute(() -> { + ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( + 0L, Collections.singletonList(vhost), filterConfigs))); + }); + } + void deliverLdsUpdateWithFaultInjection( final String cluster, FaultConfig httpFilterFaultConfig, @@ -2191,9 +2527,15 @@ void deliverLdsUpdateForRdsNameWithFaultInjection( } void deliverLdsUpdateForRdsName(String rdsName) { + deliverLdsUpdateForRdsNameWithFilters(rdsName, null); + } + + void deliverLdsUpdateForRdsNameWithFilters( + String rdsName, + @Nullable List filterConfigs) { syncContext.execute(() -> { ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forRdsName( - 0, rdsName, null))); + 0, rdsName, filterConfigs))); }); } @@ -2251,6 +2593,10 @@ void deliverRdsUpdate(String resourceName, List virtualHosts) { }); } + void deliverRdsUpdate(String resourceName, VirtualHost virtualHost) { + deliverRdsUpdate(resourceName, ImmutableList.of(virtualHost)); + } + void deliverRdsResourceNotFound(String resourceName) { if (!resourceName.equals(rdsResource)) { return; diff --git a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java index 0508b11c205..ec9f4c54c31 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java @@ -272,32 +272,53 @@ private void execute(Runnable action) { // // Note that this doesn't guarantee that any of the RDS watchers are created. // Tests should use setExpectedRdsCount(int) and awaitRds() for that. + awaitLdsResource(DEFAULT_TIMEOUT); + serverExecutor.execute(action); + } + + private String awaitLdsResource(Duration timeout) { if (ldsResource == null) { throw new IllegalStateException("xDS resource update after watcher cancel"); } try { - ldsResource.get(DEFAULT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + return ldsResource.get(timeout.toMillis(), TimeUnit.MILLISECONDS); } catch (ExecutionException | TimeoutException e) { - throw new RuntimeException("Can't resolve LDS resource name in " + DEFAULT_TIMEOUT, e); + throw new RuntimeException("Can't resolve LDS resource name in " + timeout, e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } - serverExecutor.execute(action); } - void deliverLdsUpdate(List filterChains, - FilterChain defaultFilterChain) { + void deliverLdsUpdate(LdsUpdate ldsUpdate) { + execute(() -> ldsWatcher.onChanged(ldsUpdate)); + } + + void deliverLdsUpdate( + List filterChains, + @Nullable FilterChain defaultFilterChain) { deliverLdsUpdate(LdsUpdate.forTcpListener(Listener.create( "listener", "0.0.0.0:1", ImmutableList.copyOf(filterChains), defaultFilterChain))); } - void deliverLdsUpdate(LdsUpdate ldsUpdate) { - execute(() -> ldsWatcher.onChanged(ldsUpdate)); + void deliverLdsUpdate(FilterChain filterChain, @Nullable FilterChain defaultFilterChain) { + deliverLdsUpdate(ImmutableList.of(filterChain), defaultFilterChain); + } + + void deliverLdsResourceNotFound() { + execute(() -> ldsWatcher.onResourceDoesNotExist(awaitLdsResource(DEFAULT_TIMEOUT))); } void deliverRdsUpdate(String resourceName, List virtualHosts) { execute(() -> rdsWatchers.get(resourceName).onChanged(new RdsUpdate(virtualHosts))); } + + void deliverRdsUpdate(String resourceName, VirtualHost virtualHost) { + deliverRdsUpdate(resourceName, ImmutableList.of(virtualHost)); + } + + void deliverRdsResourceNotFound(String resourceName) { + execute(() -> rdsWatchers.get(resourceName).onResourceDoesNotExist(resourceName)); + } } } diff --git a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java index 388052a3dc8..b866e10c559 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java @@ -18,6 +18,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static io.grpc.xds.XdsServerWrapper.ATTR_SERVER_ROUTING_CONFIG; import static io.grpc.xds.XdsServerWrapper.RETRY_DELAY_NANOS; import static org.junit.Assert.fail; @@ -34,6 +35,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.net.InetAddresses; import com.google.common.util.concurrent.SettableFuture; import io.grpc.Attributes; import io.grpc.InsecureChannelCredentials; @@ -49,10 +51,13 @@ import io.grpc.SynchronizationContext; import io.grpc.internal.FakeClock; import io.grpc.testing.TestMethodDescriptors; +import io.grpc.xds.EnvoyServerProtoData.CidrRange; import io.grpc.xds.EnvoyServerProtoData.FilterChain; +import io.grpc.xds.EnvoyServerProtoData.FilterChainMatch; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.Filter.NamedFilterConfig; import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler.FilterChainSelector; +import io.grpc.xds.StatefulFilter.Config; import io.grpc.xds.VirtualHost.Route; import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher; @@ -70,10 +75,12 @@ import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import io.grpc.xds.internal.security.SslContextProviderSupplier; import java.io.IOException; +import java.net.InetAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -93,6 +100,14 @@ @RunWith(JUnit4.class) public class XdsServerWrapperTest { private static final int START_WAIT_AFTER_LISTENER_MILLIS = 100; + private static final String ROUTER_FILTER_INSTANCE_NAME = "envoy.router"; + private static final RouterFilter.Provider ROUTER_FILTER_PROVIDER = new RouterFilter.Provider(); + + // Readability: makes it simpler to distinguish resource parameters. + private static final ImmutableMap NO_FILTER_OVERRIDES = ImmutableMap.of(); + + private static final String STATEFUL_1 = "stateful_1"; + private static final String STATEFUL_2 = "stateful_2"; @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @@ -1268,9 +1283,507 @@ public ServerCall.Listener interceptCall(ServerCall serverStart = filterStateTestStartServer(filterRegistry); + + VirtualHost vhost = filterStateTestVhost(); + + // LDS 1. + FilterChain lds1FilterChain = createFilterChain("chain_0", + createHcm(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2))); + xdsClient.deliverLdsUpdate(lds1FilterChain, null); + verifyServerStarted(serverStart); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + // Verify that StatefulFilter with different filter names result in different Filter instances. + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); + // Naming: ldsFilter + StatefulFilter lds1Filter1 = lds1Snapshot.get(0); + StatefulFilter lds1Filter2 = lds1Snapshot.get(1); + assertThat(lds1Filter1).isNotSameInstanceAs(lds1Filter2); + // Redundant check just in case StatefulFilter synchronization is broken. + assertThat(lds1Filter1.idx).isEqualTo(0); + assertThat(lds1Filter2.idx).isEqualTo(1); + + // LDS 2: filter configs with the same names. + FilterChain lds2FilterChain = createFilterChain("chain_0", + createHcm(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2))); + xdsClient.deliverLdsUpdate(lds2FilterChain, null); + ImmutableList lds2Snapshot = statefulFilterProvider.getAllInstances(); + // Filter names hasn't changed, so expecting no new StatefulFilter instances. + assertWithMessage("LDS 2: Expected Filter instances to be reused across LDS updates") + .that(lds2Snapshot).isEqualTo(lds1Snapshot); + + // LDS 3: Filter "STATEFUL_2" removed. + FilterChain lds3FilterChain = createFilterChain("chain_0", + createHcm(vhost, filterStateTestConfigs(STATEFUL_1))); + xdsClient.deliverLdsUpdate(lds3FilterChain, null); + ImmutableList lds3Snapshot = statefulFilterProvider.getAllInstances(); + // Again, no new StatefulFilter instances should be created. + assertWithMessage("LDS 3: Expected Filter instances to be reused across LDS updates") + .that(lds3Snapshot).isEqualTo(lds1Snapshot); + // Verify the shutdown state. + assertThat(lds1Filter1.isShutdown()).isFalse(); + assertWithMessage("LDS 3: Expected %s to be shut down", lds1Filter2) + .that(lds1Filter2.isShutdown()).isTrue(); + + // LDS 4: Filter "STATEFUL_2" added back. + FilterChain lds4FilterChain = createFilterChain("chain_0", + createHcm(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2))); + xdsClient.deliverLdsUpdate(lds4FilterChain, null); + ImmutableList lds4Snapshot = statefulFilterProvider.getAllInstances(); + // Filter "STATEFUL_2" should be treated as any other new filter name in an LDS update: + // a new instance should be created. + assertWithMessage("LDS 4: Expected a new filter instance for %s", STATEFUL_2) + .that(lds4Snapshot).hasSize(3); + StatefulFilter lds4Filter2 = lds4Snapshot.get(2); + assertThat(lds4Filter2.idx).isEqualTo(2); + assertThat(lds4Filter2).isNotSameInstanceAs(lds1Filter2); + assertThat(lds4Snapshot).containsAtLeastElementsIn(lds1Snapshot); + // Verify the shutdown state. + assertThat(lds1Filter1.isShutdown()).isFalse(); + assertThat(lds1Filter2.isShutdown()).isTrue(); + assertThat(lds4Filter2.isShutdown()).isFalse(); + } + + @Test + public void filterState_survivesRds() throws Exception { + StatefulFilter.Provider statefulFilterProvider = new StatefulFilter.Provider(); + FilterRegistry filterRegistry = filterStateTestFilterRegistry(statefulFilterProvider); + SettableFuture serverStart = filterStateTestStartServer(filterRegistry); + + String rdsName = "rds.example.com"; + + // LDS 1. + FilterChain fc1 = createFilterChain("fc1", + createHcmForRds(rdsName, filterStateTestConfigs(STATEFUL_1, STATEFUL_2))); + xdsClient.deliverLdsUpdate(fc1, null); + xdsClient.awaitRds(FakeXdsClient.DEFAULT_TIMEOUT); + verify(listener, never()).onServing(); + // Server didn't start, but filter instances should have already been created. + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); + // Naming: ldsFilter + StatefulFilter lds1Filter1 = lds1Snapshot.get(0); + StatefulFilter lds1Filter2 = lds1Snapshot.get(1); + assertThat(lds1Filter1).isNotSameInstanceAs(lds1Filter2); + + // RDS 1. + VirtualHost vhost1 = filterStateTestVhost(); + xdsClient.deliverRdsUpdate(rdsName, vhost1); + verifyServerStarted(serverStart); + assertThat(getSelectorRoutingConfigs()).hasSize(1); + assertThat(getSelectorVhosts(fc1)).containsExactly(vhost1); + // Initial RDS update should not generate Filter instances. + ImmutableList rds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("RDS 1: Expected Filter instances to be reused across RDS route updates") + .that(rds1Snapshot).isEqualTo(lds1Snapshot); + + // RDS 2: exactly the same as RDS 1. + xdsClient.deliverRdsUpdate(rdsName, vhost1); + assertThat(getSelectorRoutingConfigs()).hasSize(1); + assertThat(getSelectorVhosts(fc1)).containsExactly(vhost1); + ImmutableList rds2Snapshot = statefulFilterProvider.getAllInstances(); + // Neither should any subsequent RDS updates. + assertWithMessage("RDS 2: Expected Filter instances to be reused across RDS route updates") + .that(rds2Snapshot).isEqualTo(lds1Snapshot); + + // RDS 3: Contains a per-route override for STATEFUL_1. + VirtualHost vhost3 = filterStateTestVhost(vhost1.name(), ImmutableMap.of( + STATEFUL_1, new Config("RDS3") + )); + xdsClient.deliverRdsUpdate(rdsName, vhost3); + assertThat(getSelectorRoutingConfigs()).hasSize(1); + assertThat(getSelectorVhosts(fc1)).containsExactly(vhost3); + ImmutableList rds3Snapshot = statefulFilterProvider.getAllInstances(); + // As with any other Route update, typed_per_filter_config overrides should not result in + // creating new filter instances. + assertWithMessage("RDS 3: Expected Filter instances to be reused on per-route filter overrides") + .that(rds3Snapshot).isEqualTo(lds1Snapshot); + } + + @Test + public void filterState_uniquePerFilterChain() { + StatefulFilter.Provider statefulFilterProvider = new StatefulFilter.Provider(); + FilterRegistry filterRegistry = filterStateTestFilterRegistry(statefulFilterProvider); + SettableFuture serverStart = filterStateTestStartServer(filterRegistry); + + // Prepare multiple filter chains matchers for testing. + FilterChainMatch matcherA = createMatchSrcIp("3fff:a::/32"); + FilterChainMatch matcherB = createMatchSrcIp("3fff:b::/32"); + + // Vhosts won't change too. + VirtualHost vhostA = filterStateTestVhost("stateful_vhost_a"); + VirtualHost vhostB = filterStateTestVhost("stateful_vhost_b"); + + // LDS 1. + FilterChain lds1ChainA = createFilterChain("chain_a", + createHcm(vhostA, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)), + matcherA); + FilterChain lds1ChainB = createFilterChain("chain_b", + createHcm(vhostB, filterStateTestConfigs(STATEFUL_2)), + matcherB); + + xdsClient.deliverLdsUpdate(ImmutableList.of(lds1ChainA, lds1ChainB), null); + verifyServerStarted(serverStart); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + // Verify that filter with name STATEFUL_2 produced separate instances unique per filter chain. + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(3); + // Naming: ldsChainFilter + StatefulFilter lds1ChainAFilter1 = lds1Snapshot.get(0); + StatefulFilter lds1ChainAFilter2 = lds1Snapshot.get(1); + StatefulFilter lds1ChainBFilter2 = lds1Snapshot.get(2); + assertThat(lds1ChainAFilter2).isNotSameInstanceAs(lds1ChainBFilter2); + + // LDS 2: In chain B filter with name STATEFUL_1 is replaced STATEFUL_2. + FilterChain lds2ChainA = createFilterChain("chain_a", + createHcm(vhostA, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)), + matcherA); + FilterChain lds2ChainB = createFilterChain("chain_b", + createHcm(vhostB, filterStateTestConfigs(STATEFUL_1)), + matcherB); + + xdsClient.deliverLdsUpdate(ImmutableList.of(lds2ChainA, lds2ChainB), null); + ImmutableList lds2Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("LDS 2: expected a distinct instance of filter %s for Chain B", STATEFUL_1) + .that(lds2Snapshot).hasSize(4); + StatefulFilter lds2ChainBFilter1 = lds2Snapshot.get(3); + assertThat(lds2ChainBFilter1).isNotSameInstanceAs(lds1ChainAFilter1); + // Confirm correct STATEFUL_2 has been shut down. + assertThat(lds1ChainBFilter2.isShutdown()).isTrue(); + assertThat(lds1ChainAFilter2.isShutdown()).isFalse(); + + // LDS 3: Add default chain + // Default filter chain is an exception from the uniqueness rule, and we need to make sure + // that this is accounted for when we're tracking active filters per unique FilterChain. + FilterChain lds3ChainDefault = createFilterChain("chain_default", + createHcm(vhostA, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)), + matcherA); + xdsClient.deliverLdsUpdate(ImmutableList.of(lds2ChainA, lds2ChainB), lds3ChainDefault); + ImmutableList lds3Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("LDS 3: Expected two new distinct filter instances for default chain") + .that(lds3Snapshot).hasSize(6); + StatefulFilter lds3ChainDefaultFilter1 = lds3Snapshot.get(4); + StatefulFilter lds3ChainDefaultFilter2 = lds3Snapshot.get(5); + // STATEFUL_1 in default chain not the same STATEFUL_1 in chain A or B + assertThat(lds3ChainDefaultFilter1).isNotSameInstanceAs(lds1ChainAFilter1); + assertThat(lds3ChainDefaultFilter1).isNotSameInstanceAs(lds2ChainBFilter1); + // STATEFUL_2 in default chain not the same STATEFUL_1 in chain A + assertThat(lds3ChainDefaultFilter2).isNotSameInstanceAs(lds1ChainAFilter2); + } + + /** + * Verifies a special case where an existing filter is has a different typeUrl in a subsequent + * LDS update. + * + *

Expectations: + * 1. The old filter instance must be shutdown. + * 2. A new filter instance must be created for the new filter with different typeUrl. + */ + @Test + public void filterState_specialCase_sameNameDifferentTypeUrl() { + // Setup the server with filter containing StatefulFilter.Provider for two distict type URLs. + StatefulFilter.Provider statefulFilterProvider = new StatefulFilter.Provider(); + String altTypeUrl = "type.googleapis.com/grpc.test.AltStatefulFilter"; + StatefulFilter.Provider altStatefulFilterProvider = new StatefulFilter.Provider(altTypeUrl); + FilterRegistry filterRegistry = FilterRegistry.newRegistry() + .register(statefulFilterProvider, altStatefulFilterProvider, ROUTER_FILTER_PROVIDER); + SettableFuture serverStart = filterStateTestStartServer(filterRegistry); + + // Test a normal chain and the default chain, as it's handled separately. + VirtualHost vhost = filterStateTestVhost(); + + // LDS 1. + ImmutableList lds1Confgs = filterStateTestConfigs(STATEFUL_1, STATEFUL_2); + FilterChain lds1ChainA = createFilterChain("chain_a", createHcm(vhost, lds1Confgs)); + FilterChain lds1ChainDefault = createFilterChain("chain_default", createHcm(vhost, lds1Confgs)); + xdsClient.deliverLdsUpdate(lds1ChainA, lds1ChainDefault); + verifyServerStarted(serverStart); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(4); + // Naming: ldsChainFilter + StatefulFilter lds1ChainAFilter1 = lds1Snapshot.get(0); + StatefulFilter lds1ChainAFilter2 = lds1Snapshot.get(1); + StatefulFilter lds1ChainDefaultFilter1 = lds1Snapshot.get(2); + StatefulFilter lds1ChainDefaultFilter2 = lds1Snapshot.get(3); + + // LDS 2: Filter STATEFUL_2 present, but with a different typeUrl: altTypeUrl. + ImmutableList lds2Confgs = ImmutableList.of( + new NamedFilterConfig(STATEFUL_1, new StatefulFilter.Config(STATEFUL_1)), + new NamedFilterConfig(STATEFUL_2, new StatefulFilter.Config(STATEFUL_2, altTypeUrl)), + new NamedFilterConfig(ROUTER_FILTER_INSTANCE_NAME, RouterFilter.ROUTER_CONFIG) + ); + FilterChain lds2ChainA = createFilterChain("chain_a", createHcm(vhost, lds2Confgs)); + FilterChain lds2ChainDefault = createFilterChain("chain_default", createHcm(vhost, lds2Confgs)); + xdsClient.deliverLdsUpdate(lds2ChainA, lds2ChainDefault); + ImmutableList lds2Snapshot = statefulFilterProvider.getAllInstances(); + ImmutableList lds2SnapshotAlt = altStatefulFilterProvider.getAllInstances(); + // Filter "STATEFUL_2" has different typeUrl, and should be treated as a new filter. + // No changes in the snapshot of normal stateful filters. + assertThat(lds2Snapshot).isEqualTo(lds1Snapshot); + // Two new filter instances is created by altStatefulFilterProvider for chainA and chainDefault. + assertWithMessage("LDS 2: expected new filter instances for type %s", altTypeUrl) + .that(lds2SnapshotAlt).hasSize(2); + StatefulFilter lds2ChainAFilter2Alt = lds2SnapshotAlt.get(0); + StatefulFilter lds2ChainADefault2Alt = lds2SnapshotAlt.get(1); + // Confirm two new distict instances of STATEFUL_2 were created. + assertThat(lds2ChainAFilter2Alt).isNotSameInstanceAs(lds1ChainAFilter2); + assertThat(lds2ChainADefault2Alt).isNotSameInstanceAs(lds1ChainDefaultFilter2); + assertThat(lds2ChainAFilter2Alt).isNotSameInstanceAs(lds2ChainADefault2Alt); + // Verify the instance of STATEFUL_2 of the old type are shutdown. + assertThat(lds1ChainAFilter2.isShutdown()).isTrue(); + assertThat(lds1ChainDefaultFilter2.isShutdown()).isTrue(); + // Verify the new instances of STATEFUL_2 and the old instances of STATEFUL_1 are running. + assertThat(lds2ChainAFilter2Alt.isShutdown()).isFalse(); + assertThat(lds2ChainADefault2Alt.isShutdown()).isFalse(); + assertThat(lds1ChainAFilter1.isShutdown()).isFalse(); + assertThat(lds1ChainDefaultFilter1.isShutdown()).isFalse(); + } + + /** + * Verifies that all filter instances are shutdown (closed) on LDS resource not found. + */ + @Test + public void filterState_shutdown_onLdsNotFound() { + StatefulFilter.Provider statefulFilterProvider = new StatefulFilter.Provider(); + FilterRegistry filterRegistry = filterStateTestFilterRegistry(statefulFilterProvider); + SettableFuture serverStart = filterStateTestStartServer(filterRegistry); + + // Test a normal chain and the default chain, as it's handled separately. + VirtualHost vhost = filterStateTestVhost(); + FilterChain chainA = createFilterChain("chain_a", + createHcm(vhost, filterStateTestConfigs(STATEFUL_1))); + FilterChain chainDefault = createFilterChain("chain_default", + createHcm(vhost, filterStateTestConfigs(STATEFUL_2))); + + // LDS 1. + xdsClient.deliverLdsUpdate(chainA, chainDefault); + verifyServerStarted(serverStart); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); + // Naming: ldsChainFilter + StatefulFilter lds1ChainAFilter1 = lds1Snapshot.get(0); + StatefulFilter lds1ChainDefaultFilter2 = lds1Snapshot.get(1); + + // LDS 2: resource not found. + xdsClient.deliverLdsResourceNotFound(); + // Verify shutdown. + assertThat(lds1ChainAFilter1.isShutdown()).isTrue(); + assertThat(lds1ChainDefaultFilter2.isShutdown()).isTrue(); + } + + /** + * Verifies that all filter instances of a filter chain are shutdown when said chain is removed. + */ + @Test + public void filterState_shutdown_onChainRemoved() { + StatefulFilter.Provider statefulFilterProvider = new StatefulFilter.Provider(); + FilterRegistry filterRegistry = filterStateTestFilterRegistry(statefulFilterProvider); + SettableFuture serverStart = filterStateTestStartServer(filterRegistry); + + ImmutableList configs = filterStateTestConfigs(STATEFUL_1, STATEFUL_2); + FilterChain chainA = createFilterChain("chain_a", + createHcm(filterStateTestVhost("stateful_vhost_a"), configs), + createMatchSrcIp("3fff:a::/32")); + FilterChain chainB = createFilterChain("chain_b", + createHcm(filterStateTestVhost("stateful_vhost_b"), configs), + createMatchSrcIp("3fff:b::/32")); + FilterChain chainDefault = createFilterChain("chain_default", + createHcm(filterStateTestVhost("stateful_vhost_default"), configs), + createMatchSrcIp("3fff:defa::/32")); + + // LDS 1. + xdsClient.deliverLdsUpdate(ImmutableList.of(chainA, chainB), chainDefault); + verifyServerStarted(serverStart); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(6); + StatefulFilter chainAFilter1 = lds1Snapshot.get(0); + StatefulFilter chainAFilter2 = lds1Snapshot.get(1); + StatefulFilter chainBFilter1 = lds1Snapshot.get(2); + StatefulFilter chainBFilter2 = lds1Snapshot.get(3); + StatefulFilter chainDefaultFilter1 = lds1Snapshot.get(4); + StatefulFilter chainDefaultFilter2 = lds1Snapshot.get(5); + + // LDS 2: ChainB and ChainDefault are gone. + xdsClient.deliverLdsUpdate(chainA, null); + assertThat(statefulFilterProvider.getAllInstances()).isEqualTo(lds1Snapshot); + // ChainA filters not shutdown (just in case). + assertThat(chainAFilter1.isShutdown()).isFalse(); + assertThat(chainAFilter2.isShutdown()).isFalse(); + // ChainB and ChainDefault filters shutdown. + assertWithMessage("chainBFilter1").that(chainBFilter1.isShutdown()).isTrue(); + assertWithMessage("chainBFilter2").that(chainBFilter2.isShutdown()).isTrue(); + assertWithMessage("chainDefaultFilter1").that(chainDefaultFilter1.isShutdown()).isTrue(); + assertWithMessage("chainDefaultFilter2").that(chainDefaultFilter2.isShutdown()).isTrue(); + } + + /** + * Verifies that all filter instances are shutdown (closed) on LDS ResourceWatcher shutdown. + */ + @Test + public void filterState_shutdown_onServerShutdown() { + StatefulFilter.Provider statefulFilterProvider = new StatefulFilter.Provider(); + FilterRegistry filterRegistry = filterStateTestFilterRegistry(statefulFilterProvider); + SettableFuture serverStart = filterStateTestStartServer(filterRegistry); + + // Test a normal chain and the default chain, as it's handled separately. + VirtualHost vhost = filterStateTestVhost(); + FilterChain chainA = createFilterChain("chain_a", + createHcm(vhost, filterStateTestConfigs(STATEFUL_1))); + FilterChain chainDefault = createFilterChain("chain_default", + createHcm(vhost, filterStateTestConfigs(STATEFUL_2))); + + // LDS 1. + xdsClient.deliverLdsUpdate(chainA, chainDefault); + verifyServerStarted(serverStart); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); + // Naming: ldsChainFilter + StatefulFilter lds1ChainAFilter1 = lds1Snapshot.get(0); + StatefulFilter lds1ChainDefaultFilter2 = lds1Snapshot.get(1); + + // Shutdown. + xdsServerWrapper.shutdown(); + assertThat(xdsServerWrapper.isShutdown()).isTrue(); + assertThat(xdsClient.isShutDown()).isTrue(); + // Verify shutdown. + assertThat(lds1ChainAFilter1.isShutdown()).isTrue(); + assertThat(lds1ChainDefaultFilter2.isShutdown()).isTrue(); + } + + /** + * Verifies that filter instances are NOT shutdown on RDS_RESOURCE_NAME not found. + */ + @Test + public void filterState_shutdown_noShutdownOnRdsNotFound() throws Exception { + StatefulFilter.Provider statefulFilterProvider = new StatefulFilter.Provider(); + FilterRegistry filterRegistry = filterStateTestFilterRegistry(statefulFilterProvider); + SettableFuture serverStart = filterStateTestStartServer(filterRegistry); + + String rdsName = "rds.example.com"; + // Test a normal chain and the default chain, as it's handled separately. + FilterChain chainA = createFilterChain("chain_a", + createHcmForRds(rdsName, filterStateTestConfigs(STATEFUL_1))); + FilterChain chainDefault = createFilterChain("chain_default", + createHcmForRds(rdsName, filterStateTestConfigs(STATEFUL_2))); + + xdsClient.deliverLdsUpdate(chainA, chainDefault); + xdsClient.awaitRds(FakeXdsClient.DEFAULT_TIMEOUT); + verify(listener, never()).onServing(); + // Server didn't start, but filter instances should have already been created. + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); + // Naming: ldsChainFilter + StatefulFilter lds1ChainAFilter1 = lds1Snapshot.get(0); + StatefulFilter lds1ChainDefaultFilter2 = lds1Snapshot.get(1); + + // RDS 1: Standard vhost with a route. + xdsClient.deliverRdsUpdate(rdsName, filterStateTestVhost()); + verifyServerStarted(serverStart); + assertThat(statefulFilterProvider.getAllInstances()).isEqualTo(lds1Snapshot); + + // RDS 2: RDS_RESOURCE_NAME not found. + xdsClient.deliverRdsResourceNotFound(rdsName); + assertThat(lds1ChainAFilter1.isShutdown()).isFalse(); + assertThat(lds1ChainDefaultFilter2.isShutdown()).isFalse(); + } + + private FilterRegistry filterStateTestFilterRegistry( + StatefulFilter.Provider statefulFilterProvider) { + return FilterRegistry.newRegistry().register(statefulFilterProvider, ROUTER_FILTER_PROVIDER); + } + + private SettableFuture filterStateTestStartServer(FilterRegistry filterRegistry) { + xdsServerWrapper = new XdsServerWrapper("0.0.0.0:1", mockBuilder, listener, + selectorManager, new FakeXdsClientPoolFactory(xdsClient), filterRegistry); + SettableFuture serverStart = SettableFuture.create(); + scheduleServerStart(xdsServerWrapper, serverStart); + return serverStart; + } + + private static ImmutableList filterStateTestConfigs(String... names) { + ImmutableList.Builder result = ImmutableList.builder(); + for (String name : names) { + result.add(new NamedFilterConfig(name, new StatefulFilter.Config(name))); + } + result.add(new NamedFilterConfig(ROUTER_FILTER_INSTANCE_NAME, RouterFilter.ROUTER_CONFIG)); + return result.build(); + } + + private static Route filterStateTestRoute(ImmutableMap perRouteOverrides) { + // Standard basic route for filterState tests. + return Route.forAction( + RouteMatch.withPathExactOnly("/grpc.test.HelloService/SayHello"), null, perRouteOverrides); + } + + private static VirtualHost filterStateTestVhost() { + return filterStateTestVhost("stateful-vhost", NO_FILTER_OVERRIDES); + } + + private static VirtualHost filterStateTestVhost(String name) { + return filterStateTestVhost(name, NO_FILTER_OVERRIDES); + } + + private static VirtualHost filterStateTestVhost( + String name, ImmutableMap perRouteOverrides) { + return VirtualHost.create( + name, + ImmutableList.of("stateful.test.example.com"), + ImmutableList.of(filterStateTestRoute(perRouteOverrides)), + NO_FILTER_OVERRIDES); + } + + // End filter state tests. + + private void verifyServerStarted(SettableFuture serverStart) { + try { + serverStart.get(5, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new AssertionError("serverStart future failed to resolve within the timeout", e); + } + verify(listener).onServing(); + try { + verify(mockServer).start(); + } catch (IOException e) { + throw new AssertionError("mockServer.start() shouldn't throw", e); + } + } + + private Map> getSelectorRoutingConfigs() { + return selectorManager.getSelectorToUpdateSelector().getRoutingConfigs(); + } + + private ServerRoutingConfig getSelectorRoutingConfig(FilterChain fc) { + return getSelectorRoutingConfigs().get(fc).get(); + } + + private ImmutableList getSelectorVhosts(FilterChain fc) { + return getSelectorRoutingConfig(fc).virtualHosts(); + } + + public static void scheduleServerStart( + XdsServerWrapper xdsServerWrapper, SettableFuture serverStart) { + Executors.newSingleThreadExecutor().execute(() -> { + try { + serverStart.set(xdsServerWrapper.start()); + } catch (Exception e) { + serverStart.setException(e); + } + }); + } + private static FilterChain createFilterChain(String name, HttpConnectionManager hcm) { - return EnvoyServerProtoData.FilterChain.create(name, createMatch(), - hcm, createTls(), mock(TlsContextManager.class)); + return createFilterChain(name, hcm, createMatch()); + } + + private static FilterChain createFilterChain( + String name, HttpConnectionManager hcm, FilterChainMatch filterChainMatch) { + TlsContextManager tlsContextManager = mock(TlsContextManager.class); + return FilterChain.create(name, filterChainMatch, hcm, createTls(), tlsContextManager); } private static VirtualHost createVirtualHost(String name) { @@ -1279,17 +1792,27 @@ private static VirtualHost createVirtualHost(String name) { ImmutableMap.of()); } - private static HttpConnectionManager createRds(String name) { - return createRds(name, null); + private static HttpConnectionManager createHcm( + VirtualHost vhost, List filterConfigs) { + return HttpConnectionManager.forVirtualHosts(0L, ImmutableList.of(vhost), filterConfigs); + } + + private static HttpConnectionManager createHcmForRds( + String name, List filterConfigs) { + return HttpConnectionManager.forRdsName(0L, name, filterConfigs); } - private static HttpConnectionManager createRds(String name, FilterConfig filterConfig) { - return HttpConnectionManager.forRdsName(0L, name, - Arrays.asList(new NamedFilterConfig("named-config-" + name, filterConfig))); + private static HttpConnectionManager createRds(String name) { + NamedFilterConfig config = + new NamedFilterConfig(ROUTER_FILTER_INSTANCE_NAME, RouterFilter.ROUTER_CONFIG); + return createHcmForRds(name, ImmutableList.of(config)); } - private static EnvoyServerProtoData.FilterChainMatch createMatch() { - return EnvoyServerProtoData.FilterChainMatch.create( + /** + * Returns the least-specific match-all Filter Chain Match. + */ + private static FilterChainMatch createMatch() { + return FilterChainMatch.create( 0, ImmutableList.of(), ImmutableList.of(), @@ -1300,6 +1823,21 @@ private static EnvoyServerProtoData.FilterChainMatch createMatch() { ""); } + private static FilterChainMatch createMatchSrcIp(String srcCidr) { + String[] srcParts = srcCidr.split("/", 2); + InetAddress ip = InetAddresses.forString(srcParts[0]); + Integer subnetMask = Integer.valueOf(srcParts[1], 10); + return FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(CidrRange.create(ip, subnetMask)), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); + } + private static ServerRoutingConfig createRoutingConfig(String path, String domain) { return createRoutingConfig(path, domain, null); } From ca4819ac6dbd3d99a1f20c1e08625020b8a48994 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 6 Mar 2025 07:57:32 -0800 Subject: [PATCH 205/591] core: Apply ManagedChannelImpl's updateBalancingState() immediately ffcc360ba adjusted updateBalancingState() to require being run within the sync context. However, it still queued the work into the sync context, which was unnecessary. This re-entering the sync context unnecessarily delays the new state from being used. --- .../io/grpc/internal/ManagedChannelImpl.java | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 45819954473..dd4d0aef5be 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -1388,24 +1388,18 @@ public void updateBalancingState( syncContext.throwIfNotInThisSynchronizationContext(); checkNotNull(newState, "newState"); checkNotNull(newPicker, "newPicker"); - final class UpdateBalancingState implements Runnable { - @Override - public void run() { - if (LbHelperImpl.this != lbHelper || panicMode) { - return; - } - updateSubchannelPicker(newPicker); - // It's not appropriate to report SHUTDOWN state from lb. - // Ignore the case of newState == SHUTDOWN for now. - if (newState != SHUTDOWN) { - channelLogger.log( - ChannelLogLevel.INFO, "Entering {0} state with picker: {1}", newState, newPicker); - channelStateManager.gotoState(newState); - } - } - } - syncContext.execute(new UpdateBalancingState()); + if (LbHelperImpl.this != lbHelper || panicMode) { + return; + } + updateSubchannelPicker(newPicker); + // It's not appropriate to report SHUTDOWN state from lb. + // Ignore the case of newState == SHUTDOWN for now. + if (newState != SHUTDOWN) { + channelLogger.log( + ChannelLogLevel.INFO, "Entering {0} state with picker: {1}", newState, newPicker); + channelStateManager.gotoState(newState); + } } @Override From d82613a74cbecb3933ba79d569d13ddf6af71a10 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 7 Mar 2025 10:33:35 -0800 Subject: [PATCH 206/591] xds: Fix cluster selection races when updating config selector Listener2.onResult() doesn't require running in the sync context, so when called from the sync context it is guaranteed not to do its processing immediately (instead, it schedules work into the sync context). The code was doing an update dance: 1) update service config to add new cluster, 2) update config selector to use new cluster, 3) update service config to remove old clusters. But the onResult() wasn't being processed immediately, so the actual execution order was 2, 1, 3 which has a small window where RPCs will fail. But onResult2() does run immediately. And since ca4819ac6, updateBalancingState() updates the picker immediately. cleanUpRoutes() was also racy because it updated the routingConfig before swapping to the new config selector, so RPCs could fail saying there was no route instead of the useful error message. Even with the opposite order, some RPCs may be executing the while loop of selectConfig(), trying to acquire a cluster. The code unreffed the clusters before updating the routingConfig, so those RPCs could go into a tight loop until the routingConfig was updated. Also, once the routingConfig was updated to EMPTY those RPCs would similarly see the wrong error message. To give the correct error message, selectConfig() must fail such RPCs directly, and once it can do that there's no need to stop using the config selector in error cases. This has the benefit of fewer moving parts and more consistent threading among cases. The added test was able to detect the race 2% of the time. The slower the code/machine, the more reliable the test failed. ca4819ac6 along with this commit reduced it to 0 failures in 1000 runs. Discovered when investigating b/394850611 --- .../java/io/grpc/xds/XdsNameResolver.java | 53 ++++---- .../FakeControlPlaneXdsIntegrationTest.java | 118 ++++++++++++++++-- .../java/io/grpc/xds/XdsNameResolverTest.java | 90 ++++++------- 3 files changed, 177 insertions(+), 84 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 507808d718b..4b6da6ac8b8 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -132,7 +132,7 @@ final class XdsNameResolver extends NameResolver { // NamedFilterConfig.filterStateKey -> filter_instance. private final HashMap activeFilters = new HashMap<>(); - private volatile RoutingConfig routingConfig = RoutingConfig.EMPTY; + private volatile RoutingConfig routingConfig; private Listener2 listener; private ObjectPool xdsClientPool; private XdsClient xdsClient; @@ -306,7 +306,7 @@ private void updateResolutionResult() { if (logger.isLoggable(XdsLogLevel.INFO)) { logger.log( - XdsLogLevel.INFO, "Generated service config:\n{0}", new Gson().toJson(rawServiceConfig)); + XdsLogLevel.INFO, "Generated service config: {0}", new Gson().toJson(rawServiceConfig)); } ConfigOrError parsedServiceConfig = serviceConfigParser.parseServiceConfig(rawServiceConfig); Attributes attrs = @@ -320,7 +320,7 @@ private void updateResolutionResult() { .setAttributes(attrs) .setServiceConfig(parsedServiceConfig) .build(); - listener.onResult(result); + listener.onResult2(result); receivedConfig = true; } @@ -395,6 +395,9 @@ public Result selectConfig(PickSubchannelArgs args) { String path = "/" + args.getMethodDescriptor().getFullMethodName(); do { routingCfg = routingConfig; + if (routingCfg.errorStatus != null) { + return Result.forError(routingCfg.errorStatus); + } selectedRoute = null; for (RouteData route : routingCfg.routes) { if (RoutingUtils.matchRoute(route.routeMatch, path, headers, random)) { @@ -626,19 +629,6 @@ private static String prefixedClusterSpecifierPluginName(String pluginName) { return "cluster_specifier_plugin:" + pluginName; } - private static final class FailingConfigSelector extends InternalConfigSelector { - private final Result result; - - public FailingConfigSelector(Status error) { - this.result = Result.forError(error); - } - - @Override - public Result selectConfig(PickSubchannelArgs args) { - return result; - } - } - private class ResolveState implements ResourceWatcher { private final ConfigOrError emptyServiceConfig = serviceConfigParser.parseServiceConfig(Collections.emptyMap()); @@ -835,13 +825,13 @@ private void updateRoutes(List virtualHosts, long httpMaxStreamDura } } // Update service config to include newly added clusters. - if (shouldUpdateResult) { + if (shouldUpdateResult && routingConfig != null) { updateResolutionResult(); + shouldUpdateResult = false; } // Make newly added clusters selectable by config selector and deleted clusters no longer // selectable. routingConfig = new RoutingConfig(httpMaxStreamDurationNano, routesData.build()); - shouldUpdateResult = false; for (String cluster : deletedClusters) { int count = clusterRefs.get(cluster).refCount.decrementAndGet(); if (count == 0) { @@ -893,6 +883,9 @@ private ClientInterceptor createFilters( } private void cleanUpRoutes(String error) { + String errorWithNodeId = + error + ", xDS node ID: " + xdsClient.getBootstrapInfo().node().getId(); + routingConfig = new RoutingConfig(Status.UNAVAILABLE.withDescription(errorWithNodeId)); if (existingClusters != null) { for (String cluster : existingClusters) { int count = clusterRefs.get(cluster).refCount.decrementAndGet(); @@ -902,17 +895,12 @@ private void cleanUpRoutes(String error) { } existingClusters = null; } - routingConfig = RoutingConfig.EMPTY; + // Without addresses the default LB (normally pick_first) should become TRANSIENT_FAILURE, and - // the config selector handles the error message itself. Once the LB API allows providing - // failure information for addresses yet still providing a service config, the config seector - // could be avoided. - String errorWithNodeId = - error + ", xDS node ID: " + xdsClient.getBootstrapInfo().node().getId(); - listener.onResult(ResolutionResult.newBuilder() + // the config selector handles the error message itself. + listener.onResult2(ResolutionResult.newBuilder() .setAttributes(Attributes.newBuilder() - .set(InternalConfigSelector.KEY, - new FailingConfigSelector(Status.UNAVAILABLE.withDescription(errorWithNodeId))) + .set(InternalConfigSelector.KEY, configSelector) .build()) .setServiceConfig(emptyServiceConfig) .build()); @@ -983,12 +971,19 @@ public void onResourceDoesNotExist(final String resourceName) { private static class RoutingConfig { private final long fallbackTimeoutNano; final ImmutableList routes; - - private static final RoutingConfig EMPTY = new RoutingConfig(0, ImmutableList.of()); + final Status errorStatus; private RoutingConfig(long fallbackTimeoutNano, ImmutableList routes) { this.fallbackTimeoutNano = fallbackTimeoutNano; this.routes = checkNotNull(routes, "routes"); + this.errorStatus = null; + } + + private RoutingConfig(Status errorStatus) { + this.fallbackTimeoutNano = 0; + this.routes = null; + this.errorStatus = checkNotNull(errorStatus, "errorStatus"); + checkArgument(!errorStatus.isOk(), "errorStatus should not be okay"); } } diff --git a/xds/src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java b/xds/src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java index a3106bd20ae..c8f2b8932ef 100644 --- a/xds/src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java +++ b/xds/src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java @@ -19,9 +19,12 @@ import static com.google.common.truth.Truth.assertThat; import static io.grpc.xds.DataPlaneRule.ENDPOINT_HOST_NAME; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_EDS; import static org.junit.Assert.assertEquals; import com.github.xds.type.v3.TypedStruct; +import com.google.common.collect.ImmutableMap; import com.google.protobuf.Any; import com.google.protobuf.Struct; import com.google.protobuf.Value; @@ -36,11 +39,17 @@ import io.envoyproxy.envoy.config.endpoint.v3.Endpoint; import io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint; import io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints; +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.load_balancing_policies.wrr_locality.v3.WrrLocality; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; import io.grpc.ClientInterceptor; +import io.grpc.ClientStreamTracer; import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; import io.grpc.ForwardingClientCallListener; import io.grpc.LoadBalancerRegistry; @@ -89,8 +98,7 @@ public void pingPong() throws Exception { ManagedChannel channel = dataPlane.getManagedChannel(); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = SimpleServiceGrpc.newBlockingStub( channel); - SimpleRequest request = SimpleRequest.newBuilder() - .build(); + SimpleRequest request = SimpleRequest.getDefaultInstance(); SimpleResponse goldenResponse = SimpleResponse.newBuilder() .setResponseMessage("Hi, xDS! Authority= test-server") .build(); @@ -104,8 +112,7 @@ public void pingPong_edsEndpoint_authorityOverride() throws Exception { ManagedChannel channel = dataPlane.getManagedChannel(); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = SimpleServiceGrpc.newBlockingStub( channel); - SimpleRequest request = SimpleRequest.newBuilder() - .build(); + SimpleRequest request = SimpleRequest.getDefaultInstance(); SimpleResponse goldenResponse = SimpleResponse.newBuilder() .setResponseMessage("Hi, xDS! Authority= " + ENDPOINT_HOST_NAME) .build(); @@ -145,8 +152,7 @@ public void pingPong_metadataLoadBalancer() throws Exception { // We add an interceptor to catch the response headers from the server. SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = SimpleServiceGrpc.newBlockingStub( dataPlane.getManagedChannel()).withInterceptors(responseHeaderInterceptor); - SimpleRequest request = SimpleRequest.newBuilder() - .build(); + SimpleRequest request = SimpleRequest.getDefaultInstance(); SimpleResponse goldenResponse = SimpleResponse.newBuilder() .setResponseMessage("Hi, xDS! Authority= test-server") .build(); @@ -160,6 +166,100 @@ public void pingPong_metadataLoadBalancer() throws Exception { } } + // Try to trigger "UNAVAILABLE: CDS encountered error: unable to find available subchannel for + // cluster cluster:cluster1" race, if XdsNameResolver updates its ConfigSelector before + // cluster_manager config. + @Test + public void changeClusterForRoute() throws Exception { + // Start with route to cluster0 + InetSocketAddress edsInetSocketAddress + = (InetSocketAddress) dataPlane.getServer().getListenSockets().get(0); + controlPlane.getService().setXdsConfig( + ADS_TYPE_URL_EDS, + ImmutableMap.of( + "eds-service-0", + ControlPlaneRule.buildClusterLoadAssignment( + edsInetSocketAddress.getHostName(), "", edsInetSocketAddress.getPort(), + "eds-service-0"), + "eds-service-1", + ControlPlaneRule.buildClusterLoadAssignment( + edsInetSocketAddress.getHostName(), "", edsInetSocketAddress.getPort(), + "eds-service-1"))); + controlPlane.getService().setXdsConfig( + ADS_TYPE_URL_CDS, + ImmutableMap.of( + "cluster0", + ControlPlaneRule.buildCluster("cluster0", "eds-service-0"), + "cluster1", + ControlPlaneRule.buildCluster("cluster1", "eds-service-1"))); + controlPlane.setRdsConfig(RouteConfiguration.newBuilder() + .setName("route-config.googleapis.com") + .addVirtualHosts(VirtualHost.newBuilder() + .addDomains("test-server") + .addRoutes(Route.newBuilder() + .setMatch(RouteMatch.newBuilder().setPrefix("/").build()) + .setRoute(RouteAction.newBuilder().setCluster("cluster0").build()) + .build()) + .build()) + .build()); + + class ClusterClientStreamTracer extends ClientStreamTracer { + boolean usedCluster1; + + @Override + public void addOptionalLabel(String key, String value) { + if ("grpc.lb.backend_service".equals(key)) { + usedCluster1 = "cluster1".equals(value); + } + } + } + + ClusterClientStreamTracer tracer = new ClusterClientStreamTracer(); + ClientStreamTracer.Factory tracerFactory = new ClientStreamTracer.Factory() { + @Override + public ClientStreamTracer newClientStreamTracer( + ClientStreamTracer.StreamInfo info, Metadata headers) { + return tracer; + } + }; + ClientInterceptor tracerInterceptor = new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return next.newCall(method, callOptions.withStreamTracerFactory(tracerFactory)); + } + }; + SimpleServiceGrpc.SimpleServiceBlockingStub stub = SimpleServiceGrpc + .newBlockingStub(dataPlane.getManagedChannel()) + .withInterceptors(tracerInterceptor); + SimpleRequest request = SimpleRequest.getDefaultInstance(); + SimpleResponse goldenResponse = SimpleResponse.newBuilder() + .setResponseMessage("Hi, xDS! Authority= test-server") + .build(); + assertThat(stub.unaryRpc(request)).isEqualTo(goldenResponse); + assertThat(tracer.usedCluster1).isFalse(); + + // Check for errors when swapping route to cluster1 + controlPlane.setRdsConfig(RouteConfiguration.newBuilder() + .setName("route-config.googleapis.com") + .addVirtualHosts(VirtualHost.newBuilder() + .addDomains("test-server") + .addRoutes(Route.newBuilder() + .setMatch(RouteMatch.newBuilder().setPrefix("/").build()) + .setRoute(RouteAction.newBuilder().setCluster("cluster1").build()) + .build()) + .build()) + .build()); + + for (int j = 0; j < 10; j++) { + stub.unaryRpc(request); + if (tracer.usedCluster1) { + break; + } + } + assertThat(tracer.usedCluster1).isTrue(); + } + // Captures response headers from the server. private static class ResponseHeaderClientInterceptor implements ClientInterceptor { Metadata reponseHeaders; @@ -199,8 +299,7 @@ public void pingPong_ringHash() { ManagedChannel channel = dataPlane.getManagedChannel(); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = SimpleServiceGrpc.newBlockingStub( channel); - SimpleRequest request = SimpleRequest.newBuilder() - .build(); + SimpleRequest request = SimpleRequest.getDefaultInstance(); SimpleResponse goldenResponse = SimpleResponse.newBuilder() .setResponseMessage("Hi, xDS! Authority= test-server") .build(); @@ -231,8 +330,7 @@ public void pingPong_logicalDns_authorityOverride() { ManagedChannel channel = dataPlane.getManagedChannel(); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = SimpleServiceGrpc.newBlockingStub( channel); - SimpleRequest request = SimpleRequest.newBuilder() - .build(); + SimpleRequest request = SimpleRequest.getDefaultInstance(); SimpleResponse goldenResponse = SimpleResponse.newBuilder() .setResponseMessage("Hi, xDS! Authority= localhost:" + serverAddress.getPort()) .build(); diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 877fa6f88dc..4a8193c932d 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -413,7 +413,7 @@ public void resolving_ldsResourceUpdateRdsName() { Collections.singletonList(route1), ImmutableMap.of()); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster1), (Map) resolutionResultCaptor.getValue().getServiceConfig().getConfig()); @@ -432,7 +432,7 @@ public void resolving_ldsResourceUpdateRdsName() { // Two new service config updates triggered: // - with load balancing config being able to select cluster1 and cluster2 // - with load balancing config being able to select cluster2 only - verify(mockListener, times(2)).onResult(resultCaptor.capture()); + verify(mockListener, times(2)).onResult2(resultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Arrays.asList(cluster1, cluster2), (Map) resultCaptor.getAllValues().get(0).getServiceConfig().getConfig()); @@ -467,7 +467,7 @@ public void resolving_ldsResourceRevokedAndAddedBack() { Collections.singletonList(route), ImmutableMap.of()); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster1), (Map) resolutionResultCaptor.getValue().getServiceConfig().getConfig()); @@ -483,7 +483,7 @@ public void resolving_ldsResourceRevokedAndAddedBack() { verifyNoInteractions(mockListener); assertThat(xdsClient.rdsResource).isEqualTo(RDS_RESOURCE_NAME); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster1), (Map) resolutionResultCaptor.getValue().getServiceConfig().getConfig()); @@ -506,7 +506,7 @@ public void resolving_rdsResourceRevokedAndAddedBack() { Collections.singletonList(route), ImmutableMap.of()); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster1), (Map) resolutionResultCaptor.getValue().getServiceConfig().getConfig()); @@ -518,7 +518,7 @@ public void resolving_rdsResourceRevokedAndAddedBack() { // Simulate management server adds back the previously used RDS resource. reset(mockListener); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster1), (Map) resolutionResultCaptor.getValue().getServiceConfig().getConfig()); @@ -585,7 +585,7 @@ public void resolving_matchingVirtualHostNotFound_matchingOverrideAuthority() { resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate(0L, Arrays.asList(virtualHost)); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster1), (Map) resolutionResultCaptor.getValue().getServiceConfig().getConfig()); @@ -671,7 +671,7 @@ public void resolved_noTimeout() { Collections.singletonList(AUTHORITY), Collections.singletonList(route), ImmutableMap.of()); xdsClient.deliverLdsUpdate(0L, Collections.singletonList(virtualHost)); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); assertCallSelectClusterResult(call1, configSelector, cluster1, null); @@ -690,7 +690,7 @@ public void resolved_fallbackToHttpMaxStreamDurationAsTimeout() { ImmutableMap.of()); xdsClient.deliverLdsUpdate(TimeUnit.SECONDS.toNanos(5L), Collections.singletonList(virtualHost)); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); assertCallSelectClusterResult(call1, configSelector, cluster1, 5.0); @@ -719,7 +719,7 @@ public void retryPolicyInPerMethodConfigGeneratedByResolverIsValid() { retryPolicy, false), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); Result selectResult = configSelector.selectConfig( @@ -777,7 +777,7 @@ public void resolved_simpleCallFailedToRoute_routeWithNonForwardingAction() { RouteAction.forCluster(cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); assertThat(result.getAddressesOrError().getValue()).isEmpty(); assertServiceConfigForLoadBalancingConfig( @@ -814,7 +814,7 @@ public void resolved_rpcHashingByHeader_withoutSubstitution() { null, false), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); InternalConfigSelector configSelector = resolutionResultCaptor.getValue().getAttributes().get(InternalConfigSelector.KEY); @@ -850,7 +850,7 @@ public void resolved_rpcHashingByHeader_withSubstitution() { null, false), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); InternalConfigSelector configSelector = resolutionResultCaptor.getValue().getAttributes().get(InternalConfigSelector.KEY); @@ -890,7 +890,7 @@ public void resolved_rpcHashingByChannelId() { null, false), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); InternalConfigSelector configSelector = resolutionResultCaptor.getValue().getAttributes().get(InternalConfigSelector.KEY); @@ -928,7 +928,7 @@ public void resolved_rpcHashingByChannelId() { null, false), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); configSelector = resolutionResultCaptor.getValue().getAttributes().get( InternalConfigSelector.KEY); @@ -962,7 +962,7 @@ public void resolved_routeActionHasAutoHostRewrite_emitsCallOptionForTheSame() { null, true), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); InternalConfigSelector configSelector = resolutionResultCaptor.getValue().getAttributes().get(InternalConfigSelector.KEY); @@ -993,7 +993,7 @@ public void resolved_routeActionNoAutoHostRewrite_doesntEmitCallOptionForTheSame null, false), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); InternalConfigSelector configSelector = resolutionResultCaptor.getValue().getAttributes().get(InternalConfigSelector.KEY); @@ -1027,7 +1027,7 @@ public void resolved_resourceUpdateAfterCallStarted() { cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); // Updated service config still contains cluster1 while it is removed resource. New calls no // longer routed to cluster1. @@ -1039,7 +1039,7 @@ public void resolved_resourceUpdateAfterCallStarted() { assertCallSelectClusterResult(call1, configSelector, "another-cluster", 20.0); firstCall.deliverErrorStatus(); // completes previous call - verify(mockListener, times(2)).onResult(resolutionResultCaptor.capture()); + verify(mockListener, times(2)).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); assertServiceConfigForLoadBalancingConfig( Arrays.asList(cluster2, "another-cluster"), @@ -1069,7 +1069,7 @@ public void resolved_resourceUpdatedBeforeCallStarted() { ImmutableMap.of()))); // Two consecutive service config updates: one for removing clcuster1, // one for adding "another=cluster". - verify(mockListener, times(2)).onResult(resolutionResultCaptor.capture()); + verify(mockListener, times(2)).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); assertServiceConfigForLoadBalancingConfig( Arrays.asList(cluster2, "another-cluster"), @@ -1104,7 +1104,7 @@ public void resolved_raceBetweenCallAndRepeatedResourceUpdate() { TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); assertServiceConfigForLoadBalancingConfig( Arrays.asList(cluster1, cluster2, "another-cluster"), @@ -1179,7 +1179,7 @@ public void resolved_simpleCallSucceeds_routeToWeightedCluster() { TimeUnit.SECONDS.toNanos(20L), null, false), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); assertThat(result.getAddressesOrError().getValue()).isEmpty(); assertServiceConfigForLoadBalancingConfig( @@ -1208,7 +1208,7 @@ public void resolved_simpleCallSucceeds_routeToRls() { TimeUnit.SECONDS.toNanos(20L), null, false), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); assertThat(result.getAddressesOrError().getValue()).isEmpty(); @SuppressWarnings("unchecked") @@ -1256,7 +1256,7 @@ public void resolved_simpleCallSucceeds_routeToRls() { TimeUnit.SECONDS.toNanos(30L), null, false), ImmutableMap.of()))); - verify(mockListener, times(2)).onResult(resolutionResultCaptor.capture()); + verify(mockListener, times(2)).onResult2(resolutionResultCaptor.capture()); ResolutionResult result2 = resolutionResultCaptor.getValue(); @SuppressWarnings("unchecked") Map resultServiceConfig2 = (Map) result2.getServiceConfig().getConfig(); @@ -1599,7 +1599,7 @@ private VirtualHost filterStateTestVhost(ImmutableMap perR @SuppressWarnings("unchecked") private void assertEmptyResolutionResult(String resource) { - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); assertThat(result.getAddressesOrError().getValue()).isEmpty(); assertThat((Map) result.getServiceConfig().getConfig()).isEmpty(); @@ -1611,7 +1611,7 @@ private void assertEmptyResolutionResult(String resource) { } private void assertClusterResolutionResult(CallInfo call, String expectedCluster) { - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); assertCallSelectClusterResult(call, configSelector, expectedCluster, null); @@ -1685,7 +1685,7 @@ private InternalConfigSelector resolveToClusters() { cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()))); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); assertThat(result.getAddressesOrError().getValue()).isEmpty(); assertServiceConfigForLoadBalancingConfig( @@ -1763,7 +1763,7 @@ public void generateServiceConfig_forClusterManagerLoadBalancingConfig() throws ImmutableMap.of()); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); String expectedServiceConfigJson = "{\n" + " \"loadBalancingConfig\": [{\n" @@ -1946,7 +1946,7 @@ public void resolved_faultAbortInLdsUpdate() { FaultAbort.forHeader(FaultConfig.FractionalPercent.perHundred(70)), null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); // no header abort key provided in metadata, rpc should succeed @@ -1985,7 +1985,7 @@ public void resolved_faultAbortInLdsUpdate() { FaultAbort.forHeader(FaultConfig.FractionalPercent.perMillion(600_000)), null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2001,7 +2001,7 @@ public void resolved_faultAbortInLdsUpdate() { FaultAbort.forHeader(FaultConfig.FractionalPercent.perMillion(0)), null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2016,7 +2016,7 @@ public void resolved_faultAbortInLdsUpdate() { FaultConfig.FractionalPercent.perMillion(600_000)), null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2034,7 +2034,7 @@ public void resolved_faultAbortInLdsUpdate() { FaultConfig.FractionalPercent.perMillion(400_000)), null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2052,7 +2052,7 @@ public void resolved_faultDelayInLdsUpdate() { FaultConfig httpFilterFaultConfig = FaultConfig.create( FaultDelay.forHeader(FaultConfig.FractionalPercent.perHundred(70)), null, null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); // no header delay key provided in metadata, rpc should succeed immediately @@ -2069,7 +2069,7 @@ public void resolved_faultDelayInLdsUpdate() { httpFilterFaultConfig = FaultConfig.create( FaultDelay.forHeader(FaultConfig.FractionalPercent.perMillion(600_000)), null, null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2080,7 +2080,7 @@ public void resolved_faultDelayInLdsUpdate() { httpFilterFaultConfig = FaultConfig.create( FaultDelay.forHeader(FaultConfig.FractionalPercent.perMillion(0)), null, null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2093,7 +2093,7 @@ public void resolved_faultDelayInLdsUpdate() { null, null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2106,7 +2106,7 @@ public void resolved_faultDelayInLdsUpdate() { null, null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2125,7 +2125,7 @@ public void resolved_faultDelayWithMaxActiveStreamsInLdsUpdate() { null, /* maxActiveFaults= */ 1); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); @@ -2155,7 +2155,7 @@ public void resolved_faultDelayInLdsUpdate_callWithEarlyDeadline() { null, null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); @@ -2187,7 +2187,7 @@ public void resolved_faultAbortAndDelayInLdsUpdateInLdsUpdate() { FaultConfig.FractionalPercent.perMillion(1000_000)), null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); ClientCall.Listener observer = startNewCall(TestMethodDescriptors.voidMethod(), @@ -2216,7 +2216,7 @@ public void resolved_faultConfigOverrideInLdsUpdate() { null); xdsClient.deliverLdsUpdateWithFaultInjection( cluster1, httpFilterFaultConfig, virtualHostFaultConfig, null, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); ClientCall.Listener observer = startNewCall(TestMethodDescriptors.voidMethod(), @@ -2231,7 +2231,7 @@ public void resolved_faultConfigOverrideInLdsUpdate() { null); xdsClient.deliverLdsUpdateWithFaultInjection( cluster1, httpFilterFaultConfig, virtualHostFaultConfig, routeFaultConfig, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2248,7 +2248,7 @@ public void resolved_faultConfigOverrideInLdsUpdate() { xdsClient.deliverLdsUpdateWithFaultInjection( cluster1, httpFilterFaultConfig, virtualHostFaultConfig, routeFaultConfig, weightedClusterFaultConfig); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2277,7 +2277,7 @@ public void resolved_faultConfigOverrideInLdsAndInRdsUpdate() { FaultAbort.forStatus(Status.UNKNOWN, FaultConfig.FractionalPercent.perMillion(1000_000)), null); xdsClient.deliverRdsUpdateWithFaultInjection(RDS_RESOURCE_NAME, null, routeFaultConfig, null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); ClientCall.Listener observer = startNewCall(TestMethodDescriptors.voidMethod(), From f3f054a0a4933aeb726c0d6995bcdb30fc0ea85a Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 5 Mar 2025 13:27:48 -0800 Subject: [PATCH 207/591] xds: Log cluster_manager config update before applying config It is confusing/harder to read the logs when the activations/deactivations because of the config happen before the log entry describing the new config. --- .../main/java/io/grpc/xds/ClusterManagerLoadBalancer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ClusterManagerLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterManagerLoadBalancer.java index 2573a7293d3..22b5aaa7d73 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterManagerLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterManagerLoadBalancer.java @@ -81,6 +81,9 @@ protected Map createChildAddressesMap( ClusterManagerConfig config = (ClusterManagerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); + logger.log( + XdsLogLevel.INFO, + "Received cluster_manager lb config: child names={0}", config.childPolicies.keySet()); Map childAddresses = new HashMap<>(); // Reactivate children with config; deactivate children without config @@ -108,9 +111,6 @@ protected Map createChildAddressesMap( .build(); childAddresses.put(childPolicy.getKey(), addresses); } - logger.log( - XdsLogLevel.INFO, - "Received cluster_manager lb config: child names={0}", config.childPolicies.keySet()); return childAddresses; } From 61a110d9628349a465ccbfe49477cc7525a41d06 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Mon, 10 Mar 2025 07:20:20 +0200 Subject: [PATCH 208/591] examples: Update in-process sources in examples (#11952) Update in-process sources location in examples since they have been migrated from core artifacts. --- examples/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/README.md b/examples/README.md index a07d1b38a14..91fde2c045c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -153,9 +153,9 @@ Example bugs not caught by mocked stub tests include: For testing a gRPC client, create the client with a real stub using an -[InProcessChannel](../core/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java), +[InProcessChannel](../inprocess/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java), and test it against an -[InProcessServer](../core/src/main/java/io/grpc/inprocess/InProcessServerBuilder.java) +[InProcessServer](../inprocess/src/main/java/io/grpc/inprocess/InProcessServerBuilder.java) with a mock/fake service implementation. For testing a gRPC server, create the server as an InProcessServer, From 24b9f6ff0d0498a3136fd51c505e699ee1f5a940 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 10 Mar 2025 07:03:45 +0000 Subject: [PATCH 209/591] Update psm-dualstack.cfg (#11950) 120 minutes has not been sufficient, causing frequent VM timeout errors in the test runs: https://testgrid.corp.google.com/grpc-psm-java#v1.67.x&width=20&graph-metrics=test-duration-minutes&include-filter-by-regex=psm-dualstack$ --- buildscripts/kokoro/psm-dualstack.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildscripts/kokoro/psm-dualstack.cfg b/buildscripts/kokoro/psm-dualstack.cfg index 55c906bc4ec..a55d91a95b0 100644 --- a/buildscripts/kokoro/psm-dualstack.cfg +++ b/buildscripts/kokoro/psm-dualstack.cfg @@ -2,7 +2,7 @@ # Location of the continuous shell script in repository. build_file: "grpc-java/buildscripts/kokoro/psm-interop-test-java.sh" -timeout_mins: 120 +timeout_mins: 240 action { define_artifacts { From 4933cddd0002d2e2e3387cbd30a54965356664ca Mon Sep 17 00:00:00 2001 From: Arjan Singh Bal <46515553+arjan-bal@users.noreply.github.com> Date: Tue, 11 Mar 2025 16:05:05 +0530 Subject: [PATCH 210/591] Fix typo in dualstack example (#11916) --- examples/example-dualstack/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/example-dualstack/README.md b/examples/example-dualstack/README.md index 6c191661d1b..5a26886e259 100644 --- a/examples/example-dualstack/README.md +++ b/examples/example-dualstack/README.md @@ -2,7 +2,7 @@ The dualstack example uses a custom name resolver that provides both IPv4 and IPv6 localhost endpoints for each of 3 server instances. The client will first use the default name resolver and -load balancers which will only connect tot he first server. It will then use the +load balancers which will only connect to the first server. It will then use the custom name resolver with round robin to connect to each of the servers in turn. The 3 instances of the server will bind respectively to: both IPv4 and IPv6, IPv4 only, and IPv6 only. From 21915575824a0f88fe468a87843977db0c9ba7fa Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 11 Mar 2025 10:35:39 +0000 Subject: [PATCH 211/591] Update README etc to reference 1.71.0 (#11940) --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c9f68d4ccb9..756519d0ad0 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.70.0/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.70.0/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.71.0/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.71.0/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.70.0 + 1.71.0 runtime io.grpc grpc-protobuf - 1.70.0 + 1.71.0 io.grpc grpc-stub - 1.70.0 + 1.71.0 org.apache.tomcat @@ -79,18 +79,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.70.0' -implementation 'io.grpc:grpc-protobuf:1.70.0' -implementation 'io.grpc:grpc-stub:1.70.0' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.71.0' +implementation 'io.grpc:grpc-protobuf:1.71.0' +implementation 'io.grpc:grpc-stub:1.71.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.70.0' -implementation 'io.grpc:grpc-protobuf-lite:1.70.0' -implementation 'io.grpc:grpc-stub:1.70.0' +implementation 'io.grpc:grpc-okhttp:1.71.0' +implementation 'io.grpc:grpc-protobuf-lite:1.71.0' +implementation 'io.grpc:grpc-stub:1.71.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` @@ -99,7 +99,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.70.0 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.71.0 Development snapshots are available in [Sonatypes's snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/). @@ -131,7 +131,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.70.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.71.0:exe:${os.detected.classifier} @@ -161,7 +161,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.71.0' } } generateProtoTasks { @@ -194,7 +194,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.71.0' } } generateProtoTasks { From 2f52a00364d5a267df259ecd1c34e7bdd55fbe0f Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 11 Mar 2025 22:39:54 +0530 Subject: [PATCH 212/591] netty: Swap to UniformStreamByteDistributor (#11954) --- netty/src/main/java/io/grpc/netty/AbstractNettyHandler.java | 1 + netty/src/main/java/io/grpc/netty/NettyClientHandler.java | 6 +++--- netty/src/main/java/io/grpc/netty/NettyServerHandler.java | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/AbstractNettyHandler.java b/netty/src/main/java/io/grpc/netty/AbstractNettyHandler.java index 6743b4fd5f7..c4ec5913cde 100644 --- a/netty/src/main/java/io/grpc/netty/AbstractNettyHandler.java +++ b/netty/src/main/java/io/grpc/netty/AbstractNettyHandler.java @@ -50,6 +50,7 @@ abstract class AbstractNettyHandler extends GrpcHttp2ConnectionHandler { private final Ticker ticker; private static final long BDP_MEASUREMENT_PING = 1234; + protected static final int MIN_ALLOCATED_CHUNK = 16 * 1024; AbstractNettyHandler( ChannelPromise channelUnused, diff --git a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java index 5a93dbc982b..19f1903c0b5 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java @@ -78,7 +78,7 @@ import io.netty.handler.codec.http2.Http2Stream; import io.netty.handler.codec.http2.Http2StreamVisitor; import io.netty.handler.codec.http2.StreamBufferingEncoder; -import io.netty.handler.codec.http2.WeightedFairQueueByteDistributor; +import io.netty.handler.codec.http2.UniformStreamByteDistributor; import io.netty.handler.logging.LogLevel; import io.perfmark.PerfMark; import io.perfmark.Tag; @@ -169,8 +169,8 @@ static NettyClientHandler newHandler( Http2HeadersEncoder.NEVER_SENSITIVE, false, 16, Integer.MAX_VALUE); Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(encoder); Http2Connection connection = new DefaultHttp2Connection(false); - WeightedFairQueueByteDistributor dist = new WeightedFairQueueByteDistributor(connection); - dist.allocationQuantum(16 * 1024); // Make benchmarks fast again. + UniformStreamByteDistributor dist = new UniformStreamByteDistributor(connection); + dist.minAllocationChunk(MIN_ALLOCATED_CHUNK); // Increased for benchmarks performance. DefaultHttp2RemoteFlowController controller = new DefaultHttp2RemoteFlowController(connection, dist); connection.remote().flowController(controller); diff --git a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java index 85a4886b766..668c3b5c90c 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java @@ -87,7 +87,7 @@ import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Stream; import io.netty.handler.codec.http2.Http2StreamVisitor; -import io.netty.handler.codec.http2.WeightedFairQueueByteDistributor; +import io.netty.handler.codec.http2.UniformStreamByteDistributor; import io.netty.handler.logging.LogLevel; import io.netty.util.AsciiString; import io.netty.util.ReferenceCountUtil; @@ -243,8 +243,8 @@ static NettyServerHandler newHandler( maxMessageSize); final Http2Connection connection = new DefaultHttp2Connection(true); - WeightedFairQueueByteDistributor dist = new WeightedFairQueueByteDistributor(connection); - dist.allocationQuantum(16 * 1024); // Make benchmarks fast again. + UniformStreamByteDistributor dist = new UniformStreamByteDistributor(connection); + dist.minAllocationChunk(MIN_ALLOCATED_CHUNK); // Increased for benchmarks performance. DefaultHttp2RemoteFlowController controller = new DefaultHttp2RemoteFlowController(connection, dist); connection.remote().flowController(controller); From fca1d3cf433beccd0741fc3d1fe1d09e8b1374e7 Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:39:49 +0200 Subject: [PATCH 213/591] servlet: set description for CANCELLED status (#11927) --- .../src/main/java/io/grpc/servlet/ServletServerStream.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java b/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java index bc367587cc6..5dd20567c08 100644 --- a/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java +++ b/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java @@ -297,7 +297,9 @@ public void cancel(Status status) { } transportState.runOnTransportThread(() -> transportState.transportReportStatus(status)); // There is no way to RST_STREAM with CANCEL code, so write trailers instead - close(Status.CANCELLED.withCause(status.asRuntimeException()), new Metadata()); + close(Status.CANCELLED.withDescription("Servlet stream cancelled") + .withCause(status.asRuntimeException()), + new Metadata()); CountDownLatch countDownLatch = new CountDownLatch(1); transportState.runOnTransportThread(() -> { asyncCtx.complete(); From b69bd64ce798a9d42bec4720a568147a3af5ec51 Mon Sep 17 00:00:00 2001 From: Dennis Shao <67387070+shaod2@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:16:28 -0400 Subject: [PATCH 214/591] Populate the pb::java feature extension to gprc proto plugin (#11885) Populate the pb::java feature extension to the protoc plugins that require Protobuf Java feature resolution for the edition. --- compiler/src/java_plugin/cpp/java_plugin.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/compiler/src/java_plugin/cpp/java_plugin.cpp b/compiler/src/java_plugin/cpp/java_plugin.cpp index c3aec58ed8e..6b7cc03d486 100644 --- a/compiler/src/java_plugin/cpp/java_plugin.cpp +++ b/compiler/src/java_plugin/cpp/java_plugin.cpp @@ -23,6 +23,9 @@ #include "java_generator.h" #include +#if GOOGLE_PROTOBUF_VERSION >= 5027000 +#include +#endif #include #include #include @@ -57,6 +60,10 @@ class JavaGrpcGenerator : public protobuf::compiler::CodeGenerator { protobuf::Edition GetMaximumEdition() const override { return protobuf::Edition::EDITION_2023; } + std::vector GetFeatureExtensions() + const override { + return {GetExtensionReflection(pb::java)}; + } #else uint64_t GetSupportedFeatures() const override { return Feature::FEATURE_PROTO3_OPTIONAL; From e388ef3975ead0d549947bf75da8cd53d675a632 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 18 Mar 2025 18:43:03 +0530 Subject: [PATCH 215/591] documentation: upgrade to junit 4.13.2 (#11967) --- documentation/server-reflection-tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/server-reflection-tutorial.md b/documentation/server-reflection-tutorial.md index c4d2e8c4aea..f452174738a 100644 --- a/documentation/server-reflection-tutorial.md +++ b/documentation/server-reflection-tutorial.md @@ -28,7 +28,7 @@ need to make the following changes: + compile "io.grpc:grpc-services:${grpcVersion}" compile "io.grpc:grpc-stub:${grpcVersion}" - testCompile "junit:junit:4.12" + testCompile "junit:junit:4.13.2" --- a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java +++ b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java @@ -33,6 +33,7 @@ package io.grpc.examples.helloworld; From e80c19745589730941003ed2f46d29c64ca53bb6 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 18 Mar 2025 14:05:01 -0700 Subject: [PATCH 216/591] xds: Use XdsDependencyManager for XdsNameResolver Contributes to the gRFC A74 effort. https://github.com/grpc/proposal/blob/master/A74-xds-config-tears.md The alternative to using Mockito's ArgumentMatcher is to use Hamcrest. However, Hamcrest did not impress me. ArgumentMatcher is trivial if you don't care about the error message. This fixes a pre-existing issue where ConfigSelector.releaseCluster could revert the LB config back to using cluster manager after releasing all RPCs using a cluster have committed. Co-authored-by: Larry Safran --- .../java/io/grpc/StatusMatcher.java | 118 +++++ .../java/io/grpc/StatusOrMatcher.java | 66 +++ .../main/java/io/grpc/xds/XdsAttributes.java | 16 + xds/src/main/java/io/grpc/xds/XdsConfig.java | 16 +- .../io/grpc/xds/XdsDependencyManager.java | 430 +++++++++--------- .../java/io/grpc/xds/XdsNameResolver.java | 196 +++----- .../io/grpc/xds/XdsNameResolverProvider.java | 2 +- .../io/grpc/xds/XdsDependencyManagerTest.java | 405 ++++++++--------- .../java/io/grpc/xds/XdsNameResolverTest.java | 252 +++++++--- .../test/java/io/grpc/xds/XdsTestUtils.java | 26 +- 10 files changed, 878 insertions(+), 649 deletions(-) create mode 100644 api/src/testFixtures/java/io/grpc/StatusMatcher.java create mode 100644 api/src/testFixtures/java/io/grpc/StatusOrMatcher.java diff --git a/api/src/testFixtures/java/io/grpc/StatusMatcher.java b/api/src/testFixtures/java/io/grpc/StatusMatcher.java new file mode 100644 index 00000000000..f464b2d709d --- /dev/null +++ b/api/src/testFixtures/java/io/grpc/StatusMatcher.java @@ -0,0 +1,118 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import org.mockito.ArgumentMatcher; + +/** + * Mockito matcher for {@link Status}. + */ +public final class StatusMatcher implements ArgumentMatcher { + public static StatusMatcher statusHasCode(ArgumentMatcher codeMatcher) { + return new StatusMatcher(codeMatcher, null); + } + + public static StatusMatcher statusHasCode(Status.Code code) { + return statusHasCode(new EqualsMatcher<>(code)); + } + + private final ArgumentMatcher codeMatcher; + private final ArgumentMatcher descriptionMatcher; + + private StatusMatcher( + ArgumentMatcher codeMatcher, + ArgumentMatcher descriptionMatcher) { + this.codeMatcher = checkNotNull(codeMatcher, "codeMatcher"); + this.descriptionMatcher = descriptionMatcher; + } + + public StatusMatcher andDescription(ArgumentMatcher descriptionMatcher) { + checkState(this.descriptionMatcher == null, "Already has a description matcher"); + return new StatusMatcher(codeMatcher, descriptionMatcher); + } + + public StatusMatcher andDescription(String description) { + return andDescription(new EqualsMatcher<>(description)); + } + + public StatusMatcher andDescriptionContains(String substring) { + return andDescription(new StringContainsMatcher(substring)); + } + + @Override + public boolean matches(Status status) { + return status != null + && codeMatcher.matches(status.getCode()) + && (descriptionMatcher == null || descriptionMatcher.matches(status.getDescription())); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{code="); + sb.append(codeMatcher); + if (descriptionMatcher != null) { + sb.append(", description="); + sb.append(descriptionMatcher); + } + sb.append("}"); + return sb.toString(); + } + + // Use instead of lambda for better error message. + static final class EqualsMatcher implements ArgumentMatcher { + private final T obj; + + EqualsMatcher(T obj) { + this.obj = checkNotNull(obj, "obj"); + } + + @Override + public boolean matches(Object other) { + return obj.equals(other); + } + + @Override + public String toString() { + return obj.toString(); + } + } + + static final class StringContainsMatcher implements ArgumentMatcher { + private final String needle; + + StringContainsMatcher(String needle) { + this.needle = checkNotNull(needle, "needle"); + } + + @Override + public boolean matches(String haystack) { + if (haystack == null) { + return false; + } + return haystack.contains(needle); + } + + @Override + public String toString() { + return "contains " + needle; + } + } +} diff --git a/api/src/testFixtures/java/io/grpc/StatusOrMatcher.java b/api/src/testFixtures/java/io/grpc/StatusOrMatcher.java new file mode 100644 index 00000000000..1e70ae97853 --- /dev/null +++ b/api/src/testFixtures/java/io/grpc/StatusOrMatcher.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import static com.google.common.base.Preconditions.checkNotNull; + +import org.mockito.ArgumentMatcher; + +/** + * Mockito matcher for {@link StatusOr}. + */ +public final class StatusOrMatcher implements ArgumentMatcher> { + public static StatusOrMatcher hasValue(ArgumentMatcher valueMatcher) { + return new StatusOrMatcher(checkNotNull(valueMatcher, "valueMatcher"), null); + } + + public static StatusOrMatcher hasStatus(ArgumentMatcher statusMatcher) { + return new StatusOrMatcher(null, checkNotNull(statusMatcher, "statusMatcher")); + } + + private final ArgumentMatcher valueMatcher; + private final ArgumentMatcher statusMatcher; + + private StatusOrMatcher(ArgumentMatcher valueMatcher, ArgumentMatcher statusMatcher) { + this.valueMatcher = valueMatcher; + this.statusMatcher = statusMatcher; + } + + @Override + public boolean matches(StatusOr statusOr) { + if (statusOr == null) { + return false; + } + if (statusOr.hasValue() != (valueMatcher != null)) { + return false; + } + if (valueMatcher != null) { + return valueMatcher.matches(statusOr.getValue()); + } else { + return statusMatcher.matches(statusOr.getStatus()); + } + } + + @Override + public String toString() { + if (valueMatcher != null) { + return "{value=" + valueMatcher + "}"; + } else { + return "{status=" + statusMatcher + "}"; + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/XdsAttributes.java b/xds/src/main/java/io/grpc/xds/XdsAttributes.java index af8139d8ff4..4a64fdb1453 100644 --- a/xds/src/main/java/io/grpc/xds/XdsAttributes.java +++ b/xds/src/main/java/io/grpc/xds/XdsAttributes.java @@ -36,6 +36,22 @@ final class XdsAttributes { static final Attributes.Key> XDS_CLIENT_POOL = Attributes.Key.create("io.grpc.xds.XdsAttributes.xdsClientPool"); + /** + * Attribute key for passing around the latest XdsConfig across NameResolver/LoadBalancers. + */ + @NameResolver.ResolutionResultAttr + static final Attributes.Key XDS_CONFIG = + Attributes.Key.create("io.grpc.xds.XdsAttributes.xdsConfig"); + + + /** + * Attribute key for passing around the XdsDependencyManager across NameResolver/LoadBalancers. + */ + @NameResolver.ResolutionResultAttr + static final Attributes.Key + XDS_CLUSTER_SUBSCRIPT_REGISTRY = + Attributes.Key.create("io.grpc.xds.XdsAttributes.xdsConfig.XdsClusterSubscriptionRegistry"); + /** * Attribute key for obtaining the global provider that provides atomics for aggregating * outstanding RPCs sent to each cluster. diff --git a/xds/src/main/java/io/grpc/xds/XdsConfig.java b/xds/src/main/java/io/grpc/xds/XdsConfig.java index 7af03caf4be..ec8f3dc076d 100644 --- a/xds/src/main/java/io/grpc/xds/XdsConfig.java +++ b/xds/src/main/java/io/grpc/xds/XdsConfig.java @@ -26,9 +26,9 @@ import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import java.io.Closeable; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; /** * Represents the xDS configuration tree for a specified Listener. @@ -178,13 +178,22 @@ public boolean equals(Object obj) { public StatusOr getEndpoint() { return endpoint; } + + @Override + public String toString() { + if (endpoint.hasValue()) { + return "EndpointConfig{endpoint=" + endpoint.getValue() + "}"; + } else { + return "EndpointConfig{error=" + endpoint.getStatus() + "}"; + } + } } // The list of leaf clusters for an aggregate cluster. static final class AggregateConfig implements ClusterChild { - private final List leafNames; + private final Set leafNames; - public AggregateConfig(List leafNames) { + public AggregateConfig(Set leafNames) { this.leafNames = checkNotNull(leafNames, "leafNames"); } @@ -234,6 +243,7 @@ XdsConfigBuilder setVirtualHost(VirtualHost virtualHost) { XdsConfig build() { checkNotNull(listener, "listener"); checkNotNull(route, "route"); + checkNotNull(virtualHost, "virtualHost"); return new XdsConfig(listener, route, clusters, virtualHost); } } diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index 7eff0c549e2..8cd3119727d 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -22,9 +22,9 @@ import static io.grpc.xds.client.XdsLogger.XdsLogLevel.DEBUG; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import io.grpc.InternalLogId; +import io.grpc.NameResolver; import io.grpc.Status; import io.grpc.StatusOr; import io.grpc.SynchronizationContext; @@ -39,7 +39,6 @@ import io.grpc.xds.client.XdsResourceType; import java.io.Closeable; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -47,6 +46,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -67,25 +67,28 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi private final InternalLogId logId; private final XdsLogger logger; - private XdsConfig lastXdsConfig = null; + private StatusOr lastUpdate = null; private final Map, TypeWatchers> resourceWatchers = new HashMap<>(); XdsDependencyManager(XdsClient xdsClient, XdsConfigWatcher xdsConfigWatcher, SynchronizationContext syncContext, String dataPlaneAuthority, - String listenerName) { + String listenerName, NameResolver.Args nameResolverArgs, + ScheduledExecutorService scheduler) { logId = InternalLogId.allocate("xds-dependency-manager", listenerName); logger = XdsLogger.withLogId(logId); this.xdsClient = checkNotNull(xdsClient, "xdsClient"); this.xdsConfigWatcher = checkNotNull(xdsConfigWatcher, "xdsConfigWatcher"); this.syncContext = checkNotNull(syncContext, "syncContext"); this.dataPlaneAuthority = checkNotNull(dataPlaneAuthority, "dataPlaneAuthority"); + checkNotNull(nameResolverArgs, "nameResolverArgs"); + checkNotNull(scheduler, "scheduler"); // start the ball rolling syncContext.execute(() -> addWatcher(new LdsWatcher(listenerName))); } - public static String toContextStr(String typeName, String resourceName) { - return typeName + " resource: " + resourceName; + public static String toContextStr(String typeName, String resourceName) { + return typeName + " resource " + resourceName; } @Override @@ -225,7 +228,7 @@ private void cancelClusterWatcherTree(CdsWatcher root, Object parentContext) { XdsClusterResource.CdsUpdate cdsUpdate = root.getData().getValue(); switch (cdsUpdate.clusterType()) { case EDS: - String edsServiceName = cdsUpdate.edsServiceName(); + String edsServiceName = root.getEdsServiceName(); EdsWatcher edsWatcher = (EdsWatcher) resourceWatchers.get(ENDPOINT_RESOURCE).watchers.get(edsServiceName); cancelEdsWatcher(edsWatcher, root); @@ -240,7 +243,7 @@ private void cancelClusterWatcherTree(CdsWatcher root, Object parentContext) { } break; case LOGICAL_DNS: - // no eds needed + // no eds needed, so everything happens in cancelCdsWatcher() break; default: throw new AssertionError("Unknown cluster type: " + cdsUpdate.clusterType()); @@ -260,54 +263,61 @@ private void maybePublishConfig() { return; } - XdsConfig newConfig = buildConfig(); - if (Objects.equals(newConfig, lastXdsConfig)) { + StatusOr newUpdate = buildUpdate(); + if (Objects.equals(newUpdate, lastUpdate)) { return; } - lastXdsConfig = newConfig; - xdsConfigWatcher.onUpdate(lastXdsConfig); + assert newUpdate.hasValue() + || (newUpdate.getStatus().getCode() == Status.Code.UNAVAILABLE + || newUpdate.getStatus().getCode() == Status.Code.INTERNAL); + lastUpdate = newUpdate; + xdsConfigWatcher.onUpdate(lastUpdate); } @VisibleForTesting - XdsConfig buildConfig() { + StatusOr buildUpdate() { XdsConfig.XdsConfigBuilder builder = new XdsConfig.XdsConfigBuilder(); // Iterate watchers and build the XdsConfig // Will only be 1 listener and 1 route resource - VirtualHost activeVirtualHost = getActiveVirtualHost(); - for (XdsWatcherBase xdsWatcherBase : - resourceWatchers.get(XdsListenerResource.getInstance()).watchers.values()) { - XdsListenerResource.LdsUpdate ldsUpdate = ((LdsWatcher) xdsWatcherBase).getData().getValue(); - builder.setListener(ldsUpdate); - if (activeVirtualHost == null) { - activeVirtualHost = RoutingUtils.findVirtualHostForHostName( - ldsUpdate.httpConnectionManager().virtualHosts(), dataPlaneAuthority); - } - - if (ldsUpdate.httpConnectionManager() != null - && ldsUpdate.httpConnectionManager().virtualHosts() != null) { - RdsUpdate rdsUpdate = new RdsUpdate(ldsUpdate.httpConnectionManager().virtualHosts()); - builder.setRoute(rdsUpdate); + RdsUpdateSupplier routeSource = null; + for (XdsWatcherBase ldsWatcher : + getWatchers(XdsListenerResource.getInstance()).values()) { + if (!ldsWatcher.getData().hasValue()) { + return StatusOr.fromStatus(ldsWatcher.getData().getStatus()); } + XdsListenerResource.LdsUpdate ldsUpdate = ldsWatcher.getData().getValue(); + builder.setListener(ldsUpdate); + routeSource = ((LdsWatcher) ldsWatcher).getRouteSource(); } - resourceWatchers.get(XdsRouteConfigureResource.getInstance()).watchers.values().stream() - .map(watcher -> (RdsWatcher) watcher) - .forEach(watcher -> builder.setRoute(watcher.getData().getValue())); + StatusOr statusOrRdsUpdate = routeSource.getRdsUpdate(); + if (!statusOrRdsUpdate.hasValue()) { + return StatusOr.fromStatus(statusOrRdsUpdate.getStatus()); + } + RdsUpdate rdsUpdate = statusOrRdsUpdate.getValue(); + builder.setRoute(rdsUpdate); + VirtualHost activeVirtualHost = + RoutingUtils.findVirtualHostForHostName(rdsUpdate.virtualHosts, dataPlaneAuthority); + if (activeVirtualHost == null) { + String error = "Failed to find virtual host matching hostname: " + dataPlaneAuthority; + return StatusOr.fromStatus(Status.UNAVAILABLE.withDescription(error)); + } builder.setVirtualHost(activeVirtualHost); - Map> edsWatchers = - resourceWatchers.get(ENDPOINT_RESOURCE).watchers; - Map> cdsWatchers = - resourceWatchers.get(CLUSTER_RESOURCE).watchers; + Map> edsWatchers = + getWatchers(ENDPOINT_RESOURCE); + Map> cdsWatchers = + getWatchers(CLUSTER_RESOURCE); // Only care about aggregates from LDS/RDS or subscriptions and the leaf clusters List topLevelClusters = cdsWatchers.values().stream() .filter(XdsDependencyManager::isTopLevelCluster) - .map(w -> w.resourceName()) + .map(XdsWatcherBase::resourceName) + .distinct() .collect(Collectors.toList()); // Flatten multi-level aggregates into lists of leaf clusters @@ -316,43 +326,60 @@ XdsConfig buildConfig() { addLeavesToBuilder(builder, edsWatchers, leafNames); - return builder.build(); + return StatusOr.fromValue(builder.build()); } - private void addLeavesToBuilder(XdsConfig.XdsConfigBuilder builder, - Map> edsWatchers, - Set leafNames) { + private Map> getWatchers( + XdsResourceType resourceType) { + TypeWatchers typeWatchers = resourceWatchers.get(resourceType); + if (typeWatchers == null) { + return Collections.emptyMap(); + } + assert typeWatchers.resourceType == resourceType; + @SuppressWarnings("unchecked") + TypeWatchers tTypeWatchers = (TypeWatchers) typeWatchers; + return tTypeWatchers.watchers; + } + + private void addLeavesToBuilder( + XdsConfig.XdsConfigBuilder builder, + Map> edsWatchers, + Set leafNames) { for (String clusterName : leafNames) { CdsWatcher cdsWatcher = getCluster(clusterName); StatusOr cdsUpdateOr = cdsWatcher.getData(); - if (cdsUpdateOr.hasValue()) { - XdsClusterResource.CdsUpdate cdsUpdate = cdsUpdateOr.getValue(); - if (cdsUpdate.clusterType() == ClusterType.EDS) { - EdsWatcher edsWatcher = (EdsWatcher) edsWatchers.get(cdsUpdate.edsServiceName()); - if (edsWatcher != null) { - EndpointConfig child = new EndpointConfig(edsWatcher.getData()); - builder.addCluster(clusterName, StatusOr.fromValue( - new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, child))); - } else { - builder.addCluster(clusterName, StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( - "EDS resource not found for cluster " + clusterName))); - } - } else if (cdsUpdate.clusterType() == ClusterType.LOGICAL_DNS) { - // TODO get the resolved endpoint configuration - builder.addCluster(clusterName, StatusOr.fromValue( - new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, new EndpointConfig(null)))); - } - } else { + if (!cdsUpdateOr.hasValue()) { builder.addCluster(clusterName, StatusOr.fromStatus(cdsUpdateOr.getStatus())); + continue; + } + + XdsClusterResource.CdsUpdate cdsUpdate = cdsUpdateOr.getValue(); + if (cdsUpdate.clusterType() == ClusterType.EDS) { + XdsWatcherBase edsWatcher = + edsWatchers.get(cdsWatcher.getEdsServiceName()); + EndpointConfig child; + if (edsWatcher != null) { + child = new EndpointConfig(edsWatcher.getData()); + } else { + child = new EndpointConfig(StatusOr.fromStatus(Status.INTERNAL.withDescription( + "EDS resource not found for cluster " + clusterName))); + } + builder.addCluster(clusterName, StatusOr.fromValue( + new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, child))); + } else if (cdsUpdate.clusterType() == ClusterType.LOGICAL_DNS) { + builder.addCluster(clusterName, StatusOr.fromStatus( + Status.INTERNAL.withDescription("Logical DNS in dependency manager unsupported"))); } } } // Adds the top-level clusters to the builder and returns the leaf cluster names private Set addTopLevelClustersToBuilder( - XdsConfig.XdsConfigBuilder builder, Map> edsWatchers, - Map> cdsWatchers, List topLevelClusters) { + XdsConfig.XdsConfigBuilder builder, + Map> edsWatchers, + Map> cdsWatchers, + List topLevelClusters) { Set leafClusterNames = new HashSet<>(); for (String clusterName : topLevelClusters) { @@ -367,23 +394,25 @@ private Set addTopLevelClustersToBuilder( XdsConfig.XdsClusterConfig.ClusterChild child; switch (cdsUpdate.clusterType()) { case AGGREGATE: - List leafNames = getLeafNames(cdsUpdate); + Set leafNames = new HashSet<>(); + addLeafNames(leafNames, cdsUpdate); child = new AggregateConfig(leafNames); leafClusterNames.addAll(leafNames); break; case EDS: - EdsWatcher edsWatcher = (EdsWatcher) edsWatchers.get(cdsUpdate.edsServiceName()); + XdsWatcherBase edsWatcher = + edsWatchers.get(cdsWatcher.getEdsServiceName()); if (edsWatcher != null) { child = new EndpointConfig(edsWatcher.getData()); } else { - builder.addCluster(clusterName, StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( + child = new EndpointConfig(StatusOr.fromStatus(Status.INTERNAL.withDescription( "EDS resource not found for cluster " + clusterName))); - continue; } break; case LOGICAL_DNS: // TODO get the resolved endpoint configuration - child = new EndpointConfig(null); + child = new EndpointConfig(StatusOr.fromStatus( + Status.INTERNAL.withDescription("Logical DNS in dependency manager unsupported"))); break; default: throw new IllegalStateException("Unexpected value: " + cdsUpdate.clusterType()); @@ -395,29 +424,26 @@ private Set addTopLevelClustersToBuilder( return leafClusterNames; } - private List getLeafNames(XdsClusterResource.CdsUpdate cdsUpdate) { - List childNames = new ArrayList<>(); - + private void addLeafNames(Set leafNames, XdsClusterResource.CdsUpdate cdsUpdate) { for (String cluster : cdsUpdate.prioritizedClusterNames()) { + if (leafNames.contains(cluster)) { + continue; + } StatusOr data = getCluster(cluster).getData(); if (data == null || !data.hasValue() || data.getValue() == null) { - childNames.add(cluster); + leafNames.add(cluster); continue; } if (data.getValue().clusterType() == ClusterType.AGGREGATE) { - childNames.addAll(getLeafNames(data.getValue())); + addLeafNames(leafNames, data.getValue()); } else { - childNames.add(cluster); + leafNames.add(cluster); } } - - return childNames; } - private static boolean isTopLevelCluster(XdsWatcherBase cdsWatcher) { - if (! (cdsWatcher instanceof CdsWatcher)) { - return false; - } + private static boolean isTopLevelCluster( + XdsWatcherBase cdsWatcher) { return ((CdsWatcher)cdsWatcher).parentContexts.values().stream() .anyMatch(depth -> depth == 1); } @@ -454,17 +480,11 @@ private void addClusterWatcher(String clusterName, Object parentContext, int dep } private void updateRoutes(List virtualHosts, Object newParentContext, - VirtualHost oldVirtualHost, boolean sameParentContext) { + List oldVirtualHosts, boolean sameParentContext) { + VirtualHost oldVirtualHost = + RoutingUtils.findVirtualHostForHostName(oldVirtualHosts, dataPlaneAuthority); VirtualHost virtualHost = RoutingUtils.findVirtualHostForHostName(virtualHosts, dataPlaneAuthority); - if (virtualHost == null) { - String error = "Failed to find virtual host matching hostname: " + dataPlaneAuthority; - logger.log(XdsLogger.XdsLogLevel.WARNING, error); - cleanUpRoutes(); - xdsConfigWatcher.onError( - "xDS node ID:" + dataPlaneAuthority, Status.UNAVAILABLE.withDescription(error)); - return; - } Set newClusters = getClusterNamesFromVirtualHost(virtualHost); Set oldClusters = getClusterNamesFromVirtualHost(oldVirtualHost); @@ -482,6 +502,10 @@ private void updateRoutes(List virtualHosts, Object newParentContex } } + private String nodeInfo() { + return " nodeID: " + xdsClient.getBootstrapInfo().node().getId(); + } + private static Set getClusterNamesFromVirtualHost(VirtualHost virtualHost) { if (virtualHost == null) { return Collections.emptySet(); @@ -506,46 +530,6 @@ private static Set getClusterNamesFromVirtualHost(VirtualHost virtualHos return clusters; } - @Nullable - private VirtualHost getActiveVirtualHost() { - TypeWatchers rdsWatchers = resourceWatchers.get(XdsRouteConfigureResource.getInstance()); - if (rdsWatchers == null) { - return null; - } - - RdsWatcher activeRdsWatcher = - (RdsWatcher) rdsWatchers.watchers.values().stream().findFirst().orElse(null); - if (activeRdsWatcher == null || activeRdsWatcher.missingResult() - || !activeRdsWatcher.getData().hasValue()) { - return null; - } - - return RoutingUtils.findVirtualHostForHostName( - activeRdsWatcher.getData().getValue().virtualHosts, dataPlaneAuthority); - } - - // Must be in SyncContext - private void cleanUpRoutes() { - // Remove RdsWatcher & CDS Watchers - TypeWatchers rdsResourceWatcher = - resourceWatchers.get(XdsRouteConfigureResource.getInstance()); - if (rdsResourceWatcher == null || rdsResourceWatcher.watchers.isEmpty()) { - return; - } - - XdsWatcherBase watcher = rdsResourceWatcher.watchers.values().stream().findFirst().get(); - cancelWatcher(watcher); - - // Remove CdsWatchers pointed to by the RdsWatcher - RdsWatcher rdsWatcher = (RdsWatcher) watcher; - for (String cName : rdsWatcher.getCdsNames()) { - CdsWatcher cdsWatcher = getCluster(cName); - if (cdsWatcher != null) { - cancelClusterWatcherTree(cdsWatcher, rdsWatcher); - } - } - } - private CdsWatcher getCluster(String clusterName) { return (CdsWatcher) resourceWatchers.get(CLUSTER_RESOURCE).watchers.get(clusterName); } @@ -565,16 +549,11 @@ public void add(String resourceName, XdsWatcherBase watcher) { } public interface XdsConfigWatcher { - - void onUpdate(XdsConfig config); - - // These 2 methods are invoked when there is an error or - // does-not-exist on LDS or RDS only. The context will be a - // human-readable string indicating the scope in which the error - // occurred (e.g., the resource type and name). - void onError(String resourceContext, Status status); - - void onResourceDoesNotExist(String resourceContext); + /** + * An updated XdsConfig or RPC-safe Status. The status code will be either UNAVAILABLE or + * INTERNAL. + */ + void onUpdate(StatusOr config); } private class ClusterSubscription implements Closeable { @@ -594,7 +573,7 @@ public void close() throws IOException { } } - private abstract static class XdsWatcherBase + private abstract class XdsWatcherBase implements ResourceWatcher { private final XdsResourceType type; private final String resourceName; @@ -612,12 +591,25 @@ private XdsWatcherBase(XdsResourceType type, String resourceName) { @Override public void onError(Status error) { checkNotNull(error, "error"); - setDataAsStatus(error); + // Don't update configuration on error, if we've already received configuration + if (!hasDataValue()) { + setDataAsStatus(Status.UNAVAILABLE.withDescription( + String.format("Error retrieving %s: %s: %s", + toContextString(), error.getCode(), error.getDescription()))); + maybePublishConfig(); + } } - protected void handleDoesNotExist(String resourceName) { + @Override + public void onResourceDoesNotExist(String resourceName) { + if (cancelled) { + return; + } + checkArgument(this.resourceName.equals(resourceName), "Resource name does not match"); - setDataAsStatus(Status.UNAVAILABLE.withDescription("No " + toContextString())); + setDataAsStatus(Status.UNAVAILABLE.withDescription( + toContextString() + " does not exist" + nodeInfo())); + maybePublishConfig(); } boolean missingResult() { @@ -647,12 +639,17 @@ protected void setDataAsStatus(Status status) { this.data = StatusOr.fromStatus(status); } - String toContextString() { + public String toContextString() { return toContextStr(type.typeName(), resourceName); } } - private class LdsWatcher extends XdsWatcherBase { + private interface RdsUpdateSupplier { + StatusOr getRdsUpdate(); + } + + private class LdsWatcher extends XdsWatcherBase + implements RdsUpdateSupplier { String rdsName; private LdsWatcher(String resourceName) { @@ -664,9 +661,20 @@ public void onChanged(XdsListenerResource.LdsUpdate update) { checkNotNull(update, "update"); HttpConnectionManager httpConnectionManager = update.httpConnectionManager(); - List virtualHosts = httpConnectionManager.virtualHosts(); - String rdsName = httpConnectionManager.rdsName(); - VirtualHost activeVirtualHost = getActiveVirtualHost(); + List virtualHosts; + String rdsName; + if (httpConnectionManager == null) { + // TCP listener. Unsupported config + virtualHosts = Collections.emptyList(); // Not null, to not delegate to RDS + rdsName = null; + } else { + virtualHosts = httpConnectionManager.virtualHosts(); + rdsName = httpConnectionManager.rdsName(); + } + StatusOr activeRdsUpdate = getRouteSource().getRdsUpdate(); + List activeVirtualHosts = activeRdsUpdate.hasValue() + ? activeRdsUpdate.getValue().virtualHosts + : Collections.emptyList(); boolean changedRdsName = !Objects.equals(rdsName, this.rdsName); if (changedRdsName) { @@ -675,10 +683,9 @@ public void onChanged(XdsListenerResource.LdsUpdate update) { if (virtualHosts != null) { // No RDS watcher since we are getting RDS updates via LDS - updateRoutes(virtualHosts, this, activeVirtualHost, this.rdsName == null); + updateRoutes(virtualHosts, this, activeVirtualHosts, this.rdsName == null); this.rdsName = null; } else if (changedRdsName) { - cleanUpRdsWatcher(); this.rdsName = rdsName; addWatcher(new RdsWatcher(rdsName)); logger.log(XdsLogger.XdsLogLevel.INFO, "Start watching RDS resource {0}", rdsName); @@ -688,20 +695,18 @@ public void onChanged(XdsListenerResource.LdsUpdate update) { maybePublishConfig(); } - @Override - public void onError(Status error) { - super.onError(checkNotNull(error, "error")); - xdsConfigWatcher.onError(toContextString(), error); - } - @Override public void onResourceDoesNotExist(String resourceName) { if (cancelled) { return; } - handleDoesNotExist(resourceName); - xdsConfigWatcher.onResourceDoesNotExist(toContextString()); + checkArgument(resourceName().equals(resourceName), "Resource name does not match"); + setDataAsStatus(Status.UNAVAILABLE.withDescription( + toContextString() + " does not exist" + nodeInfo())); + cleanUpRdsWatcher(); + rdsName = null; + maybePublishConfig(); } private void cleanUpRdsWatcher() { @@ -711,8 +716,7 @@ private void cleanUpRdsWatcher() { logger.log(XdsLogger.XdsLogLevel.DEBUG, "Stop watching RDS resource {0}", rdsName); // Cleanup clusters (as appropriate) that had the old rds watcher as a parent - if (!oldRdsWatcher.hasDataValue() || !oldRdsWatcher.getData().hasValue() - || resourceWatchers.get(CLUSTER_RESOURCE) == null) { + if (!oldRdsWatcher.hasDataValue() || resourceWatchers.get(CLUSTER_RESOURCE) == null) { return; } for (XdsWatcherBase watcher : @@ -723,16 +727,58 @@ private void cleanUpRdsWatcher() { } private RdsWatcher getRdsWatcher() { + if (rdsName == null) { + return null; + } TypeWatchers watchers = resourceWatchers.get(XdsRouteConfigureResource.getInstance()); - if (watchers == null || rdsName == null || watchers.watchers.isEmpty()) { + if (watchers == null) { return null; } - return (RdsWatcher) watchers.watchers.get(rdsName); } + + public RdsUpdateSupplier getRouteSource() { + if (!hasDataValue()) { + return this; + } + HttpConnectionManager hcm = getData().getValue().httpConnectionManager(); + if (hcm == null) { + return this; + } + List virtualHosts = hcm.virtualHosts(); + if (virtualHosts != null) { + return this; + } + RdsWatcher rdsWatcher = getRdsWatcher(); + assert rdsWatcher != null; + return rdsWatcher; + } + + @Override + public StatusOr getRdsUpdate() { + if (missingResult()) { + return StatusOr.fromStatus(Status.UNAVAILABLE.withDescription("Not yet loaded")); + } + if (!getData().hasValue()) { + return StatusOr.fromStatus(getData().getStatus()); + } + HttpConnectionManager hcm = getData().getValue().httpConnectionManager(); + if (hcm == null) { + return StatusOr.fromStatus( + Status.UNAVAILABLE.withDescription("Not an API listener" + nodeInfo())); + } + List virtualHosts = hcm.virtualHosts(); + if (virtualHosts == null) { + // Code shouldn't trigger this case, as it should be calling RdsWatcher instead. This would + // be easily implemented with getRdsWatcher().getRdsUpdate(), but getting here is likely a + // bug + return StatusOr.fromStatus(Status.INTERNAL.withDescription("Routes are in RDS, not LDS")); + } + return StatusOr.fromValue(new RdsUpdate(virtualHosts)); + } } - private class RdsWatcher extends XdsWatcherBase { + private class RdsWatcher extends XdsWatcherBase implements RdsUpdateSupplier { public RdsWatcher(String resourceName) { super(XdsRouteConfigureResource.getInstance(), checkNotNull(resourceName, "resourceName")); @@ -741,37 +787,20 @@ public RdsWatcher(String resourceName) { @Override public void onChanged(RdsUpdate update) { checkNotNull(update, "update"); - RdsUpdate oldData = hasDataValue() ? getData().getValue() : null; - VirtualHost oldVirtualHost = - (oldData != null) - ? RoutingUtils.findVirtualHostForHostName(oldData.virtualHosts, dataPlaneAuthority) - : null; + List oldVirtualHosts = hasDataValue() + ? getData().getValue().virtualHosts + : Collections.emptyList(); setData(update); - updateRoutes(update.virtualHosts, this, oldVirtualHost, true); + updateRoutes(update.virtualHosts, this, oldVirtualHosts, true); maybePublishConfig(); } @Override - public void onError(Status error) { - super.onError(checkNotNull(error, "error")); - xdsConfigWatcher.onError(toContextString(), error); - } - - @Override - public void onResourceDoesNotExist(String resourceName) { - if (cancelled) { - return; + public StatusOr getRdsUpdate() { + if (missingResult()) { + return StatusOr.fromStatus(Status.UNAVAILABLE.withDescription("Not yet loaded")); } - handleDoesNotExist(checkNotNull(resourceName, "resourceName")); - xdsConfigWatcher.onResourceDoesNotExist(toContextString()); - } - - ImmutableList getCdsNames() { - if (!hasDataValue() || getData().getValue().virtualHosts == null) { - return ImmutableList.of(); - } - - return ImmutableList.copyOf(getClusterNamesFromVirtualHost(getActiveVirtualHost())); + return getData(); } } @@ -789,7 +818,7 @@ public void onChanged(XdsClusterResource.CdsUpdate update) { switch (update.clusterType()) { case EDS: setData(update); - if (!addEdsWatcher(update.edsServiceName(), this)) { + if (!addEdsWatcher(getEdsServiceName(), this)) { maybePublishConfig(); } break; @@ -805,14 +834,15 @@ public void onChanged(XdsClusterResource.CdsUpdate update) { logger.log(XdsLogger.XdsLogLevel.WARNING, "Cluster recursion depth limit exceeded for cluster {0}", resourceName()); Status error = Status.UNAVAILABLE.withDescription( - "aggregate cluster graph exceeds max depth"); + "aggregate cluster graph exceeds max depth at " + resourceName() + nodeInfo()); setDataAsStatus(error); } if (hasDataValue()) { - Set oldNames = new HashSet<>(getData().getValue().prioritizedClusterNames()); + Set oldNames = getData().getValue().clusterType() == ClusterType.AGGREGATE + ? new HashSet<>(getData().getValue().prioritizedClusterNames()) + : Collections.emptySet(); Set newNames = new HashSet<>(update.prioritizedClusterNames()); - Set deletedClusters = Sets.difference(oldNames, newNames); deletedClusters.forEach((cluster) -> cancelClusterWatcherTree(getCluster(cluster), parentContext)); @@ -838,19 +868,20 @@ public void onChanged(XdsClusterResource.CdsUpdate update) { break; default: Status error = Status.UNAVAILABLE.withDescription( - "aggregate cluster graph exceeds max depth"); + "aggregate cluster graph exceeds max depth at " + resourceName() + nodeInfo()); setDataAsStatus(error); maybePublishConfig(); } } - @Override - public void onResourceDoesNotExist(String resourceName) { - if (cancelled) { - return; + public String getEdsServiceName() { + XdsClusterResource.CdsUpdate cdsUpdate = getData().getValue(); + assert cdsUpdate.clusterType() == ClusterType.EDS; + String edsServiceName = cdsUpdate.edsServiceName(); + if (edsServiceName == null) { + edsServiceName = cdsUpdate.clusterName(); } - handleDoesNotExist(checkNotNull(resourceName, "resourceName")); - maybePublishConfig(); + return edsServiceName; } } @@ -868,15 +899,6 @@ public void onChanged(XdsEndpointResource.EdsUpdate update) { maybePublishConfig(); } - @Override - public void onResourceDoesNotExist(String resourceName) { - if (cancelled) { - return; - } - handleDoesNotExist(checkNotNull(resourceName, "resourceName")); - maybePublishConfig(); - } - void addParentContext(CdsWatcher parentContext) { parentContexts.add(checkNotNull(parentContext, "parentContext")); } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 4b6da6ac8b8..5c1b3105c45 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -45,6 +45,7 @@ import io.grpc.NameResolver; import io.grpc.Status; import io.grpc.Status.Code; +import io.grpc.StatusOr; import io.grpc.SynchronizationContext; import io.grpc.internal.GrpcUtil; import io.grpc.internal.ObjectPool; @@ -60,11 +61,9 @@ import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy; import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider; -import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import io.grpc.xds.client.Bootstrapper.AuthorityInfo; import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.XdsClient; -import io.grpc.xds.client.XdsClient.ResourceWatcher; import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger.XdsLogLevel; import java.net.URI; @@ -127,6 +126,7 @@ final class XdsNameResolver extends NameResolver { private final ConfigSelector configSelector = new ConfigSelector(); private final long randomChannelId; private final MetricRecorder metricRecorder; + private final Args nameResolverArgs; // Must be accessed in syncContext. // Filter instances are unique per channel, and per filter (name+typeUrl). // NamedFilterConfig.filterStateKey -> filter_instance. @@ -138,20 +138,17 @@ final class XdsNameResolver extends NameResolver { private XdsClient xdsClient; private CallCounterProvider callCounterProvider; private ResolveState resolveState; - // Workaround for https://github.com/grpc/grpc-java/issues/8886 . This should be handled in - // XdsClient instead of here. - private boolean receivedConfig; XdsNameResolver( URI targetUri, String name, @Nullable String overrideAuthority, ServiceConfigParser serviceConfigParser, SynchronizationContext syncContext, ScheduledExecutorService scheduler, @Nullable Map bootstrapOverride, - MetricRecorder metricRecorder) { + MetricRecorder metricRecorder, Args nameResolverArgs) { this(targetUri, targetUri.getAuthority(), name, overrideAuthority, serviceConfigParser, syncContext, scheduler, SharedXdsClientPoolProvider.getDefaultProvider(), ThreadSafeRandomImpl.instance, FilterRegistry.getDefaultRegistry(), bootstrapOverride, - metricRecorder); + metricRecorder, nameResolverArgs); } @VisibleForTesting @@ -161,7 +158,7 @@ final class XdsNameResolver extends NameResolver { SynchronizationContext syncContext, ScheduledExecutorService scheduler, XdsClientPoolFactory xdsClientPoolFactory, ThreadSafeRandom random, FilterRegistry filterRegistry, @Nullable Map bootstrapOverride, - MetricRecorder metricRecorder) { + MetricRecorder metricRecorder, Args nameResolverArgs) { this.targetAuthority = targetAuthority; target = targetUri.toString(); @@ -180,6 +177,8 @@ final class XdsNameResolver extends NameResolver { this.random = checkNotNull(random, "random"); this.filterRegistry = checkNotNull(filterRegistry, "filterRegistry"); this.metricRecorder = metricRecorder; + this.nameResolverArgs = checkNotNull(nameResolverArgs, "nameResolverArgs"); + randomChannelId = random.nextLong(); logId = InternalLogId.allocate("xds-resolver", name); logger = XdsLogger.withLogId(logId); @@ -228,9 +227,8 @@ public void start(Listener2 listener) { } ldsResourceName = XdsClient.canonifyResourceName(ldsResourceName); callCounterProvider = SharedCallCounterMap.getInstance(); - resolveState = new ResolveState(ldsResourceName); - resolveState.start(); + resolveState = new ResolveState(ldsResourceName); // auto starts } private static String expandPercentS(String template, String replacement) { @@ -241,7 +239,7 @@ private static String expandPercentS(String template, String replacement) { public void shutdown() { logger.log(XdsLogLevel.INFO, "Shutdown"); if (resolveState != null) { - resolveState.stop(); + resolveState.shutdown(); } if (xdsClient != null) { xdsClient = xdsClientPool.returnObject(xdsClient); @@ -290,7 +288,7 @@ XdsClient getXdsClient() { } // called in syncContext - private void updateResolutionResult() { + private void updateResolutionResult(XdsConfig xdsConfig) { syncContext.throwIfNotInThisSynchronizationContext(); ImmutableMap.Builder childPolicy = new ImmutableMap.Builder<>(); @@ -312,6 +310,8 @@ private void updateResolutionResult() { Attributes attrs = Attributes.newBuilder() .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) + .set(XdsAttributes.XDS_CONFIG, xdsConfig) + .set(XdsAttributes.XDS_CLUSTER_SUBSCRIPT_REGISTRY, resolveState.xdsDependencyManager) .set(XdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider) .set(InternalConfigSelector.KEY, configSelector) .build(); @@ -321,7 +321,6 @@ private void updateResolutionResult() { .setServiceConfig(parsedServiceConfig) .build(); listener.onResult2(result); - receivedConfig = true; } /** @@ -540,7 +539,11 @@ private void releaseCluster(final String cluster) { public void run() { if (clusterRefs.get(cluster).refCount.get() == 0) { clusterRefs.remove(cluster); - updateResolutionResult(); + if (resolveState.lastConfigOrStatus.hasValue()) { + updateResolutionResult(resolveState.lastConfigOrStatus.getValue()); + } else { + resolveState.cleanUpRoutes(resolveState.lastConfigOrStatus.getStatus()); + } } } }); @@ -629,82 +632,56 @@ private static String prefixedClusterSpecifierPluginName(String pluginName) { return "cluster_specifier_plugin:" + pluginName; } - private class ResolveState implements ResourceWatcher { + class ResolveState implements XdsDependencyManager.XdsConfigWatcher { private final ConfigOrError emptyServiceConfig = serviceConfigParser.parseServiceConfig(Collections.emptyMap()); - private final String ldsResourceName; + private final String authority; + private final XdsDependencyManager xdsDependencyManager; private boolean stopped; @Nullable private Set existingClusters; // clusters to which new requests can be routed - @Nullable - private RouteDiscoveryState routeDiscoveryState; + private StatusOr lastConfigOrStatus; - ResolveState(String ldsResourceName) { - this.ldsResourceName = ldsResourceName; + private ResolveState(String ldsResourceName) { + authority = overrideAuthority != null ? overrideAuthority : encodedServiceAuthority; + xdsDependencyManager = + new XdsDependencyManager(xdsClient, this, syncContext, authority, ldsResourceName, + nameResolverArgs, scheduler); } - @Override - public void onChanged(final XdsListenerResource.LdsUpdate update) { + private void shutdown() { if (stopped) { return; } - logger.log(XdsLogLevel.INFO, "Receive LDS resource update: {0}", update); - HttpConnectionManager httpConnectionManager = update.httpConnectionManager(); - List virtualHosts = httpConnectionManager.virtualHosts(); - String rdsName = httpConnectionManager.rdsName(); - ImmutableList filterConfigs = httpConnectionManager.httpFilterConfigs(); - long streamDurationNano = httpConnectionManager.httpMaxStreamDurationNano(); - - // Create/update HCM-bound state. - cleanUpRouteDiscoveryState(); - updateActiveFilters(filterConfigs); - // Routes specified directly in LDS. - if (virtualHosts != null) { - updateRoutes(virtualHosts, streamDurationNano, filterConfigs); - return; - } - // Routes provided by RDS. - routeDiscoveryState = new RouteDiscoveryState(rdsName, streamDurationNano, filterConfigs); - logger.log(XdsLogLevel.INFO, "Start watching RDS resource {0}", rdsName); - xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), - rdsName, routeDiscoveryState, syncContext); + stopped = true; + xdsDependencyManager.shutdown(); + updateActiveFilters(null); } @Override - public void onError(final Status error) { - if (stopped || receivedConfig) { + public void onUpdate(StatusOr updateOrStatus) { + if (stopped) { return; } - listener.onError(Status.UNAVAILABLE.withCause(error.getCause()).withDescription( - String.format("Unable to load LDS %s. xDS server returned: %s: %s", - ldsResourceName, error.getCode(), error.getDescription()))); - } + logger.log(XdsLogLevel.INFO, "Receive XDS resource update: {0}", updateOrStatus); - @Override - public void onResourceDoesNotExist(final String resourceName) { - if (stopped) { + lastConfigOrStatus = updateOrStatus; + if (!updateOrStatus.hasValue()) { + updateActiveFilters(null); + cleanUpRoutes(updateOrStatus.getStatus()); return; } - String error = "LDS resource does not exist: " + resourceName; - logger.log(XdsLogLevel.INFO, error); - cleanUpRouteDiscoveryState(); - updateActiveFilters(null); - cleanUpRoutes(error); - } - private void start() { - logger.log(XdsLogLevel.INFO, "Start watching LDS resource {0}", ldsResourceName); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(), - ldsResourceName, this, syncContext); - } + // Process Route + XdsConfig update = updateOrStatus.getValue(); + HttpConnectionManager httpConnectionManager = update.getListener().httpConnectionManager(); + VirtualHost virtualHost = update.getVirtualHost(); + ImmutableList filterConfigs = httpConnectionManager.httpFilterConfigs(); + long streamDurationNano = httpConnectionManager.httpMaxStreamDurationNano(); - private void stop() { - logger.log(XdsLogLevel.INFO, "Stop watching LDS resource {0}", ldsResourceName); - stopped = true; - cleanUpRouteDiscoveryState(); - updateActiveFilters(null); - xdsClient.cancelXdsResourceWatch(XdsListenerResource.getInstance(), ldsResourceName, this); + updateActiveFilters(filterConfigs); + updateRoutes(update, virtualHost, streamDurationNano, filterConfigs); } // called in syncContext @@ -732,18 +709,11 @@ private void updateActiveFilters(@Nullable List filterConfigs } } - // called in syncContext - private void updateRoutes(List virtualHosts, long httpMaxStreamDurationNano, + private void updateRoutes( + XdsConfig xdsConfig, + @Nullable VirtualHost virtualHost, + long httpMaxStreamDurationNano, @Nullable List filterConfigs) { - String authority = overrideAuthority != null ? overrideAuthority : encodedServiceAuthority; - VirtualHost virtualHost = RoutingUtils.findVirtualHostForHostName(virtualHosts, authority); - if (virtualHost == null) { - String error = "Failed to find virtual host matching hostname: " + authority; - logger.log(XdsLogLevel.WARNING, error); - cleanUpRoutes(error); - return; - } - List routes = virtualHost.routes(); ImmutableList.Builder routesData = ImmutableList.builder(); @@ -826,7 +796,7 @@ private void updateRoutes(List virtualHosts, long httpMaxStreamDura } // Update service config to include newly added clusters. if (shouldUpdateResult && routingConfig != null) { - updateResolutionResult(); + updateResolutionResult(xdsConfig); shouldUpdateResult = false; } // Make newly added clusters selectable by config selector and deleted clusters no longer @@ -840,7 +810,7 @@ private void updateRoutes(List virtualHosts, long httpMaxStreamDura } } if (shouldUpdateResult) { - updateResolutionResult(); + updateResolutionResult(xdsConfig); } } @@ -882,10 +852,8 @@ private ClientInterceptor createFilters( return combineInterceptors(filterInterceptors.build()); } - private void cleanUpRoutes(String error) { - String errorWithNodeId = - error + ", xDS node ID: " + xdsClient.getBootstrapInfo().node().getId(); - routingConfig = new RoutingConfig(Status.UNAVAILABLE.withDescription(errorWithNodeId)); + private void cleanUpRoutes(Status error) { + routingConfig = new RoutingConfig(error); if (existingClusters != null) { for (String cluster : existingClusters) { int count = clusterRefs.get(cluster).refCount.decrementAndGet(); @@ -904,64 +872,6 @@ private void cleanUpRoutes(String error) { .build()) .setServiceConfig(emptyServiceConfig) .build()); - receivedConfig = true; - } - - private void cleanUpRouteDiscoveryState() { - if (routeDiscoveryState != null) { - String rdsName = routeDiscoveryState.resourceName; - logger.log(XdsLogLevel.INFO, "Stop watching RDS resource {0}", rdsName); - xdsClient.cancelXdsResourceWatch(XdsRouteConfigureResource.getInstance(), rdsName, - routeDiscoveryState); - routeDiscoveryState = null; - } - } - - /** - * Discovery state for RouteConfiguration resource. One instance for each Listener resource - * update. - */ - private class RouteDiscoveryState implements ResourceWatcher { - private final String resourceName; - private final long httpMaxStreamDurationNano; - @Nullable - private final List filterConfigs; - - private RouteDiscoveryState(String resourceName, long httpMaxStreamDurationNano, - @Nullable List filterConfigs) { - this.resourceName = resourceName; - this.httpMaxStreamDurationNano = httpMaxStreamDurationNano; - this.filterConfigs = filterConfigs; - } - - @Override - public void onChanged(final RdsUpdate update) { - if (RouteDiscoveryState.this != routeDiscoveryState) { - return; - } - logger.log(XdsLogLevel.INFO, "Received RDS resource update: {0}", update); - updateRoutes(update.virtualHosts, httpMaxStreamDurationNano, filterConfigs); - } - - @Override - public void onError(final Status error) { - if (RouteDiscoveryState.this != routeDiscoveryState || receivedConfig) { - return; - } - listener.onError(Status.UNAVAILABLE.withCause(error.getCause()).withDescription( - String.format("Unable to load RDS %s. xDS server returned: %s: %s", - resourceName, error.getCode(), error.getDescription()))); - } - - @Override - public void onResourceDoesNotExist(final String resourceName) { - if (RouteDiscoveryState.this != routeDiscoveryState) { - return; - } - String error = "RDS resource does not exist: " + resourceName; - logger.log(XdsLogLevel.INFO, error); - cleanUpRoutes(error); - } } } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java b/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java index 74518331269..eb3887396a0 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java @@ -82,7 +82,7 @@ public XdsNameResolver newNameResolver(URI targetUri, Args args) { args.getServiceConfigParser(), args.getSynchronizationContext(), args.getScheduledExecutorService(), bootstrapOverride, - args.getMetricRecorder()); + args.getMetricRecorder(), args); } return null; } diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index f94a94a9446..2af04a3aedf 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.StatusMatcher.statusHasCode; import static io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType.AGGREGATE; import static io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType.EDS; import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS; @@ -32,7 +33,6 @@ import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -47,24 +47,26 @@ import io.envoyproxy.envoy.config.listener.v3.Listener; import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; import io.grpc.BindableService; +import io.grpc.ChannelLogger; import io.grpc.ManagedChannel; +import io.grpc.NameResolver; import io.grpc.Server; import io.grpc.Status; import io.grpc.StatusOr; +import io.grpc.StatusOrMatcher; import io.grpc.SynchronizationContext; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.internal.ExponentialBackoffPolicy; import io.grpc.internal.FakeClock; +import io.grpc.internal.GrpcUtil; import io.grpc.testing.GrpcCleanupRule; +import io.grpc.xds.XdsClusterResource.CdsUpdate; import io.grpc.xds.XdsConfig.XdsClusterConfig; import io.grpc.xds.XdsEndpointResource.EdsUpdate; -import io.grpc.xds.XdsListenerResource.LdsUpdate; import io.grpc.xds.client.CommonBootstrapperTestUtils; -import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsClientImpl; import io.grpc.xds.client.XdsClientMetricReporter; -import io.grpc.xds.client.XdsResourceType; import io.grpc.xds.client.XdsTransportFactory; import java.io.Closeable; import java.io.IOException; @@ -77,7 +79,7 @@ import java.util.Map; import java.util.Queue; import java.util.Set; -import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; @@ -108,7 +110,9 @@ public class XdsDependencyManagerTest { private XdsClientMetricReporter xdsClientMetricReporter; private final SynchronizationContext syncContext = - new SynchronizationContext(mock(Thread.UncaughtExceptionHandler.class)); + new SynchronizationContext((t, e) -> { + throw new AssertionError(e); + }); private ManagedChannel channel; private XdsClientImpl xdsClient; @@ -133,9 +137,17 @@ public class XdsDependencyManagerTest { private XdsConfig defaultXdsConfig; // set in setUp() @Captor - private ArgumentCaptor xdsConfigCaptor; - @Captor - private ArgumentCaptor statusCaptor; + private ArgumentCaptor> xdsUpdateCaptor; + private final NameResolver.Args nameResolverArgs = NameResolver.Args.newBuilder() + .setDefaultPort(8080) + .setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR) + .setSynchronizationContext(syncContext) + .setServiceConfigParser(mock(NameResolver.ServiceConfigParser.class)) + .setChannelLogger(mock(ChannelLogger.class)) + .setScheduledExecutorService(fakeClock.getScheduledExecutorService()) + .build(); + + private final ScheduledExecutorService scheduler = fakeClock.getScheduledExecutorService(); @Before public void setUp() throws Exception { @@ -180,36 +192,36 @@ public void tearDown() throws InterruptedException { @Test public void verify_basic_config() { - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); - verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); - testWatcher.verifyStats(1, 0, 0); + verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig)); + testWatcher.verifyStats(1, 0); } @Test public void verify_config_update() { - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); - testWatcher.verifyStats(1, 0, 0); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig)); + testWatcher.verifyStats(1, 0); assertThat(testWatcher.lastConfig).isEqualTo(defaultXdsConfig); XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS2", "CDS2", "EDS2", ENDPOINT_HOSTNAME + "2", ENDPOINT_PORT + 2); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(ArgumentMatchers.notNull()); - testWatcher.verifyStats(2, 0, 0); + testWatcher.verifyStats(2, 0); assertThat(testWatcher.lastConfig).isNotEqualTo(defaultXdsConfig); } @Test public void verify_simple_aggregate() { InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig)); List childNames = Arrays.asList("clusterC", "clusterB", "clusterA"); String rootName = "root_c"; @@ -226,14 +238,14 @@ public void verify_simple_aggregate() { testWatcher.lastConfig.getClusters(); assertThat(lastConfigClusters).hasSize(childNames.size() + 1); StatusOr rootC = lastConfigClusters.get(rootName); - XdsClusterResource.CdsUpdate rootUpdate = rootC.getValue().getClusterResource(); + CdsUpdate rootUpdate = rootC.getValue().getClusterResource(); assertThat(rootUpdate.clusterType()).isEqualTo(AGGREGATE); assertThat(rootUpdate.prioritizedClusterNames()).isEqualTo(childNames); for (String childName : childNames) { assertThat(lastConfigClusters).containsKey(childName); StatusOr childConfigOr = lastConfigClusters.get(childName); - XdsClusterResource.CdsUpdate childResource = + CdsUpdate childResource = childConfigOr.getValue().getClusterResource(); assertThat(childResource.clusterType()).isEqualTo(EDS); assertThat(childResource.edsServiceName()).isEqualTo(getEdsNameForCluster(childName)); @@ -266,54 +278,57 @@ public void testComplexRegisteredAggregate() throws IOException { List childNames2 = Arrays.asList("clusterA", "clusterX"); XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName2, childNames2); - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); Closeable subscription2 = xdsDependencyManager.subscribeToCluster(rootName2); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); - testWatcher.verifyStats(3, 0, 0); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + testWatcher.verifyStats(3, 0); ImmutableSet.Builder builder = ImmutableSet.builder(); Set expectedClusters = builder.add(rootName1).add(rootName2).add(CLUSTER_NAME) .addAll(childNames).addAll(childNames2).build(); - assertThat(xdsConfigCaptor.getValue().getClusters().keySet()).isEqualTo(expectedClusters); + assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet()) + .isEqualTo(expectedClusters); // Close 1 subscription shouldn't affect the other or RDS subscriptions subscription1.close(); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); builder = ImmutableSet.builder(); Set expectedClusters2 = builder.add(rootName2).add(CLUSTER_NAME).addAll(childNames2).build(); - assertThat(xdsConfigCaptor.getValue().getClusters().keySet()).isEqualTo(expectedClusters2); + assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet()) + .isEqualTo(expectedClusters2); subscription2.close(); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig)); } @Test public void testDelayedSubscription() { InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig)); String rootName1 = "root_c"; Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1); assertThat(subscription1).isNotNull(); fakeClock.forwardTime(16, TimeUnit.SECONDS); - inOrder.verify(xdsConfigWatcher).onUpdate(xdsConfigCaptor.capture()); - assertThat(xdsConfigCaptor.getValue().getClusters().get(rootName1).toString()).isEqualTo( - StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( - "No " + toContextStr(CLUSTER_TYPE_NAME, rootName1))).toString()); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); + Status status = xdsUpdateCaptor.getValue().getValue().getClusters().get(rootName1).getStatus(); + assertThat(status.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(status.getDescription()).contains(rootName1); List childNames = Arrays.asList("clusterC", "clusterB", "clusterA"); XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName1, childNames); - inOrder.verify(xdsConfigWatcher).onUpdate(xdsConfigCaptor.capture()); - assertThat(xdsConfigCaptor.getValue().getClusters().get(rootName1).hasValue()).isTrue(); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); + assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().get(rootName1).hasValue()) + .isTrue(); } @Test @@ -342,109 +357,99 @@ public void testMissingCdsAndEds() { edsMap.put("garbageEds", clusterLoadAssignment); controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); fakeClock.forwardTime(16, TimeUnit.SECONDS); - verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); + verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); List> returnedClusters = new ArrayList<>(); for (String childName : childNames) { - returnedClusters.add(xdsConfigCaptor.getValue().getClusters().get(childName)); + returnedClusters.add(xdsUpdateCaptor.getValue().getValue().getClusters().get(childName)); } // Check that missing cluster reported Status and the other 2 are present - Status expectedClusterStatus = Status.UNAVAILABLE.withDescription( - "No " + toContextStr(CLUSTER_TYPE_NAME, childNames.get(2))); StatusOr missingCluster = returnedClusters.get(2); - assertThat(missingCluster.getStatus().toString()).isEqualTo(expectedClusterStatus.toString()); + assertThat(missingCluster.getStatus().getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(missingCluster.getStatus().getDescription()).contains(childNames.get(2)); assertThat(returnedClusters.get(0).hasValue()).isTrue(); assertThat(returnedClusters.get(1).hasValue()).isTrue(); // Check that missing EDS reported Status, the other one is present and the garbage EDS is not - Status expectedEdsStatus = Status.UNAVAILABLE.withDescription( - "No " + toContextStr(ENDPOINT_TYPE_NAME, XdsTestUtils.EDS_NAME + 1)); assertThat(getEndpoint(returnedClusters.get(0)).hasValue()).isTrue(); - assertThat(getEndpoint(returnedClusters.get(1)).hasValue()).isFalse(); - assertThat(getEndpoint(returnedClusters.get(1)).getStatus().toString()) - .isEqualTo(expectedEdsStatus.toString()); - - verify(xdsConfigWatcher, never()).onResourceDoesNotExist(any()); - testWatcher.verifyStats(1, 0, 0); + assertThat(getEndpoint(returnedClusters.get(1)).getStatus().getCode()) + .isEqualTo(Status.Code.UNAVAILABLE); + assertThat(getEndpoint(returnedClusters.get(1)).getStatus().getDescription()) + .contains(XdsTestUtils.EDS_NAME + 1); + + verify(xdsConfigWatcher, never()).onUpdate( + argThat(StatusOrMatcher.hasStatus(statusHasCode(Status.Code.UNAVAILABLE)))); + testWatcher.verifyStats(1, 0); } @Test public void testMissingLds() { - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, "badLdsName"); + String ldsName = "badLdsName"; + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, ldsName, nameResolverArgs, scheduler); + + fakeClock.forwardTime(16, TimeUnit.SECONDS); + verify(xdsConfigWatcher, timeout(1000)).onUpdate( + argThat(StatusOrMatcher.hasStatus(statusHasCode(Status.Code.UNAVAILABLE) + .andDescriptionContains(ldsName)))); + + testWatcher.verifyStats(0, 1); + } + + @Test + public void testTcpListenerErrors() { + Listener serverListener = + ControlPlaneRule.buildServerListener().toBuilder().setName(serverName).build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, ImmutableMap.of(serverName, serverListener)); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); fakeClock.forwardTime(16, TimeUnit.SECONDS); - verify(xdsConfigWatcher, timeout(1000)).onResourceDoesNotExist( - toContextStr(XdsListenerResource.getInstance().typeName(), "badLdsName")); + verify(xdsConfigWatcher, timeout(1000)).onUpdate( + argThat(StatusOrMatcher.hasStatus( + statusHasCode(Status.Code.UNAVAILABLE).andDescriptionContains("Not an API listener")))); - testWatcher.verifyStats(0, 0, 1); + testWatcher.verifyStats(0, 1); } @Test public void testMissingRds() { - Listener serverListener = ControlPlaneRule.buildServerListener(); - Listener clientListener = - ControlPlaneRule.buildClientListener(serverName, serverName, "badRdsName"); + String rdsName = "badRdsName"; + Listener clientListener = ControlPlaneRule.buildClientListener(serverName, serverName, rdsName); controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, - ImmutableMap.of(XdsTestUtils.SERVER_LISTENER, serverListener, serverName, clientListener)); + ImmutableMap.of(serverName, clientListener)); - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); fakeClock.forwardTime(16, TimeUnit.SECONDS); - verify(xdsConfigWatcher, timeout(1000)).onResourceDoesNotExist( - toContextStr(XdsRouteConfigureResource.getInstance().typeName(), "badRdsName")); + verify(xdsConfigWatcher, timeout(1000)).onUpdate( + argThat(StatusOrMatcher.hasStatus(statusHasCode(Status.Code.UNAVAILABLE) + .andDescriptionContains(rdsName)))); - testWatcher.verifyStats(0, 0, 1); + testWatcher.verifyStats(0, 1); } @Test public void testUpdateToMissingVirtualHost() { - InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - WrappedXdsClient wrappedXdsClient = new WrappedXdsClient(xdsClient, syncContext); - xdsDependencyManager = new XdsDependencyManager( - wrappedXdsClient, xdsConfigWatcher, syncContext, serverName, serverName); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); + RouteConfiguration routeConfig = XdsTestUtils.buildRouteConfiguration( + "wrong-virtual-host", XdsTestUtils.RDS_NAME, XdsTestUtils.CLUSTER_NAME); + controlPlaneService.setXdsConfig( + ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig)); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); // Update with a config that has a virtual host that doesn't match the server name - wrappedXdsClient.deliverLdsUpdate(0L, buildUnmatchedVirtualHosts()); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onError(any(), statusCaptor.capture()); - assertThat(statusCaptor.getValue().getDescription()) - .isEqualTo("Failed to find virtual host matching hostname: " + serverName); - - testWatcher.verifyStats(1, 1, 0); + verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + assertThat(xdsUpdateCaptor.getValue().getStatus().getDescription()) + .contains("Failed to find virtual host matching hostname: " + serverName); - wrappedXdsClient.shutdown(); - } - - private List buildUnmatchedVirtualHosts() { - io.grpc.xds.VirtualHost.Route route1 = - io.grpc.xds.VirtualHost.Route.forAction( - io.grpc.xds.VirtualHost.Route.RouteMatch.withPathExactOnly("/GreetService/bye"), - io.grpc.xds.VirtualHost.Route.RouteAction.forCluster( - "cluster-bar.googleapis.com", Collections.emptyList(), - TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()); - io.grpc.xds.VirtualHost.Route route2 = - io.grpc.xds.VirtualHost.Route.forAction( - io.grpc.xds.VirtualHost.Route.RouteMatch.withPathExactOnly("/HelloService/hi"), - io.grpc.xds.VirtualHost.Route.RouteAction.forCluster( - "cluster-foo.googleapis.com", Collections.emptyList(), - TimeUnit.SECONDS.toNanos(15L), null, false), - ImmutableMap.of()); - return Arrays.asList( - io.grpc.xds.VirtualHost.create("virtualhost-foo", Collections.singletonList("hello" - + ".googleapis.com"), - Collections.singletonList(route1), - ImmutableMap.of()), - io.grpc.xds.VirtualHost.create("virtualhost-bar", Collections.singletonList("hi" - + ".googleapis.com"), - Collections.singletonList(route2), - ImmutableMap.of())); + testWatcher.verifyStats(0, 1); } @Test @@ -452,37 +457,32 @@ public void testCorruptLds() { String ldsResourceName = "xdstp://unknown.example.com/envoy.config.listener.v3.Listener/listener1"; - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, ldsResourceName); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, ldsResourceName, nameResolverArgs, scheduler); - Status expectedStatus = Status.INVALID_ARGUMENT.withDescription( - "Wrong configuration: xds server does not exist for resource " + ldsResourceName); - String context = toContextStr(XdsListenerResource.getInstance().typeName(), ldsResourceName); - verify(xdsConfigWatcher, timeout(1000)) - .onError(eq(context), argThat(new XdsTestUtils.StatusMatcher(expectedStatus))); + verify(xdsConfigWatcher, timeout(1000)).onUpdate( + argThat(StatusOrMatcher.hasStatus( + statusHasCode(Status.Code.UNAVAILABLE).andDescriptionContains(ldsResourceName)))); fakeClock.forwardTime(16, TimeUnit.SECONDS); - testWatcher.verifyStats(0, 1, 0); + testWatcher.verifyStats(0, 1); } @Test public void testChangeRdsName_fromLds() { - // TODO implement InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - Listener serverListener = ControlPlaneRule.buildServerListener(); - - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig)); String newRdsName = "newRdsName1"; Listener clientListener = buildInlineClientListener(newRdsName, CLUSTER_NAME); controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, - ImmutableMap.of(XdsTestUtils.SERVER_LISTENER, serverListener, serverName, clientListener)); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); - assertThat(xdsConfigCaptor.getValue()).isNotEqualTo(defaultXdsConfig); - assertThat(xdsConfigCaptor.getValue().getVirtualHost().name()).isEqualTo(newRdsName); + ImmutableMap.of(serverName, clientListener)); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + assertThat(xdsUpdateCaptor.getValue().getValue()).isNotEqualTo(defaultXdsConfig); + assertThat(xdsUpdateCaptor.getValue().getValue().getVirtualHost().name()).isEqualTo(newRdsName); } @Test @@ -527,22 +527,22 @@ public void testMultipleParentsInCdsTree() throws IOException { // Start the actual test InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); - XdsConfig initialConfig = xdsConfigCaptor.getValue(); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + XdsConfig initialConfig = xdsUpdateCaptor.getValue().getValue(); // Make sure that adding subscriptions that rds points at doesn't change the config Closeable rootSub = xdsDependencyManager.subscribeToCluster("root"); - assertThat(xdsDependencyManager.buildConfig()).isEqualTo(initialConfig); + assertThat(xdsDependencyManager.buildUpdate().getValue()).isEqualTo(initialConfig); Closeable clusterAB11Sub = xdsDependencyManager.subscribeToCluster("clusterAB11"); - assertThat(xdsDependencyManager.buildConfig()).isEqualTo(initialConfig); + assertThat(xdsDependencyManager.buildUpdate().getValue()).isEqualTo(initialConfig); // Make sure that closing subscriptions that rds points at doesn't change the config rootSub.close(); - assertThat(xdsDependencyManager.buildConfig()).isEqualTo(initialConfig); + assertThat(xdsDependencyManager.buildUpdate().getValue()).isEqualTo(initialConfig); clusterAB11Sub.close(); - assertThat(xdsDependencyManager.buildConfig()).isEqualTo(initialConfig); + assertThat(xdsDependencyManager.buildUpdate().getValue()).isEqualTo(initialConfig); // Make an explicit root subscription and then change RDS to point to A11 rootSub = xdsDependencyManager.subscribeToCluster("root"); @@ -550,13 +550,14 @@ public void testMultipleParentsInCdsTree() throws IOException { XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "clusterA11"); controlPlaneService.setXdsConfig( ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, newRouteConfig)); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); - assertThat(xdsConfigCaptor.getValue().getClusters().keySet().size()).isEqualTo(4); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet().size()).isEqualTo(4); // Now that it is released, we should only have A11 rootSub.close(); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); - assertThat(xdsConfigCaptor.getValue().getClusters().keySet()).containsExactly("clusterA11"); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet()) + .containsExactly("clusterA11"); } @Test @@ -587,10 +588,10 @@ public void testMultipleCdsReferToSameEds() { controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); // Start the actual test - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); - verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); - XdsConfig initialConfig = xdsConfigCaptor.getValue(); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + XdsConfig initialConfig = xdsUpdateCaptor.getValue().getValue(); assertThat(initialConfig.getClusters().keySet()) .containsExactly("root", "clusterA", "clusterB"); @@ -605,8 +606,8 @@ public void testMultipleCdsReferToSameEds() { @Test public void testChangeRdsName_FromLds_complexTree() { - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); // Create the same tree as in testMultipleParentsInCdsTree Cluster rootCluster = @@ -639,11 +640,10 @@ public void testChangeRdsName_FromLds_complexTree() { // Do the test String newRdsName = "newRdsName1"; Listener clientListener = buildInlineClientListener(newRdsName, "root"); - Listener serverListener = ControlPlaneRule.buildServerListener(); controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, - ImmutableMap.of(XdsTestUtils.SERVER_LISTENER, serverListener, serverName, clientListener)); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); - XdsConfig config = xdsConfigCaptor.getValue(); + ImmutableMap.of(serverName, clientListener)); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + XdsConfig config = xdsUpdateCaptor.getValue().getValue(); assertThat(config.getVirtualHost().name()).isEqualTo(newRdsName); assertThat(config.getClusters().size()).isEqualTo(4); } @@ -652,9 +652,9 @@ public void testChangeRdsName_FromLds_complexTree() { public void testChangeAggCluster() { InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - xdsDependencyManager = new XdsDependencyManager( - xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); - inOrder.verify(xdsConfigWatcher, atLeastOnce()).onUpdate(any()); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); // Setup initial config A -> A1 -> (A11, A12) Cluster rootCluster = @@ -673,9 +673,8 @@ public void testChangeAggCluster() { XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterA11", "clusterA12"); Listener clientListener = buildInlineClientListener(RDS_NAME, "root"); - Listener serverListener = ControlPlaneRule.buildServerListener(); controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, - ImmutableMap.of(XdsTestUtils.SERVER_LISTENER, serverListener, serverName, clientListener)); + ImmutableMap.of(serverName, clientListener)); controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); @@ -702,96 +701,50 @@ public void testChangeAggCluster() { inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(argThat(nameMatcher)); } - private Listener buildInlineClientListener(String rdsName, String clusterName) { - return XdsTestUtils.buildInlineClientListener(rdsName, clusterName, serverName); + @Test + public void testCdsError() throws IOException { + controlPlaneService.setXdsConfig( + ADS_TYPE_URL_CDS, ImmutableMap.of(XdsTestUtils.CLUSTER_NAME, + Cluster.newBuilder().setName(XdsTestUtils.CLUSTER_NAME).build())); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + + verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + Status status = xdsUpdateCaptor.getValue().getValue() + .getClusters().get(CLUSTER_NAME).getStatus(); + assertThat(status.getDescription()).contains(XdsTestUtils.CLUSTER_NAME); } - - private static String toContextStr(String type, String resourceName) { - return type + " resource: " + resourceName; + private Listener buildInlineClientListener(String rdsName, String clusterName) { + return XdsTestUtils.buildInlineClientListener(rdsName, clusterName, serverName); } private static class TestWatcher implements XdsDependencyManager.XdsConfigWatcher { XdsConfig lastConfig; int numUpdates = 0; int numError = 0; - int numDoesNotExist = 0; @Override - public void onUpdate(XdsConfig config) { - log.fine("Config changed: " + config); - lastConfig = config; - numUpdates++; - } - - @Override - public void onError(String resourceContext, Status status) { - log.fine(String.format("Error %s for %s: ", status, resourceContext)); - numError++; - } - - @Override - public void onResourceDoesNotExist(String resourceName) { - log.fine("Resource does not exist: " + resourceName); - numDoesNotExist++; - } - - private List getStats() { - return Arrays.asList(numUpdates, numError, numDoesNotExist); - } - - private void verifyStats(int updt, int err, int notExist) { - assertThat(getStats()).isEqualTo(Arrays.asList(updt, err, notExist)); - } - } - - private static class WrappedXdsClient extends XdsClient { - private final XdsClient delegate; - private final SynchronizationContext syncContext; - private ResourceWatcher ldsWatcher; - - WrappedXdsClient(XdsClient delegate, SynchronizationContext syncContext) { - this.delegate = delegate; - this.syncContext = syncContext; - } - - @Override - public void shutdown() { - delegate.shutdown(); - } - - @Override - @SuppressWarnings("unchecked") - public void watchXdsResource( - XdsResourceType type, String resourceName, ResourceWatcher watcher, - Executor executor) { - if (type.equals(XdsListenerResource.getInstance())) { - ldsWatcher = (ResourceWatcher) watcher; + public void onUpdate(StatusOr update) { + log.fine("Config update: " + update); + if (update.hasValue()) { + lastConfig = update.getValue(); + numUpdates++; + } else { + numError++; } - delegate.watchXdsResource(type, resourceName, watcher, executor); } - - - @Override - public void cancelXdsResourceWatch(XdsResourceType type, - String resourceName, - ResourceWatcher watcher) { - delegate.cancelXdsResourceWatch(type, resourceName, watcher); + private List getStats() { + return Arrays.asList(numUpdates, numError); } - void deliverLdsUpdate(long httpMaxStreamDurationNano, - List virtualHosts) { - syncContext.execute(() -> { - LdsUpdate ldsUpdate = LdsUpdate.forApiListener( - io.grpc.xds.HttpConnectionManager.forVirtualHosts( - httpMaxStreamDurationNano, virtualHosts, null)); - ldsWatcher.onChanged(ldsUpdate); - }); + private void verifyStats(int updt, int err) { + assertThat(getStats()).isEqualTo(Arrays.asList(updt, err)); } } - static class ClusterNameMatcher implements ArgumentMatcher { + static class ClusterNameMatcher implements ArgumentMatcher> { private final List expectedNames; ClusterNameMatcher(List expectedNames) { @@ -799,7 +752,11 @@ static class ClusterNameMatcher implements ArgumentMatcher { } @Override - public boolean matches(XdsConfig xdsConfig) { + public boolean matches(StatusOr update) { + if (!update.hasValue()) { + return false; + } + XdsConfig xdsConfig = update.getValue(); if (xdsConfig == null || xdsConfig.getClusters() == null) { return false; } diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 4a8193c932d..7425e3e31de 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -46,6 +46,7 @@ import com.google.re2j.Pattern; import io.grpc.CallOptions; import io.grpc.Channel; +import io.grpc.ChannelLogger; import io.grpc.ClientCall; import io.grpc.ClientInterceptor; import io.grpc.ClientInterceptors; @@ -70,6 +71,7 @@ import io.grpc.SynchronizationContext; import io.grpc.internal.AutoConfiguredLoadBalancerFactory; import io.grpc.internal.FakeClock; +import io.grpc.internal.GrpcUtil; import io.grpc.internal.JsonParser; import io.grpc.internal.JsonUtil; import io.grpc.internal.ObjectPool; @@ -89,6 +91,8 @@ import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy; import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher; +import io.grpc.xds.XdsClusterResource.CdsUpdate; +import io.grpc.xds.XdsEndpointResource.EdsUpdate; import io.grpc.xds.XdsListenerResource.LdsUpdate; import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import io.grpc.xds.client.Bootstrapper.AuthorityInfo; @@ -104,6 +108,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -187,6 +192,15 @@ public ConfigOrError parseServiceConfig(Map rawServiceConfig) { private TestCall testCall; private boolean originalEnableTimeout; private URI targetUri; + private final NameResolver.Args nameResolverArgs = NameResolver.Args.newBuilder() + .setDefaultPort(8080) + .setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR) + .setSynchronizationContext(syncContext) + .setServiceConfigParser(mock(NameResolver.ServiceConfigParser.class)) + .setChannelLogger(mock(ChannelLogger.class)) + .setScheduledExecutorService(fakeClock.getScheduledExecutorService()) + .build(); + @Before public void setUp() { @@ -213,7 +227,7 @@ public void setUp() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, filterRegistry, null, metricRecorder); + xdsClientPoolFactory, mockRandom, filterRegistry, null, metricRecorder, nameResolverArgs); } @After @@ -259,7 +273,7 @@ public List getTargets() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder); + metricRecorder, nameResolverArgs); resolver.start(mockListener); verify(mockListener).onError(errorCaptor.capture()); Status error = errorCaptor.getValue(); @@ -273,7 +287,7 @@ public void resolving_withTargetAuthorityNotFound() { resolver = new XdsNameResolver(targetUri, "notfound.google.com", AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder); + metricRecorder, nameResolverArgs); resolver.start(mockListener); verify(mockListener).onError(errorCaptor.capture()); Status error = errorCaptor.getValue(); @@ -295,7 +309,7 @@ public void resolving_noTargetAuthority_templateWithoutXdstp() { resolver = new XdsNameResolver( targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, - mockRandom, FilterRegistry.getDefaultRegistry(), null, metricRecorder); + mockRandom, FilterRegistry.getDefaultRegistry(), null, metricRecorder, nameResolverArgs); resolver.start(mockListener); verify(mockListener, never()).onError(any(Status.class)); } @@ -316,7 +330,7 @@ public void resolving_noTargetAuthority_templateWithXdstp() { resolver = new XdsNameResolver( targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder); + metricRecorder, nameResolverArgs); resolver.start(mockListener); verify(mockListener, never()).onError(any(Status.class)); } @@ -337,7 +351,7 @@ public void resolving_noTargetAuthority_xdstpWithMultipleSlashes() { resolver = new XdsNameResolver( targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder); + metricRecorder, nameResolverArgs); // The Service Authority must be URL encoded, but unlike the LDS resource name. @@ -366,7 +380,7 @@ public void resolving_targetAuthorityInAuthoritiesMap() { resolver = new XdsNameResolver(targetUri, "xds.authority.com", serviceAuthority, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder); + metricRecorder, nameResolverArgs); resolver.start(mockListener); verify(mockListener, never()).onError(any(Status.class)); } @@ -399,7 +413,7 @@ public void resolving_ldsResourceUpdateRdsName() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder); + metricRecorder, nameResolverArgs); // use different ldsResourceName and service authority. The virtualhost lookup should use // service authority. expectedLdsResourceName = "test-" + expectedLdsResourceName; @@ -413,6 +427,7 @@ public void resolving_ldsResourceUpdateRdsName() { Collections.singletonList(route1), ImmutableMap.of()); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); + createAndDeliverClusterUpdates(xdsClient, cluster1); verify(mockListener).onResult2(resolutionResultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster1), @@ -429,6 +444,7 @@ public void resolving_ldsResourceUpdateRdsName() { Collections.singletonList(route2), ImmutableMap.of()); xdsClient.deliverRdsUpdate(alternativeRdsResource, Collections.singletonList(virtualHost)); + createAndDeliverClusterUpdates(xdsClient, cluster2); // Two new service config updates triggered: // - with load balancing config being able to select cluster1 and cluster2 // - with load balancing config being able to select cluster2 only @@ -467,6 +483,7 @@ public void resolving_ldsResourceRevokedAndAddedBack() { Collections.singletonList(route), ImmutableMap.of()); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); + createAndDeliverClusterUpdates(xdsClient, cluster1); verify(mockListener).onResult2(resolutionResultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster1), @@ -483,6 +500,7 @@ public void resolving_ldsResourceRevokedAndAddedBack() { verifyNoInteractions(mockListener); assertThat(xdsClient.rdsResource).isEqualTo(RDS_RESOURCE_NAME); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); + createAndDeliverClusterUpdates(xdsClient, cluster1); verify(mockListener).onResult2(resolutionResultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster1), @@ -506,6 +524,7 @@ public void resolving_rdsResourceRevokedAndAddedBack() { Collections.singletonList(route), ImmutableMap.of()); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); + createAndDeliverClusterUpdates(xdsClient, cluster1); verify(mockListener).onResult2(resolutionResultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster1), @@ -529,11 +548,15 @@ public void resolving_encounterErrorLdsWatcherOnly() { resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverError(Status.UNAVAILABLE.withDescription("server unreachable")); - verify(mockListener).onError(errorCaptor.capture()); - Status error = errorCaptor.getValue(); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); + InternalConfigSelector configSelector = resolutionResultCaptor.getValue() + .getAttributes().get(InternalConfigSelector.KEY); + Result selectResult = configSelector.selectConfig( + newPickSubchannelArgs(call1.methodDescriptor, new Metadata(), CallOptions.DEFAULT)); + Status error = selectResult.getStatus(); assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); - assertThat(error.getDescription()).isEqualTo("Unable to load LDS " + AUTHORITY - + ". xDS server returned: UNAVAILABLE: server unreachable"); + assertThat(error.getDescription()).contains(AUTHORITY); + assertThat(error.getDescription()).contains("UNAVAILABLE: server unreachable"); } @Test @@ -541,11 +564,15 @@ public void resolving_translateErrorLds() { resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverError(Status.NOT_FOUND.withDescription("server unreachable")); - verify(mockListener).onError(errorCaptor.capture()); - Status error = errorCaptor.getValue(); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); + InternalConfigSelector configSelector = resolutionResultCaptor.getValue() + .getAttributes().get(InternalConfigSelector.KEY); + Result selectResult = configSelector.selectConfig( + newPickSubchannelArgs(call1.methodDescriptor, new Metadata(), CallOptions.DEFAULT)); + Status error = selectResult.getStatus(); assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); - assertThat(error.getDescription()).isEqualTo("Unable to load LDS " + AUTHORITY - + ". xDS server returned: NOT_FOUND: server unreachable"); + assertThat(error.getDescription()).contains(AUTHORITY); + assertThat(error.getDescription()).contains("NOT_FOUND: server unreachable"); assertThat(error.getCause()).isNull(); } @@ -555,15 +582,15 @@ public void resolving_encounterErrorLdsAndRdsWatchers() { FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdateForRdsName(RDS_RESOURCE_NAME); xdsClient.deliverError(Status.UNAVAILABLE.withDescription("server unreachable")); - verify(mockListener, times(2)).onError(errorCaptor.capture()); - Status error = errorCaptor.getAllValues().get(0); - assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); - assertThat(error.getDescription()).isEqualTo("Unable to load LDS " + AUTHORITY - + ". xDS server returned: UNAVAILABLE: server unreachable"); - error = errorCaptor.getAllValues().get(1); + verify(mockListener).onResult2(resolutionResultCaptor.capture()); + InternalConfigSelector configSelector = resolutionResultCaptor.getValue() + .getAttributes().get(InternalConfigSelector.KEY); + Result selectResult = configSelector.selectConfig( + newPickSubchannelArgs(call1.methodDescriptor, new Metadata(), CallOptions.DEFAULT)); + Status error = selectResult.getStatus(); assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); - assertThat(error.getDescription()).isEqualTo("Unable to load RDS " + RDS_RESOURCE_NAME - + ". xDS server returned: UNAVAILABLE: server unreachable"); + assertThat(error.getDescription()).contains(RDS_RESOURCE_NAME); + assertThat(error.getDescription()).contains("UNAVAILABLE: server unreachable"); } @SuppressWarnings("unchecked") @@ -581,10 +608,11 @@ public void resolving_matchingVirtualHostNotFound_matchingOverrideAuthority() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, "random", serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder); + metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate(0L, Arrays.asList(virtualHost)); + createAndDeliverClusterUpdates(xdsClient, cluster1); verify(mockListener).onResult2(resolutionResultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster1), @@ -605,10 +633,11 @@ public void resolving_matchingVirtualHostNotFound_notMatchingOverrideAuthority() resolver = new XdsNameResolver(targetUri, null, AUTHORITY, "random", serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder); + metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); - xdsClient.deliverLdsUpdate(0L, Arrays.asList(virtualHost)); + xdsClient.deliverLdsUpdateOnly(0L, Arrays.asList(virtualHost)); + fakeClock.forwardTime(15, TimeUnit.SECONDS); assertEmptyResolutionResult("random"); } @@ -617,7 +646,7 @@ public void resolving_matchingVirtualHostNotFoundForOverrideAuthority() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, AUTHORITY, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder); + metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate(0L, buildUnmatchedVirtualHosts()); @@ -702,7 +731,7 @@ public void retryPolicyInPerMethodConfigGeneratedByResolverIsValid() { true, 5, 5, new AutoConfiguredLoadBalancerFactory("pick-first")); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, realParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder); + metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); RetryPolicy retryPolicy = RetryPolicy.create( @@ -913,7 +942,7 @@ public void resolved_rpcHashingByChannelId() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder); + metricRecorder, nameResolverArgs); resolver.start(mockListener); xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( @@ -946,7 +975,7 @@ public void resolved_rpcHashingByChannelId() { public void resolved_routeActionHasAutoHostRewrite_emitsCallOptionForTheSame() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, - FilterRegistry.getDefaultRegistry(), null, metricRecorder); + FilterRegistry.getDefaultRegistry(), null, metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( @@ -977,7 +1006,7 @@ public void resolved_routeActionHasAutoHostRewrite_emitsCallOptionForTheSame() { public void resolved_routeActionNoAutoHostRewrite_doesntEmitCallOptionForTheSame() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, - FilterRegistry.getDefaultRegistry(), null, metricRecorder); + FilterRegistry.getDefaultRegistry(), null, metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( @@ -1190,6 +1219,20 @@ public void resolved_simpleCallSucceeds_routeToWeightedCluster() { assertCallSelectClusterResult(call1, configSelector, cluster1, 20.0); } + /** Creates and delivers both CDS and EDS updates for the given clusters. */ + private static void createAndDeliverClusterUpdates( + FakeXdsClient xdsClient, String... clusterNames) { + for (String clusterName : clusterNames) { + CdsUpdate.Builder forEds = CdsUpdate + .forEds(clusterName, clusterName, null, null, null, null, false) + .roundRobinLbPolicy(); + xdsClient.deliverCdsUpdate(clusterName, forEds.build()); + EdsUpdate edsUpdate = new EdsUpdate(clusterName, + XdsTestUtils.createMinimalLbEndpointsMap("host"), Collections.emptyList()); + xdsClient.deliverEdsUpdate(clusterName, edsUpdate); + } + } + @Test public void resolved_simpleCallSucceeds_routeToRls() { when(mockRandom.nextInt(anyInt())).thenReturn(90, 10); @@ -1305,6 +1348,7 @@ public void filterState_survivesLds() { // LDS 1. xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + createAndDeliverClusterUpdates(xdsClient, cluster1); assertClusterResolutionResult(call1, cluster1); ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); // Verify that StatefulFilter with different filter names result in different Filter instances. @@ -1359,7 +1403,7 @@ public void filterState_survivesLds() { * Verifies the lifecycle of HCM filter instances across RDS updates. * *

Filter instances: - * 1. Must have instantiated by the initial LDS. + * 1. Must have instantiated by the initial LDS/RDS. * 2. Must be reused by all subsequent RDS updates. * 3. Must be not shutdown (closed) by valid RDS updates. */ @@ -1371,22 +1415,19 @@ public void filterState_survivesRds() { // LDS 1. xdsClient.deliverLdsUpdateForRdsNameWithFilters(RDS_RESOURCE_NAME, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); - ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); - // Verify that StatefulFilter with different filter names result in different Filter instances. - assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); - // Naming: ldsFilter - StatefulFilter lds1Filter1 = lds1Snapshot.get(0); - StatefulFilter lds1Filter2 = lds1Snapshot.get(1); - assertThat(lds1Filter1).isNotSameInstanceAs(lds1Filter2); - // RDS 1. VirtualHost vhost1 = filterStateTestVhost(); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, vhost1); + createAndDeliverClusterUpdates(xdsClient, cluster1); assertClusterResolutionResult(call1, cluster1); // Initial RDS update should not generate Filter instances. ImmutableList rds1Snapshot = statefulFilterProvider.getAllInstances(); - assertWithMessage("RDS 1: Expected Filter instances to be reused across RDS route updates") - .that(rds1Snapshot).isEqualTo(lds1Snapshot); + // Verify that StatefulFilter with different filter names result in different Filter instances. + assertWithMessage("RDS 1: expected to create filter instances").that(rds1Snapshot).hasSize(2); + // Naming: ldsFilter + StatefulFilter lds1Filter1 = rds1Snapshot.get(0); + StatefulFilter lds1Filter2 = rds1Snapshot.get(1); + assertThat(lds1Filter1).isNotSameInstanceAs(lds1Filter2); // RDS 2: exactly the same as RDS 1. xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, vhost1); @@ -1394,7 +1435,7 @@ public void filterState_survivesRds() { ImmutableList rds2Snapshot = statefulFilterProvider.getAllInstances(); // Neither should any subsequent RDS updates. assertWithMessage("RDS 2: Expected Filter instances to be reused across RDS route updates") - .that(rds2Snapshot).isEqualTo(lds1Snapshot); + .that(rds2Snapshot).isEqualTo(rds1Snapshot); // RDS 3: Contains a per-route override for STATEFUL_1. VirtualHost vhost3 = filterStateTestVhost(ImmutableMap.of( @@ -1406,7 +1447,7 @@ public void filterState_survivesRds() { // As with any other Route update, typed_per_filter_config overrides should not result in // creating new filter instances. assertWithMessage("RDS 3: Expected Filter instances to be reused on per-route filter overrides") - .that(rds3Snapshot).isEqualTo(lds1Snapshot); + .that(rds3Snapshot).isEqualTo(rds1Snapshot); } /** @@ -1427,7 +1468,7 @@ public void filterState_specialCase_sameNameDifferentTypeUrl() { .register(statefulFilterProvider, altStatefulFilterProvider, ROUTER_FILTER_PROVIDER); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, null, - metricRecorder); + metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); @@ -1435,6 +1476,7 @@ public void filterState_specialCase_sameNameDifferentTypeUrl() { // LDS 1. xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + createAndDeliverClusterUpdates(xdsClient, cluster1); assertClusterResolutionResult(call1, cluster1); ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); ImmutableList lds1SnapshotAlt = altStatefulFilterProvider.getAllInstances(); @@ -1483,6 +1525,7 @@ public void filterState_shutdown_onLdsNotFound() { // LDS 1. xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + createAndDeliverClusterUpdates(xdsClient, cluster1); assertClusterResolutionResult(call1, cluster1); ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); @@ -1510,6 +1553,7 @@ public void filterState_shutdown_onResolverShutdown() { // LDS 1. xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + createAndDeliverClusterUpdates(xdsClient, cluster1); assertClusterResolutionResult(call1, cluster1); ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); @@ -1526,33 +1570,32 @@ public void filterState_shutdown_onResolverShutdown() { } /** - * Verifies that filter instances are NOT shutdown on RDS_RESOURCE_NAME not found. + * Verifies that all filter instances are shutdown (closed) on RDS resource not found. */ @Test - public void filterState_shutdown_noShutdownOnRdsNotFound() { + public void filterState_shutdown_onRdsNotFound() { StatefulFilter.Provider statefulFilterProvider = filterStateTestSetupResolver(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); // LDS 1. xdsClient.deliverLdsUpdateForRdsNameWithFilters(RDS_RESOURCE_NAME, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); - ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); - assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); - // Naming: ldsFilter - StatefulFilter lds1Filter1 = lds1Snapshot.get(0); - StatefulFilter lds1Filter2 = lds1Snapshot.get(1); - // RDS 1: Standard vhost with a route. xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, filterStateTestVhost()); + createAndDeliverClusterUpdates(xdsClient, cluster1); assertClusterResolutionResult(call1, cluster1); - assertThat(statefulFilterProvider.getAllInstances()).isEqualTo(lds1Snapshot); + ImmutableList rds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("RDS 1: expected to create filter instances").that(rds1Snapshot).hasSize(2); + // Naming: ldsFilter + StatefulFilter lds1Filter1 = rds1Snapshot.get(0); + StatefulFilter lds1Filter2 = rds1Snapshot.get(1); // RDS 2: RDS_RESOURCE_NAME not found. reset(mockListener); xdsClient.deliverRdsResourceNotFound(RDS_RESOURCE_NAME); assertEmptyResolutionResult(RDS_RESOURCE_NAME); - assertThat(lds1Filter1.isShutdown()).isFalse(); - assertThat(lds1Filter2.isShutdown()).isFalse(); + assertThat(lds1Filter1.isShutdown()).isTrue(); + assertThat(lds1Filter2.isShutdown()).isTrue(); } private StatefulFilter.Provider filterStateTestSetupResolver() { @@ -1561,7 +1604,7 @@ private StatefulFilter.Provider filterStateTestSetupResolver() { .register(statefulFilterProvider, ROUTER_FILTER_PROVIDER); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, null, - metricRecorder); + metricRecorder, nameResolverArgs); resolver.start(mockListener); return statefulFilterProvider; } @@ -1762,6 +1805,7 @@ public void generateServiceConfig_forClusterManagerLoadBalancingConfig() throws ImmutableList.of(route1, route2, route3), ImmutableMap.of()); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); + createAndDeliverClusterUpdates(xdsClient, "cluster-foo", "cluster-bar", "cluster-baz"); verify(mockListener).onResult2(resolutionResultCaptor.capture()); String expectedServiceConfigJson = @@ -2385,6 +2429,8 @@ private class FakeXdsClient extends XdsClient { private String rdsResource; private ResourceWatcher ldsWatcher; private ResourceWatcher rdsWatcher; + private final Map>> cdsWatchers = new HashMap<>(); + private final Map>> edsWatchers = new HashMap<>(); @Override public BootstrapInfo getBootstrapInfo() { @@ -2412,10 +2458,19 @@ public void watchXdsResource(XdsResourceType resou rdsResource = resourceName; rdsWatcher = (ResourceWatcher) watcher; break; + case "CDS": + cdsWatchers.computeIfAbsent(resourceName, k -> new ArrayList<>()) + .add((ResourceWatcher) watcher); + break; + case "EDS": + edsWatchers.computeIfAbsent(resourceName, k -> new ArrayList<>()) + .add((ResourceWatcher) watcher); + break; default: } } + @SuppressWarnings("unchecked") @Override public void cancelXdsResourceWatch(XdsResourceType type, String resourceName, @@ -2434,14 +2489,37 @@ public void cancelXdsResourceWatch(XdsResourceType rdsResource = null; rdsWatcher = null; break; + case "CDS": + assertThat(cdsWatchers).containsKey(resourceName); + assertThat(cdsWatchers.get(resourceName)).contains(watcher); + cdsWatchers.get(resourceName).remove((ResourceWatcher) watcher); + break; + case "EDS": + assertThat(edsWatchers).containsKey(resourceName); + assertThat(edsWatchers.get(resourceName)).contains(watcher); + edsWatchers.get(resourceName).remove((ResourceWatcher) watcher); + break; default: } } + void deliverLdsUpdateOnly(long httpMaxStreamDurationNano, List virtualHosts) { + syncContext.execute(() -> { + ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( + httpMaxStreamDurationNano, virtualHosts, null))); + }); + } + void deliverLdsUpdate(long httpMaxStreamDurationNano, List virtualHosts) { + List clusterNames = new ArrayList<>(); + for (VirtualHost vh : virtualHosts) { + clusterNames.addAll(getClusterNames(vh.routes())); + } + syncContext.execute(() -> { ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( httpMaxStreamDurationNano, virtualHosts, null))); + createAndDeliverClusterUpdates(this, clusterNames.toArray(new String[0])); }); } @@ -2450,9 +2528,14 @@ void deliverLdsUpdate(final List routes) { VirtualHost.create( "virtual-host", Collections.singletonList(expectedLdsResourceName), routes, ImmutableMap.of()); + List clusterNames = getClusterNames(routes); + syncContext.execute(() -> { ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( 0L, Collections.singletonList(virtualHost), null))); + if (!clusterNames.isEmpty()) { + createAndDeliverClusterUpdates(this, clusterNames.toArray(new String[0])); + } }); } @@ -2508,6 +2591,7 @@ void deliverLdsUpdateWithFaultInjection( syncContext.execute(() -> { ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( 0L, Collections.singletonList(virtualHost), filterChain))); + createAndDeliverClusterUpdates(this, cluster); }); } @@ -2545,6 +2629,29 @@ void deliverLdsResourceNotFound() { }); } + private List getClusterNames(List routes) { + List clusterNames = new ArrayList<>(); + for (Route r : routes) { + if (r.routeAction() == null) { + continue; + } + String cluster = r.routeAction().cluster(); + if (cluster != null) { + clusterNames.add(cluster); + } else { + List weightedClusters = r.routeAction().weightedClusters(); + if (weightedClusters == null) { + continue; + } + for (ClusterWeight wc : weightedClusters) { + clusterNames.add(wc.name()); + } + } + } + + return clusterNames; + } + void deliverRdsUpdateWithFaultInjection( String resourceName, @Nullable FaultConfig virtualHostFaultConfig, @Nullable FaultConfig routFaultConfig, @Nullable FaultConfig weightedClusterFaultConfig) { @@ -2581,6 +2688,7 @@ void deliverRdsUpdateWithFaultInjection( overrideConfig); syncContext.execute(() -> { rdsWatcher.onChanged(new RdsUpdate(Collections.singletonList(virtualHost))); + createAndDeliverClusterUpdates(this, cluster1); }); } @@ -2606,6 +2714,29 @@ void deliverRdsResourceNotFound(String resourceName) { }); } + private void deliverCdsUpdate(String clusterName, CdsUpdate update) { + if (!cdsWatchers.containsKey(clusterName)) { + return; + } + syncContext.execute(() -> { + List> resourceWatchers = + ImmutableList.copyOf(cdsWatchers.get(clusterName)); + resourceWatchers.forEach(w -> w.onChanged(update)); + }); + } + + private void deliverEdsUpdate(String name, EdsUpdate update) { + syncContext.execute(() -> { + if (!edsWatchers.containsKey(name)) { + return; + } + List> resourceWatchers = + ImmutableList.copyOf(edsWatchers.get(name)); + resourceWatchers.forEach(w -> w.onChanged(update)); + }); + } + + void deliverError(final Status error) { if (ldsWatcher != null) { syncContext.execute(() -> { @@ -2617,6 +2748,11 @@ void deliverError(final Status error) { rdsWatcher.onError(error); }); } + syncContext.execute(() -> { + cdsWatchers.values().stream() + .flatMap(List::stream) + .forEach(w -> w.onError(error)); + }); } } diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java index d0580ae2667..9f90777be3d 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -51,7 +51,6 @@ import io.grpc.BindableService; import io.grpc.Context; import io.grpc.Context.CancellationListener; -import io.grpc.Status; import io.grpc.StatusOr; import io.grpc.internal.JsonParser; import io.grpc.stub.StreamObserver; @@ -281,6 +280,16 @@ static XdsConfig getDefaultXdsConfig(String serverHostName) return builder.build(); } + static Map createMinimalLbEndpointsMap(String serverHostName) { + Map lbEndpointsMap = new HashMap<>(); + LbEndpoint lbEndpoint = LbEndpoint.create( + serverHostName, ENDPOINT_PORT, 0, true, ENDPOINT_HOSTNAME, ImmutableMap.of()); + lbEndpointsMap.put( + Locality.create("", "", ""), + LocalityLbEndpoints.create(ImmutableList.of(lbEndpoint), 10, 0, ImmutableMap.of())); + return lbEndpointsMap; + } + @SuppressWarnings("unchecked") private static ImmutableMap getWrrLbConfigAsMap() throws IOException { String lbConfigStr = "{\"wrr_locality_experimental\" : " @@ -353,7 +362,6 @@ static Listener buildInlineClientListener(String rdsName, String clusterName, St return Listener.newBuilder() .setName(serverName) .setApiListener(clientListenerBuilder.build()).build(); - } /** @@ -407,18 +415,4 @@ protected void sendResponse(List clusters, long loadReportIntervalNano) responseObserver.onNext(response); } } - - static class StatusMatcher implements ArgumentMatcher { - private final Status expectedStatus; - - StatusMatcher(Status expectedStatus) { - this.expectedStatus = expectedStatus; - } - - @Override - public boolean matches(Status status) { - return status != null && expectedStatus.getCode().equals(status.getCode()) - && expectedStatus.getDescription().equals(status.getDescription()); - } - } } From a57c14a51ea0d1551ff2bef54844d6da8ed5c7db Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Wed, 19 Mar 2025 03:57:34 +0000 Subject: [PATCH 217/591] refactor: Stops exception allocation on channel shutdown This fixes #11955. Stops exception allocation and its propagation on channel shutdown. --- .../io/grpc/binder/internal/PingTracker.java | 2 +- .../grpc/binder/internal/PingTrackerTest.java | 10 ++--- .../io/grpc/internal/ClientTransport.java | 3 +- .../grpc/internal/FailingClientTransport.java | 2 +- .../main/java/io/grpc/internal/Http2Ping.java | 9 +++-- .../io/grpc/internal/KeepAliveManager.java | 2 +- .../grpc/internal/KeepAliveManagerTest.java | 2 +- .../grpc/internal/AbstractTransportTest.java | 6 +-- .../io/grpc/inprocess/InProcessTransport.java | 2 +- .../ClientTransportLifecycleManager.java | 5 --- .../io/grpc/netty/NettyClientHandler.java | 37 ++++++++++--------- .../io/grpc/netty/NettyClientTransport.java | 4 +- .../io/grpc/netty/NettyClientHandlerTest.java | 9 ++--- .../grpc/netty/NettyClientTransportTest.java | 4 +- .../io/grpc/okhttp/OkHttpClientTransport.java | 6 +-- .../okhttp/OkHttpClientTransportTest.java | 19 +++------- 16 files changed, 56 insertions(+), 66 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/PingTracker.java b/binder/src/main/java/io/grpc/binder/internal/PingTracker.java index ab20af4d6ef..5a4300443ba 100644 --- a/binder/src/main/java/io/grpc/binder/internal/PingTracker.java +++ b/binder/src/main/java/io/grpc/binder/internal/PingTracker.java @@ -99,7 +99,7 @@ private final class Ping { private synchronized void fail(Status status) { if (!done) { done = true; - executor.execute(() -> callback.onFailure(status.asException())); + executor.execute(() -> callback.onFailure(status)); } } diff --git a/binder/src/test/java/io/grpc/binder/internal/PingTrackerTest.java b/binder/src/test/java/io/grpc/binder/internal/PingTrackerTest.java index 60e7c163105..c662cafe5fa 100644 --- a/binder/src/test/java/io/grpc/binder/internal/PingTrackerTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/PingTrackerTest.java @@ -96,7 +96,7 @@ private static final class TestCallback implements ClientTransport.PingCallback private int numCallbacks; private boolean success; private boolean failure; - private Throwable failureException; + private Status failureStatus; private long roundtripTimeNanos; @Override @@ -107,10 +107,10 @@ public synchronized void onSuccess(long roundtripTimeNanos) { } @Override - public synchronized void onFailure(Throwable failureException) { + public synchronized void onFailure(Status failureStatus) { numCallbacks += 1; failure = true; - this.failureException = failureException; + this.failureStatus = failureStatus; } public void assertNotCalled() { @@ -130,13 +130,13 @@ public void assertSuccess(long expectRoundTripTimeNanos) { public void assertFailure(Status status) { assertThat(numCallbacks).isEqualTo(1); assertThat(failure).isTrue(); - assertThat(((StatusException) failureException).getStatus()).isSameInstanceAs(status); + assertThat(failureStatus).isSameInstanceAs(status); } public void assertFailure(Status.Code statusCode) { assertThat(numCallbacks).isEqualTo(1); assertThat(failure).isTrue(); - assertThat(((StatusException) failureException).getStatus().getCode()).isEqualTo(statusCode); + assertThat(failureStatus.getCode()).isEqualTo(statusCode); } } } diff --git a/core/src/main/java/io/grpc/internal/ClientTransport.java b/core/src/main/java/io/grpc/internal/ClientTransport.java index 98041cc6e79..fd0f30b8bf1 100644 --- a/core/src/main/java/io/grpc/internal/ClientTransport.java +++ b/core/src/main/java/io/grpc/internal/ClientTransport.java @@ -22,6 +22,7 @@ import io.grpc.InternalInstrumented; import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import io.grpc.Status; import java.util.concurrent.Executor; import javax.annotation.concurrent.ThreadSafe; @@ -90,6 +91,6 @@ interface PingCallback { * * @param cause the cause of the ping failure */ - void onFailure(Throwable cause); + void onFailure(Status cause); } } diff --git a/core/src/main/java/io/grpc/internal/FailingClientTransport.java b/core/src/main/java/io/grpc/internal/FailingClientTransport.java index 5b31e6e5073..37194c46a29 100644 --- a/core/src/main/java/io/grpc/internal/FailingClientTransport.java +++ b/core/src/main/java/io/grpc/internal/FailingClientTransport.java @@ -55,7 +55,7 @@ public ClientStream newStream( public void ping(final PingCallback callback, Executor executor) { executor.execute(new Runnable() { @Override public void run() { - callback.onFailure(error.asException()); + callback.onFailure(error); } }); } diff --git a/core/src/main/java/io/grpc/internal/Http2Ping.java b/core/src/main/java/io/grpc/internal/Http2Ping.java index d96ac3ef214..e3520295625 100644 --- a/core/src/main/java/io/grpc/internal/Http2Ping.java +++ b/core/src/main/java/io/grpc/internal/Http2Ping.java @@ -18,6 +18,7 @@ import com.google.common.base.Stopwatch; import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.grpc.Status; import io.grpc.internal.ClientTransport.PingCallback; import java.util.LinkedHashMap; import java.util.Map; @@ -62,7 +63,7 @@ public class Http2Ping { /** * If non-null, indicates the ping failed. */ - @GuardedBy("this") private Throwable failureCause; + @GuardedBy("this") private Status failureCause; /** * The round-trip time for the ping, in nanoseconds. This value is only meaningful when @@ -144,7 +145,7 @@ public boolean complete() { * * @param failureCause the cause of failure */ - public void failed(Throwable failureCause) { + public void failed(Status failureCause) { Map callbacks; synchronized (this) { if (completed) { @@ -167,7 +168,7 @@ public void failed(Throwable failureCause) { * @param executor the executor used to invoke the callback * @param cause the cause of failure */ - public static void notifyFailed(PingCallback callback, Executor executor, Throwable cause) { + public static void notifyFailed(PingCallback callback, Executor executor, Status cause) { doExecute(executor, asRunnable(callback, cause)); } @@ -203,7 +204,7 @@ public void run() { * failure. */ private static Runnable asRunnable(final ClientTransport.PingCallback callback, - final Throwable failureCause) { + final Status failureCause) { return new Runnable() { @Override public void run() { diff --git a/core/src/main/java/io/grpc/internal/KeepAliveManager.java b/core/src/main/java/io/grpc/internal/KeepAliveManager.java index aed590c3051..d831a096087 100644 --- a/core/src/main/java/io/grpc/internal/KeepAliveManager.java +++ b/core/src/main/java/io/grpc/internal/KeepAliveManager.java @@ -275,7 +275,7 @@ public void ping() { public void onSuccess(long roundTripTimeNanos) {} @Override - public void onFailure(Throwable cause) { + public void onFailure(Status cause) { transport.shutdownNow(Status.UNAVAILABLE.withDescription( "Keepalive failed. The connection is likely gone")); } diff --git a/core/src/test/java/io/grpc/internal/KeepAliveManagerTest.java b/core/src/test/java/io/grpc/internal/KeepAliveManagerTest.java index 411a9fbe9fc..3cf7bfcedfe 100644 --- a/core/src/test/java/io/grpc/internal/KeepAliveManagerTest.java +++ b/core/src/test/java/io/grpc/internal/KeepAliveManagerTest.java @@ -127,7 +127,7 @@ public void clientKeepAlivePinger_pingFailure() { verify(transport).ping(pingCallbackCaptor.capture(), isA(Executor.class)); ClientTransport.PingCallback pingCallback = pingCallbackCaptor.getValue(); - pingCallback.onFailure(new Throwable()); + pingCallback.onFailure(Status.UNAVAILABLE.withDescription("I must write descriptions")); ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); verify(transport).shutdownNow(statusCaptor.capture()); diff --git a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java index aea7ff49032..4a518895db6 100644 --- a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java +++ b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java @@ -181,7 +181,7 @@ public void log(ChannelLogLevel level, String messageFormat, Object... args) {} protected ManagedClientTransport.Listener mockClientTransportListener = mock(ManagedClientTransport.Listener.class); protected MockServerListener serverListener = new MockServerListener(); - private ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + private ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); protected final TestClientStreamTracer clientStreamTracer1 = new TestHeaderClientStreamTracer(); private final TestClientStreamTracer clientStreamTracer2 = new TestHeaderClientStreamTracer(); protected final ClientStreamTracer[] tracers = new ClientStreamTracer[] { @@ -626,8 +626,8 @@ public void ping_afterTermination() throws Exception { // Transport doesn't support ping, so this neither passes nor fails. assumeTrue(false); } - verify(mockPingCallback, timeout(TIMEOUT_MS)).onFailure(throwableCaptor.capture()); - Status status = Status.fromThrowable(throwableCaptor.getValue()); + verify(mockPingCallback, timeout(TIMEOUT_MS)).onFailure(statusCaptor.capture()); + Status status = statusCaptor.getValue(); assertSame(shutdownReason, status); } diff --git a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java index 39ebe6e0ab7..e294b4eb63f 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java @@ -246,7 +246,7 @@ public synchronized void ping(final PingCallback callback, Executor executor) { executor.execute(new Runnable() { @Override public void run() { - callback.onFailure(shutdownStatus.asRuntimeException()); + callback.onFailure(shutdownStatus); } }); } else { diff --git a/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java b/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java index 34f72ab97bd..b4e53d5568c 100644 --- a/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java +++ b/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java @@ -30,7 +30,6 @@ final class ClientTransportLifecycleManager { /** null iff !transportShutdown. */ private Status shutdownStatus; /** null iff !transportShutdown. */ - private Throwable shutdownThrowable; private boolean transportTerminated; public ClientTransportLifecycleManager(ManagedClientTransport.Listener listener) { @@ -72,7 +71,6 @@ public boolean notifyShutdown(Status s) { return false; } shutdownStatus = s; - shutdownThrowable = s.asException(); return true; } @@ -97,7 +95,4 @@ public Status getShutdownStatus() { return shutdownStatus; } - public Throwable getShutdownThrowable() { - return shutdownThrowable; - } } diff --git a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java index 19f1903c0b5..a5fa0f80077 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java @@ -499,7 +499,7 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { streamStatus = lifecycleManager.getShutdownStatus(); } try { - cancelPing(lifecycleManager.getShutdownThrowable()); + cancelPing(lifecycleManager.getShutdownStatus()); // Report status to the application layer for any open streams connection().forEachActiveStream(new Http2StreamVisitor() { @Override @@ -593,13 +593,14 @@ protected boolean isGracefulShutdownComplete() { */ private void createStream(CreateStreamCommand command, ChannelPromise promise) throws Exception { - if (lifecycleManager.getShutdownThrowable() != null) { + if (lifecycleManager.getShutdownStatus() != null) { command.stream().setNonExistent(); // The connection is going away (it is really the GOAWAY case), // just terminate the stream now. command.stream().transportReportStatus( lifecycleManager.getShutdownStatus(), RpcProgress.MISCARRIED, true, new Metadata()); - promise.setFailure(lifecycleManager.getShutdownThrowable()); + promise.setFailure(InternalStatus.asRuntimeExceptionWithoutStacktrace( + lifecycleManager.getShutdownStatus(), null)); return; } @@ -852,19 +853,21 @@ private void sendPingFrameTraced(ChannelHandlerContext ctx, SendPingCommand msg, public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { transportTracer.reportKeepAliveSent(); - } else { - Throwable cause = future.cause(); - if (cause instanceof ClosedChannelException) { - cause = lifecycleManager.getShutdownThrowable(); - if (cause == null) { - cause = Status.UNKNOWN.withDescription("Ping failed but for unknown reason.") - .withCause(future.cause()).asException(); - } - } - finalPing.failed(cause); - if (ping == finalPing) { - ping = null; + return; + } + Throwable cause = future.cause(); + Status status = lifecycleManager.getShutdownStatus(); + if (cause instanceof ClosedChannelException) { + if (status == null) { + status = Status.UNKNOWN.withDescription("Ping failed but for unknown reason.") + .withCause(future.cause()); } + } else { + status = Utils.statusFromThrowable(cause); + } + finalPing.failed(status); + if (ping == finalPing) { + ping = null; } } }); @@ -963,9 +966,9 @@ public boolean visit(Http2Stream stream) throws Http2Exception { } } - private void cancelPing(Throwable t) { + private void cancelPing(Status s) { if (ping != null) { - ping.failed(t); + ping.failed(s); ping = null; } } diff --git a/netty/src/main/java/io/grpc/netty/NettyClientTransport.java b/netty/src/main/java/io/grpc/netty/NettyClientTransport.java index 86d8991ba95..e03989e9906 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientTransport.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientTransport.java @@ -165,7 +165,7 @@ public void ping(final PingCallback callback, final Executor executor) { executor.execute(new Runnable() { @Override public void run() { - callback.onFailure(statusExplainingWhyTheChannelIsNull.asException()); + callback.onFailure(statusExplainingWhyTheChannelIsNull); } }); return; @@ -177,7 +177,7 @@ public void run() { public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { Status s = statusFromFailedFuture(future); - Http2Ping.notifyFailed(callback, executor, s.asException()); + Http2Ping.notifyFailed(callback, executor, s); } } }; diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java index 945c6c1267a..f8fbeea9b82 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java @@ -59,7 +59,6 @@ import io.grpc.CallOptions; import io.grpc.Metadata; import io.grpc.Status; -import io.grpc.StatusException; import io.grpc.internal.AbstractStream; import io.grpc.internal.ClientStreamListener; import io.grpc.internal.ClientStreamListener.RpcProgress; @@ -812,9 +811,7 @@ public void ping_failsWhenChannelCloses() throws Exception { handler().channelInactive(ctx()); // ping failed on channel going inactive assertEquals(1, callback.invocationCount); - assertTrue(callback.failureCause instanceof StatusException); - assertEquals(Status.Code.UNAVAILABLE, - ((StatusException) callback.failureCause).getStatus().getCode()); + assertEquals(Status.Code.UNAVAILABLE, callback.failureCause.getCode()); // A failed ping is still counted assertEquals(1, transportTracer.getStats().keepAlivesSent); } @@ -1169,7 +1166,7 @@ private static CreateStreamCommand newCreateStreamCommand( private static class PingCallbackImpl implements ClientTransport.PingCallback { int invocationCount; long roundTripTime; - Throwable failureCause; + Status failureCause; @Override public void onSuccess(long roundTripTimeNanos) { @@ -1178,7 +1175,7 @@ public void onSuccess(long roundTripTimeNanos) { } @Override - public void onFailure(Throwable cause) { + public void onFailure(Status cause) { invocationCount++; this.failureCause = cause; } diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index 9b3b2e386d3..65683dd8396 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -548,8 +548,8 @@ public void onSuccess(long roundTripTimeNanos) { } @Override - public void onFailure(Throwable cause) { - pingResult.setException(cause); + public void onFailure(Status cause) { + pingResult.setException(cause.asException()); } }; transport.ping(pingCallback, clock.getScheduledExecutorService()); diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java index 055d6e08161..7c107188518 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java @@ -1062,12 +1062,12 @@ private void setInUse(OkHttpClientStream stream) { } } - private Throwable getPingFailure() { + private Status getPingFailure() { synchronized (lock) { if (goAwayStatus != null) { - return goAwayStatus.asException(); + return goAwayStatus; } else { - return Status.UNAVAILABLE.withDescription("Connection closed").asException(); + return Status.UNAVAILABLE.withDescription("Connection closed"); } } } diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java index daf5073992e..826dee8e2b4 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java @@ -67,7 +67,6 @@ import io.grpc.MethodDescriptor.MethodType; import io.grpc.Status; import io.grpc.Status.Code; -import io.grpc.StatusException; import io.grpc.internal.AbstractStream; import io.grpc.internal.ClientStreamListener; import io.grpc.internal.ClientTransport; @@ -1664,16 +1663,14 @@ public void ping_failsWhenTransportShutdown() throws Exception { clientTransport.shutdown(SHUTDOWN_REASON); // ping failed on channel shutdown assertEquals(1, callback.invocationCount); - assertTrue(callback.failureCause instanceof StatusException); - assertSame(SHUTDOWN_REASON, ((StatusException) callback.failureCause).getStatus()); + assertSame(SHUTDOWN_REASON, callback.failureCause); // now that handler is in terminal state, all future pings fail immediately callback = new PingCallbackImpl(); clientTransport.ping(callback, MoreExecutors.directExecutor()); assertEquals(1, getTransportStats(clientTransport).keepAlivesSent); assertEquals(1, callback.invocationCount); - assertTrue(callback.failureCause instanceof StatusException); - assertSame(SHUTDOWN_REASON, ((StatusException) callback.failureCause).getStatus()); + assertSame(SHUTDOWN_REASON, callback.failureCause); shutdownAndVerify(); } @@ -1688,18 +1685,14 @@ public void ping_failsIfTransportFails() throws Exception { clientTransport.onException(new IOException()); // ping failed on error assertEquals(1, callback.invocationCount); - assertTrue(callback.failureCause instanceof StatusException); - assertEquals(Status.Code.UNAVAILABLE, - ((StatusException) callback.failureCause).getStatus().getCode()); + assertEquals(Status.Code.UNAVAILABLE, callback.failureCause.getCode()); // now that handler is in terminal state, all future pings fail immediately callback = new PingCallbackImpl(); clientTransport.ping(callback, MoreExecutors.directExecutor()); assertEquals(1, getTransportStats(clientTransport).keepAlivesSent); assertEquals(1, callback.invocationCount); - assertTrue(callback.failureCause instanceof StatusException); - assertEquals(Status.Code.UNAVAILABLE, - ((StatusException) callback.failureCause).getStatus().getCode()); + assertEquals(Status.Code.UNAVAILABLE, callback.failureCause.getCode()); shutdownAndVerify(); } @@ -2385,7 +2378,7 @@ public InputStream getInputStream() { static class PingCallbackImpl implements ClientTransport.PingCallback { int invocationCount; long roundTripTime; - Throwable failureCause; + Status failureCause; @Override public void onSuccess(long roundTripTimeNanos) { @@ -2394,7 +2387,7 @@ public void onSuccess(long roundTripTimeNanos) { } @Override - public void onFailure(Throwable cause) { + public void onFailure(Status cause) { invocationCount++; this.failureCause = cause; } From bc3c7640588e5bc4ee4175f36a05ba59f67d0415 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 18 Mar 2025 15:17:13 -0700 Subject: [PATCH 218/591] xds: Include XdsConfig as a CallOption This allows Filters to access the xds configuration for their own processing. From gRFC A83: > This data is available via the XdsConfig attribute introduced in A74. > If the xDS ConfigSelector is not already passing that attribute to the > filters, it will need to be changed to do so. --- xds/src/main/java/io/grpc/xds/XdsNameResolver.java | 14 +++++++++++--- .../test/java/io/grpc/xds/XdsNameResolverTest.java | 4 ++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 5c1b3105c45..123d3a77172 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -94,6 +94,8 @@ final class XdsNameResolver extends NameResolver { static final CallOptions.Key CLUSTER_SELECTION_KEY = CallOptions.Key.create("io.grpc.xds.CLUSTER_SELECTION_KEY"); + static final CallOptions.Key XDS_CONFIG_CALL_OPTION_KEY = + CallOptions.Key.create("io.grpc.xds.XDS_CONFIG_CALL_OPTION_KEY"); static final CallOptions.Key RPC_HASH_KEY = CallOptions.Key.create("io.grpc.xds.RPC_HASH_KEY"); static final CallOptions.Key AUTO_HOST_REWRITE_KEY = @@ -467,6 +469,7 @@ public Result selectConfig(PickSubchannelArgs args) { "Failed to parse service config (method config)")); } final String finalCluster = cluster; + final XdsConfig xdsConfig = routingCfg.xdsConfig; final long hash = generateHash(routeAction.hashPolicies(), headers); class ClusterSelectionInterceptor implements ClientInterceptor { @Override @@ -475,6 +478,7 @@ public ClientCall interceptCall( final Channel next) { CallOptions callOptionsForCluster = callOptions.withOption(CLUSTER_SELECTION_KEY, finalCluster) + .withOption(XDS_CONFIG_CALL_OPTION_KEY, xdsConfig) .withOption(RPC_HASH_KEY, hash); if (routeAction.autoHostRewrite()) { callOptionsForCluster = callOptionsForCluster.withOption(AUTO_HOST_REWRITE_KEY, true); @@ -801,7 +805,7 @@ private void updateRoutes( } // Make newly added clusters selectable by config selector and deleted clusters no longer // selectable. - routingConfig = new RoutingConfig(httpMaxStreamDurationNano, routesData.build()); + routingConfig = new RoutingConfig(xdsConfig, httpMaxStreamDurationNano, routesData.build()); for (String cluster : deletedClusters) { int count = clusterRefs.get(cluster).refCount.decrementAndGet(); if (count == 0) { @@ -879,17 +883,21 @@ private void cleanUpRoutes(Status error) { * VirtualHost-level configuration for request routing. */ private static class RoutingConfig { - private final long fallbackTimeoutNano; + final XdsConfig xdsConfig; + final long fallbackTimeoutNano; final ImmutableList routes; final Status errorStatus; - private RoutingConfig(long fallbackTimeoutNano, ImmutableList routes) { + private RoutingConfig( + XdsConfig xdsConfig, long fallbackTimeoutNano, ImmutableList routes) { + this.xdsConfig = checkNotNull(xdsConfig, "xdsConfig"); this.fallbackTimeoutNano = fallbackTimeoutNano; this.routes = checkNotNull(routes, "routes"); this.errorStatus = null; } private RoutingConfig(Status errorStatus) { + this.xdsConfig = null; this.fallbackTimeoutNano = 0; this.routes = null; this.errorStatus = checkNotNull(errorStatus, "errorStatus"); diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 7425e3e31de..371c4213738 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -1672,6 +1672,10 @@ private void assertCallSelectClusterResult( clientCall.start(new NoopClientCallListener<>(), new Metadata()); assertThat(testCall.callOptions.getOption(XdsNameResolver.CLUSTER_SELECTION_KEY)) .isEqualTo("cluster:" + expectedCluster); + XdsConfig xdsConfig = + testCall.callOptions.getOption(XdsNameResolver.XDS_CONFIG_CALL_OPTION_KEY); + assertThat(xdsConfig).isNotNull(); + assertThat(xdsConfig.getClusters()).containsKey(expectedCluster); // Without "cluster:" prefix @SuppressWarnings("unchecked") Map config = (Map) result.getConfig(); if (expectedTimeoutSec != null) { From bb120a8cbb9de63d1514ac8c6a52568d946a60a7 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 5 Mar 2025 13:29:55 -0800 Subject: [PATCH 219/591] xds: Assert XdsNR's cluster ref counting is consistent It is much harder to debug refcounting problems when we ignore impossible situations. So make such impossible cases complain loudly so the bug is obvious. --- .../main/java/io/grpc/xds/XdsNameResolver.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 123d3a77172..bbe36bdd744 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -537,17 +537,21 @@ private boolean retainCluster(String cluster) { private void releaseCluster(final String cluster) { int count = clusterRefs.get(cluster).refCount.decrementAndGet(); + if (count < 0) { + throw new AssertionError(); + } if (count == 0) { syncContext.execute(new Runnable() { @Override public void run() { - if (clusterRefs.get(cluster).refCount.get() == 0) { - clusterRefs.remove(cluster); - if (resolveState.lastConfigOrStatus.hasValue()) { - updateResolutionResult(resolveState.lastConfigOrStatus.getValue()); - } else { - resolveState.cleanUpRoutes(resolveState.lastConfigOrStatus.getStatus()); - } + if (clusterRefs.get(cluster).refCount.get() != 0) { + throw new AssertionError(); + } + clusterRefs.remove(cluster); + if (resolveState.lastConfigOrStatus.hasValue()) { + updateResolutionResult(resolveState.lastConfigOrStatus.getValue()); + } else { + resolveState.cleanUpRoutes(resolveState.lastConfigOrStatus.getStatus()); } } }); From d2d72cda83256919a49432a792192c9313d171cb Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 20 Mar 2025 22:31:16 -0700 Subject: [PATCH 220/591] xds: Expose filter names to filter instances (#11971) This is to support gRFC A83 xDS GCP Authentication Filter: > Otherwise, the filter will look in the CDS resource's metadata for a > key corresponding to the filter's instance name. --- xds/src/main/java/io/grpc/xds/FaultFilter.java | 2 +- xds/src/main/java/io/grpc/xds/Filter.java | 2 +- .../main/java/io/grpc/xds/GcpAuthenticationFilter.java | 2 +- xds/src/main/java/io/grpc/xds/InternalRbacFilter.java | 2 +- xds/src/main/java/io/grpc/xds/RbacFilter.java | 2 +- xds/src/main/java/io/grpc/xds/RouterFilter.java | 2 +- xds/src/main/java/io/grpc/xds/XdsNameResolver.java | 3 ++- xds/src/main/java/io/grpc/xds/XdsServerWrapper.java | 3 ++- .../java/io/grpc/xds/GrpcXdsClientImplDataTest.java | 2 +- xds/src/test/java/io/grpc/xds/RbacFilterTest.java | 10 ++++++---- xds/src/test/java/io/grpc/xds/StatefulFilter.java | 2 +- xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java | 2 +- .../test/java/io/grpc/xds/XdsServerWrapperTest.java | 4 ++-- 13 files changed, 21 insertions(+), 17 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/FaultFilter.java b/xds/src/main/java/io/grpc/xds/FaultFilter.java index 2012fd36b62..0f3bb5b0557 100644 --- a/xds/src/main/java/io/grpc/xds/FaultFilter.java +++ b/xds/src/main/java/io/grpc/xds/FaultFilter.java @@ -99,7 +99,7 @@ public boolean isClientFilter() { } @Override - public FaultFilter newInstance() { + public FaultFilter newInstance(String name) { return INSTANCE; } diff --git a/xds/src/main/java/io/grpc/xds/Filter.java b/xds/src/main/java/io/grpc/xds/Filter.java index aa326b55ad7..416d929becf 100644 --- a/xds/src/main/java/io/grpc/xds/Filter.java +++ b/xds/src/main/java/io/grpc/xds/Filter.java @@ -87,7 +87,7 @@ default boolean isServerFilter() { *

  • Filter name+typeUrl in FilterChain's HCM.http_filters.
  • * */ - Filter newInstance(); + Filter newInstance(String name); /** * Parses the top-level filter config from raw proto message. The message may be either a {@link diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java index 41687817c47..add885c6416 100644 --- a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java +++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java @@ -64,7 +64,7 @@ public boolean isClientFilter() { } @Override - public GcpAuthenticationFilter newInstance() { + public GcpAuthenticationFilter newInstance(String name) { return new GcpAuthenticationFilter(); } diff --git a/xds/src/main/java/io/grpc/xds/InternalRbacFilter.java b/xds/src/main/java/io/grpc/xds/InternalRbacFilter.java index cedb3f4c85b..476adbf9cfd 100644 --- a/xds/src/main/java/io/grpc/xds/InternalRbacFilter.java +++ b/xds/src/main/java/io/grpc/xds/InternalRbacFilter.java @@ -33,7 +33,7 @@ public static ServerInterceptor createInterceptor(RBAC rbac) { throw new IllegalArgumentException( String.format("Failed to parse Rbac policy: %s", filterConfig.errorDetail)); } - return new RbacFilter.Provider().newInstance() + return new RbacFilter.Provider().newInstance("internalRbacFilter") .buildServerInterceptor(filterConfig.config, null); } } diff --git a/xds/src/main/java/io/grpc/xds/RbacFilter.java b/xds/src/main/java/io/grpc/xds/RbacFilter.java index 2bc4eeb846b..d91884735e9 100644 --- a/xds/src/main/java/io/grpc/xds/RbacFilter.java +++ b/xds/src/main/java/io/grpc/xds/RbacFilter.java @@ -89,7 +89,7 @@ public boolean isServerFilter() { } @Override - public RbacFilter newInstance() { + public RbacFilter newInstance(String name) { return INSTANCE; } diff --git a/xds/src/main/java/io/grpc/xds/RouterFilter.java b/xds/src/main/java/io/grpc/xds/RouterFilter.java index 939bd0b12ab..504c4213149 100644 --- a/xds/src/main/java/io/grpc/xds/RouterFilter.java +++ b/xds/src/main/java/io/grpc/xds/RouterFilter.java @@ -56,7 +56,7 @@ public boolean isServerFilter() { } @Override - public RouterFilter newInstance() { + public RouterFilter newInstance(String name) { return INSTANCE; } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index bbe36bdd744..7704a4a09db 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -704,7 +704,8 @@ private void updateActiveFilters(@Nullable List filterConfigs Filter.Provider provider = filterRegistry.get(typeUrl); checkNotNull(provider, "provider %s", typeUrl); - Filter filter = activeFilters.computeIfAbsent(filterKey, k -> provider.newInstance()); + Filter filter = activeFilters.computeIfAbsent( + filterKey, k -> provider.newInstance(namedFilter.name)); checkNotNull(filter, "filter %s", filterKey); filtersToShutdown.remove(filterKey); } diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index e0185974861..6625bd8178a 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -560,7 +560,8 @@ private void updateActiveFiltersForChain( Filter.Provider provider = filterRegistry.get(typeUrl); checkNotNull(provider, "provider %s", typeUrl); - Filter filter = chainFilters.computeIfAbsent(filterKey, k -> provider.newInstance()); + Filter filter = chainFilters.computeIfAbsent( + filterKey, k -> provider.newInstance(namedFilter.name)); checkNotNull(filter, "filter %s", filterKey); filtersToShutdown.remove(filterKey); } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 90b83320d63..bfaa17245cf 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -1267,7 +1267,7 @@ public boolean isClientFilter() { } @Override - public TestFilter newInstance() { + public TestFilter newInstance(String name) { return new TestFilter(); } diff --git a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java index 7f195693d84..334e159dd1d 100644 --- a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java @@ -80,6 +80,8 @@ public class RbacFilterTest { StringMatcher.newBuilder().setExact("/" + PATH).setIgnoreCase(true).build(); private static final RbacFilter.Provider FILTER_PROVIDER = new RbacFilter.Provider(); + private final String name = "theFilterName"; + @Test public void filterType_serverOnly() { assertThat(FILTER_PROVIDER.isClientFilter()).isFalse(); @@ -259,7 +261,7 @@ public void testAuthorizationInterceptor() { OrMatcher.create(AlwaysTrueMatcher.INSTANCE)); AuthConfig authconfig = AuthConfig.create(Collections.singletonList(policyMatcher), GrpcAuthorizationEngine.Action.ALLOW); - FILTER_PROVIDER.newInstance().buildServerInterceptor(RbacConfig.create(authconfig), null) + FILTER_PROVIDER.newInstance(name).buildServerInterceptor(RbacConfig.create(authconfig), null) .interceptCall(mockServerCall, new Metadata(), mockHandler); verify(mockHandler, never()).startCall(eq(mockServerCall), any(Metadata.class)); ArgumentCaptor captor = ArgumentCaptor.forClass(Status.class); @@ -271,7 +273,7 @@ public void testAuthorizationInterceptor() { authconfig = AuthConfig.create(Collections.singletonList(policyMatcher), GrpcAuthorizationEngine.Action.DENY); - FILTER_PROVIDER.newInstance().buildServerInterceptor(RbacConfig.create(authconfig), null) + FILTER_PROVIDER.newInstance(name).buildServerInterceptor(RbacConfig.create(authconfig), null) .interceptCall(mockServerCall, new Metadata(), mockHandler); verify(mockHandler).startCall(eq(mockServerCall), any(Metadata.class)); } @@ -322,7 +324,7 @@ public void overrideConfig() { RbacConfig override = FILTER_PROVIDER.parseFilterConfigOverride(Any.pack(rbacPerRoute)).config; assertThat(override).isEqualTo(RbacConfig.create(null)); ServerInterceptor interceptor = - FILTER_PROVIDER.newInstance().buildServerInterceptor(original, override); + FILTER_PROVIDER.newInstance(name).buildServerInterceptor(original, override); assertThat(interceptor).isNull(); policyMatcher = PolicyMatcher.create("policy-matcher-override", @@ -332,7 +334,7 @@ public void overrideConfig() { GrpcAuthorizationEngine.Action.ALLOW); override = RbacConfig.create(authconfig); - FILTER_PROVIDER.newInstance().buildServerInterceptor(original, override) + FILTER_PROVIDER.newInstance(name).buildServerInterceptor(original, override) .interceptCall(mockServerCall, new Metadata(), mockHandler); verify(mockHandler).startCall(eq(mockServerCall), any(Metadata.class)); verify(mockServerCall).getAttributes(); diff --git a/xds/src/test/java/io/grpc/xds/StatefulFilter.java b/xds/src/test/java/io/grpc/xds/StatefulFilter.java index 162dd380daf..4ef662c7ccd 100644 --- a/xds/src/test/java/io/grpc/xds/StatefulFilter.java +++ b/xds/src/test/java/io/grpc/xds/StatefulFilter.java @@ -108,7 +108,7 @@ public boolean isServerFilter() { } @Override - public synchronized StatefulFilter newInstance() { + public synchronized StatefulFilter newInstance(String name) { StatefulFilter filter = new StatefulFilter(counter++); instances.put(filter.idx, filter); return filter; diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 371c4213738..622084d4306 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -219,7 +219,7 @@ public void setUp() { // Lenient: suppress [MockitoHint] Unused warning, only used in resolved_fault* tests. lenient() .doReturn(new FaultFilter(mockRandom, new AtomicLong())) - .when(faultFilterProvider).newInstance(); + .when(faultFilterProvider).newInstance(any(String.class)); FilterRegistry filterRegistry = FilterRegistry.newRegistry().register( ROUTER_FILTER_PROVIDER, diff --git a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java index b866e10c559..e5f0f44cbae 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java @@ -1135,7 +1135,7 @@ public void run() { Filter.Provider filterProvider = mock(Filter.Provider.class); when(filterProvider.typeUrls()).thenReturn(new String[]{"filter-type-url"}); when(filterProvider.isServerFilter()).thenReturn(true); - when(filterProvider.newInstance()).thenReturn(filter); + when(filterProvider.newInstance(any(String.class))).thenReturn(filter); filterRegistry.register(filterProvider); FilterConfig f0 = mock(FilterConfig.class); @@ -1208,7 +1208,7 @@ public void run() { Filter.Provider filterProvider = mock(Filter.Provider.class); when(filterProvider.typeUrls()).thenReturn(new String[]{"filter-type-url"}); when(filterProvider.isServerFilter()).thenReturn(true); - when(filterProvider.newInstance()).thenReturn(filter); + when(filterProvider.newInstance(any(String.class))).thenReturn(filter); filterRegistry.register(filterProvider); FilterConfig f0 = mock(FilterConfig.class); From d60e6fc251d0c5c65b9e52e9ce1a84d3ec17c159 Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Fri, 21 Mar 2025 09:30:24 +0200 Subject: [PATCH 221/591] Replace usages of deprecated ExpectedException in grpc-api and grpc-core (#11962) --- api/build.gradle | 1 + api/src/test/java/io/grpc/MetadataTest.java | 35 +++++-------- .../java/io/grpc/MethodDescriptorTest.java | 6 --- .../java/io/grpc/ServerInterceptorsTest.java | 18 +++---- .../io/grpc/ServerServiceDefinitionTest.java | 19 ++----- .../java/io/grpc/ServiceDescriptorTest.java | 51 +++++++++---------- .../internal/AbstractClientStreamTest.java | 23 +++------ .../internal/AbstractServerStreamTest.java | 30 ++++------- .../ConnectivityStateManagerTest.java | 8 +-- .../io/grpc/internal/DnsNameResolverTest.java | 28 +++++----- .../java/io/grpc/internal/GrpcUtilTest.java | 23 ++++----- .../grpc/internal/InternalSubchannelTest.java | 13 ++--- .../java/io/grpc/internal/JsonParserTest.java | 51 ++++++------------- .../ManagedChannelImplBuilderTest.java | 33 +++++------- .../ManagedChannelServiceConfigTest.java | 49 +++++++----------- .../io/grpc/internal/MessageDeframerTest.java | 32 +++++------- .../io/grpc/internal/ServerCallImplTest.java | 42 ++++++++------- .../java/io/grpc/internal/ServerImplTest.java | 16 +++--- .../grpc/internal/AbstractTransportTest.java | 10 +--- 19 files changed, 185 insertions(+), 303 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index dc3eaea3f4e..415a17f61f8 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -47,6 +47,7 @@ dependencies { testImplementation project(':grpc-core') testImplementation project(':grpc-testing') testImplementation libraries.guava.testlib + testImplementation libraries.truth signature (libraries.signature.java) { artifact { diff --git a/api/src/test/java/io/grpc/MetadataTest.java b/api/src/test/java/io/grpc/MetadataTest.java index 14ba8ca9b23..a858fff5e5a 100644 --- a/api/src/test/java/io/grpc/MetadataTest.java +++ b/api/src/test/java/io/grpc/MetadataTest.java @@ -16,6 +16,7 @@ package io.grpc; +import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertArrayEquals; @@ -24,6 +25,7 @@ import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -37,9 +39,7 @@ import java.util.Arrays; import java.util.Iterator; import java.util.Locale; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -49,9 +49,6 @@ @RunWith(JUnit4.class) public class MetadataTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); - private static final Metadata.BinaryMarshaller FISH_MARSHALLER = new Metadata.BinaryMarshaller() { @Override @@ -65,7 +62,7 @@ public Fish parseBytes(byte[] serialized) { } }; - private static class FishStreamMarsaller implements Metadata.BinaryStreamMarshaller { + private static class FishStreamMarshaller implements Metadata.BinaryStreamMarshaller { @Override public InputStream toStream(Fish fish) { return new ByteArrayInputStream(FISH_MARSHALLER.toBytes(fish)); @@ -82,7 +79,7 @@ public Fish parseStream(InputStream stream) { } private static final Metadata.BinaryStreamMarshaller FISH_STREAM_MARSHALLER = - new FishStreamMarsaller(); + new FishStreamMarshaller(); /** A pattern commonly used to avoid unnecessary serialization of immutable objects. */ private static final class FakeFishStream extends InputStream { @@ -121,10 +118,9 @@ public Fish parseStream(InputStream stream) { @Test public void noPseudoHeaders() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid character"); - - Metadata.Key.of(":test-bin", FISH_MARSHALLER); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> Metadata.Key.of(":test-bin", FISH_MARSHALLER)); + assertThat(e).hasMessageThat().isEqualTo("Invalid character ':' in key name ':test-bin'"); } @Test @@ -186,8 +182,7 @@ public void testGetAllNoRemove() { Iterator i = metadata.getAll(KEY).iterator(); assertEquals(lance, i.next()); - thrown.expect(UnsupportedOperationException.class); - i.remove(); + assertThrows(UnsupportedOperationException.class, i::remove); } @Test @@ -271,17 +266,15 @@ public void mergeExpands() { @Test public void shortBinaryKeyName() { - thrown.expect(IllegalArgumentException.class); - - Metadata.Key.of("-bin", FISH_MARSHALLER); + assertThrows(IllegalArgumentException.class, () -> Metadata.Key.of("-bin", FISH_MARSHALLER)); } @Test public void invalidSuffixBinaryKeyName() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Binary header is named"); - - Metadata.Key.of("nonbinary", FISH_MARSHALLER); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> Metadata.Key.of("nonbinary", FISH_MARSHALLER)); + assertThat(e).hasMessageThat() + .isEqualTo("Binary header is named nonbinary. It must end with -bin"); } @Test @@ -415,7 +408,7 @@ public void streamedValueDifferentMarshaller() { h.put(KEY_STREAMED, salmon); // Get using a different marshaller instance. - Fish fish = h.get(copyKey(KEY_STREAMED, new FishStreamMarsaller())); + Fish fish = h.get(copyKey(KEY_STREAMED, new FishStreamMarshaller())); assertEquals(salmon, fish); } diff --git a/api/src/test/java/io/grpc/MethodDescriptorTest.java b/api/src/test/java/io/grpc/MethodDescriptorTest.java index 9431190984b..e068e0c1108 100644 --- a/api/src/test/java/io/grpc/MethodDescriptorTest.java +++ b/api/src/test/java/io/grpc/MethodDescriptorTest.java @@ -26,9 +26,7 @@ import io.grpc.MethodDescriptor.Marshaller; import io.grpc.MethodDescriptor.MethodType; import io.grpc.testing.TestMethodDescriptors; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -37,10 +35,6 @@ */ @RunWith(JUnit4.class) public class MethodDescriptorTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); - @Test public void createMethodDescriptor() { MethodDescriptor descriptor = MethodDescriptor.newBuilder() diff --git a/api/src/test/java/io/grpc/ServerInterceptorsTest.java b/api/src/test/java/io/grpc/ServerInterceptorsTest.java index abfb3540fe4..b84b3838afa 100644 --- a/api/src/test/java/io/grpc/ServerInterceptorsTest.java +++ b/api/src/test/java/io/grpc/ServerInterceptorsTest.java @@ -19,6 +19,7 @@ import static com.google.common.collect.Iterables.getOnlyElement; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.same; @@ -40,7 +41,6 @@ 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.ArgumentMatchers; @@ -55,10 +55,6 @@ public class ServerInterceptorsTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); - @Mock private Marshaller requestMarshaller; @@ -111,21 +107,21 @@ public void makeSureExpectedMocksUnused() { public void npeForNullServiceDefinition() { ServerServiceDefinition serviceDef = null; List interceptors = Arrays.asList(); - thrown.expect(NullPointerException.class); - ServerInterceptors.intercept(serviceDef, interceptors); + assertThrows(NullPointerException.class, + () -> ServerInterceptors.intercept(serviceDef, interceptors)); } @Test public void npeForNullInterceptorList() { - thrown.expect(NullPointerException.class); - ServerInterceptors.intercept(serviceDefinition, (List) null); + assertThrows(NullPointerException.class, + () -> ServerInterceptors.intercept(serviceDefinition, (List) null)); } @Test public void npeForNullInterceptor() { List interceptors = Arrays.asList((ServerInterceptor) null); - thrown.expect(NullPointerException.class); - ServerInterceptors.intercept(serviceDefinition, interceptors); + assertThrows(NullPointerException.class, + () -> ServerInterceptors.intercept(serviceDefinition, interceptors)); } @Test diff --git a/api/src/test/java/io/grpc/ServerServiceDefinitionTest.java b/api/src/test/java/io/grpc/ServerServiceDefinitionTest.java index 6a84d640d78..9e43302e210 100644 --- a/api/src/test/java/io/grpc/ServerServiceDefinitionTest.java +++ b/api/src/test/java/io/grpc/ServerServiceDefinitionTest.java @@ -18,14 +18,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -52,9 +51,6 @@ public class ServerServiceDefinitionTest { = ServerMethodDefinition.create(method1, methodHandler1); private ServerMethodDefinition methodDef2 = ServerMethodDefinition.create(method2, methodHandler2); - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public ExpectedException thrown = ExpectedException.none(); @Test public void noMethods() { @@ -91,9 +87,7 @@ public void addMethod_duplicateName() { ServiceDescriptor sd = new ServiceDescriptor(serviceName, method1); ServerServiceDefinition.Builder ssd = ServerServiceDefinition.builder(sd) .addMethod(method1, methodHandler1); - thrown.expect(IllegalStateException.class); - ssd.addMethod(diffMethod1, methodHandler2) - .build(); + assertThrows(IllegalStateException.class, () -> ssd.addMethod(diffMethod1, methodHandler2)); } @Test @@ -101,8 +95,7 @@ public void buildMisaligned_extraMethod() { ServiceDescriptor sd = new ServiceDescriptor(serviceName); ServerServiceDefinition.Builder ssd = ServerServiceDefinition.builder(sd) .addMethod(methodDef1); - thrown.expect(IllegalStateException.class); - ssd.build(); + assertThrows(IllegalStateException.class, ssd::build); } @Test @@ -110,16 +103,14 @@ public void buildMisaligned_diffMethodInstance() { ServiceDescriptor sd = new ServiceDescriptor(serviceName, method1); ServerServiceDefinition.Builder ssd = ServerServiceDefinition.builder(sd) .addMethod(diffMethod1, methodHandler1); - thrown.expect(IllegalStateException.class); - ssd.build(); + assertThrows(IllegalStateException.class, ssd::build); } @Test public void buildMisaligned_missingMethod() { ServiceDescriptor sd = new ServiceDescriptor(serviceName, method1); ServerServiceDefinition.Builder ssd = ServerServiceDefinition.builder(sd); - thrown.expect(IllegalStateException.class); - ssd.build(); + assertThrows(IllegalStateException.class, ssd::build); } @Test diff --git a/api/src/test/java/io/grpc/ServiceDescriptorTest.java b/api/src/test/java/io/grpc/ServiceDescriptorTest.java index a05858680d5..89bdead3632 100644 --- a/api/src/test/java/io/grpc/ServiceDescriptorTest.java +++ b/api/src/test/java/io/grpc/ServiceDescriptorTest.java @@ -16,17 +16,18 @@ package io.grpc; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.google.common.truth.StringSubject; import io.grpc.MethodDescriptor.MethodType; import io.grpc.testing.TestMethodDescriptors; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -36,32 +37,27 @@ @RunWith(JUnit4.class) public class ServiceDescriptorTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); - @Test public void failsOnNullName() { - thrown.expect(NullPointerException.class); - thrown.expectMessage("name"); - - new ServiceDescriptor(null, Collections.>emptyList()); + List> methods = Collections.emptyList(); + NullPointerException e = assertThrows(NullPointerException.class, + () -> new ServiceDescriptor(null, methods)); + assertThat(e).hasMessageThat().isEqualTo("name"); } @Test public void failsOnNullMethods() { - thrown.expect(NullPointerException.class); - thrown.expectMessage("methods"); - - new ServiceDescriptor("name", (Collection>) null); + NullPointerException e = assertThrows(NullPointerException.class, + () -> new ServiceDescriptor("name", (Collection>) null)); + assertThat(e).hasMessageThat().isEqualTo("methods"); } @Test public void failsOnNullMethod() { - thrown.expect(NullPointerException.class); - thrown.expectMessage("method"); - - new ServiceDescriptor("name", Collections.>singletonList(null)); + List> methods = Collections.singletonList(null); + NullPointerException e = assertThrows(NullPointerException.class, + () -> new ServiceDescriptor("name", methods)); + assertThat(e).hasMessageThat().isEqualTo("method"); } @Test @@ -69,15 +65,17 @@ public void failsOnNonMatchingNames() { List> descriptors = Collections.>singletonList( MethodDescriptor.newBuilder() .setType(MethodType.UNARY) - .setFullMethodName(MethodDescriptor.generateFullMethodName("wrongservice", "method")) + .setFullMethodName(MethodDescriptor.generateFullMethodName("wrongService", "method")) .setRequestMarshaller(TestMethodDescriptors.voidMarshaller()) .setResponseMarshaller(TestMethodDescriptors.voidMarshaller()) .build()); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("service names"); - - new ServiceDescriptor("name", descriptors); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> new ServiceDescriptor("fooService", descriptors)); + StringSubject error = assertThat(e).hasMessageThat(); + error.contains("service names"); + error.contains("fooService"); + error.contains("wrongService"); } @Test @@ -96,10 +94,9 @@ public void failsOnNonDuplicateNames() { .setResponseMarshaller(TestMethodDescriptors.voidMarshaller()) .build()); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("duplicate"); - - new ServiceDescriptor("name", descriptors); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> new ServiceDescriptor("name", descriptors)); + assertThat(e).hasMessageThat().isEqualTo("duplicate name name/method"); } @Test diff --git a/core/src/test/java/io/grpc/internal/AbstractClientStreamTest.java b/core/src/test/java/io/grpc/internal/AbstractClientStreamTest.java index ad3b59030d7..18fafe6557d 100644 --- a/core/src/test/java/io/grpc/internal/AbstractClientStreamTest.java +++ b/core/src/test/java/io/grpc/internal/AbstractClientStreamTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.AdditionalAnswers.delegatesTo; @@ -57,7 +58,6 @@ 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; @@ -76,8 +76,6 @@ public class AbstractClientStreamTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); private final StatsTraceContext statsTraceCtx = StatsTraceContext.NOOP; private final TransportTracer transportTracer = new TransportTracer(); @@ -136,9 +134,7 @@ public void cancel_failsOnNull() { AbstractClientStream stream = new BaseAbstractClientStream(allocator, statsTraceCtx, transportTracer); stream.start(listener); - thrown.expect(NullPointerException.class); - - stream.cancel(null); + assertThrows(NullPointerException.class, () -> stream.cancel(null)); } @Test @@ -164,9 +160,7 @@ public void startFailsOnNullListener() { AbstractClientStream stream = new BaseAbstractClientStream(allocator, statsTraceCtx, transportTracer); - thrown.expect(NullPointerException.class); - - stream.start(null); + assertThrows(NullPointerException.class, () -> stream.start(null)); } @Test @@ -174,9 +168,7 @@ public void cantCallStartTwice() { AbstractClientStream stream = new BaseAbstractClientStream(allocator, statsTraceCtx, transportTracer); stream.start(mockListener); - thrown.expect(IllegalStateException.class); - - stream.start(mockListener); + assertThrows(IllegalStateException.class, () -> stream.start(mockListener)); } @Test @@ -188,8 +180,7 @@ public void inboundDataReceived_failsOnNullFrame() { TransportState state = stream.transportState(); - thrown.expect(NullPointerException.class); - state.inboundDataReceived(null); + assertThrows(NullPointerException.class, () -> state.inboundDataReceived(null)); } @Test @@ -212,8 +203,8 @@ public void inboundHeadersReceived_failsIfStatusReported() { TransportState state = stream.transportState(); - thrown.expect(IllegalStateException.class); - state.inboundHeadersReceived(new Metadata()); + Metadata headers = new Metadata(); + assertThrows(IllegalStateException.class, () -> state.inboundHeadersReceived(headers)); } @Test diff --git a/core/src/test/java/io/grpc/internal/AbstractServerStreamTest.java b/core/src/test/java/io/grpc/internal/AbstractServerStreamTest.java index b41d45e972e..137ba19bfea 100644 --- a/core/src/test/java/io/grpc/internal/AbstractServerStreamTest.java +++ b/core/src/test/java/io/grpc/internal/AbstractServerStreamTest.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.any; @@ -45,9 +46,7 @@ import java.util.Queue; import java.util.concurrent.TimeUnit; 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; @@ -60,9 +59,6 @@ public class AbstractServerStreamTest { private static final int TIMEOUT_MS = 1000; private static final int MAX_MESSAGE_SIZE = 100; - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); - private final WritableBufferAllocator allocator = new WritableBufferAllocator() { @Override public WritableBuffer allocate(int capacityHint) { @@ -226,9 +222,9 @@ public void completeWithoutClose() { public void setListener_setOnlyOnce() { TransportState state = stream.transportState(); state.setListener(new ServerStreamListenerBase()); - thrown.expect(IllegalStateException.class); - state.setListener(new ServerStreamListenerBase()); + ServerStreamListenerBase listener2 = new ServerStreamListenerBase(); + assertThrows(IllegalStateException.class, () -> state.setListener(listener2)); } @Test @@ -238,8 +234,7 @@ public void listenerReady_onlyOnce() { TransportState state = stream.transportState(); - thrown.expect(IllegalStateException.class); - state.onStreamAllocated(); + assertThrows(IllegalStateException.class, state::onStreamAllocated); } @Test @@ -255,8 +250,7 @@ public void listenerReady_readyCalled() { public void setListener_failsOnNull() { TransportState state = stream.transportState(); - thrown.expect(NullPointerException.class); - state.setListener(null); + assertThrows(NullPointerException.class, () -> state.setListener(null)); } // TODO(ericgribkoff) This test is only valid if deframeInTransportThread=true, as otherwise the @@ -284,9 +278,7 @@ public void messagesAvailable(MessageProducer producer) { @Test public void writeHeaders_failsOnNullHeaders() { - thrown.expect(NullPointerException.class); - - stream.writeHeaders(null, true); + assertThrows(NullPointerException.class, () -> stream.writeHeaders(null, true)); } @Test @@ -336,16 +328,13 @@ public void writeMessage_closesStream() throws Exception { @Test public void close_failsOnNullStatus() { - thrown.expect(NullPointerException.class); - - stream.close(null, new Metadata()); + Metadata trailers = new Metadata(); + assertThrows(NullPointerException.class, () -> stream.close(null, trailers)); } @Test public void close_failsOnNullMetadata() { - thrown.expect(NullPointerException.class); - - stream.close(Status.INTERNAL, null); + assertThrows(NullPointerException.class, () -> stream.close(Status.INTERNAL, null)); } @Test @@ -451,4 +440,3 @@ public int streamId() { } } } - diff --git a/core/src/test/java/io/grpc/internal/ConnectivityStateManagerTest.java b/core/src/test/java/io/grpc/internal/ConnectivityStateManagerTest.java index 2a759a4f386..dfd6ed56a1e 100644 --- a/core/src/test/java/io/grpc/internal/ConnectivityStateManagerTest.java +++ b/core/src/test/java/io/grpc/internal/ConnectivityStateManagerTest.java @@ -27,9 +27,7 @@ import io.grpc.ConnectivityState; import java.util.LinkedList; import java.util.concurrent.Executor; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -38,10 +36,6 @@ */ @RunWith(JUnit4.class) public class ConnectivityStateManagerTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); - private final FakeClock executor = new FakeClock(); private final ConnectivityStateManager state = new ConnectivityStateManager(); private final LinkedList sink = new LinkedList<>(); @@ -75,7 +69,7 @@ public void run() { assertEquals(1, sink.size()); assertEquals(TRANSIENT_FAILURE, sink.poll()); } - + @Test public void registerCallbackAfterStateChanged() { state.gotoState(CONNECTING); diff --git a/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java b/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java index be304ad326b..130c01d1c04 100644 --- a/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java +++ b/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; @@ -35,6 +36,7 @@ import static org.mockito.Mockito.when; import com.google.common.base.Stopwatch; +import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.net.InetAddresses; @@ -82,7 +84,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.DisableOnDebug; -import org.junit.rules.ExpectedException; import org.junit.rules.TestRule; import org.junit.rules.Timeout; import org.junit.runner.RunWith; @@ -99,8 +100,6 @@ public class DnsNameResolverTest { @Rule public final TestRule globalTimeout = new DisableOnDebug(Timeout.seconds(10)); @Rule public final MockitoRule mocks = MockitoJUnit.rule(); - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); private final Map serviceConfig = new LinkedHashMap<>(); @@ -914,9 +913,10 @@ public HttpConnectProxiedSocketAddress proxyFor(SocketAddress targetAddress) { public void maybeChooseServiceConfig_failsOnMisspelling() { Map bad = new LinkedHashMap<>(); bad.put("parcentage", 1.0); - thrown.expectMessage("Bad key"); - - DnsNameResolver.maybeChooseServiceConfig(bad, new Random(), "host"); + Random random = new Random(); + VerifyException e = assertThrows(VerifyException.class, + () -> DnsNameResolver.maybeChooseServiceConfig(bad, random, "host")); + assertThat(e).hasMessageThat().isEqualTo("Bad key: parcentage=1.0"); } @Test @@ -1155,25 +1155,25 @@ public void parseTxtResults_misspelledName() throws Exception { } @Test - public void parseTxtResults_badTypeFails() throws Exception { + public void parseTxtResults_badTypeFails() { List txtRecords = new ArrayList<>(); txtRecords.add("some_record"); txtRecords.add("grpc_config={}"); - thrown.expect(ClassCastException.class); - thrown.expectMessage("wrong type"); - DnsNameResolver.parseTxtResults(txtRecords); + ClassCastException e = assertThrows(ClassCastException.class, + () -> DnsNameResolver.parseTxtResults(txtRecords)); + assertThat(e).hasMessageThat().isEqualTo("wrong type {}"); } @Test - public void parseTxtResults_badInnerTypeFails() throws Exception { + public void parseTxtResults_badInnerTypeFails() { List txtRecords = new ArrayList<>(); txtRecords.add("some_record"); txtRecords.add("grpc_config=[\"bogus\"]"); - thrown.expect(ClassCastException.class); - thrown.expectMessage("not object"); - DnsNameResolver.parseTxtResults(txtRecords); + ClassCastException e = assertThrows(ClassCastException.class, + () -> DnsNameResolver.parseTxtResults(txtRecords)); + assertThat(e).hasMessageThat().isEqualTo("value bogus for idx 0 in [bogus] is not object"); } @Test diff --git a/core/src/test/java/io/grpc/internal/GrpcUtilTest.java b/core/src/test/java/io/grpc/internal/GrpcUtilTest.java index 39acb582d28..229c593ef80 100644 --- a/core/src/test/java/io/grpc/internal/GrpcUtilTest.java +++ b/core/src/test/java/io/grpc/internal/GrpcUtilTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -41,7 +42,6 @@ import java.util.ArrayList; 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; @@ -57,8 +57,6 @@ public class GrpcUtilTest { new ClientStreamTracer() {} }; - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @Captor @@ -201,9 +199,7 @@ public void urlAuthorityEscape_unicodeAreNotEncoded() { @Test public void checkAuthority_failsOnNull() { - thrown.expect(NullPointerException.class); - - GrpcUtil.checkAuthority(null); + assertThrows(NullPointerException.class, () -> GrpcUtil.checkAuthority(null)); } @Test @@ -229,19 +225,18 @@ public void checkAuthority_succeedsOnIpV6() { @Test public void checkAuthority_failsOnInvalidAuthority() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid authority"); - - GrpcUtil.checkAuthority("[ : : 1]"); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> GrpcUtil.checkAuthority("[ : : 1]")); + assertThat(e).hasMessageThat().isEqualTo("Invalid authority: [ : : 1]"); } @Test public void checkAuthority_userInfoNotAllowed() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Userinfo"); - - GrpcUtil.checkAuthority("foo@valid"); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> GrpcUtil.checkAuthority("foo@valid")); + assertThat(e).hasMessageThat() + .isEqualTo("Userinfo must not be present on authority: 'foo@valid'"); } @Test diff --git a/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java b/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java index b75fd43a743..bed722f5f3a 100644 --- a/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java +++ b/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java @@ -27,6 +27,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -65,7 +66,6 @@ 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.Mock; @@ -79,9 +79,6 @@ public class InternalSubchannelTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); private static final String AUTHORITY = "fakeauthority"; private static final String USER_AGENT = "mosaic"; @@ -544,8 +541,9 @@ public void constructor_eagListWithNull_throws() { public void updateAddresses_emptyEagList_throws() { SocketAddress addr = new FakeSocketAddress(); createInternalSubchannel(addr); - thrown.expect(IllegalArgumentException.class); - internalSubchannel.updateAddresses(Arrays.asList()); + List newAddressGroups = Collections.emptyList(); + assertThrows(IllegalArgumentException.class, + () -> internalSubchannel.updateAddresses(newAddressGroups)); } @Test @@ -553,8 +551,7 @@ public void updateAddresses_eagListWithNull_throws() { SocketAddress addr = new FakeSocketAddress(); createInternalSubchannel(addr); List eags = Arrays.asList((EquivalentAddressGroup) null); - thrown.expect(NullPointerException.class); - internalSubchannel.updateAddresses(eags); + assertThrows(NullPointerException.class, () -> internalSubchannel.updateAddresses(eags)); } @Test public void updateAddresses_intersecting_ready() { diff --git a/core/src/test/java/io/grpc/internal/JsonParserTest.java b/core/src/test/java/io/grpc/internal/JsonParserTest.java index cfee566fa4a..a0dd81c20ce 100644 --- a/core/src/test/java/io/grpc/internal/JsonParserTest.java +++ b/core/src/test/java/io/grpc/internal/JsonParserTest.java @@ -17,15 +17,14 @@ package io.grpc.internal; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import com.google.gson.stream.MalformedJsonException; import java.io.EOFException; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -35,10 +34,6 @@ @RunWith(JUnit4.class) public class JsonParserTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); - @Test public void emptyObject() throws IOException { assertEquals(new LinkedHashMap(), JsonParser.parse("{}")); @@ -75,45 +70,33 @@ public void nullValue() throws IOException { } @Test - public void nanFails() throws IOException { - thrown.expect(MalformedJsonException.class); - - JsonParser.parse("NaN"); + public void nanFails() { + assertThrows(MalformedJsonException.class, () -> JsonParser.parse("NaN")); } @Test - public void objectEarlyEnd() throws IOException { - thrown.expect(MalformedJsonException.class); - - JsonParser.parse("{foo:}"); + public void objectEarlyEnd() { + assertThrows(MalformedJsonException.class, () -> JsonParser.parse("{foo:}")); } @Test - public void earlyEndArray() throws IOException { - thrown.expect(EOFException.class); - - JsonParser.parse("[1, 2, "); + public void earlyEndArray() { + assertThrows(EOFException.class, () -> JsonParser.parse("[1, 2, ")); } @Test - public void arrayMissingElement() throws IOException { - thrown.expect(MalformedJsonException.class); - - JsonParser.parse("[1, 2, ]"); + public void arrayMissingElement() { + assertThrows(MalformedJsonException.class, () -> JsonParser.parse("[1, 2, ]")); } @Test - public void objectMissingElement() throws IOException { - thrown.expect(MalformedJsonException.class); - - JsonParser.parse("{1: "); + public void objectMissingElement() { + assertThrows(MalformedJsonException.class, () -> JsonParser.parse("{1: ")); } @Test - public void objectNoName() throws IOException { - thrown.expect(MalformedJsonException.class); - - JsonParser.parse("{: 1"); + public void objectNoName() { + assertThrows(MalformedJsonException.class, () -> JsonParser.parse("{: 1")); } @Test @@ -125,9 +108,7 @@ public void objectStringName() throws IOException { } @Test - public void duplicate() throws IOException { - thrown.expect(IllegalArgumentException.class); - - JsonParser.parse("{\"hi\": 2, \"hi\": 3}"); + public void duplicate() { + assertThrows(IllegalArgumentException.class, () -> JsonParser.parse("{\"hi\": 2, \"hi\": 3}")); } -} \ No newline at end of file +} diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java index cf131a79d87..861412653fb 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.doReturn; @@ -67,7 +68,6 @@ 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.Mock; @@ -99,8 +99,6 @@ public ClientCall interceptCall( }; @Rule public final MockitoRule mocks = MockitoJUnit.rule(); - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); @Rule public final GrpcCleanupRule grpcCleanupRule = new GrpcCleanupRule(); @Mock private ClientTransportFactory mockClientTransportFactory; @@ -424,10 +422,9 @@ public void checkAuthority_validAuthorityAllowed() { @Test public void checkAuthority_invalidAuthorityFailed() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid authority"); - - builder.checkAuthority(DUMMY_AUTHORITY_INVALID); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.checkAuthority(DUMMY_AUTHORITY_INVALID)); + assertThat(e).hasMessageThat().isEqualTo("Invalid authority: [ : : 1]"); } @Test @@ -450,11 +447,10 @@ public void enableCheckAuthority_validAuthorityAllowed() { @Test public void disableCheckAuthority_invalidAuthorityFailed() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid authority"); - builder.disableCheckAuthority().enableCheckAuthority(); - builder.checkAuthority(DUMMY_AUTHORITY_INVALID); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.checkAuthority(DUMMY_AUTHORITY_INVALID)); + assertThat(e).hasMessageThat().isEqualTo("Invalid authority: [ : : 1]"); } @Test @@ -680,14 +676,12 @@ public void perRpcBufferLimit() { @Test public void retryBufferSizeInvalidArg() { - thrown.expect(IllegalArgumentException.class); - builder.retryBufferSize(0L); + assertThrows(IllegalArgumentException.class, () -> builder.retryBufferSize(0L)); } @Test public void perRpcBufferLimitInvalidArg() { - thrown.expect(IllegalArgumentException.class); - builder.perRpcBufferLimit(0L); + assertThrows(IllegalArgumentException.class, () -> builder.perRpcBufferLimit(0L)); } @Test @@ -710,8 +704,7 @@ public void defaultServiceConfig_nullKey() { Map config = new HashMap<>(); config.put(null, "val"); - thrown.expect(IllegalArgumentException.class); - builder.defaultServiceConfig(config); + assertThrows(IllegalArgumentException.class, () -> builder.defaultServiceConfig(config)); } @Test @@ -721,8 +714,7 @@ public void defaultServiceConfig_intKey() { Map config = new HashMap<>(); config.put("key", subConfig); - thrown.expect(IllegalArgumentException.class); - builder.defaultServiceConfig(config); + assertThrows(IllegalArgumentException.class, () -> builder.defaultServiceConfig(config)); } @Test @@ -730,8 +722,7 @@ public void defaultServiceConfig_intValue() { Map config = new HashMap<>(); config.put("key", 3); - thrown.expect(IllegalArgumentException.class); - builder.defaultServiceConfig(config); + assertThrows(IllegalArgumentException.class, () -> builder.defaultServiceConfig(config)); } @Test diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelServiceConfigTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelServiceConfigTest.java index 493714dfd41..fefc37e4fdc 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelServiceConfigTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelServiceConfigTest.java @@ -20,6 +20,7 @@ import static io.grpc.MethodDescriptor.MethodType.UNARY; import static io.grpc.Status.Code.UNAVAILABLE; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; @@ -34,19 +35,13 @@ import io.grpc.testing.TestMethodDescriptors; import java.util.Collections; import java.util.Map; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class ManagedChannelServiceConfigTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); - @Test public void managedChannelServiceConfig_shouldParseHealthCheckingConfig() throws Exception { Map rawServiceConfig = @@ -79,10 +74,9 @@ public void createManagedChannelServiceConfig_failsOnDuplicateMethod() { Map methodConfig = ImmutableMap.of("name", ImmutableList.of(name1, name2)); Map serviceConfig = ImmutableMap.of("methodConfig", ImmutableList.of(methodConfig)); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Duplicate method"); - - ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null)); + assertThat(e).hasMessageThat().isEqualTo("Duplicate method name service/method"); } @Test @@ -92,10 +86,9 @@ public void createManagedChannelServiceConfig_failsOnDuplicateService() { Map methodConfig = ImmutableMap.of("name", ImmutableList.of(name1, name2)); Map serviceConfig = ImmutableMap.of("methodConfig", ImmutableList.of(methodConfig)); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Duplicate service"); - - ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null)); + assertThat(e).hasMessageThat().isEqualTo("Duplicate service service"); } @Test @@ -107,10 +100,9 @@ public void createManagedChannelServiceConfig_failsOnDuplicateServiceMultipleCon Map serviceConfig = ImmutableMap.of("methodConfig", ImmutableList.of(methodConfig1, methodConfig2)); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Duplicate service"); - - ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null)); + assertThat(e).hasMessageThat().isEqualTo("Duplicate service service"); } @Test @@ -119,10 +111,9 @@ public void createManagedChannelServiceConfig_failsOnMethodNameWithEmptyServiceN Map methodConfig = ImmutableMap.of("name", ImmutableList.of(name)); Map serviceConfig = ImmutableMap.of("methodConfig", ImmutableList.of(methodConfig)); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("missing service name for method method1"); - - ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null)); + assertThat(e).hasMessageThat().isEqualTo("missing service name for method method1"); } @Test @@ -131,10 +122,9 @@ public void createManagedChannelServiceConfig_failsOnMethodNameWithoutServiceNam Map methodConfig = ImmutableMap.of("name", ImmutableList.of(name)); Map serviceConfig = ImmutableMap.of("methodConfig", ImmutableList.of(methodConfig)); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("missing service name for method method1"); - - ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null)); + assertThat(e).hasMessageThat().isEqualTo("missing service name for method method1"); } @Test @@ -143,10 +133,9 @@ public void createManagedChannelServiceConfig_failsOnMissingServiceName() { Map methodConfig = ImmutableMap.of("name", ImmutableList.of(name)); Map serviceConfig = ImmutableMap.of("methodConfig", ImmutableList.of(methodConfig)); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("missing service"); - - ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null)); + assertThat(e).hasMessageThat().isEqualTo("missing service name for method method"); } @Test diff --git a/core/src/test/java/io/grpc/internal/MessageDeframerTest.java b/core/src/test/java/io/grpc/internal/MessageDeframerTest.java index 8f1b908e999..54758bc096f 100644 --- a/core/src/test/java/io/grpc/internal/MessageDeframerTest.java +++ b/core/src/test/java/io/grpc/internal/MessageDeframerTest.java @@ -20,6 +20,7 @@ import static io.grpc.internal.GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.anyInt; @@ -53,10 +54,8 @@ import java.util.concurrent.TimeUnit; import java.util.zip.GZIPOutputStream; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.junit.runners.Parameterized; @@ -341,9 +340,6 @@ public Void answer(InvocationOnMock invocation) throws Throwable { @RunWith(JUnit4.class) public static class SizeEnforcingInputStreamTests { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); private TestBaseStreamTracer tracer = new TestBaseStreamTracer(); private StatsTraceContext statsTraceCtx = new StatsTraceContext(new StreamTracer[]{tracer}); @@ -381,11 +377,12 @@ public void sizeEnforcingInputStream_readByteAboveLimit() throws IOException { new MessageDeframer.SizeEnforcingInputStream(in, 2, statsTraceCtx); try { - thrown.expect(StatusRuntimeException.class); - thrown.expectMessage("RESOURCE_EXHAUSTED: Decompressed gRPC message exceeds"); - - while (stream.read() != -1) { - } + StatusRuntimeException e = assertThrows(StatusRuntimeException.class, () -> { + while (stream.read() != -1) { + } + }); + assertThat(e).hasMessageThat() + .isEqualTo("RESOURCE_EXHAUSTED: Decompressed gRPC message exceeds maximum size 2"); } finally { stream.close(); } @@ -427,10 +424,10 @@ public void sizeEnforcingInputStream_readAboveLimit() throws IOException { byte[] buf = new byte[10]; try { - thrown.expect(StatusRuntimeException.class); - thrown.expectMessage("RESOURCE_EXHAUSTED: Decompressed gRPC message exceeds"); - - stream.read(buf, 0, buf.length); + StatusRuntimeException e = assertThrows(StatusRuntimeException.class, + () -> stream.read(buf, 0, buf.length)); + assertThat(e).hasMessageThat() + .isEqualTo("RESOURCE_EXHAUSTED: Decompressed gRPC message exceeds maximum size 2"); } finally { stream.close(); } @@ -470,10 +467,9 @@ public void sizeEnforcingInputStream_skipAboveLimit() throws IOException { new MessageDeframer.SizeEnforcingInputStream(in, 2, statsTraceCtx); try { - thrown.expect(StatusRuntimeException.class); - thrown.expectMessage("RESOURCE_EXHAUSTED: Decompressed gRPC message exceeds"); - - stream.skip(4); + StatusRuntimeException e = assertThrows(StatusRuntimeException.class, () -> stream.skip(4)); + assertThat(e).hasMessageThat() + .isEqualTo("RESOURCE_EXHAUSTED: Decompressed gRPC message exceeds maximum size 2"); } finally { stream.close(); } diff --git a/core/src/test/java/io/grpc/internal/ServerCallImplTest.java b/core/src/test/java/io/grpc/internal/ServerCallImplTest.java index 652c94a4640..7394c83eab2 100644 --- a/core/src/test/java/io/grpc/internal/ServerCallImplTest.java +++ b/core/src/test/java/io/grpc/internal/ServerCallImplTest.java @@ -16,12 +16,14 @@ package io.grpc.internal; +import static com.google.common.truth.Truth.assertThat; import static io.grpc.internal.GrpcUtil.CONTENT_LENGTH_KEY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; @@ -54,7 +56,6 @@ 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; @@ -64,8 +65,6 @@ @RunWith(JUnit4.class) public class ServerCallImplTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @Mock private ServerStream stream; @@ -175,20 +174,20 @@ public void sendHeader_contentLengthDiscarded() { @Test public void sendHeader_failsOnSecondCall() { call.sendHeaders(new Metadata()); - thrown.expect(IllegalStateException.class); - thrown.expectMessage("sendHeaders has already been called"); - - call.sendHeaders(new Metadata()); + Metadata headers = new Metadata(); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> call.sendHeaders(headers)); + assertThat(e).hasMessageThat().isEqualTo("sendHeaders has already been called"); } @Test public void sendHeader_failsOnClosed() { call.close(Status.CANCELLED, new Metadata()); - thrown.expect(IllegalStateException.class); - thrown.expectMessage("call is closed"); - - call.sendHeaders(new Metadata()); + Metadata headers = new Metadata(); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> call.sendHeaders(headers)); + assertThat(e).hasMessageThat().isEqualTo("call is closed"); } @Test @@ -204,18 +203,16 @@ public void sendMessage_failsOnClosed() { call.sendHeaders(new Metadata()); call.close(Status.CANCELLED, new Metadata()); - thrown.expect(IllegalStateException.class); - thrown.expectMessage("call is closed"); - - call.sendMessage(1234L); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> call.sendMessage(1234L)); + assertThat(e).hasMessageThat().isEqualTo("call is closed"); } @Test public void sendMessage_failsIfheadersUnsent() { - thrown.expect(IllegalStateException.class); - thrown.expectMessage("sendHeaders has not been called"); - - call.sendMessage(1234L); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> call.sendMessage(1234L)); + assertThat(e).hasMessageThat().isEqualTo("sendHeaders has not been called"); } @Test @@ -490,9 +487,10 @@ public void streamListener_unexpectedRuntimeException() { InputStream inputStream = UNARY_METHOD.streamRequest(1234L); - thrown.expect(RuntimeException.class); - thrown.expectMessage("unexpected exception"); - streamListener.messagesAvailable(new SingleMessageProducer(inputStream)); + SingleMessageProducer producer = new SingleMessageProducer(inputStream); + RuntimeException e = assertThrows(RuntimeException.class, + () -> streamListener.messagesAvailable(producer)); + assertThat(e).hasMessageThat().isEqualTo("unexpected exception"); } private static class LongMarshaller implements Marshaller { diff --git a/core/src/test/java/io/grpc/internal/ServerImplTest.java b/core/src/test/java/io/grpc/internal/ServerImplTest.java index 3125edca1e6..2ddaba751e4 100644 --- a/core/src/test/java/io/grpc/internal/ServerImplTest.java +++ b/core/src/test/java/io/grpc/internal/ServerImplTest.java @@ -26,6 +26,7 @@ import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.AdditionalAnswers.delegatesTo; @@ -104,7 +105,6 @@ import org.junit.BeforeClass; 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; @@ -140,8 +140,6 @@ public boolean shouldAccept(Runnable runnable) { }; private static final String AUTHORITY = "some_authority"; - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @BeforeClass @@ -1228,7 +1226,7 @@ public void testStreamClose_deadlineExceededTriggersImmediateCancellation() thro assertFalse(context.get().isCancelled()); assertEquals(1, timer.forwardNanos(1)); - + assertTrue(callReference.get().isCancelled()); assertTrue(context.get().isCancelled()); assertThat(context.get().cancellationCause()).isNotNull(); @@ -1260,9 +1258,8 @@ public List getListenSocketAddresses() { public void getPortBeforeStartedFails() { transportServer = new SimpleServer(); createServer(); - thrown.expect(IllegalStateException.class); - thrown.expectMessage("started"); - server.getPort(); + IllegalStateException e = assertThrows(IllegalStateException.class, () -> server.getPort()); + assertThat(e).hasMessageThat().isEqualTo("Not started"); } @Test @@ -1271,9 +1268,8 @@ public void getPortAfterTerminationFails() throws Exception { createAndStartServer(); server.shutdown(); server.awaitTermination(); - thrown.expect(IllegalStateException.class); - thrown.expectMessage("terminated"); - server.getPort(); + IllegalStateException e = assertThrows(IllegalStateException.class, () -> server.getPort()); + assertThat(e).hasMessageThat().isEqualTo("Already terminated"); } @Test diff --git a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java index 4a518895db6..1f4c2b41f15 100644 --- a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java +++ b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; @@ -76,9 +77,7 @@ import java.util.concurrent.TimeoutException; 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; @@ -209,10 +208,6 @@ public ServerStreamTracer newServerStreamTracer(String fullMethodName, Metadata } })); - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public ExpectedException thrown = ExpectedException.none(); - @Before public void setUp() { server = newServer(Arrays.asList(serverStreamTracerFactory)); @@ -396,8 +391,7 @@ public void serverAlreadyListening() throws Exception { port = ((InetSocketAddress) addr).getPort(); } InternalServer server2 = newServer(port, Arrays.asList(serverStreamTracerFactory)); - thrown.expect(IOException.class); - server2.start(new MockServerListener()); + assertThrows(IOException.class, () -> server2.start(new MockServerListener())); } @Test From 94f8e936912b1f5d9495a84db5bded15e9773af9 Mon Sep 17 00:00:00 2001 From: yifeizhuang Date: Fri, 21 Mar 2025 15:19:25 -0700 Subject: [PATCH 222/591] otel tracing: fix span names (#11974) --- .../OpenTelemetryTracingModule.java | 4 ++-- .../OpenTelemetryTracingModuleTest.java | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java index 838ee0797a7..8c42a189ac2 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java @@ -446,7 +446,7 @@ private void recordOutboundMessageSentEvent(Span span, if (optionalWireSize != -1 && optionalWireSize != optionalUncompressedSize) { attributesBuilder.put("message-size-compressed", optionalWireSize); } - span.addEvent("Outbound message sent", attributesBuilder.build()); + span.addEvent("Outbound message", attributesBuilder.build()); } private void recordInboundCompressedMessage(Span span, int seqNo, long optionalWireSize) { @@ -460,7 +460,7 @@ private void recordInboundMessageSize(Span span, int seqNo, long bytes) { AttributesBuilder attributesBuilder = io.opentelemetry.api.common.Attributes.builder(); attributesBuilder.put("sequence-number", seqNo); attributesBuilder.put("message-size", bytes); - span.addEvent("Inbound message received", attributesBuilder.build()); + span.addEvent("Inbound message", attributesBuilder.build()); } private String generateErrorStatusDescription(io.grpc.Status status) { diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java index b4486bcf2e4..bca6be94b9f 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java @@ -231,7 +231,7 @@ public void clientBasicTracingMocking() { List events = eventNameCaptor.getAllValues(); List attributes = attributesCaptor.getAllValues(); assertEquals( - "Outbound message sent" , + "Outbound message" , events.get(0)); assertEquals( io.opentelemetry.api.common.Attributes.builder() @@ -241,7 +241,7 @@ public void clientBasicTracingMocking() { attributes.get(0)); assertEquals( - "Outbound message sent" , + "Outbound message" , events.get(1)); assertEquals( io.opentelemetry.api.common.Attributes.builder() @@ -313,7 +313,7 @@ public void clientBasicTracingRule() { assertTrue(clientSpanEvents.get(0).getAttributes().isEmpty()); assertEquals( - "Inbound message received" , + "Inbound message" , clientSpanEvents.get(1).getName()); assertEquals( io.opentelemetry.api.common.Attributes.builder() @@ -323,7 +323,7 @@ public void clientBasicTracingRule() { clientSpanEvents.get(1).getAttributes()); assertEquals( - "Inbound message received" , + "Inbound message" , clientSpanEvents.get(2).getName()); assertEquals( io.opentelemetry.api.common.Attributes.builder() @@ -342,7 +342,7 @@ public void clientBasicTracingRule() { assertTrue(clientSpanEvents.get(0).getAttributes().isEmpty()); assertEquals( - "Outbound message sent" , + "Outbound message" , attemptSpanEvents.get(1).getName()); assertEquals( io.opentelemetry.api.common.Attributes.builder() @@ -352,7 +352,7 @@ public void clientBasicTracingRule() { attemptSpanEvents.get(1).getAttributes()); assertEquals( - "Outbound message sent" , + "Outbound message" , attemptSpanEvents.get(2).getName()); assertEquals( io.opentelemetry.api.common.Attributes.builder() @@ -518,7 +518,7 @@ public void serverBasicTracingNoHeaders() { List events = spans.get(0).getEvents(); assertEquals(events.size(), 4); assertEquals( - "Outbound message sent" , + "Outbound message" , events.get(0).getName()); assertEquals( io.opentelemetry.api.common.Attributes.builder() @@ -529,7 +529,7 @@ public void serverBasicTracingNoHeaders() { events.get(0).getAttributes()); assertEquals( - "Outbound message sent" , + "Outbound message" , events.get(1).getName()); assertEquals( io.opentelemetry.api.common.Attributes.builder() @@ -549,7 +549,7 @@ public void serverBasicTracingNoHeaders() { events.get(2).getAttributes()); assertEquals( - "Inbound message received" , + "Inbound message" , events.get(3).getName()); assertEquals( io.opentelemetry.api.common.Attributes.builder() From 1958e42370d58dc313ed8810f2476ba577e8a7df Mon Sep 17 00:00:00 2001 From: Ashley Zhang Date: Fri, 21 Mar 2025 15:19:40 -0700 Subject: [PATCH 223/591] xds: add support for custom per-target credentials on the transport (#11951) --- .../io/grpc/xds/GrpcXdsTransportFactory.java | 54 +++++++++---- .../InternalSharedXdsClientPoolProvider.java | 10 ++- .../grpc/xds/SharedXdsClientPoolProvider.java | 50 ++++++++---- .../grpc/xds/GrpcXdsClientImplTestBase.java | 3 +- .../grpc/xds/GrpcXdsTransportFactoryTest.java | 7 +- .../io/grpc/xds/LoadReportClientTest.java | 14 ++-- .../xds/SharedXdsClientPoolProviderTest.java | 77 +++++++++++++++++++ .../io/grpc/xds/XdsClientFallbackTest.java | 34 +++++--- 8 files changed, 198 insertions(+), 51 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java b/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java index 74c28ba2d2d..0da51bf47f7 100644 --- a/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java +++ b/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; +import io.grpc.CallCredentials; import io.grpc.CallOptions; import io.grpc.ChannelCredentials; import io.grpc.ClientCall; @@ -34,35 +35,50 @@ final class GrpcXdsTransportFactory implements XdsTransportFactory { - static final GrpcXdsTransportFactory DEFAULT_XDS_TRANSPORT_FACTORY = - new GrpcXdsTransportFactory(); + private final CallCredentials callCredentials; + + GrpcXdsTransportFactory(CallCredentials callCredentials) { + this.callCredentials = callCredentials; + } @Override public XdsTransport create(Bootstrapper.ServerInfo serverInfo) { - return new GrpcXdsTransport(serverInfo); + return new GrpcXdsTransport(serverInfo, callCredentials); } @VisibleForTesting public XdsTransport createForTest(ManagedChannel channel) { - return new GrpcXdsTransport(channel); + return new GrpcXdsTransport(channel, callCredentials); } @VisibleForTesting static class GrpcXdsTransport implements XdsTransport { private final ManagedChannel channel; + private final CallCredentials callCredentials; public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo) { + this(serverInfo, null); + } + + @VisibleForTesting + public GrpcXdsTransport(ManagedChannel channel) { + this(channel, null); + } + + public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo, CallCredentials callCredentials) { String target = serverInfo.target(); ChannelCredentials channelCredentials = (ChannelCredentials) serverInfo.implSpecificConfig(); this.channel = Grpc.newChannelBuilder(target, channelCredentials) .keepAliveTime(5, TimeUnit.MINUTES) .build(); + this.callCredentials = callCredentials; } @VisibleForTesting - public GrpcXdsTransport(ManagedChannel channel) { + public GrpcXdsTransport(ManagedChannel channel, CallCredentials callCredentials) { this.channel = checkNotNull(channel, "channel"); + this.callCredentials = callCredentials; } @Override @@ -72,7 +88,8 @@ public StreamingCall createStreamingCall( MethodDescriptor.Marshaller respMarshaller) { Context prevContext = Context.ROOT.attach(); try { - return new XdsStreamingCall<>(fullMethodName, reqMarshaller, respMarshaller); + return new XdsStreamingCall<>( + fullMethodName, reqMarshaller, respMarshaller, callCredentials); } finally { Context.ROOT.detach(prevContext); } @@ -89,16 +106,21 @@ private class XdsStreamingCall implements private final ClientCall call; - public XdsStreamingCall(String methodName, MethodDescriptor.Marshaller reqMarshaller, - MethodDescriptor.Marshaller respMarshaller) { - this.call = channel.newCall( - MethodDescriptor.newBuilder() - .setFullMethodName(methodName) - .setType(MethodDescriptor.MethodType.BIDI_STREAMING) - .setRequestMarshaller(reqMarshaller) - .setResponseMarshaller(respMarshaller) - .build(), - CallOptions.DEFAULT); // TODO(zivy): support waitForReady + public XdsStreamingCall( + String methodName, + MethodDescriptor.Marshaller reqMarshaller, + MethodDescriptor.Marshaller respMarshaller, + CallCredentials callCredentials) { + this.call = + channel.newCall( + MethodDescriptor.newBuilder() + .setFullMethodName(methodName) + .setType(MethodDescriptor.MethodType.BIDI_STREAMING) + .setRequestMarshaller(reqMarshaller) + .setResponseMarshaller(respMarshaller) + .build(), + CallOptions.DEFAULT.withCallCredentials( + callCredentials)); // TODO(zivy): support waitForReady } @Override diff --git a/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java index 85b59fabfa0..9c98bba93cf 100644 --- a/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java +++ b/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java @@ -16,6 +16,7 @@ package io.grpc.xds; +import io.grpc.CallCredentials; import io.grpc.Internal; import io.grpc.MetricRecorder; import io.grpc.internal.ObjectPool; @@ -42,6 +43,13 @@ public static ObjectPool getOrCreate(String target) public static ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) throws XdsInitializationException { - return SharedXdsClientPoolProvider.getDefaultProvider().getOrCreate(target, metricRecorder); + return getOrCreate(target, metricRecorder, null); + } + + public static ObjectPool getOrCreate( + String target, MetricRecorder metricRecorder, CallCredentials transportCallCredentials) + throws XdsInitializationException { + return SharedXdsClientPoolProvider.getDefaultProvider() + .getOrCreate(target, metricRecorder, transportCallCredentials); } } diff --git a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java index 2bc7be4a014..5302880d48c 100644 --- a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java +++ b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java @@ -17,11 +17,11 @@ package io.grpc.xds; import static com.google.common.base.Preconditions.checkNotNull; -import static io.grpc.xds.GrpcXdsTransportFactory.DEFAULT_XDS_TRANSPORT_FACTORY; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.grpc.CallCredentials; import io.grpc.MetricRecorder; import io.grpc.internal.ExponentialBackoffPolicy; import io.grpc.internal.GrpcUtil; @@ -87,6 +87,12 @@ public ObjectPool get(String target) { @Override public ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) throws XdsInitializationException { + return getOrCreate(target, metricRecorder, null); + } + + public ObjectPool getOrCreate( + String target, MetricRecorder metricRecorder, CallCredentials transportCallCredentials) + throws XdsInitializationException { ObjectPool ref = targetToXdsClientMap.get(target); if (ref == null) { synchronized (lock) { @@ -102,7 +108,9 @@ public ObjectPool getOrCreate(String target, MetricRecorder metricRec if (bootstrapInfo.servers().isEmpty()) { throw new XdsInitializationException("No xDS server provided"); } - ref = new RefCountedXdsClientObjectPool(bootstrapInfo, target, metricRecorder); + ref = + new RefCountedXdsClientObjectPool( + bootstrapInfo, target, metricRecorder, transportCallCredentials); targetToXdsClientMap.put(target, ref); } } @@ -126,6 +134,7 @@ class RefCountedXdsClientObjectPool implements ObjectPool { private final BootstrapInfo bootstrapInfo; private final String target; // The target associated with the xDS client. private final MetricRecorder metricRecorder; + private final CallCredentials transportCallCredentials; private final Object lock = new Object(); @GuardedBy("lock") private ScheduledExecutorService scheduler; @@ -137,11 +146,21 @@ class RefCountedXdsClientObjectPool implements ObjectPool { private XdsClientMetricReporterImpl metricReporter; @VisibleForTesting - RefCountedXdsClientObjectPool(BootstrapInfo bootstrapInfo, String target, - MetricRecorder metricRecorder) { + RefCountedXdsClientObjectPool( + BootstrapInfo bootstrapInfo, String target, MetricRecorder metricRecorder) { + this(bootstrapInfo, target, metricRecorder, null); + } + + @VisibleForTesting + RefCountedXdsClientObjectPool( + BootstrapInfo bootstrapInfo, + String target, + MetricRecorder metricRecorder, + CallCredentials transportCallCredentials) { this.bootstrapInfo = checkNotNull(bootstrapInfo); this.target = target; this.metricRecorder = metricRecorder; + this.transportCallCredentials = transportCallCredentials; } @Override @@ -153,16 +172,19 @@ public XdsClient getObject() { } scheduler = SharedResourceHolder.get(GrpcUtil.TIMER_SERVICE); metricReporter = new XdsClientMetricReporterImpl(metricRecorder, target); - xdsClient = new XdsClientImpl( - DEFAULT_XDS_TRANSPORT_FACTORY, - bootstrapInfo, - scheduler, - BACKOFF_POLICY_PROVIDER, - GrpcUtil.STOPWATCH_SUPPLIER, - TimeProvider.SYSTEM_TIME_PROVIDER, - MessagePrinter.INSTANCE, - new TlsContextManagerImpl(bootstrapInfo), - metricReporter); + GrpcXdsTransportFactory xdsTransportFactory = + new GrpcXdsTransportFactory(transportCallCredentials); + xdsClient = + new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + scheduler, + BACKOFF_POLICY_PROVIDER, + GrpcUtil.STOPWATCH_SUPPLIER, + TimeProvider.SYSTEM_TIME_PROVIDER, + MessagePrinter.INSTANCE, + new TlsContextManagerImpl(bootstrapInfo), + metricReporter); metricReporter.setXdsClient(xdsClient); } refCount++; diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 51c07cb3537..36131464d08 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import static io.grpc.xds.GrpcXdsTransportFactory.DEFAULT_XDS_TRANSPORT_FACTORY; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; @@ -4193,7 +4192,7 @@ public void serverFailureMetricReport_forRetryAndBackoff() { private XdsClientImpl createXdsClient(String serverUri) { BootstrapInfo bootstrapInfo = buildBootStrap(serverUri); return new XdsClientImpl( - DEFAULT_XDS_TRANSPORT_FACTORY, + new GrpcXdsTransportFactory(null), bootstrapInfo, fakeClock.getScheduledExecutorService(), backoffPolicyProvider, diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsTransportFactoryTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsTransportFactoryTest.java index 703e429fa23..66e0d4b3198 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsTransportFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsTransportFactoryTest.java @@ -92,9 +92,10 @@ public void onCompleted() { @Test public void callApis() throws Exception { XdsTransportFactory.XdsTransport xdsTransport = - GrpcXdsTransportFactory.DEFAULT_XDS_TRANSPORT_FACTORY.create( - Bootstrapper.ServerInfo.create("localhost:" + server.getPort(), - InsecureChannelCredentials.create())); + new GrpcXdsTransportFactory(null) + .create( + Bootstrapper.ServerInfo.create( + "localhost:" + server.getPort(), InsecureChannelCredentials.create())); MethodDescriptor methodDescriptor = AggregatedDiscoveryServiceGrpc.getStreamAggregatedResourcesMethod(); XdsTransportFactory.StreamingCall streamingCall = diff --git a/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java b/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java index c11a3a6e0d2..9bdf86132b6 100644 --- a/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java +++ b/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java @@ -178,11 +178,15 @@ public void cancelled(Context context) { when(backoffPolicy2.nextBackoffNanos()) .thenReturn(TimeUnit.SECONDS.toNanos(2L), TimeUnit.SECONDS.toNanos(20L)); addFakeStatsData(); - lrsClient = new LoadReportClient(loadStatsManager, - GrpcXdsTransportFactory.DEFAULT_XDS_TRANSPORT_FACTORY.createForTest(channel), - NODE, - syncContext, fakeClock.getScheduledExecutorService(), backoffPolicyProvider, - fakeClock.getStopwatchSupplier()); + lrsClient = + new LoadReportClient( + loadStatsManager, + new GrpcXdsTransportFactory(null).createForTest(channel), + NODE, + syncContext, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier()); syncContext.execute(new Runnable() { @Override public void run() { diff --git a/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java b/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java index 4fb77f0be42..86e4fc83a8c 100644 --- a/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java @@ -18,20 +18,36 @@ import static com.google.common.truth.Truth.assertThat; +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.OAuth2Credentials; +import com.google.common.util.concurrent.SettableFuture; +import io.grpc.CallCredentials; +import io.grpc.Grpc; import io.grpc.InsecureChannelCredentials; +import io.grpc.InsecureServerCredentials; +import io.grpc.Metadata; import io.grpc.MetricRecorder; +import io.grpc.Server; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.auth.MoreCallCredentials; import io.grpc.internal.ObjectPool; import io.grpc.xds.SharedXdsClientPoolProvider.RefCountedXdsClientObjectPool; +import io.grpc.xds.XdsListenerResource.LdsUpdate; import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.Bootstrapper.ServerInfo; import io.grpc.xds.client.EnvoyProtoData.Node; import io.grpc.xds.client.XdsClient; +import io.grpc.xds.client.XdsClient.ResourceWatcher; import io.grpc.xds.client.XdsInitializationException; import java.util.Collections; +import java.util.concurrent.TimeUnit; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -54,9 +70,12 @@ public class SharedXdsClientPoolProviderTest { private final Node node = Node.newBuilder().setId("SharedXdsClientPoolProviderTest").build(); private final MetricRecorder metricRecorder = new MetricRecorder() {}; private static final String DUMMY_TARGET = "dummy"; + static final Metadata.Key AUTHORIZATION_METADATA_KEY = + Metadata.Key.of("Authorization", ASCII_STRING_MARSHALLER); @Mock private GrpcBootstrapperImpl bootstrapper; + @Mock private ResourceWatcher ldsResourceWatcher; @Test public void noServer() throws XdsInitializationException { @@ -138,4 +157,62 @@ public void refCountedXdsClientObjectPool_getObjectCreatesNewInstanceIfAlreadySh assertThat(xdsClient2).isNotSameInstanceAs(xdsClient1); xdsClientPool.returnObject(xdsClient2); } + + private class CallCredsServerInterceptor implements ServerInterceptor { + private SettableFuture tokenFuture = SettableFuture.create(); + + @Override + public ServerCall.Listener interceptCall( + ServerCall serverCall, + Metadata metadata, + ServerCallHandler next) { + tokenFuture.set(metadata.get(AUTHORIZATION_METADATA_KEY)); + return next.startCall(serverCall, metadata); + } + + public String getTokenWithTimeout(long timeout, TimeUnit unit) throws Exception { + return tokenFuture.get(timeout, unit); + } + } + + @Test + public void xdsClient_usesCallCredentials() throws Exception { + // Set up fake xDS server + XdsTestControlPlaneService fakeXdsService = new XdsTestControlPlaneService(); + CallCredsServerInterceptor callCredentialsInterceptor = new CallCredsServerInterceptor(); + Server xdsServer = + Grpc.newServerBuilderForPort(0, InsecureServerCredentials.create()) + .addService(fakeXdsService) + .intercept(callCredentialsInterceptor) + .build() + .start(); + String xdsServerUri = "localhost:" + xdsServer.getPort(); + + // Set up bootstrap & xDS client pool provider + ServerInfo server = ServerInfo.create(xdsServerUri, InsecureChannelCredentials.create()); + BootstrapInfo bootstrapInfo = + BootstrapInfo.builder().servers(Collections.singletonList(server)).node(node).build(); + when(bootstrapper.bootstrap()).thenReturn(bootstrapInfo); + SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); + + // Create custom xDS transport CallCredentials + CallCredentials sampleCreds = + MoreCallCredentials.from( + OAuth2Credentials.create(new AccessToken("token", /* expirationTime= */ null))); + + // Create xDS client that uses the CallCredentials on the transport + ObjectPool xdsClientPool = + provider.getOrCreate("target", metricRecorder, sampleCreds); + XdsClient xdsClient = xdsClientPool.getObject(); + xdsClient.watchXdsResource( + XdsListenerResource.getInstance(), "someLDSresource", ldsResourceWatcher); + + // Wait for xDS server to get the request and verify that it received the CallCredentials + assertThat(callCredentialsInterceptor.getTokenWithTimeout(5, TimeUnit.SECONDS)) + .isEqualTo("Bearer token"); + + // Clean up + xdsClientPool.returnObject(xdsClient); + xdsServer.shutdownNow(); + } } diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java index 97c2695f209..036b9f6f55d 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java @@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import static io.grpc.xds.GrpcXdsTransportFactory.DEFAULT_XDS_TRANSPORT_FACTORY; import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -442,9 +441,14 @@ public void fallbackFromBadUrlToGoodOne() { String garbageUri = "some. garbage"; String validUri = "localhost:" + mainXdsServer.getServer().getPort(); - XdsClientImpl client = CommonBootstrapperTestUtils.createXdsClient( - Arrays.asList(garbageUri, validUri), DEFAULT_XDS_TRANSPORT_FACTORY, fakeClock, - new ExponentialBackoffPolicy.Provider(), MessagePrinter.INSTANCE, xdsClientMetricReporter); + XdsClientImpl client = + CommonBootstrapperTestUtils.createXdsClient( + Arrays.asList(garbageUri, validUri), + new GrpcXdsTransportFactory(null), + fakeClock, + new ExponentialBackoffPolicy.Provider(), + MessagePrinter.INSTANCE, + xdsClientMetricReporter); client.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); fakeClock.forwardTime(20, TimeUnit.SECONDS); @@ -462,9 +466,14 @@ public void testGoodUrlFollowedByBadUrl() { String garbageUri = "some. garbage"; String validUri = "localhost:" + mainXdsServer.getServer().getPort(); - XdsClientImpl client = CommonBootstrapperTestUtils.createXdsClient( - Arrays.asList(validUri, garbageUri), DEFAULT_XDS_TRANSPORT_FACTORY, fakeClock, - new ExponentialBackoffPolicy.Provider(), MessagePrinter.INSTANCE, xdsClientMetricReporter); + XdsClientImpl client = + CommonBootstrapperTestUtils.createXdsClient( + Arrays.asList(validUri, garbageUri), + new GrpcXdsTransportFactory(null), + fakeClock, + new ExponentialBackoffPolicy.Provider(), + MessagePrinter.INSTANCE, + xdsClientMetricReporter); client.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); verify(ldsWatcher, timeout(5000)).onChanged( @@ -481,9 +490,14 @@ public void testTwoBadUrl() { String garbageUri1 = "some. garbage"; String garbageUri2 = "other garbage"; - XdsClientImpl client = CommonBootstrapperTestUtils.createXdsClient( - Arrays.asList(garbageUri1, garbageUri2), DEFAULT_XDS_TRANSPORT_FACTORY, fakeClock, - new ExponentialBackoffPolicy.Provider(), MessagePrinter.INSTANCE, xdsClientMetricReporter); + XdsClientImpl client = + CommonBootstrapperTestUtils.createXdsClient( + Arrays.asList(garbageUri1, garbageUri2), + new GrpcXdsTransportFactory(null), + fakeClock, + new ExponentialBackoffPolicy.Provider(), + MessagePrinter.INSTANCE, + xdsClientMetricReporter); client.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); fakeClock.forwardTime(20, TimeUnit.SECONDS); From 3961a923ac553be623d071902ab42a64d1233d08 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 24 Mar 2025 21:32:53 +0000 Subject: [PATCH 224/591] core: Log any exception during panic because of exception panic() calls a good amount of code, so it could get another exception. The SynchronizationContext is running on an arbitrary thread and we don't want to propagate this secondary exception up its stack (to be handled by its UncaughtExceptionHandler); it we wanted that we'd propagate the original exception. This second exception will only be seen in the logs; the first exception was logged and will be used to fail RPCs. Also related to http://yaqs/8493785598685872128 and b692b9d26 --- .../src/main/java/io/grpc/internal/ManagedChannelImpl.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index dd4d0aef5be..1b51c2dbb32 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -187,7 +187,12 @@ public void uncaughtException(Thread t, Throwable e) { Level.SEVERE, "[" + getLogId() + "] Uncaught exception in the SynchronizationContext. Panic!", e); - panic(e); + try { + panic(e); + } catch (Throwable anotherT) { + logger.log( + Level.SEVERE, "[" + getLogId() + "] Uncaught exception while panicking", anotherT); + } } }); From 350f90e1a30abf884f8fa4f40c4a1e8fca9425df Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Tue, 25 Mar 2025 20:12:28 +0800 Subject: [PATCH 225/591] services: Avoid cancellation exceptions when notifying watchers that already have their connections cancelled (#11934) Some clients watching health status can cancel their watch and `HealthService` when trying to notify these watchers were getting CANCELLED exception because there was no cancellation handler set on the `StreamObserver`. This change sets the cancellation handler that removes the watcher from the set of watcher clients to be notified of the health status. --- .../protobuf/services/HealthServiceImpl.java | 30 ++++++++++++------- .../services/HealthStatusManagerTest.java | 18 +++++++++++ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/services/src/main/java/io/grpc/protobuf/services/HealthServiceImpl.java b/services/src/main/java/io/grpc/protobuf/services/HealthServiceImpl.java index 2efe4b3951a..5cd294b4fbe 100644 --- a/services/src/main/java/io/grpc/protobuf/services/HealthServiceImpl.java +++ b/services/src/main/java/io/grpc/protobuf/services/HealthServiceImpl.java @@ -27,6 +27,7 @@ import io.grpc.health.v1.HealthCheckResponse; import io.grpc.health.v1.HealthCheckResponse.ServingStatus; import io.grpc.health.v1.HealthGrpc; +import io.grpc.stub.ServerCallStreamObserver; import io.grpc.stub.StreamObserver; import java.util.HashMap; import java.util.IdentityHashMap; @@ -83,6 +84,11 @@ public void watch(HealthCheckRequest request, final StreamObserver responseObserver) { final String service = request.getService(); synchronized (watchLock) { + if (responseObserver instanceof ServerCallStreamObserver) { + ((ServerCallStreamObserver) responseObserver).setOnCancelHandler(() -> { + removeWatcher(service, responseObserver); + }); + } ServingStatus status = statusMap.get(service); responseObserver.onNext(getResponseForWatch(status)); IdentityHashMap, Boolean> serviceWatchers = @@ -98,21 +104,25 @@ public void watch(HealthCheckRequest request, @Override // Called when the client has closed the stream public void cancelled(Context context) { - synchronized (watchLock) { - IdentityHashMap, Boolean> serviceWatchers = - watchers.get(service); - if (serviceWatchers != null) { - serviceWatchers.remove(responseObserver); - if (serviceWatchers.isEmpty()) { - watchers.remove(service); - } - } - } + removeWatcher(service, responseObserver); } }, MoreExecutors.directExecutor()); } + void removeWatcher(String service, StreamObserver responseObserver) { + synchronized (watchLock) { + IdentityHashMap, Boolean> serviceWatchers = + watchers.get(service); + if (serviceWatchers != null) { + serviceWatchers.remove(responseObserver); + if (serviceWatchers.isEmpty()) { + watchers.remove(service); + } + } + } + } + void setStatus(String service, ServingStatus status) { synchronized (watchLock) { if (terminal) { diff --git a/services/src/test/java/io/grpc/protobuf/services/HealthStatusManagerTest.java b/services/src/test/java/io/grpc/protobuf/services/HealthStatusManagerTest.java index 87d4ac29be8..b2652e92771 100644 --- a/services/src/test/java/io/grpc/protobuf/services/HealthStatusManagerTest.java +++ b/services/src/test/java/io/grpc/protobuf/services/HealthStatusManagerTest.java @@ -18,6 +18,11 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import io.grpc.BindableService; import io.grpc.Context; @@ -28,6 +33,7 @@ import io.grpc.health.v1.HealthCheckResponse; import io.grpc.health.v1.HealthCheckResponse.ServingStatus; import io.grpc.health.v1.HealthGrpc; +import io.grpc.stub.ServerCallStreamObserver; import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcServerRule; import java.util.ArrayDeque; @@ -109,6 +115,18 @@ public void enterTerminalState_watch() throws Exception { assertThat(obs.responses).isEmpty(); } + @Test + @SuppressWarnings("unchecked") + public void serverCallStreamObserver_watch() throws Exception { + manager.setStatus(SERVICE1, ServingStatus.SERVING); + ServerCallStreamObserver observer = mock(ServerCallStreamObserver.class); + service.watch(HealthCheckRequest.newBuilder().setService(SERVICE1).build(), observer); + + verify(observer, times(1)) + .onNext(eq(HealthCheckResponse.newBuilder().setStatus(ServingStatus.SERVING).build())); + verify(observer, times(1)).setOnCancelHandler(any(Runnable.class)); + } + @Test public void enterTerminalState_ignoreClear() throws Exception { manager.setStatus(SERVICE1, ServingStatus.SERVING); From a332eddc132a90b8c0d5a51c8902b2a06444ab0b Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Wed, 26 Mar 2025 06:13:05 +0000 Subject: [PATCH 226/591] fix: cleans up FileWatcherCertificateProvider in XdsSecurityClientServerTest --- .../io/grpc/xds/XdsSecurityClientServerTest.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java index cd3ef293369..380c0591812 100644 --- a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java @@ -130,6 +130,7 @@ public class XdsSecurityClientServerTest { private FakeXdsClient xdsClient = new FakeXdsClient(); private FakeXdsClientPoolFactory fakePoolFactory = new FakeXdsClientPoolFactory(xdsClient); private static final String OVERRIDE_AUTHORITY = "foo.test.google.fr"; + private Attributes sslContextAttributes; @Parameters(name = "enableSpiffe={0}") public static Collection data() { @@ -152,6 +153,14 @@ public void tearDown() throws IOException { NameResolverRegistry.getDefaultRegistry().deregister(fakeNameResolverFactory); } FileWatcherCertificateProviderProvider.enableSpiffe = originalEnableSpiffe; + if (sslContextAttributes != null) { + SslContextProviderSupplier sslContextProviderSupplier = sslContextAttributes.get( + SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER); + if (sslContextProviderSupplier != null) { + sslContextProviderSupplier.close(); + } + sslContextAttributes = null; + } } @Test @@ -651,7 +660,7 @@ private SimpleServiceGrpc.SimpleServiceBlockingStub getBlockingStub( InetSocketAddress socketAddress = new InetSocketAddress(Inet4Address.getLoopbackAddress(), port); tlsContextManagerForClient = new TlsContextManagerImpl(bootstrapInfoForClient); - Attributes attrs = + sslContextAttributes = (upstreamTlsContext != null) ? Attributes.newBuilder() .set(SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER, @@ -660,7 +669,7 @@ private SimpleServiceGrpc.SimpleServiceBlockingStub getBlockingStub( .build() : Attributes.EMPTY; fakeNameResolverFactory.setServers( - ImmutableList.of(new EquivalentAddressGroup(socketAddress, attrs))); + ImmutableList.of(new EquivalentAddressGroup(socketAddress, sslContextAttributes))); return SimpleServiceGrpc.newBlockingStub(cleanupRule.register(channelBuilder.build())); } From 7507a9ec06fc9490c5bb967c3afea953e9d1c98d Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:19:21 +0200 Subject: [PATCH 227/591] core: Use java.time.Time.getNano in InstantTimeProvider without reflection (#11977) Fixes #11975 --- .../io/grpc/internal/InstantTimeProvider.java | 30 ++++--------------- .../internal/TimeProviderResolverFactory.java | 4 +-- .../internal/InstantTimeProviderTest.java | 3 +- 3 files changed, 9 insertions(+), 28 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/InstantTimeProvider.java b/core/src/main/java/io/grpc/internal/InstantTimeProvider.java index 38c840d2594..12996163753 100644 --- a/core/src/main/java/io/grpc/internal/InstantTimeProvider.java +++ b/core/src/main/java/io/grpc/internal/InstantTimeProvider.java @@ -18,37 +18,19 @@ import static com.google.common.math.LongMath.saturatedAdd; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.time.Instant; import java.util.concurrent.TimeUnit; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; /** * {@link InstantTimeProvider} resolves InstantTimeProvider which implements {@link TimeProvider}. */ final class InstantTimeProvider implements TimeProvider { - private Method now; - private Method getNano; - private Method getEpochSecond; - - public InstantTimeProvider(Class instantClass) { - try { - this.now = instantClass.getMethod("now"); - this.getNano = instantClass.getMethod("getNano"); - this.getEpochSecond = instantClass.getMethod("getEpochSecond"); - } catch (NoSuchMethodException ex) { - throw new AssertionError(ex); - } - } - @Override + @IgnoreJRERequirement public long currentTimeNanos() { - try { - Object instant = now.invoke(null); - int nanos = (int) getNano.invoke(instant); - long epochSeconds = (long) getEpochSecond.invoke(instant); - return saturatedAdd(TimeUnit.SECONDS.toNanos(epochSeconds), nanos); - } catch (IllegalAccessException | InvocationTargetException ex) { - throw new RuntimeException(ex); - } + Instant now = Instant.now(); + long epochSeconds = now.getEpochSecond(); + return saturatedAdd(TimeUnit.SECONDS.toNanos(epochSeconds), now.getNano()); } } diff --git a/core/src/main/java/io/grpc/internal/TimeProviderResolverFactory.java b/core/src/main/java/io/grpc/internal/TimeProviderResolverFactory.java index d88d9bb9eb5..04272034ce9 100644 --- a/core/src/main/java/io/grpc/internal/TimeProviderResolverFactory.java +++ b/core/src/main/java/io/grpc/internal/TimeProviderResolverFactory.java @@ -23,8 +23,8 @@ final class TimeProviderResolverFactory { static TimeProvider resolveTimeProvider() { try { - Class instantClass = Class.forName("java.time.Instant"); - return new InstantTimeProvider(instantClass); + Class.forName("java.time.Instant"); + return new InstantTimeProvider(); } catch (ClassNotFoundException ex) { return new ConcurrentTimeProvider(); } diff --git a/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java b/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java index ac9a02fa936..6702bc421a5 100644 --- a/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java +++ b/core/src/test/java/io/grpc/internal/InstantTimeProviderTest.java @@ -34,8 +34,7 @@ public class InstantTimeProviderTest { @Test public void testInstantCurrentTimeNanos() throws Exception { - InstantTimeProvider instantTimeProvider = new InstantTimeProvider( - Class.forName("java.time.Instant")); + InstantTimeProvider instantTimeProvider = new InstantTimeProvider(); // Get the current time from the InstantTimeProvider long actualTimeNanos = instantTimeProvider.currentTimeNanos(); From 2e260a4bbc4f19669ca8bd91c86425414d48946c Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 27 Mar 2025 13:52:30 -0700 Subject: [PATCH 228/591] util: Graceful switch to new LB when leaving CONNECTING Previously it would wait for the new LB to enter READY. However, that prevents there being an upper-bound on how long the old policy will continue to be used. The point of graceful switch is to avoid RPCs seeing increased latency when we swap config. We don't want it to prevent the system from becoming eventually consistent. --- .../grpc/util/GracefulSwitchLoadBalancer.java | 5 ++-- .../util/GracefulSwitchLoadBalancerTest.java | 26 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java b/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java index 72c41886ad4..e36eec1ff25 100644 --- a/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java @@ -38,7 +38,8 @@ /** * A load balancer that gracefully swaps to a new lb policy. If the channel is currently in a state * other than READY, the new policy will be swapped into place immediately. Otherwise, the channel - * will keep using the old policy until the new policy reports READY or the old policy exits READY. + * will keep using the old policy until the new policy leaves CONNECTING or the old policy exits + * READY. * *

    The child balancer and configuration is specified using service config. Config objects are * generally created by calling {@link #parseLoadBalancingPolicyConfig(List)} from a @@ -147,7 +148,7 @@ public void updateBalancingState(ConnectivityState newState, SubchannelPicker ne checkState(currentLbIsReady, "there's pending lb while current lb has been out of READY"); pendingState = newState; pendingPicker = newPicker; - if (newState == ConnectivityState.READY) { + if (newState != ConnectivityState.CONNECTING) { swap(); } } else if (lb == currentLb) { diff --git a/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java b/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java index 9a4f569c144..5192d6a2a64 100644 --- a/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static io.grpc.ConnectivityState.CONNECTING; +import static io.grpc.ConnectivityState.IDLE; import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; import static io.grpc.util.GracefulSwitchLoadBalancer.BUFFER_PICKER; @@ -32,6 +33,7 @@ import static org.mockito.Mockito.when; import com.google.common.testing.EqualsTester; +import io.grpc.ConnectivityState; import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; import io.grpc.LoadBalancer; @@ -363,7 +365,21 @@ public void createSubchannelForwarded() { } @Test - public void updateBalancingStateIsGraceful() { + public void updateBalancingStateIsGraceful_Ready() { + updateBalancingStateIsGraceful(READY); + } + + @Test + public void updateBalancingStateIsGraceful_TransientFailure() { + updateBalancingStateIsGraceful(TRANSIENT_FAILURE); + } + + @Test + public void updateBalancingStateIsGraceful_Idle() { + updateBalancingStateIsGraceful(IDLE); + } + + public void updateBalancingStateIsGraceful(ConnectivityState swapsOnState) { assertIsOk(gracefulSwitchLb.acceptResolvedAddresses(addressesBuilder() .setLoadBalancingPolicyConfig(createConfig(lbPolicies[0], new Object())) .build())); @@ -392,11 +408,11 @@ public void updateBalancingStateIsGraceful() { helper2.updateBalancingState(CONNECTING, picker); verify(mockHelper, never()).updateBalancingState(CONNECTING, picker); - // lb2 reports READY + // lb2 reports swapsOnState SubchannelPicker picker2 = mock(SubchannelPicker.class); - helper2.updateBalancingState(READY, picker2); + helper2.updateBalancingState(swapsOnState, picker2); verify(lb0).shutdown(); - verify(mockHelper).updateBalancingState(READY, picker2); + verify(mockHelper).updateBalancingState(swapsOnState, picker2); assertIsOk(gracefulSwitchLb.acceptResolvedAddresses(addressesBuilder() .setLoadBalancingPolicyConfig(createConfig(lbPolicies[3], new Object())) @@ -407,7 +423,7 @@ public void updateBalancingStateIsGraceful() { helper3.updateBalancingState(CONNECTING, picker3); verify(mockHelper, never()).updateBalancingState(CONNECTING, picker3); - // lb2 out of READY + // lb2 out of swapsOnState picker2 = mock(SubchannelPicker.class); helper2.updateBalancingState(CONNECTING, picker2); verify(mockHelper, never()).updateBalancingState(CONNECTING, picker2); From 2448c8b6b9dd019a782c7e3824030e8b650ff700 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 28 Mar 2025 19:49:36 +0000 Subject: [PATCH 229/591] util: Replace BUFFER_PICKER with FixedResultPicker I think at some point there were more usages in the tests. But now it is pretty easy. PriorityLb.ChildLbState.picker is initialized to FixedResultPicker(NoResult). So now that GracefulSwitchLb is using the same picker, equals() is able to de-dup an update. --- .../io/grpc/util/GracefulSwitchLoadBalancer.java | 16 +--------------- .../util/GracefulSwitchLoadBalancerTest.java | 7 +++++-- .../io/grpc/xds/PriorityLoadBalancerTest.java | 9 ++++----- 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java b/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java index e36eec1ff25..1dc4fb6750a 100644 --- a/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java @@ -19,7 +19,6 @@ 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.MoreObjects; import com.google.common.base.Objects; import io.grpc.ConnectivityState; @@ -66,19 +65,6 @@ public void handleNameResolutionError(final Status error) { public void shutdown() {} }; - @VisibleForTesting - static final SubchannelPicker BUFFER_PICKER = new SubchannelPicker() { - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withNoResult(); - } - - @Override - public String toString() { - return "BUFFER_PICKER"; - } - }; - private final Helper helper; // While the new policy is not fully switched on, the pendingLb is handling new updates from name @@ -128,7 +114,7 @@ private void switchToInternal(LoadBalancer.Factory newBalancerFactory) { pendingLb = defaultBalancer; pendingBalancerFactory = null; pendingState = ConnectivityState.CONNECTING; - pendingPicker = BUFFER_PICKER; + pendingPicker = new FixedResultPicker(PickResult.withNoResult()); if (newBalancerFactory.equals(currentBalancerFactory)) { return; diff --git a/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java b/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java index 5192d6a2a64..843e16194c5 100644 --- a/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java @@ -21,7 +21,6 @@ import static io.grpc.ConnectivityState.IDLE; import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import static io.grpc.util.GracefulSwitchLoadBalancer.BUFFER_PICKER; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; @@ -521,7 +520,11 @@ public void switchWhileOldPolicyGoesFromReadyToNotReadyWhileNewPolicyStillIdle() helper0.updateBalancingState(CONNECTING, picker); verify(mockHelper, never()).updateBalancingState(CONNECTING, picker); - inOrder.verify(mockHelper).updateBalancingState(CONNECTING, BUFFER_PICKER); + ArgumentCaptor pickerCaptor = ArgumentCaptor.forClass(SubchannelPicker.class); + inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); + assertThat(pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)).hasResult()) + .isFalse(); + inOrder.verify(lb0).shutdown(); // shutdown after update picker = mock(SubchannelPicker.class); diff --git a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java index 9823501dcd9..08d4863d194 100644 --- a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java @@ -522,8 +522,7 @@ public void connectingResetFailOverIfSeenReadyOrIdleSinceTransientFailure() { .setLoadBalancingPolicyConfig(priorityLbConfig) .build()); // Nothing important about this verify, other than to provide a baseline - verify(helper, times(2)) - .updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult())); + verify(helper).updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult())); assertThat(fooBalancers).hasSize(1); assertThat(fooHelpers).hasSize(1); Helper helper0 = Iterables.getOnlyElement(fooHelpers); @@ -539,7 +538,7 @@ public void connectingResetFailOverIfSeenReadyOrIdleSinceTransientFailure() { helper0.updateBalancingState( CONNECTING, EMPTY_PICKER); - verify(helper, times(3)) + verify(helper, times(2)) .updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult())); // failover happens @@ -805,7 +804,7 @@ public void raceBetweenShutdownAndChildLbBalancingStateUpdate() { .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(priorityLbConfig) .build()); - verify(helper, times(2)).updateBalancingState(eq(CONNECTING), isA(SubchannelPicker.class)); + verify(helper).updateBalancingState(eq(CONNECTING), isA(SubchannelPicker.class)); // LB shutdown and subchannel state change can happen simultaneously. If shutdown runs first, // any further balancing state update should be ignored. @@ -843,7 +842,7 @@ public void noDuplicateOverallBalancingStateUpdate() { .setLoadBalancingPolicyConfig(priorityLbConfig) .build()); - verify(helper, times(6)).updateBalancingState(any(), any()); + verify(helper, times(4)).updateBalancingState(any(), any()); } private void assertLatestConnectivityState(ConnectivityState expectedState) { From 8f6a16f846d7ffe28609c773dd1630b44cf5b2db Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:57:39 +0000 Subject: [PATCH 230/591] Start 1.73.0 development cycle (#11987) --- MODULE.bazel | 2 +- build.gradle | 2 +- .../src/test/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/test/golden/TestService.java.txt | 2 +- core/src/main/java/io/grpc/internal/GrpcUtil.java | 2 +- examples/MODULE.bazel | 2 +- examples/android/clientcache/app/build.gradle | 10 +++++----- examples/android/helloworld/app/build.gradle | 8 ++++---- examples/android/routeguide/app/build.gradle | 8 ++++---- examples/android/strictmode/app/build.gradle | 8 ++++---- examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 4 ++-- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 4 ++-- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 4 ++-- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 4 ++-- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 4 ++-- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- 34 files changed, 55 insertions(+), 55 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 88f3a524060..d69bb1927aa 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "grpc-java", compatibility_level = 0, repo_name = "io_grpc_grpc_java", - version = "1.72.0-SNAPSHOT", # CURRENT_GRPC_VERSION + version = "1.73.0-SNAPSHOT", # CURRENT_GRPC_VERSION ) # GRPC_DEPS_START diff --git a/build.gradle b/build.gradle index 93ce60054bc..42e16fbfef9 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.72.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.73.0-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index 82fe6a81d18..a870a5a82e1 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; *

    */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.72.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.73.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index 912bd50da12..1f62ca26718 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; *
    */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.72.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.73.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index 937854ac3ff..15fcdfb6300 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -219,7 +219,7 @@ public byte[] parseAsciiString(byte[] serialized) { public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); - public static final String IMPLEMENTATION_VERSION = "1.72.0-SNAPSHOT"; // CURRENT_GRPC_VERSION + public static final String IMPLEMENTATION_VERSION = "1.73.0-SNAPSHOT"; // CURRENT_GRPC_VERSION /** * The default timeout in nanos for a keepalive ping request. diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index 4d72ae9c395..b960555e8c5 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,5 +1,5 @@ bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") -bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.72.0-SNAPSHOT") # CURRENT_GRPC_VERSION +bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.73.0-SNAPSHOT") # CURRENT_GRPC_VERSION bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") bazel_dep(name = "rules_jvm_external", version = "6.0") diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index 1f2a17ae6bb..670193167fe 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -34,7 +34,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -54,12 +54,12 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.5' - testImplementation 'io.grpc:grpc-testing:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + testImplementation 'io.grpc:grpc-testing:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index 09b994a4954..81f79c67440 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index bdad129845b..12e6430d2ad 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index f38110a741b..f37e970ed7b 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -33,7 +33,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -53,8 +53,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/build.gradle b/examples/build.gradle index e807d09f407..206fd38e0e3 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index e7142f3fb5a..c7d804973a5 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index 1d07a7cb8ec..c2449833cdc 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index 00fdecdb6c4..ce46b13c019 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -6,13 +6,13 @@ jar - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT example-debug https://github.com/grpc/grpc-java UTF-8 - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index b73902095a1..7a37c46b536 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index 28539851934..4af44beae46 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -6,13 +6,13 @@ jar - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT example-dualstack https://github.com/grpc/grpc-java UTF-8 - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 0ef0dcaefe2..404cab907de 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index d2a32578550..b8f8d4d930e 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,13 +6,13 @@ jar - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index e16e32e3bc1..dcb1f254263 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 1cad10bbb87..673d3f4461d 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 86aa42f8ed0..a0b9153785e 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index 6993c40a1ac..f0aab1c8edc 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,13 +6,13 @@ jar - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index ca12c3f7872..0d6d095bf1d 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index 7e9a915bfbd..cc7171910df 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,13 +7,13 @@ jar - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 6d06f097ccb..9e40c3e33f9 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index a9fea928a34..36cf63dd602 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -7,13 +7,13 @@ jar - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT example-oauth https://github.com/grpc/grpc-java UTF-8 - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index f575d24d19b..5f98b32be60 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index 45235fa1e08..edb28e1573b 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index ad68e891436..18157b0eed1 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index 2176df5afc5..e66fda59e4e 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -15,7 +15,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index e741bfe1c3f..9603e04e417 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index de9298cf0e1..c8b87f54cd0 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,13 +6,13 @@ jar - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT example-tls https://github.com/grpc/grpc-java UTF-8 - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index c0159dce258..1e55f182d3a 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.72.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/pom.xml b/examples/pom.xml index edc9c4cda14..ff32936f2a2 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,13 +6,13 @@ jar - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT examples https://github.com/grpc/grpc-java UTF-8 - 1.72.0-SNAPSHOT + 1.73.0-SNAPSHOT 3.25.5 3.25.5 From c28a7e3e0677255c1d389053747654e678300e71 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Wed, 2 Apr 2025 04:40:41 +0000 Subject: [PATCH 231/591] okhttp: Per-rpc call option authority verification (#11754) --- .../io/grpc/internal/CertificateUtils.java | 20 +- .../java/io/grpc/okhttp/NoopSslSocket.java | 117 +++++ .../io/grpc/okhttp/OkHttpChannelBuilder.java | 18 +- .../io/grpc/okhttp/OkHttpClientStream.java | 2 +- .../io/grpc/okhttp/OkHttpClientTransport.java | 271 +++++++++-- .../io/grpc/okhttp/OkHttpServerBuilder.java | 3 +- .../io/grpc/okhttp/OkHttpTlsUpgrader.java | 7 +- .../grpc/okhttp/OkHttpClientStreamTest.java | 6 +- .../okhttp/OkHttpClientTransportTest.java | 361 +++++++++++--- .../src/test/java/io/grpc/okhttp/TlsTest.java | 459 ++++++++++++++++++ 10 files changed, 1141 insertions(+), 123 deletions(-) create mode 100644 okhttp/src/main/java/io/grpc/okhttp/NoopSslSocket.java diff --git a/core/src/main/java/io/grpc/internal/CertificateUtils.java b/core/src/main/java/io/grpc/internal/CertificateUtils.java index cc26cedb274..91d17de93cb 100644 --- a/core/src/main/java/io/grpc/internal/CertificateUtils.java +++ b/core/src/main/java/io/grpc/internal/CertificateUtils.java @@ -16,6 +16,7 @@ package io.grpc.internal; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; @@ -36,8 +37,21 @@ public final class CertificateUtils { /** * Creates X509TrustManagers using the provided CA certs. */ - public static TrustManager[] createTrustManager(InputStream rootCerts) + public static TrustManager[] createTrustManager(byte[] rootCerts) throws GeneralSecurityException { + InputStream rootCertsStream = new ByteArrayInputStream(rootCerts); + try { + return CertificateUtils.createTrustManager(rootCertsStream); + } finally { + GrpcUtil.closeQuietly(rootCertsStream); + } + } + + /** + * Creates X509TrustManagers using the provided input stream of CA certs. + */ + public static TrustManager[] createTrustManager(InputStream rootCerts) + throws GeneralSecurityException { KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); try { ks.load(null, null); @@ -52,13 +66,13 @@ public static TrustManager[] createTrustManager(InputStream rootCerts) } TrustManagerFactory trustManagerFactory = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(ks); return trustManagerFactory.getTrustManagers(); } private static X509Certificate[] getX509Certificates(InputStream inputStream) - throws CertificateException { + throws CertificateException { CertificateFactory factory = CertificateFactory.getInstance("X.509"); Collection certs = factory.generateCertificates(inputStream); return certs.toArray(new X509Certificate[0]); diff --git a/okhttp/src/main/java/io/grpc/okhttp/NoopSslSocket.java b/okhttp/src/main/java/io/grpc/okhttp/NoopSslSocket.java new file mode 100644 index 00000000000..6e6a6f12a39 --- /dev/null +++ b/okhttp/src/main/java/io/grpc/okhttp/NoopSslSocket.java @@ -0,0 +1,117 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.okhttp; + +import java.io.IOException; +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + +/** A no-op ssl socket, to facilitate overriding only the required methods in specific + * implementations. + */ +class NoopSslSocket extends SSLSocket { + @Override + public String[] getSupportedCipherSuites() { + return new String[0]; + } + + @Override + public String[] getEnabledCipherSuites() { + return new String[0]; + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + + } + + @Override + public String[] getSupportedProtocols() { + return new String[0]; + } + + @Override + public String[] getEnabledProtocols() { + return new String[0]; + } + + @Override + public void setEnabledProtocols(String[] protocols) { + + } + + @Override + public SSLSession getSession() { + return null; + } + + @Override + public void addHandshakeCompletedListener(HandshakeCompletedListener listener) { + + } + + @Override + public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) { + + } + + @Override + public void startHandshake() throws IOException { + + } + + @Override + public void setUseClientMode(boolean mode) { + + } + + @Override + public boolean getUseClientMode() { + return false; + } + + @Override + public void setNeedClientAuth(boolean need) { + + } + + @Override + public boolean getNeedClientAuth() { + return false; + } + + @Override + public void setWantClientAuth(boolean want) { + + } + + @Override + public boolean getWantClientAuth() { + return false; + } + + @Override + public void setEnableSessionCreation(boolean flag) { + + } + + @Override + public boolean getEnableSessionCreation() { + return false; + } +} diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java index 7eaaa6fd763..98f764132fe 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpChannelBuilder.java @@ -17,6 +17,7 @@ package io.grpc.okhttp; import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.internal.CertificateUtils.createTrustManager; import static io.grpc.internal.GrpcUtil.DEFAULT_KEEPALIVE_TIMEOUT_NANOS; import static io.grpc.internal.GrpcUtil.KEEPALIVE_TIME_NANOS_DISABLED; @@ -89,6 +90,7 @@ public final class OkHttpChannelBuilder extends ForwardingChannelBuilder2 ERROR_CODE_TO_STATUS = buildErrorCodeToStatusMap(); private static final Logger log = Logger.getLogger(OkHttpClientTransport.class.getName()); + private static final String GRPC_ENABLE_PER_RPC_AUTHORITY_CHECK = + "GRPC_ENABLE_PER_RPC_AUTHORITY_CHECK"; + static boolean enablePerRpcAuthorityCheck = + GrpcUtil.getFlag(GRPC_ENABLE_PER_RPC_AUTHORITY_CHECK, false); + private Socket sock; + private SSLSession sslSession; private static Map buildErrorCodeToStatusMap() { Map errorToStatus = new EnumMap<>(ErrorCode.class); @@ -144,6 +168,26 @@ private static Map buildErrorCodeToStatusMap() { return Collections.unmodifiableMap(errorToStatus); } + private static final Class x509ExtendedTrustManagerClass; + private static final Method checkServerTrustedMethod; + + static { + Class x509ExtendedTrustManagerClass1 = null; + Method checkServerTrustedMethod1 = null; + try { + x509ExtendedTrustManagerClass1 = Class.forName("javax.net.ssl.X509ExtendedTrustManager"); + checkServerTrustedMethod1 = x509ExtendedTrustManagerClass1.getMethod("checkServerTrusted", + X509Certificate[].class, String.class, Socket.class); + } catch (ClassNotFoundException e) { + // Per-rpc authority override via call options will be disallowed. + } catch (NoSuchMethodException e) { + // Should never happen since X509ExtendedTrustManager was introduced in Android API level 24 + // along with checkServerTrusted. + } + x509ExtendedTrustManagerClass = x509ExtendedTrustManagerClass1; + checkServerTrustedMethod = checkServerTrustedMethod1; + } + private final InetSocketAddress address; private final String defaultAuthority; private final String userAgent; @@ -205,6 +249,19 @@ private static Map buildErrorCodeToStatusMap() { private final boolean useGetForSafeMethods; @GuardedBy("lock") private final TransportTracer transportTracer; + private final TrustManager x509TrustManager; + + @SuppressWarnings("serial") + private static class LruCache extends LinkedHashMap { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 100; + } + } + + @GuardedBy("lock") + private final Map authorityVerificationResults = new LruCache<>(); + @GuardedBy("lock") private final InUseStateAggregator inUseState = new InUseStateAggregator() { @@ -233,13 +290,14 @@ protected void handleNotInUse() { SettableFuture connectedFuture; public OkHttpClientTransport( - OkHttpChannelBuilder.OkHttpTransportFactory transportFactory, - InetSocketAddress address, - String authority, - @Nullable String userAgent, - Attributes eagAttrs, - @Nullable HttpConnectProxiedSocketAddress proxiedAddr, - Runnable tooManyPingsRunnable) { + OkHttpTransportFactory transportFactory, + InetSocketAddress address, + String authority, + @Nullable String userAgent, + Attributes eagAttrs, + @Nullable HttpConnectProxiedSocketAddress proxiedAddr, + Runnable tooManyPingsRunnable, + ChannelCredentials channelCredentials) { this( transportFactory, address, @@ -249,19 +307,21 @@ public OkHttpClientTransport( GrpcUtil.STOPWATCH_SUPPLIER, new Http2(), proxiedAddr, - tooManyPingsRunnable); + tooManyPingsRunnable, + channelCredentials); } private OkHttpClientTransport( - OkHttpChannelBuilder.OkHttpTransportFactory transportFactory, - InetSocketAddress address, - String authority, - @Nullable String userAgent, - Attributes eagAttrs, - Supplier stopwatchFactory, - Variant variant, - @Nullable HttpConnectProxiedSocketAddress proxiedAddr, - Runnable tooManyPingsRunnable) { + OkHttpTransportFactory transportFactory, + InetSocketAddress address, + String authority, + @Nullable String userAgent, + Attributes eagAttrs, + Supplier stopwatchFactory, + Variant variant, + @Nullable HttpConnectProxiedSocketAddress proxiedAddr, + Runnable tooManyPingsRunnable, + ChannelCredentials channelCredentials) { this.address = Preconditions.checkNotNull(address, "address"); this.defaultAuthority = authority; this.maxMessageSize = transportFactory.maxMessageSize; @@ -276,7 +336,8 @@ private OkHttpClientTransport( this.socketFactory = transportFactory.socketFactory == null ? SocketFactory.getDefault() : transportFactory.socketFactory; this.sslSocketFactory = transportFactory.sslSocketFactory; - this.hostnameVerifier = transportFactory.hostnameVerifier; + this.hostnameVerifier = transportFactory.hostnameVerifier != null + ? transportFactory.hostnameVerifier : OkHostnameVerifier.INSTANCE; this.connectionSpec = Preconditions.checkNotNull( transportFactory.connectionSpec, "connectionSpec"); this.stopwatchFactory = Preconditions.checkNotNull(stopwatchFactory, "stopwatchFactory"); @@ -292,6 +353,21 @@ private OkHttpClientTransport( .set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs).build(); this.useGetForSafeMethods = transportFactory.useGetForSafeMethods; initTransportTracer(); + TrustManager tempX509TrustManager; + if (channelCredentials instanceof TlsChannelCredentials + && x509ExtendedTrustManagerClass != null) { + try { + tempX509TrustManager = getTrustManager( + (TlsChannelCredentials) channelCredentials); + } catch (GeneralSecurityException e) { + tempX509TrustManager = null; + log.log(Level.WARNING, "Obtaining X509ExtendedTrustManager for the transport failed." + + "Per-rpc authority overrides will be disallowed.", e); + } + } else { + tempX509TrustManager = null; + } + x509TrustManager = tempX509TrustManager; } /** @@ -300,7 +376,7 @@ private OkHttpClientTransport( @SuppressWarnings("AddressSelection") // An IP address always returns one address @VisibleForTesting OkHttpClientTransport( - OkHttpChannelBuilder.OkHttpTransportFactory transportFactory, + OkHttpTransportFactory transportFactory, String userAgent, Supplier stopwatchFactory, Variant variant, @@ -316,7 +392,8 @@ private OkHttpClientTransport( stopwatchFactory, variant, null, - tooManyPingsRunnable); + tooManyPingsRunnable, + null); this.connectingCallback = connectingCallback; this.connectedFuture = Preconditions.checkNotNull(connectedFuture, "connectedFuture"); } @@ -396,6 +473,7 @@ public OkHttpClientStream newStream( Preconditions.checkNotNull(headers, "headers"); StatsTraceContext statsTraceContext = StatsTraceContext.newClientContext(tracers, getAttributes(), headers); + // FIXME: it is likely wrong to pass the transportTracer here as it'll exit the lock's scope synchronized (lock) { // to make @GuardedBy linter happy return new OkHttpClientStream( @@ -416,23 +494,116 @@ public OkHttpClientStream newStream( } } + private TrustManager getTrustManager(TlsChannelCredentials tlsCreds) + throws GeneralSecurityException { + TrustManager[] tm; + // Using the same way of creating TrustManager from OkHttpChannelBuilder.sslSocketFactoryFrom() + if (tlsCreds.getTrustManagers() != null) { + tm = tlsCreds.getTrustManagers().toArray(new TrustManager[0]); + } else if (tlsCreds.getRootCertificates() != null) { + tm = CertificateUtils.createTrustManager(tlsCreds.getRootCertificates()); + } else { // else use system default + TrustManagerFactory tmf = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + tm = tmf.getTrustManagers(); + } + for (TrustManager trustManager: tm) { + if (trustManager instanceof X509TrustManager) { + return trustManager; + } + } + return null; + } + @GuardedBy("lock") - void streamReadyToStart(OkHttpClientStream clientStream) { + void streamReadyToStart(OkHttpClientStream clientStream, String authority) { if (goAwayStatus != null) { clientStream.transportState().transportReportStatus( goAwayStatus, RpcProgress.MISCARRIED, true, new Metadata()); - } else if (streams.size() >= maxConcurrentStreams) { - pendingStreams.add(clientStream); - setInUse(clientStream); } else { - startStream(clientStream); + if (socket instanceof SSLSocket && !authority.equals(defaultAuthority)) { + Status authorityVerificationResult; + if (authorityVerificationResults.containsKey(authority)) { + authorityVerificationResult = authorityVerificationResults.get(authority); + } else { + authorityVerificationResult = verifyAuthority(authority); + authorityVerificationResults.put(authority, authorityVerificationResult); + } + if (!authorityVerificationResult.isOk()) { + if (enablePerRpcAuthorityCheck) { + clientStream.transportState().transportReportStatus( + authorityVerificationResult, RpcProgress.PROCESSED, true, new Metadata()); + return; + } + } + } + if (streams.size() >= maxConcurrentStreams) { + pendingStreams.add(clientStream); + setInUse(clientStream); + } else { + startStream(clientStream); + } } } + private Status verifyAuthority(String authority) { + Status authorityVerificationResult; + if (hostnameVerifier.verify(authority, ((SSLSocket) socket).getSession())) { + authorityVerificationResult = Status.OK; + } else { + authorityVerificationResult = Status.UNAVAILABLE.withDescription(String.format( + "HostNameVerifier verification failed for authority '%s'", + authority)); + } + if (!authorityVerificationResult.isOk() && !enablePerRpcAuthorityCheck) { + log.log(Level.WARNING, String.format("HostNameVerifier verification failed for " + + "authority '%s'. This will be an error in the future.", + authority)); + } + if (authorityVerificationResult.isOk()) { + // The status is trivially assigned in this case, but we are still making use of the + // cache to keep track that a warning log had been logged for the authority when + // enablePerRpcAuthorityCheck is false. When we permanently enable the feature, the + // status won't need to be cached for case when x509TrustManager is null. + if (x509TrustManager == null) { + authorityVerificationResult = Status.UNAVAILABLE.withDescription( + String.format("Could not verify authority '%s' for the rpc with no " + + "X509TrustManager available", + authority)); + } else if (x509ExtendedTrustManagerClass.isInstance(x509TrustManager)) { + try { + Certificate[] peerCertificates = sslSession.getPeerCertificates(); + X509Certificate[] x509PeerCertificates = + new X509Certificate[peerCertificates.length]; + for (int i = 0; i < peerCertificates.length; i++) { + x509PeerCertificates[i] = (X509Certificate) peerCertificates[i]; + } + checkServerTrustedMethod.invoke(x509TrustManager, x509PeerCertificates, + "RSA", new SslSocketWrapper((SSLSocket) socket, authority)); + authorityVerificationResult = Status.OK; + } catch (SSLPeerUnverifiedException | InvocationTargetException + | IllegalAccessException e) { + authorityVerificationResult = Status.UNAVAILABLE.withCause(e).withDescription( + "Peer verification failed"); + } + if (authorityVerificationResult.getCause() != null) { + log.log(Level.WARNING, authorityVerificationResult.getDescription() + + ". This will be an error in the future.", + authorityVerificationResult.getCause()); + } else { + log.log(Level.WARNING, authorityVerificationResult.getDescription() + + ". This will be an error in the future."); + } + } + } + return authorityVerificationResult; + } + @SuppressWarnings("GuardedBy") @GuardedBy("lock") private void startStream(OkHttpClientStream stream) { - Preconditions.checkState( + checkState( stream.transportState().id() == OkHttpClientStream.ABSENT_ID, "StreamId already assigned"); streams.put(nextStreamId, stream); setInUse(stream); @@ -531,8 +702,6 @@ public Timeout timeout() { public void close() { } }); - Socket sock; - SSLSession sslSession = null; try { // This is a hack to make sure the connection preface and initial settings to be sent out // without blocking the start. By doing this essentially prevents potential deadlock when @@ -1460,4 +1629,50 @@ public void alternateService(int streamId, String origin, ByteString protocol, S // TODO(madongfly): Deal with alternateService propagation } } + + /** + * SSLSocket wrapper that provides a fake SSLSession for handshake session. + */ + static final class SslSocketWrapper extends NoopSslSocket { + + private final SSLSession sslSession; + private final SSLSocket sslSocket; + + SslSocketWrapper(SSLSocket sslSocket, String peerHost) { + this.sslSocket = sslSocket; + this.sslSession = new FakeSslSession(peerHost); + } + + @Override + public SSLSession getHandshakeSession() { + return this.sslSession; + } + + @Override + public boolean isConnected() { + return sslSocket.isConnected(); + } + + @Override + public SSLParameters getSSLParameters() { + return sslSocket.getSSLParameters(); + } + } + + /** + * Fake SSLSession instance that provides the peer host name to verify for per-rpc check. + */ + static class FakeSslSession extends NoopSslSession { + + private final String peerHost; + + FakeSslSession(String peerHost) { + this.peerHost = peerHost; + } + + @Override + public String getPeerHost() { + return peerHost; + } + } } diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerBuilder.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerBuilder.java index 068474d70bc..8daeed42a8c 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerBuilder.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerBuilder.java @@ -17,6 +17,7 @@ package io.grpc.okhttp; import static com.google.common.base.Preconditions.checkArgument; +import static io.grpc.internal.CertificateUtils.createTrustManager; import com.google.common.base.Preconditions; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -425,7 +426,7 @@ static HandshakerSocketFactoryResult handshakerSocketFactoryFrom(ServerCredentia tm = tlsCreds.getTrustManagers().toArray(new TrustManager[0]); } else if (tlsCreds.getRootCertificates() != null) { try { - tm = OkHttpChannelBuilder.createTrustManager(tlsCreds.getRootCertificates()); + tm = createTrustManager(tlsCreds.getRootCertificates()); } catch (GeneralSecurityException gse) { log.log(Level.FINE, "Exception loading root certificates from credential", gse); return HandshakerSocketFactoryResult.error( diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpTlsUpgrader.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpTlsUpgrader.java index 1004dcd93f9..a8b038c91f4 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpTlsUpgrader.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpTlsUpgrader.java @@ -19,13 +19,13 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import io.grpc.okhttp.internal.ConnectionSpec; -import io.grpc.okhttp.internal.OkHostnameVerifier; import io.grpc.okhttp.internal.Protocol; import java.io.IOException; import java.net.Socket; import java.util.Arrays; import java.util.Collections; import java.util.List; +import javax.annotation.Nonnull; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSocket; @@ -52,7 +52,7 @@ final class OkHttpTlsUpgrader { * @throws RuntimeException if the upgrade negotiation failed. */ public static SSLSocket upgrade(SSLSocketFactory sslSocketFactory, - HostnameVerifier hostnameVerifier, Socket socket, String host, int port, + @Nonnull HostnameVerifier hostnameVerifier, Socket socket, String host, int port, ConnectionSpec spec) throws IOException { Preconditions.checkNotNull(sslSocketFactory, "sslSocketFactory"); Preconditions.checkNotNull(socket, "socket"); @@ -67,9 +67,6 @@ public static SSLSocket upgrade(SSLSocketFactory sslSocketFactory, "Only " + TLS_PROTOCOLS + " are supported, but negotiated protocol is %s", negotiatedProtocol); - if (hostnameVerifier == null) { - hostnameVerifier = OkHostnameVerifier.INSTANCE; - } if (!hostnameVerifier.verify(canonicalizeHost(host), sslSocket.getSession())) { throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host); } diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientStreamTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientStreamTest.java index 1f716705968..1c98d6ee30d 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientStreamTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientStreamTest.java @@ -20,6 +20,7 @@ import static io.grpc.internal.ClientStreamListener.RpcProgress.PROCESSED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.times; @@ -244,12 +245,13 @@ public void getUnaryRequest() throws IOException { // GET streams send headers after halfClose is called. verify(mockedFrameWriter, times(0)).synStream( eq(false), eq(false), eq(3), eq(0), headersCaptor.capture()); - verify(transport, times(0)).streamReadyToStart(isA(OkHttpClientStream.class)); + verify(transport, times(0)).streamReadyToStart(isA(OkHttpClientStream.class), + isA(String.class)); byte[] msg = "request".getBytes(Charset.forName("UTF-8")); stream.writeMessage(new ByteArrayInputStream(msg)); stream.halfClose(); - verify(transport).streamReadyToStart(eq(stream)); + verify(transport).streamReadyToStart(eq(stream), any(String.class)); stream.transportState().start(3); verify(mockedFrameWriter) diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java index 826dee8e2b4..99f430be009 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java @@ -68,6 +68,7 @@ import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.internal.AbstractStream; +import io.grpc.internal.ClientStream; import io.grpc.internal.ClientStreamListener; import io.grpc.internal.ClientTransport; import io.grpc.internal.FakeClock; @@ -115,6 +116,10 @@ import java.util.logging.Logger; import javax.annotation.Nullable; import javax.net.SocketFactory; +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; import okio.Buffer; import okio.BufferedSink; import okio.BufferedSource; @@ -189,16 +194,24 @@ public void tearDown() { private void initTransport() throws Exception { startTransport( - DEFAULT_START_STREAM_ID, null, true, null); + DEFAULT_START_STREAM_ID, null, true, null, null); } private void initTransport(int startId) throws Exception { - startTransport(startId, null, true, null); + startTransport(startId, null, true, null, null); } private void startTransport(int startId, @Nullable Runnable connectingCallback, - boolean waitingForConnected, String userAgent) - throws Exception { + boolean waitingForConnected, String userAgent, + HostnameVerifier hostnameVerifier) throws Exception { + startTransport(startId, connectingCallback, waitingForConnected, userAgent, hostnameVerifier, + false); + } + + private void startTransport(int startId, @Nullable Runnable connectingCallback, + boolean waitingForConnected, String userAgent, + HostnameVerifier hostnameVerifier, boolean useSslSocket) + throws Exception { connectedFuture = SettableFuture.create(); final Ticker ticker = new Ticker() { @Override @@ -212,7 +225,11 @@ public Stopwatch get() { return Stopwatch.createUnstarted(ticker); } }; - channelBuilder.socketFactory(new FakeSocketFactory(socket)); + channelBuilder.socketFactory( + new FakeSocketFactory(useSslSocket ? new MockSslSocket(socket) : socket)); + if (hostnameVerifier != null) { + channelBuilder = channelBuilder.hostnameVerifier(hostnameVerifier); + } clientTransport = new OkHttpClientTransport( channelBuilder.buildTransportFactory(), userAgent, @@ -240,7 +257,8 @@ public void testToString() throws Exception { /*userAgent=*/ null, EAG_ATTRS, NO_PROXY, - tooManyPingsRunnable); + tooManyPingsRunnable, + null); String s = clientTransport.toString(); assertTrue("Unexpected: " + s, s.contains("OkHttpClientTransport")); assertTrue("Unexpected: " + s, s.contains(address.toString())); @@ -258,7 +276,8 @@ public void testTransportExecutorWithTooFewThreads() throws Exception { null, EAG_ATTRS, NO_PROXY, - tooManyPingsRunnable); + tooManyPingsRunnable, + null); clientTransport.start(transportListener); ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(statusCaptor.capture()); @@ -299,7 +318,7 @@ public void close() throws SecurityException { assertThat(log.getLevel()).isEqualTo(Level.FINE); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.request(1); @@ -389,7 +408,7 @@ public void maxMessageSizeShouldBeEnforced() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.request(1); @@ -442,11 +461,11 @@ public void nextFrameThrowIoException() throws Exception { initTransport(); MockStreamListener listener1 = new MockStreamListener(); MockStreamListener listener2 = new MockStreamListener(); - OkHttpClientStream stream1 = + ClientStream stream1 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream1.start(listener1); stream1.request(1); - OkHttpClientStream stream2 = + ClientStream stream2 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream2.start(listener2); stream2.request(1); @@ -476,7 +495,7 @@ public void nextFrameThrowIoException() throws Exception { public void nextFrameThrowsError() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.request(1); @@ -497,7 +516,7 @@ public void nextFrameThrowsError() throws Exception { public void nextFrameReturnFalse() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.request(1); @@ -515,7 +534,7 @@ public void readMessages() throws Exception { final int numMessages = 10; final String message = "Hello Client"; MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.request(numMessages); @@ -567,7 +586,7 @@ public void receivedDataForInvalidStreamShouldKillConnection() throws Exception public void invalidInboundHeadersCancelStream() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.request(1); @@ -592,7 +611,7 @@ public void invalidInboundHeadersCancelStream() throws Exception { public void invalidInboundTrailersPropagateToMetadata() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.request(1); @@ -612,7 +631,7 @@ public void invalidInboundTrailersPropagateToMetadata() throws Exception { public void readStatus() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); assertContainStream(3); @@ -626,7 +645,7 @@ public void readStatus() throws Exception { public void receiveReset() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); assertContainStream(3); @@ -643,7 +662,7 @@ public void receiveReset() throws Exception { public void receiveResetNoError() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); assertContainStream(3); @@ -664,7 +683,7 @@ public void receiveResetNoError() throws Exception { public void cancelStream() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); getStream(3).cancel(Status.CANCELLED); @@ -679,7 +698,7 @@ public void cancelStream() throws Exception { public void addDefaultUserAgent() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); Header userAgentHeader = new Header(GrpcUtil.USER_AGENT_KEY.name(), @@ -696,9 +715,9 @@ public void addDefaultUserAgent() throws Exception { @Test public void overrideDefaultUserAgent() throws Exception { - startTransport(3, null, true, "fakeUserAgent"); + startTransport(3, null, true, "fakeUserAgent", null); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); List
    expectedHeaders = Arrays.asList(HTTP_SCHEME_HEADER, METHOD_HEADER, @@ -717,7 +736,7 @@ public void overrideDefaultUserAgent() throws Exception { public void cancelStreamForDeadlineExceeded() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); getStream(3).cancel(Status.DEADLINE_EXCEEDED); @@ -731,7 +750,7 @@ public void writeMessage() throws Exception { initTransport(); final String message = "Hello Server"; MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); InputStream input = new ByteArrayInputStream(message.getBytes(UTF_8)); @@ -746,6 +765,65 @@ public void writeMessage() throws Exception { shutdownAndVerify(); } + @Test + public void perRpcAuthoritySpecified_verificationSkippedInPlainTextConnection() + throws Exception { + initTransport(); + final String message = "Hello Server"; + MockStreamListener listener = new MockStreamListener(); + ClientStream stream = + clientTransport.newStream(method, new Metadata(), + CallOptions.DEFAULT.withAuthority("some-authority"), tracers); + stream.start(listener); + InputStream input = new ByteArrayInputStream(message.getBytes(UTF_8)); + assertEquals(12, input.available()); + stream.writeMessage(input); + stream.flush(); + verify(frameWriter, timeout(TIME_OUT_MS)) + .data(eq(false), eq(3), any(Buffer.class), eq(12 + HEADER_LENGTH)); + Buffer sentFrame = capturedBuffer.poll(); + assertEquals(createMessageFrame(message), sentFrame); + stream.cancel(Status.CANCELLED); + shutdownAndVerify(); + } + + @Test + public void perRpcAuthoritySpecified_hostnameVerification_ignoredForNonSslSocket() + throws Exception { + startTransport( + DEFAULT_START_STREAM_ID, null, true, null, + (hostname, session) -> false, false); + ClientStream unused = + clientTransport.newStream(method, new Metadata(), + CallOptions.DEFAULT.withAuthority("some-authority"), tracers); + shutdownAndVerify(); + } + + @Test + public void perRpcAuthoritySpecified_hostnameVerification_SslSocket_successCase() + throws Exception { + startTransport( + DEFAULT_START_STREAM_ID, null, true, null, + (hostname, session) -> true, true); + ClientStream unused = + clientTransport.newStream(method, new Metadata(), + CallOptions.DEFAULT.withAuthority("some-authority"), tracers); + shutdownAndVerify(); + } + + @Test + public void perRpcAuthoritySpecified_hostnameVerification_SslSocket_flagDisabled() + throws Exception { + startTransport( + DEFAULT_START_STREAM_ID, null, true, null, + (hostname, session) -> false, true); + ClientStream clientStream = + clientTransport.newStream(method, new Metadata(), + CallOptions.DEFAULT.withAuthority("some-authority"), tracers); + assertThat(clientStream).isInstanceOf(OkHttpClientStream.class); + shutdownAndVerify(); + } + @Test public void transportTracer_windowSizeDefault() throws Exception { initTransport(); @@ -772,12 +850,12 @@ public void windowUpdate() throws Exception { initTransport(); MockStreamListener listener1 = new MockStreamListener(); MockStreamListener listener2 = new MockStreamListener(); - OkHttpClientStream stream1 = + ClientStream stream1 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream1.start(listener1); stream1.request(2); - OkHttpClientStream stream2 = + ClientStream stream2 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream2.start(listener2); stream2.request(2); @@ -842,7 +920,7 @@ public void windowUpdate() throws Exception { public void windowUpdateWithInboundFlowControl() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); int messageLength = INITIAL_WINDOW_SIZE / 2 + 1; @@ -879,7 +957,7 @@ public void windowUpdateWithInboundFlowControl() throws Exception { public void outboundFlowControl() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); @@ -925,7 +1003,7 @@ public void outboundFlowControl_smallWindowSize() throws Exception { setInitialWindowSize(initialOutboundWindowSize); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); @@ -968,7 +1046,7 @@ public void outboundFlowControl_bigWindowSize() throws Exception { frameHandler().windowUpdate(0, 65535); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); @@ -1004,7 +1082,7 @@ public void outboundFlowControl_bigWindowSize() throws Exception { public void outboundFlowControlWithInitialWindowSizeChange() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); int messageLength = 20; @@ -1050,7 +1128,7 @@ public void outboundFlowControlWithInitialWindowSizeChange() throws Exception { public void outboundFlowControlWithInitialWindowSizeChangeInMiddleOfStream() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); int messageLength = 20; @@ -1085,10 +1163,10 @@ public void stopNormally() throws Exception { initTransport(); MockStreamListener listener1 = new MockStreamListener(); MockStreamListener listener2 = new MockStreamListener(); - OkHttpClientStream stream1 = + ClientStream stream1 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream1.start(listener1); - OkHttpClientStream stream2 = + ClientStream stream2 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream2.start(listener2); assertEquals(2, activeStreamCount()); @@ -1115,11 +1193,11 @@ public void receiveGoAway() throws Exception { // start 2 streams. MockStreamListener listener1 = new MockStreamListener(); MockStreamListener listener2 = new MockStreamListener(); - OkHttpClientStream stream1 = + ClientStream stream1 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream1.start(listener1); stream1.request(1); - OkHttpClientStream stream2 = + ClientStream stream2 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream2.start(listener2); stream2.request(1); @@ -1142,7 +1220,7 @@ public void receiveGoAway() throws Exception { // But stream 1 should be able to send. final String sentMessage = "Should I also go away?"; - OkHttpClientStream stream = getStream(3); + ClientStream stream = getStream(3); InputStream input = new ByteArrayInputStream(sentMessage.getBytes(UTF_8)); assertEquals(22, input.available()); stream.writeMessage(input); @@ -1174,7 +1252,7 @@ public void streamIdExhausted() throws Exception { initTransport(startId); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.request(1); @@ -1210,11 +1288,11 @@ public void pendingStreamSucceed() throws Exception { setMaxConcurrentStreams(1); final MockStreamListener listener1 = new MockStreamListener(); final MockStreamListener listener2 = new MockStreamListener(); - OkHttpClientStream stream1 = + ClientStream stream1 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream1.start(listener1); // The second stream should be pending. - OkHttpClientStream stream2 = + ClientStream stream2 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream2.start(listener2); String sentMessage = "hello"; @@ -1247,7 +1325,7 @@ public void pendingStreamCancelled() throws Exception { initTransport(); setMaxConcurrentStreams(0); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); waitForStreamPending(1); @@ -1266,11 +1344,11 @@ public void pendingStreamFailedByGoAway() throws Exception { setMaxConcurrentStreams(1); final MockStreamListener listener1 = new MockStreamListener(); final MockStreamListener listener2 = new MockStreamListener(); - OkHttpClientStream stream1 = + ClientStream stream1 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream1.start(listener1); // The second stream should be pending. - OkHttpClientStream stream2 = + ClientStream stream2 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream2.start(listener2); @@ -1296,7 +1374,7 @@ public void pendingStreamSucceedAfterShutdown() throws Exception { setMaxConcurrentStreams(0); final MockStreamListener listener = new MockStreamListener(); // The second stream should be pending. - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); waitForStreamPending(1); @@ -1320,15 +1398,15 @@ public void pendingStreamFailedByIdExhausted() throws Exception { final MockStreamListener listener2 = new MockStreamListener(); final MockStreamListener listener3 = new MockStreamListener(); - OkHttpClientStream stream1 = + ClientStream stream1 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream1.start(listener1); // The second and third stream should be pending. - OkHttpClientStream stream2 = + ClientStream stream2 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream2.start(listener2); - OkHttpClientStream stream3 = + ClientStream stream3 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream3.start(listener3); @@ -1352,7 +1430,7 @@ public void pendingStreamFailedByIdExhausted() throws Exception { public void receivingWindowExceeded() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.request(1); @@ -1404,7 +1482,7 @@ public void duplexStreamingHeadersShouldNotBeFlushed() throws Exception { private void shouldHeadersBeFlushed(boolean shouldBeFlushed) throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); verify(frameWriter, timeout(TIME_OUT_MS)).synStream( @@ -1421,7 +1499,7 @@ private void shouldHeadersBeFlushed(boolean shouldBeFlushed) throws Exception { public void receiveDataWithoutHeader() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.request(1); @@ -1444,7 +1522,7 @@ public void receiveDataWithoutHeader() throws Exception { public void receiveDataWithoutHeaderAndTrailer() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.request(1); @@ -1468,7 +1546,7 @@ public void receiveDataWithoutHeaderAndTrailer() throws Exception { public void receiveLongEnoughDataWithoutHeaderAndTrailer() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.request(1); @@ -1490,7 +1568,7 @@ public void receiveLongEnoughDataWithoutHeaderAndTrailer() throws Exception { public void receiveDataForUnknownStreamUpdateConnectionWindow() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.cancel(Status.CANCELLED); @@ -1519,7 +1597,7 @@ public void receiveDataForUnknownStreamUpdateConnectionWindow() throws Exception public void receiveWindowUpdateForUnknownStream() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); stream.cancel(Status.CANCELLED); @@ -1539,7 +1617,7 @@ public void receiveWindowUpdateForUnknownStream() throws Exception { public void shouldBeInitiallyReady() throws Exception { initTransport(); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); assertTrue(stream.isReady()); @@ -1557,7 +1635,7 @@ public void notifyOnReady() throws Exception { AbstractStream.TransportState.DEFAULT_ONREADY_THRESHOLD - HEADER_LENGTH - 1; setInitialWindowSize(0); MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); assertTrue(stream.isReady()); @@ -1704,7 +1782,7 @@ public void shutdownDuringConnecting() throws Exception { DEFAULT_START_STREAM_ID, connectingCallback, false, - null); + null, null); clientTransport.shutdown(SHUTDOWN_REASON); delayed.set(null); shutdownAndVerify(); @@ -1719,7 +1797,8 @@ public void invalidAuthorityPropagates() { "userAgent", EAG_ATTRS, NO_PROXY, - tooManyPingsRunnable); + tooManyPingsRunnable, + null); String host = clientTransport.getOverridenHost(); int port = clientTransport.getOverridenPort(); @@ -1737,7 +1816,8 @@ public void unreachableServer() throws Exception { "userAgent", EAG_ATTRS, NO_PROXY, - tooManyPingsRunnable); + tooManyPingsRunnable, + null); ManagedClientTransport.Listener listener = mock(ManagedClientTransport.Listener.class); clientTransport.start(listener); @@ -1767,7 +1847,8 @@ public void customSocketFactory() throws Exception { "userAgent", EAG_ATTRS, NO_PROXY, - tooManyPingsRunnable); + tooManyPingsRunnable, + null); ManagedClientTransport.Listener listener = mock(ManagedClientTransport.Listener.class); clientTransport.start(listener); @@ -1792,7 +1873,8 @@ public void proxy_200() throws Exception { .setTargetAddress(targetAddress) .setProxyAddress(new InetSocketAddress("localhost", serverSocket.getLocalPort())) .build(), - tooManyPingsRunnable); + tooManyPingsRunnable, + null); clientTransport.start(transportListener); Socket sock = serverSocket.accept(); @@ -1841,7 +1923,8 @@ public void proxy_500() throws Exception { .setTargetAddress(targetAddress) .setProxyAddress(new InetSocketAddress("localhost", serverSocket.getLocalPort())) .build(), - tooManyPingsRunnable); + tooManyPingsRunnable, + null); clientTransport.start(transportListener); Socket sock = serverSocket.accept(); @@ -1889,7 +1972,8 @@ public void proxy_immediateServerClose() throws Exception { .setTargetAddress(targetAddress) .setProxyAddress(new InetSocketAddress("localhost", serverSocket.getLocalPort())) .build(), - tooManyPingsRunnable); + tooManyPingsRunnable, + null); clientTransport.start(transportListener); Socket sock = serverSocket.accept(); @@ -1920,7 +2004,8 @@ public void proxy_serverHangs() throws Exception { .setTargetAddress(targetAddress) .setProxyAddress(new InetSocketAddress("localhost", serverSocket.getLocalPort())) .build(), - tooManyPingsRunnable); + tooManyPingsRunnable, + null); clientTransport.proxySocketTimeout = 10; clientTransport.start(transportListener); @@ -1987,13 +2072,13 @@ public void goAway_streamListenerRpcProgress() throws Exception { MockStreamListener listener1 = new MockStreamListener(); MockStreamListener listener2 = new MockStreamListener(); MockStreamListener listener3 = new MockStreamListener(); - OkHttpClientStream stream1 = + ClientStream stream1 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream1.start(listener1); - OkHttpClientStream stream2 = + ClientStream stream2 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream2.start(listener2); - OkHttpClientStream stream3 = + ClientStream stream3 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream3.start(listener3); waitForStreamPending(1); @@ -2027,13 +2112,13 @@ public void reset_streamListenerRpcProgress() throws Exception { MockStreamListener listener1 = new MockStreamListener(); MockStreamListener listener2 = new MockStreamListener(); MockStreamListener listener3 = new MockStreamListener(); - OkHttpClientStream stream1 = + ClientStream stream1 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream1.start(listener1); - OkHttpClientStream stream2 = + ClientStream stream2 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream2.start(listener2); - OkHttpClientStream stream3 = + ClientStream stream3 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream3.start(listener3); @@ -2069,13 +2154,13 @@ public void shutdownNow_streamListenerRpcProgress() throws Exception { MockStreamListener listener1 = new MockStreamListener(); MockStreamListener listener2 = new MockStreamListener(); MockStreamListener listener3 = new MockStreamListener(); - OkHttpClientStream stream1 = + ClientStream stream1 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream1.start(listener1); - OkHttpClientStream stream2 = + ClientStream stream2 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream2.start(listener2); - OkHttpClientStream stream3 = + ClientStream stream3 = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream3.start(listener3); waitForStreamPending(1); @@ -2100,11 +2185,11 @@ public void finishedStreamRemovedFromInUseState() throws Exception { initTransport(); setMaxConcurrentStreams(1); final MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = - clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); + OkHttpClientStream stream = clientTransport.newStream( + method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); - OkHttpClientStream pendingStream = - clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); + OkHttpClientStream pendingStream = clientTransport.newStream( + method, new Metadata(), CallOptions.DEFAULT, tracers); pendingStream.start(listener); waitForStreamPending(1); clientTransport.finishStream(stream.transportState().id(), Status.OK, PROCESSED, @@ -2144,7 +2229,7 @@ private void waitForStreamPending(int expected) throws Exception { private void assertNewStreamFail() throws Exception { MockStreamListener listener = new MockStreamListener(); - OkHttpClientStream stream = + ClientStream stream = clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(listener); listener.waitUntilStreamClosed(); @@ -2375,6 +2460,124 @@ public InputStream getInputStream() { } } + private static class MockSslSocket extends SSLSocket { + private Socket delegate; + + MockSslSocket(Socket socket) { + delegate = socket; + } + + @Override + public String[] getSupportedCipherSuites() { + return new String[0]; + } + + @Override + public String[] getEnabledCipherSuites() { + return new String[0]; + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + + } + + @Override + public String[] getSupportedProtocols() { + return new String[0]; + } + + @Override + public String[] getEnabledProtocols() { + return new String[0]; + } + + @Override + public void setEnabledProtocols(String[] protocols) { + + } + + @Override + public SSLSession getSession() { + return null; + } + + @Override + public void addHandshakeCompletedListener(HandshakeCompletedListener listener) { + + } + + @Override + public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) { + + } + + @Override + public void startHandshake() throws IOException { + + } + + @Override + public void setUseClientMode(boolean mode) { + + } + + @Override + public boolean getUseClientMode() { + return false; + } + + @Override + public void setNeedClientAuth(boolean need) { + + } + + @Override + public boolean getNeedClientAuth() { + return false; + } + + @Override + public void setWantClientAuth(boolean want) { + + } + + @Override + public boolean getWantClientAuth() { + return false; + } + + @Override + public void setEnableSessionCreation(boolean flag) { + + } + + @Override + public boolean getEnableSessionCreation() { + return false; + } + + @Override + public synchronized void close() throws IOException { + delegate.close(); + } + + @Override + public SocketAddress getLocalSocketAddress() { + return delegate.getLocalSocketAddress(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return delegate.getOutputStream(); + } + + @Override + public InputStream getInputStream() throws IOException { + return delegate.getInputStream(); + } + } + static class PingCallbackImpl implements ClientTransport.PingCallback { int invocationCount; long roundTripTime; diff --git a/okhttp/src/test/java/io/grpc/okhttp/TlsTest.java b/okhttp/src/test/java/io/grpc/okhttp/TlsTest.java index a21360a89ba..20a2f1a5ca7 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/TlsTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/TlsTest.java @@ -18,8 +18,10 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.fail; import com.google.common.base.Throwables; +import io.grpc.CallOptions; import io.grpc.ChannelCredentials; import io.grpc.ConnectivityState; import io.grpc.ManagedChannel; @@ -32,18 +34,34 @@ import io.grpc.TlsServerCredentials; import io.grpc.internal.testing.TestUtils; import io.grpc.okhttp.internal.Platform; +import io.grpc.stub.ClientCalls; import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; import io.grpc.testing.TlsTesting; import io.grpc.testing.protobuf.SimpleRequest; import io.grpc.testing.protobuf.SimpleResponse; import io.grpc.testing.protobuf.SimpleServiceGrpc; +import io.grpc.util.CertificateUtils; import java.io.IOException; import java.io.InputStream; +import java.net.Socket; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Optional; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.junit.Assume; import org.junit.Before; import org.junit.Rule; @@ -53,6 +71,7 @@ /** Verify OkHttp's TLS integration. */ @RunWith(JUnit4.class) +@IgnoreJRERequirement public class TlsTest { @Rule public final GrpcCleanupRule grpcCleanupRule = new GrpcCleanupRule(); @@ -92,6 +111,325 @@ public void basicTls_succeeds() throws Exception { SimpleServiceGrpc.newBlockingStub(channel).unaryRpc(SimpleRequest.getDefaultInstance()); } + @Test + public void perRpcAuthorityOverride_hostnameVerifier_goodAuthority_succeeds() throws Exception { + OkHttpClientTransport.enablePerRpcAuthorityCheck = true; + try { + ServerCredentials serverCreds; + try (InputStream serverCert = TlsTesting.loadCert("server1.pem"); + InputStream serverPrivateKey = TlsTesting.loadCert("server1.key")) { + serverCreds = TlsServerCredentials.newBuilder() + .keyManager(serverCert, serverPrivateKey) + .build(); + } + ChannelCredentials channelCreds; + try (InputStream caCert = TlsTesting.loadCert("ca.pem")) { + channelCreds = TlsChannelCredentials.newBuilder() + .trustManager(caCert) + .build(); + } + Server server = grpcCleanupRule.register(server(serverCreds)); + ManagedChannel channel = grpcCleanupRule.register(clientChannel(server, channelCreds)); + + ClientCalls.blockingUnaryCall(channel, SimpleServiceGrpc.getUnaryRpcMethod(), + CallOptions.DEFAULT.withAuthority("good.test.google.fr"), + SimpleRequest.getDefaultInstance()); + } finally { + OkHttpClientTransport.enablePerRpcAuthorityCheck = false; + } + } + + @Test + public void perRpcAuthorityOverride_hostnameVerifier_badAuthority_fails() + throws Exception { + OkHttpClientTransport.enablePerRpcAuthorityCheck = true; + try { + ServerCredentials serverCreds; + try (InputStream serverCert = TlsTesting.loadCert("server1.pem"); + InputStream serverPrivateKey = TlsTesting.loadCert("server1.key")) { + serverCreds = TlsServerCredentials.newBuilder() + .keyManager(serverCert, serverPrivateKey) + .build(); + } + ChannelCredentials channelCreds; + try (InputStream caCert = TlsTesting.loadCert("ca.pem")) { + channelCreds = TlsChannelCredentials.newBuilder() + .trustManager(caCert) + .build(); + } + Server server = grpcCleanupRule.register(server(serverCreds)); + ManagedChannel channel = grpcCleanupRule.register(clientChannel(server, channelCreds)); + + try { + ClientCalls.blockingUnaryCall(channel, SimpleServiceGrpc.getUnaryRpcMethod(), + CallOptions.DEFAULT.withAuthority("disallowed.name.com"), + SimpleRequest.getDefaultInstance()); + fail("Expected exception for hostname verifier failure."); + } catch (StatusRuntimeException ex) { + assertThat(ex.getStatus().getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(ex.getStatus().getDescription()).isEqualTo( + "HostNameVerifier verification failed for authority 'disallowed.name.com'"); + } + } finally { + OkHttpClientTransport.enablePerRpcAuthorityCheck = false; + } + } + + @Test + public void perRpcAuthorityOverride_hostnameVerifier_badAuthority_flagDisabled_succeeds() + throws Exception { + ServerCredentials serverCreds; + try (InputStream serverCert = TlsTesting.loadCert("server1.pem"); + InputStream serverPrivateKey = TlsTesting.loadCert("server1.key")) { + serverCreds = TlsServerCredentials.newBuilder() + .keyManager(serverCert, serverPrivateKey) + .build(); + } + ChannelCredentials channelCreds; + try (InputStream caCert = TlsTesting.loadCert("ca.pem")) { + channelCreds = TlsChannelCredentials.newBuilder() + .trustManager(caCert) + .build(); + } + Server server = grpcCleanupRule.register(server(serverCreds)); + ManagedChannel channel = grpcCleanupRule.register(clientChannel(server, channelCreds)); + + ClientCalls.blockingUnaryCall(channel, SimpleServiceGrpc.getUnaryRpcMethod(), + CallOptions.DEFAULT.withAuthority("disallowed.name.com"), + SimpleRequest.getDefaultInstance()); + } + + @Test + public void perRpcAuthorityOverride_noTlsCredentialsUsedToBuildChannel_fails() throws Exception { + OkHttpClientTransport.enablePerRpcAuthorityCheck = true; + try { + ServerCredentials serverCreds; + try (InputStream serverCert = TlsTesting.loadCert("server1.pem"); + InputStream serverPrivateKey = TlsTesting.loadCert("server1.key")) { + serverCreds = TlsServerCredentials.newBuilder() + .keyManager(serverCert, serverPrivateKey) + .build(); + } + Server server = grpcCleanupRule.register(server(serverCreds)); + SSLSocketFactory sslSocketFactory = TestUtils.newSslSocketFactoryForCa( + Platform.get().getProvider(), TestUtils.loadCert("ca.pem")); + ManagedChannel channel = grpcCleanupRule.register( + OkHttpChannelBuilder.forAddress("localhost", server.getPort()) + .overrideAuthority(TestUtils.TEST_SERVER_HOST) + .directExecutor() + .sslSocketFactory(sslSocketFactory) + .build()); + + try { + ClientCalls.blockingUnaryCall(channel, SimpleServiceGrpc.getUnaryRpcMethod(), + CallOptions.DEFAULT.withAuthority("bar.test.google.fr"), + SimpleRequest.getDefaultInstance()); + fail("Expected exception for authority verification failure."); + } catch (StatusRuntimeException ex) { + assertThat(ex.getStatus().getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(ex.getStatus().getDescription()).isEqualTo( + "Could not verify authority 'bar.test.google.fr' for the rpc with no " + + "X509TrustManager available"); + } + } finally { + OkHttpClientTransport.enablePerRpcAuthorityCheck = false; + } + } + + @Test + public void perRpcAuthorityOverride_trustManager_permitted_succeeds() throws Exception { + OkHttpClientTransport.enablePerRpcAuthorityCheck = true; + try { + ServerCredentials serverCreds; + try (InputStream serverCert = TlsTesting.loadCert("server1.pem"); + InputStream serverPrivateKey = TlsTesting.loadCert("server1.key")) { + serverCreds = TlsServerCredentials.newBuilder() + .keyManager(serverCert, serverPrivateKey) + .build(); + } + ChannelCredentials channelCreds; + try (InputStream caCert = TlsTesting.loadCert("ca.pem")) { + X509ExtendedTrustManager regularTrustManager = + (X509ExtendedTrustManager) getX509ExtendedTrustManager(caCert).get(); + channelCreds = TlsChannelCredentials.newBuilder() + .trustManager(new HostnameCheckingX509ExtendedTrustManager(regularTrustManager)) + .build(); + } + Server server = grpcCleanupRule.register(server(serverCreds)); + ManagedChannel channel = grpcCleanupRule.register(clientChannel(server, channelCreds)); + + ClientCalls.blockingUnaryCall(channel, SimpleServiceGrpc.getUnaryRpcMethod(), + CallOptions.DEFAULT.withAuthority("good.test.google.fr"), + SimpleRequest.getDefaultInstance()); + } finally { + OkHttpClientTransport.enablePerRpcAuthorityCheck = false; + } + } + + @Test + public void perRpcAuthorityOverride_trustManager_denied_fails() throws Exception { + OkHttpClientTransport.enablePerRpcAuthorityCheck = true; + try { + ServerCredentials serverCreds; + try (InputStream serverCert = TlsTesting.loadCert("server1.pem"); + InputStream serverPrivateKey = TlsTesting.loadCert("server1.key")) { + serverCreds = TlsServerCredentials.newBuilder() + .keyManager(serverCert, serverPrivateKey) + .build(); + } + ChannelCredentials channelCreds; + try (InputStream caCert = TlsTesting.loadCert("ca.pem")) { + X509ExtendedTrustManager regularTrustManager = + (X509ExtendedTrustManager) getX509ExtendedTrustManager(caCert).get(); + channelCreds = TlsChannelCredentials.newBuilder() + .trustManager(new HostnameCheckingX509ExtendedTrustManager(regularTrustManager)) + .build(); + } + Server server = grpcCleanupRule.register(server(serverCreds)); + ManagedChannel channel = grpcCleanupRule.register(clientChannel(server, channelCreds)); + + try { + ClientCalls.blockingUnaryCall(channel, SimpleServiceGrpc.getUnaryRpcMethod(), + CallOptions.DEFAULT.withAuthority("bad.test.google.fr"), + SimpleRequest.getDefaultInstance()); + fail("Expected exception for authority verification failure."); + } catch (StatusRuntimeException ex) { + assertThat(ex.getStatus().getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(ex.getCause().getCause()).isInstanceOf(CertificateException.class); + } + } finally { + OkHttpClientTransport.enablePerRpcAuthorityCheck = false; + } + } + + @Test + public void perRpcAuthorityOverride_trustManager_denied_flagDisabled_succeeds() + throws Exception { + ServerCredentials serverCreds; + try (InputStream serverCert = TlsTesting.loadCert("server1.pem"); + InputStream serverPrivateKey = TlsTesting.loadCert("server1.key")) { + serverCreds = TlsServerCredentials.newBuilder() + .keyManager(serverCert, serverPrivateKey) + .build(); + } + ChannelCredentials channelCreds; + try (InputStream caCert = TlsTesting.loadCert("ca.pem")) { + X509ExtendedTrustManager regularTrustManager = + (X509ExtendedTrustManager) getX509ExtendedTrustManager(caCert).get(); + channelCreds = TlsChannelCredentials.newBuilder() + .trustManager(new HostnameCheckingX509ExtendedTrustManager(regularTrustManager)) + .build(); + } + Server server = grpcCleanupRule.register(server(serverCreds)); + ManagedChannel channel = grpcCleanupRule.register(clientChannel(server, channelCreds)); + + ClientCalls.blockingUnaryCall(channel, SimpleServiceGrpc.getUnaryRpcMethod(), + CallOptions.DEFAULT.withAuthority("bad.test.google.fr"), + SimpleRequest.getDefaultInstance()); + } + + /** + * This test simulates the absence of X509ExtendedTrustManager while still using the + * real trust manager for the connection handshake to happen. When the TrustManager is not an + * X509ExtendedTrustManager, the per-rpc check ignores the trust manager. However, the + * HostnameVerifier is still used, so only valid authorities are permitted. + */ + @Test + public void perRpcAuthorityOverride_notX509ExtendedTrustManager_goodAuthority_succeeds() + throws Exception { + OkHttpClientTransport.enablePerRpcAuthorityCheck = true; + try { + ServerCredentials serverCreds; + try (InputStream serverCert = TlsTesting.loadCert("server1.pem"); + InputStream serverPrivateKey = TlsTesting.loadCert("server1.key")) { + serverCreds = TlsServerCredentials.newBuilder() + .keyManager(serverCert, serverPrivateKey) + .build(); + } + ChannelCredentials channelCreds; + try (InputStream caCert = TlsTesting.loadCert("ca.pem")) { + X509TrustManager x509ExtendedTrustManager = + (X509TrustManager) getX509ExtendedTrustManager(caCert).get(); + channelCreds = TlsChannelCredentials.newBuilder() + .trustManager(new FakeTrustManager(x509ExtendedTrustManager)) + .build(); + } + Server server = grpcCleanupRule.register(server(serverCreds)); + ManagedChannel channel = grpcCleanupRule.register(clientChannel(server, channelCreds)); + + ClientCalls.blockingUnaryCall(channel, SimpleServiceGrpc.getUnaryRpcMethod(), + CallOptions.DEFAULT.withAuthority("foo.test.google.fr"), + SimpleRequest.getDefaultInstance()); + } finally { + OkHttpClientTransport.enablePerRpcAuthorityCheck = false; + } + } + + @Test + public void perRpcAuthorityOverride_notX509ExtendedTrustManager_badAuthority_fails() + throws Exception { + OkHttpClientTransport.enablePerRpcAuthorityCheck = true; + try { + ServerCredentials serverCreds; + try (InputStream serverCert = TlsTesting.loadCert("server1.pem"); + InputStream serverPrivateKey = TlsTesting.loadCert("server1.key")) { + serverCreds = TlsServerCredentials.newBuilder() + .keyManager(serverCert, serverPrivateKey) + .build(); + } + ChannelCredentials channelCreds; + try (InputStream caCert = TlsTesting.loadCert("ca.pem")) { + X509TrustManager x509ExtendedTrustManager = + (X509TrustManager) getX509ExtendedTrustManager(caCert).get(); + channelCreds = TlsChannelCredentials.newBuilder() + .trustManager(new FakeTrustManager(x509ExtendedTrustManager)) + .build(); + } + Server server = grpcCleanupRule.register(server(serverCreds)); + ManagedChannel channel = grpcCleanupRule.register(clientChannel(server, channelCreds)); + + try { + ClientCalls.blockingUnaryCall(channel, SimpleServiceGrpc.getUnaryRpcMethod(), + CallOptions.DEFAULT.withAuthority("disallowed.name.com"), + SimpleRequest.getDefaultInstance()); + fail("Expected exception for authority verification failure."); + } catch (StatusRuntimeException ex) { + assertThat(ex.getStatus().getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(ex.getStatus().getDescription()) + .isEqualTo("HostNameVerifier verification failed for authority 'disallowed.name.com'"); + } + } finally { + OkHttpClientTransport.enablePerRpcAuthorityCheck = false; + } + } + + @Test + public void + perRpcAuthorityOverride_notX509ExtendedTrustManager_badAuthority_flagDisabled_succeeds() + throws Exception { + ServerCredentials serverCreds; + try (InputStream serverCert = TlsTesting.loadCert("server1.pem"); + InputStream serverPrivateKey = TlsTesting.loadCert("server1.key")) { + serverCreds = TlsServerCredentials.newBuilder() + .keyManager(serverCert, serverPrivateKey) + .build(); + } + ChannelCredentials channelCreds; + try (InputStream caCert = TlsTesting.loadCert("ca.pem")) { + X509TrustManager x509ExtendedTrustManager = + (X509TrustManager) getX509ExtendedTrustManager(caCert).get(); + channelCreds = TlsChannelCredentials.newBuilder() + .trustManager(new FakeTrustManager(x509ExtendedTrustManager)) + .build(); + } + Server server = grpcCleanupRule.register(server(serverCreds)); + ManagedChannel channel = grpcCleanupRule.register(clientChannel(server, channelCreds)); + + ClientCalls.blockingUnaryCall(channel, SimpleServiceGrpc.getUnaryRpcMethod(), + CallOptions.DEFAULT.withAuthority("disallowed.name.com"), + SimpleRequest.getDefaultInstance()); + } + @Test public void mtls_succeeds() throws Exception { ServerCredentials serverCreds; @@ -282,6 +620,127 @@ public void hostnameVerifierFails_fails() assertThat(status.getCause()).isInstanceOf(SSLPeerUnverifiedException.class); } + /** Used to simulate the case of X509ExtendedTrustManager not present. */ + private static class FakeTrustManager implements X509TrustManager { + + private final X509TrustManager delegate; + + public FakeTrustManager(X509TrustManager x509ExtendedTrustManager) { + this.delegate = x509ExtendedTrustManager; + } + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException { + delegate.checkClientTrusted(x509Certificates, s); + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException { + delegate.checkServerTrusted(x509Certificates, s); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return delegate.getAcceptedIssuers(); + } + } + + /** + * Checks against a limited set of hostnames. In production, EndpointIdentificationAlgorithm is + * unset so the default trust manager will not fail based on the hostname. This class is used to + * test user-provided trust managers that may have their own behavior. + */ + private static class HostnameCheckingX509ExtendedTrustManager + extends ForwardingX509ExtendedTrustManager { + public HostnameCheckingX509ExtendedTrustManager(X509ExtendedTrustManager tm) { + super(tm); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + String peer = ((SSLSocket) socket).getHandshakeSession().getPeerHost(); + if (!"foo.test.google.fr".equals(peer) && !"good.test.google.fr".equals(peer)) { + throw new CertificateException("Peer verification failed."); + } + super.checkServerTrusted(chain, authType, socket); + } + } + + @IgnoreJRERequirement + private static class ForwardingX509ExtendedTrustManager extends X509ExtendedTrustManager { + private final X509ExtendedTrustManager delegate; + + private ForwardingX509ExtendedTrustManager(X509ExtendedTrustManager delegate) { + this.delegate = delegate; + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + delegate.checkServerTrusted(chain, authType, socket); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + delegate.checkServerTrusted(chain, authType, engine); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + delegate.checkServerTrusted(chain, authType); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + delegate.checkClientTrusted(chain, authType, engine); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + delegate.checkClientTrusted(chain, authType); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + delegate.checkClientTrusted(chain, authType, socket); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return delegate.getAcceptedIssuers(); + } + } + + private static Optional getX509ExtendedTrustManager(InputStream rootCerts) + throws GeneralSecurityException { + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + try { + ks.load(null, null); + } catch (IOException ex) { + // Shouldn't really happen, as we're not loading any data. + throw new GeneralSecurityException(ex); + } + X509Certificate[] certs = CertificateUtils.getX509Certificates(rootCerts); + for (X509Certificate cert : certs) { + X500Principal principal = cert.getSubjectX500Principal(); + ks.setCertificateEntry(principal.getName("RFC2253"), cert); + } + + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(ks); + return Arrays.stream(trustManagerFactory.getTrustManagers()) + .filter(trustManager -> trustManager instanceof X509ExtendedTrustManager).findFirst(); + } + private static Server server(ServerCredentials creds) throws IOException { return OkHttpServerBuilder.forPort(0, creds) .directExecutor() From 8ca7c4ef1f961e0540d9d0eccb4e9bf25403f6a4 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 2 Apr 2025 03:52:00 -0700 Subject: [PATCH 232/591] core: Delete stale SuppressWarnings("deprecated") for ATTR_LOAD_BALANCING_CONFIG (#11982) ATTR_LOAD_BALANCING_CONFIG was deleted in bf7a42dbd. --- .../io/grpc/internal/AutoConfiguredLoadBalancerFactory.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/AutoConfiguredLoadBalancerFactory.java b/core/src/main/java/io/grpc/internal/AutoConfiguredLoadBalancerFactory.java index a382227fd6c..a257637de22 100644 --- a/core/src/main/java/io/grpc/internal/AutoConfiguredLoadBalancerFactory.java +++ b/core/src/main/java/io/grpc/internal/AutoConfiguredLoadBalancerFactory.java @@ -40,8 +40,6 @@ import java.util.Map; import javax.annotation.Nullable; -// TODO(creamsoup) fully deprecate LoadBalancer.ATTR_LOAD_BALANCING_CONFIG -@SuppressWarnings("deprecation") public final class AutoConfiguredLoadBalancerFactory { private final LoadBalancerRegistry registry; From 908f9f19cdd06bad2214e0b89b5eeb7ce341117f Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 2 Apr 2025 03:54:32 -0700 Subject: [PATCH 233/591] core: Delete the long-deprecated GRPC_PROXY_EXP (#11988) "EXP" stood for experimental and all documentation that referenced it made it clear it was experimental. It's been some years since we started logging a message when it was used to say it will be deleted. There's no time like the present to delete it. --- .../io/grpc/internal/ProxyDetectorImpl.java | 46 +------------------ .../grpc/internal/ProxyDetectorImplTest.java | 44 +----------------- 2 files changed, 4 insertions(+), 86 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/ProxyDetectorImpl.java b/core/src/main/java/io/grpc/internal/ProxyDetectorImpl.java index b3f646d6099..58c7803346f 100644 --- a/core/src/main/java/io/grpc/internal/ProxyDetectorImpl.java +++ b/core/src/main/java/io/grpc/internal/ProxyDetectorImpl.java @@ -147,18 +147,9 @@ public ProxySelector get() { } }; - /** - * Experimental environment variable name for enabling proxy support. - * - * @deprecated Use the standard Java proxy configuration instead with flags such as: - * -Dhttps.proxyHost=HOST -Dhttps.proxyPort=PORT - */ - @Deprecated - private static final String GRPC_PROXY_ENV_VAR = "GRPC_PROXY_EXP"; // Do not hard code a ProxySelector because the global default ProxySelector can change private final Supplier proxySelector; private final AuthenticationProvider authenticationProvider; - private final InetSocketAddress overrideProxyAddress; // We want an HTTPS proxy, which operates on the entire data stream (See IETF rfc2817). static final String PROXY_SCHEME = "https"; @@ -168,21 +159,15 @@ public ProxySelector get() { * {@link ProxyDetectorImpl.AuthenticationProvider} to detect proxy parameters. */ public ProxyDetectorImpl() { - this(DEFAULT_PROXY_SELECTOR, DEFAULT_AUTHENTICATOR, System.getenv(GRPC_PROXY_ENV_VAR)); + this(DEFAULT_PROXY_SELECTOR, DEFAULT_AUTHENTICATOR); } @VisibleForTesting ProxyDetectorImpl( Supplier proxySelector, - AuthenticationProvider authenticationProvider, - @Nullable String proxyEnvString) { + AuthenticationProvider authenticationProvider) { this.proxySelector = checkNotNull(proxySelector); this.authenticationProvider = checkNotNull(authenticationProvider); - if (proxyEnvString != null) { - overrideProxyAddress = overrideProxy(proxyEnvString); - } else { - overrideProxyAddress = null; - } } @Nullable @@ -191,12 +176,6 @@ public ProxiedSocketAddress proxyFor(SocketAddress targetServerAddress) throws I if (!(targetServerAddress instanceof InetSocketAddress)) { return null; } - if (overrideProxyAddress != null) { - return HttpConnectProxiedSocketAddress.newBuilder() - .setProxyAddress(overrideProxyAddress) - .setTargetAddress((InetSocketAddress) targetServerAddress) - .build(); - } return detectProxy((InetSocketAddress) targetServerAddress); } @@ -272,27 +251,6 @@ private ProxiedSocketAddress detectProxy(InetSocketAddress targetAddr) throws IO .build(); } - /** - * GRPC_PROXY_EXP is deprecated but let's maintain compatibility for now. - */ - private static InetSocketAddress overrideProxy(String proxyHostPort) { - if (proxyHostPort == null) { - return null; - } - - String[] parts = proxyHostPort.split(":", 2); - int port = 80; - if (parts.length > 1) { - port = Integer.parseInt(parts[1]); - } - log.warning( - "Detected GRPC_PROXY_EXP and will honor it, but this feature will " - + "be removed in a future release. Use the JVM flags " - + "\"-Dhttps.proxyHost=HOST -Dhttps.proxyPort=PORT\" to set the https proxy for " - + "this JVM."); - return new InetSocketAddress(parts[0], port); - } - /** * This interface makes unit testing easier by avoiding direct calls to static methods. */ diff --git a/core/src/test/java/io/grpc/internal/ProxyDetectorImplTest.java b/core/src/test/java/io/grpc/internal/ProxyDetectorImplTest.java index 0432a474ac5..771050f119d 100644 --- a/core/src/test/java/io/grpc/internal/ProxyDetectorImplTest.java +++ b/core/src/test/java/io/grpc/internal/ProxyDetectorImplTest.java @@ -73,7 +73,7 @@ public ProxySelector get() { return proxySelector; } }; - proxyDetector = new ProxyDetectorImpl(proxySelectorSupplier, authenticator, null); + proxyDetector = new ProxyDetectorImpl(proxySelectorSupplier, authenticator); unresolvedProxy = InetSocketAddress.createUnresolved("10.0.0.1", proxyPort); proxySocketAddress = HttpConnectProxiedSocketAddress.newBuilder() .setTargetAddress(destination) @@ -82,45 +82,6 @@ public ProxySelector get() { .build(); } - @Test - public void override_hostPort() throws Exception { - final String overrideHost = "10.99.99.99"; - final int overridePort = 1234; - final String overrideHostWithPort = overrideHost + ":" + overridePort; - ProxyDetectorImpl proxyDetector = new ProxyDetectorImpl( - proxySelectorSupplier, - authenticator, - overrideHostWithPort); - ProxiedSocketAddress detected = proxyDetector.proxyFor(destination); - assertNotNull(detected); - assertEquals( - HttpConnectProxiedSocketAddress.newBuilder() - .setTargetAddress(destination) - .setProxyAddress( - new InetSocketAddress(InetAddress.getByName(overrideHost), overridePort)) - .build(), - detected); - } - - @Test - public void override_hostOnly() throws Exception { - final String overrideHostWithoutPort = "10.99.99.99"; - final int defaultPort = 80; - ProxyDetectorImpl proxyDetector = new ProxyDetectorImpl( - proxySelectorSupplier, - authenticator, - overrideHostWithoutPort); - ProxiedSocketAddress detected = proxyDetector.proxyFor(destination); - assertNotNull(detected); - assertEquals( - HttpConnectProxiedSocketAddress.newBuilder() - .setTargetAddress(destination) - .setProxyAddress( - new InetSocketAddress(InetAddress.getByName(overrideHostWithoutPort), defaultPort)) - .build(), - detected); - } - @Test public void returnNullWhenNoProxy() throws Exception { when(proxySelector.select(any(URI.class))) @@ -227,8 +188,7 @@ public ProxySelector get() { return null; } }, - authenticator, - null); + authenticator); assertNull(proxyDetector.proxyFor(destination)); } } From 84c7713b2f40a4638a8e5461a4008810615bd11a Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Wed, 2 Apr 2025 16:29:55 +0530 Subject: [PATCH 234/591] xds: propagate audience from cluster resource in gcp auth filter (#11972) --- .../io/grpc/xds/GcpAuthenticationFilter.java | 108 +++++-- .../grpc/xds/GcpAuthenticationFilterTest.java | 301 +++++++++++++++++- .../grpc/xds/GrpcXdsClientImplDataTest.java | 15 +- .../test/java/io/grpc/xds/XdsTestUtils.java | 2 +- 4 files changed, 372 insertions(+), 54 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java index add885c6416..b5568efe400 100644 --- a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java +++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java @@ -16,8 +16,13 @@ package io.grpc.xds; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.xds.XdsNameResolver.CLUSTER_SELECTION_KEY; +import static io.grpc.xds.XdsNameResolver.XDS_CONFIG_CALL_OPTION_KEY; + import com.google.auth.oauth2.ComputeEngineCredentials; import com.google.auth.oauth2.IdTokenCredentials; +import com.google.common.annotations.VisibleForTesting; import com.google.common.primitives.UnsignedLongs; import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; @@ -34,8 +39,11 @@ import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.Status; +import io.grpc.StatusOr; import io.grpc.auth.MoreCallCredentials; +import io.grpc.xds.GcpAuthenticationFilter.AudienceMetadataParser.AudienceWrapper; import io.grpc.xds.MetadataRegistry.MetadataValueParser; +import io.grpc.xds.XdsConfig.XdsClusterConfig; import io.grpc.xds.client.XdsResourceType.ResourceInvalidException; import java.util.LinkedHashMap; import java.util.Map; @@ -52,6 +60,13 @@ final class GcpAuthenticationFilter implements Filter { static final String TYPE_URL = "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig"; + final String filterInstanceName; + + GcpAuthenticationFilter(String name) { + filterInstanceName = checkNotNull(name, "name"); + } + + static final class Provider implements Filter.Provider { @Override public String[] typeUrls() { @@ -65,7 +80,7 @@ public boolean isClientFilter() { @Override public GcpAuthenticationFilter newInstance(String name) { - return new GcpAuthenticationFilter(); + return new GcpAuthenticationFilter(name); } @Override @@ -119,34 +134,57 @@ public ClientInterceptor buildClientInterceptor(FilterConfig config, public ClientCall interceptCall( MethodDescriptor method, CallOptions callOptions, Channel next) { - /*String clusterName = callOptions.getOption(XdsAttributes.ATTR_CLUSTER_NAME); + String clusterName = callOptions.getOption(CLUSTER_SELECTION_KEY); if (clusterName == null) { + return new FailingClientCall<>( + Status.UNAVAILABLE.withDescription( + String.format( + "GCP Authn for %s does not contain cluster resource", filterInstanceName))); + } + + if (!clusterName.startsWith("cluster:")) { return next.newCall(method, callOptions); - }*/ - - // TODO: Fetch the CDS resource for the cluster. - // If the CDS resource is not available, fail the RPC with Status.UNAVAILABLE. - - // TODO: Extract the audience from the CDS resource metadata. - // If the audience is not found or is in the wrong format, fail the RPC. - String audience = "TEST_AUDIENCE"; - - try { - CallCredentials existingCallCredentials = callOptions.getCredentials(); - CallCredentials newCallCredentials = - getCallCredentials(callCredentialsCache, audience, credentials); - if (existingCallCredentials != null) { - callOptions = callOptions.withCallCredentials( - new CompositeCallCredentials(existingCallCredentials, newCallCredentials)); - } else { - callOptions = callOptions.withCallCredentials(newCallCredentials); - } } - catch (Exception e) { - // If we fail to attach CallCredentials due to any reason, return a FailingClientCall - return new FailingClientCall<>(Status.UNAUTHENTICATED - .withDescription("Failed to attach CallCredentials.") - .withCause(e)); + XdsConfig xdsConfig = callOptions.getOption(XDS_CONFIG_CALL_OPTION_KEY); + if (xdsConfig == null) { + return new FailingClientCall<>( + Status.UNAVAILABLE.withDescription( + String.format( + "GCP Authn for %s with %s does not contain xds configuration", + filterInstanceName, clusterName))); + } + StatusOr xdsCluster = + xdsConfig.getClusters().get(clusterName.substring("cluster:".length())); + if (xdsCluster == null) { + return new FailingClientCall<>( + Status.UNAVAILABLE.withDescription( + String.format( + "GCP Authn for %s with %s - xds cluster config does not contain xds cluster", + filterInstanceName, clusterName))); + } + if (!xdsCluster.hasValue()) { + return new FailingClientCall<>(xdsCluster.getStatus()); + } + Object audienceObj = + xdsCluster.getValue().getClusterResource().parsedMetadata().get(filterInstanceName); + if (audienceObj == null) { + return next.newCall(method, callOptions); + } + if (!(audienceObj instanceof AudienceWrapper)) { + return new FailingClientCall<>( + Status.UNAVAILABLE.withDescription( + String.format("GCP Authn found wrong type in %s metadata: %s=%s", + clusterName, filterInstanceName, audienceObj.getClass()))); + } + AudienceWrapper audience = (AudienceWrapper) audienceObj; + CallCredentials existingCallCredentials = callOptions.getCredentials(); + CallCredentials newCallCredentials = + getCallCredentials(callCredentialsCache, audience.audience, credentials); + if (existingCallCredentials != null) { + callOptions = callOptions.withCallCredentials( + new CompositeCallCredentials(existingCallCredentials, newCallCredentials)); + } else { + callOptions = callOptions.withCallCredentials(newCallCredentials); } return next.newCall(method, callOptions); } @@ -186,9 +224,11 @@ public String typeUrl() { } /** An implementation of {@link ClientCall} that fails when started. */ - private static final class FailingClientCall extends ClientCall { + @VisibleForTesting + static final class FailingClientCall extends ClientCall { - private final Status error; + @VisibleForTesting + final Status error; public FailingClientCall(Status error) { this.error = error; @@ -235,13 +275,21 @@ V getOrInsert(K key, Function create) { static class AudienceMetadataParser implements MetadataValueParser { + static final class AudienceWrapper { + final String audience; + + AudienceWrapper(String audience) { + this.audience = checkNotNull(audience); + } + } + @Override public String getTypeUrl() { return "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience"; } @Override - public String parse(Any any) throws ResourceInvalidException { + public AudienceWrapper parse(Any any) throws ResourceInvalidException { Audience audience; try { audience = any.unpack(Audience.class); @@ -253,7 +301,7 @@ public String parse(Any any) throws ResourceInvalidException { throw new ResourceInvalidException( "Audience URL is empty. Metadata value must contain a valid URL."); } - return url; + return new AudienceWrapper(url); } } } diff --git a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java index 52efaf9bd7b..a5e142b4094 100644 --- a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java @@ -17,25 +17,60 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.xds.XdsNameResolver.CLUSTER_SELECTION_KEY; +import static io.grpc.xds.XdsNameResolver.XDS_CONFIG_CALL_OPTION_KEY; +import static io.grpc.xds.XdsTestUtils.CLUSTER_NAME; +import static io.grpc.xds.XdsTestUtils.EDS_NAME; +import static io.grpc.xds.XdsTestUtils.ENDPOINT_HOSTNAME; +import static io.grpc.xds.XdsTestUtils.ENDPOINT_PORT; +import static io.grpc.xds.XdsTestUtils.RDS_NAME; +import static io.grpc.xds.XdsTestUtils.buildRouteConfiguration; +import static io.grpc.xds.XdsTestUtils.getWrrLbConfigAsMap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.protobuf.Any; import com.google.protobuf.Empty; import com.google.protobuf.Message; import com.google.protobuf.UInt64Value; +import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig; import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfig; import io.grpc.CallOptions; import io.grpc.Channel; +import io.grpc.ClientCall; import io.grpc.ClientInterceptor; import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.StatusOr; +import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.testing.TestMethodDescriptors; +import io.grpc.xds.Endpoints.LbEndpoint; +import io.grpc.xds.Endpoints.LocalityLbEndpoints; +import io.grpc.xds.GcpAuthenticationFilter.AudienceMetadataParser.AudienceWrapper; +import io.grpc.xds.GcpAuthenticationFilter.FailingClientCall; import io.grpc.xds.GcpAuthenticationFilter.GcpAuthenticationConfig; +import io.grpc.xds.XdsClusterResource.CdsUpdate; +import io.grpc.xds.XdsConfig.XdsClusterConfig; +import io.grpc.xds.XdsConfig.XdsClusterConfig.EndpointConfig; +import io.grpc.xds.XdsEndpointResource.EdsUpdate; +import io.grpc.xds.XdsListenerResource.LdsUpdate; +import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; +import io.grpc.xds.client.Locality; +import io.grpc.xds.client.XdsResourceType; +import io.grpc.xds.client.XdsResourceType.ResourceInvalidException; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -46,6 +81,17 @@ public class GcpAuthenticationFilterTest { private static final GcpAuthenticationFilter.Provider FILTER_PROVIDER = new GcpAuthenticationFilter.Provider(); + private static final String serverName = InProcessServerBuilder.generateName(); + private static final LdsUpdate ldsUpdate = getLdsUpdate(); + private static final EdsUpdate edsUpdate = getEdsUpdate(); + private static final RdsUpdate rdsUpdate = getRdsUpdate(); + private static final CdsUpdate cdsUpdate = getCdsUpdate(); + + @Test + public void testNewFilterInstancesPerFilterName() { + assertThat(new GcpAuthenticationFilter("FILTER_INSTANCE_NAME1")) + .isNotEqualTo(new GcpAuthenticationFilter("FILTER_INSTANCE_NAME1")); + } @Test public void filterType_clientOnly() { @@ -92,35 +138,258 @@ public void testParseFilterConfig_withInvalidMessageType() { } @Test - public void testClientInterceptor_createsAndReusesCachedCredentials() { + public void testClientInterceptor_success() throws IOException, ResourceInvalidException { + XdsConfig.XdsClusterConfig clusterConfig = new XdsConfig.XdsClusterConfig( + CLUSTER_NAME, + cdsUpdate, + new EndpointConfig(StatusOr.fromValue(edsUpdate))); + XdsConfig defaultXdsConfig = new XdsConfig.XdsConfigBuilder() + .setListener(ldsUpdate) + .setRoute(rdsUpdate) + .setVirtualHost(rdsUpdate.virtualHosts.get(0)) + .addCluster(CLUSTER_NAME, StatusOr.fromValue(clusterConfig)).build(); + CallOptions callOptionsWithXds = CallOptions.DEFAULT + .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0") + .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); - GcpAuthenticationFilter filter = new GcpAuthenticationFilter(); - - // Create interceptor + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); + Channel mockChannel = Mockito.mock(Channel.class); + ArgumentCaptor callOptionsCaptor = ArgumentCaptor.forClass(CallOptions.class); + + interceptor.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); - // Mock channel and capture CallOptions + verify(mockChannel).newCall(eq(methodDescriptor), callOptionsCaptor.capture()); + CallOptions capturedOptions = callOptionsCaptor.getAllValues().get(0); + assertNotNull(capturedOptions.getCredentials()); + } + + @Test + public void testClientInterceptor_createsAndReusesCachedCredentials() + throws IOException, ResourceInvalidException { + XdsConfig.XdsClusterConfig clusterConfig = new XdsConfig.XdsClusterConfig( + CLUSTER_NAME, + cdsUpdate, + new EndpointConfig(StatusOr.fromValue(edsUpdate))); + XdsConfig defaultXdsConfig = new XdsConfig.XdsConfigBuilder() + .setListener(ldsUpdate) + .setRoute(rdsUpdate) + .setVirtualHost(rdsUpdate.virtualHosts.get(0)) + .addCluster(CLUSTER_NAME, StatusOr.fromValue(clusterConfig)).build(); + CallOptions callOptionsWithXds = CallOptions.DEFAULT + .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0") + .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); + GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); + MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); Channel mockChannel = Mockito.mock(Channel.class); ArgumentCaptor callOptionsCaptor = ArgumentCaptor.forClass(CallOptions.class); - // Execute interception twice to check caching - interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT, mockChannel); - interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT, mockChannel); + interceptor.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); + interceptor.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); - // Capture and verify CallOptions for CallCredentials presence - Mockito.verify(mockChannel, Mockito.times(2)) + verify(mockChannel, Mockito.times(2)) .newCall(eq(methodDescriptor), callOptionsCaptor.capture()); - - // Retrieve the CallOptions captured from both calls CallOptions firstCapturedOptions = callOptionsCaptor.getAllValues().get(0); CallOptions secondCapturedOptions = callOptionsCaptor.getAllValues().get(1); - - // Ensure that CallCredentials was added assertNotNull(firstCapturedOptions.getCredentials()); assertNotNull(secondCapturedOptions.getCredentials()); - - // Ensure that the CallCredentials from both calls are the same, indicating caching assertSame(firstCapturedOptions.getCredentials(), secondCapturedOptions.getCredentials()); } + + @Test + public void testClientInterceptor_withoutClusterSelectionKey() throws Exception { + GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); + MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); + Channel mockChannel = mock(Channel.class); + CallOptions callOptionsWithXds = CallOptions.DEFAULT; + + ClientCall call = interceptor.interceptCall( + methodDescriptor, callOptionsWithXds, mockChannel); + + assertTrue(call instanceof FailingClientCall); + FailingClientCall clientCall = (FailingClientCall) call; + assertThat(clientCall.error.getDescription()).contains("does not contain cluster resource"); + } + + @Test + public void testClientInterceptor_clusterSelectionKeyWithoutPrefix() throws Exception { + XdsConfig.XdsClusterConfig clusterConfig = new XdsConfig.XdsClusterConfig( + CLUSTER_NAME, + cdsUpdate, + new EndpointConfig(StatusOr.fromValue(edsUpdate))); + XdsConfig defaultXdsConfig = new XdsConfig.XdsConfigBuilder() + .setListener(ldsUpdate) + .setRoute(rdsUpdate) + .setVirtualHost(rdsUpdate.virtualHosts.get(0)) + .addCluster(CLUSTER_NAME, StatusOr.fromValue(clusterConfig)).build(); + CallOptions callOptionsWithXds = CallOptions.DEFAULT + .withOption(CLUSTER_SELECTION_KEY, "cluster0") + .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); + Channel mockChannel = mock(Channel.class); + + GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); + MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); + interceptor.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); + + verify(mockChannel).newCall(methodDescriptor, callOptionsWithXds); + } + + @Test + public void testClientInterceptor_xdsConfigDoesNotExist() throws Exception { + GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); + MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); + Channel mockChannel = mock(Channel.class); + CallOptions callOptionsWithXds = CallOptions.DEFAULT + .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0"); + + ClientCall call = + interceptor.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); + + assertTrue(call instanceof FailingClientCall); + FailingClientCall clientCall = (FailingClientCall) call; + assertThat(clientCall.error.getDescription()).contains("does not contain xds configuration"); + } + + @Test + public void testClientInterceptor_incorrectClusterName() throws Exception { + XdsConfig.XdsClusterConfig clusterConfig = new XdsConfig.XdsClusterConfig( + CLUSTER_NAME, + cdsUpdate, + new EndpointConfig(StatusOr.fromValue(edsUpdate))); + XdsConfig defaultXdsConfig = new XdsConfig.XdsConfigBuilder() + .setListener(ldsUpdate) + .setRoute(rdsUpdate) + .setVirtualHost(rdsUpdate.virtualHosts.get(0)) + .addCluster("custer0", StatusOr.fromValue(clusterConfig)).build(); + CallOptions callOptionsWithXds = CallOptions.DEFAULT + .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster") + .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); + GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); + MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); + Channel mockChannel = mock(Channel.class); + + ClientCall call = + interceptor.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); + + assertTrue(call instanceof FailingClientCall); + FailingClientCall clientCall = (FailingClientCall) call; + assertThat(clientCall.error.getDescription()).contains("does not contain xds cluster"); + } + + @Test + public void testClientInterceptor_statusOrError() throws Exception { + StatusOr errorCluster = + StatusOr.fromStatus(Status.NOT_FOUND.withDescription("Cluster resource not found")); + XdsConfig defaultXdsConfig = new XdsConfig.XdsConfigBuilder() + .setListener(ldsUpdate) + .setRoute(rdsUpdate) + .setVirtualHost(rdsUpdate.virtualHosts.get(0)) + .addCluster(CLUSTER_NAME, errorCluster).build(); + CallOptions callOptionsWithXds = CallOptions.DEFAULT + .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0") + .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); + GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); + MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); + Channel mockChannel = mock(Channel.class); + + ClientCall call = + interceptor.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); + + assertTrue(call instanceof FailingClientCall); + FailingClientCall clientCall = (FailingClientCall) call; + assertThat(clientCall.error.getDescription()).contains("Cluster resource not found"); + } + + @Test + public void testClientInterceptor_notAudienceWrapper() + throws IOException, ResourceInvalidException { + XdsConfig.XdsClusterConfig clusterConfig = new XdsConfig.XdsClusterConfig( + CLUSTER_NAME, + getCdsUpdateWithIncorrectAudienceWrapper(), + new EndpointConfig(StatusOr.fromValue(edsUpdate))); + XdsConfig defaultXdsConfig = new XdsConfig.XdsConfigBuilder() + .setListener(ldsUpdate) + .setRoute(rdsUpdate) + .setVirtualHost(rdsUpdate.virtualHosts.get(0)) + .addCluster(CLUSTER_NAME, StatusOr.fromValue(clusterConfig)).build(); + CallOptions callOptionsWithXds = CallOptions.DEFAULT + .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0") + .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); + GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); + MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); + Channel mockChannel = Mockito.mock(Channel.class); + + ClientCall call = + interceptor.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); + + assertTrue(call instanceof FailingClientCall); + FailingClientCall clientCall = (FailingClientCall) call; + assertThat(clientCall.error.getDescription()).contains("GCP Authn found wrong type"); + } + + private static LdsUpdate getLdsUpdate() { + Filter.NamedFilterConfig routerFilterConfig = new Filter.NamedFilterConfig( + serverName, RouterFilter.ROUTER_CONFIG); + HttpConnectionManager httpConnectionManager = HttpConnectionManager.forRdsName( + 0L, RDS_NAME, Collections.singletonList(routerFilterConfig)); + return XdsListenerResource.LdsUpdate.forApiListener(httpConnectionManager); + } + + private static RdsUpdate getRdsUpdate() { + RouteConfiguration routeConfiguration = + buildRouteConfiguration(serverName, RDS_NAME, CLUSTER_NAME); + XdsResourceType.Args args = new XdsResourceType.Args(null, "0", "0", null, null, null); + try { + return XdsRouteConfigureResource.getInstance().doParse(args, routeConfiguration); + } catch (ResourceInvalidException ex) { + return null; + } + } + + private static EdsUpdate getEdsUpdate() { + Map lbEndpointsMap = new HashMap<>(); + LbEndpoint lbEndpoint = LbEndpoint.create( + serverName, ENDPOINT_PORT, 0, true, ENDPOINT_HOSTNAME, ImmutableMap.of()); + lbEndpointsMap.put( + Locality.create("", "", ""), + LocalityLbEndpoints.create(ImmutableList.of(lbEndpoint), 10, 0, ImmutableMap.of())); + return new XdsEndpointResource.EdsUpdate(EDS_NAME, lbEndpointsMap, Collections.emptyList()); + } + + private static CdsUpdate getCdsUpdate() { + ImmutableMap.Builder parsedMetadata = ImmutableMap.builder(); + parsedMetadata.put("FILTER_INSTANCE_NAME", new AudienceWrapper("TEST_AUDIENCE")); + try { + CdsUpdate.Builder cdsUpdate = CdsUpdate.forEds( + CLUSTER_NAME, EDS_NAME, null, null, null, null, false) + .lbPolicyConfig(getWrrLbConfigAsMap()); + return cdsUpdate.parsedMetadata(parsedMetadata.build()).build(); + } catch (IOException ex) { + return null; + } + } + + private static CdsUpdate getCdsUpdateWithIncorrectAudienceWrapper() throws IOException { + ImmutableMap.Builder parsedMetadata = ImmutableMap.builder(); + parsedMetadata.put("FILTER_INSTANCE_NAME", "TEST_AUDIENCE"); + CdsUpdate.Builder cdsUpdate = CdsUpdate.forEds( + CLUSTER_NAME, EDS_NAME, null, null, null, null, false) + .lbPolicyConfig(getWrrLbConfigAsMap()); + return cdsUpdate.parsedMetadata(parsedMetadata.build()).build(); + } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index bfaa17245cf..e53ed9047ca 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -129,6 +129,7 @@ import io.grpc.xds.Endpoints.LbEndpoint; import io.grpc.xds.Endpoints.LocalityLbEndpoints; import io.grpc.xds.Filter.FilterConfig; +import io.grpc.xds.GcpAuthenticationFilter.AudienceMetadataParser.AudienceWrapper; import io.grpc.xds.MetadataRegistry.MetadataValueParser; import io.grpc.xds.RouteLookupServiceClusterSpecifierPlugin.RlsPluginConfig; import io.grpc.xds.VirtualHost.Route; @@ -2417,8 +2418,7 @@ public Object parse(Any value) { } @Test - public void processCluster_parsesAudienceMetadata() - throws ResourceInvalidException, InvalidProtocolBufferException { + public void processCluster_parsesAudienceMetadata() throws Exception { MetadataRegistry.getInstance(); Audience audience = Audience.newBuilder() @@ -2462,7 +2462,10 @@ public void processCluster_parsesAudienceMetadata() "FILTER_METADATA", ImmutableMap.of( "key1", "value1", "key2", 42.0)); - assertThat(update.parsedMetadata()).isEqualTo(expectedParsedMetadata); + assertThat(update.parsedMetadata().get("FILTER_METADATA")) + .isEqualTo(expectedParsedMetadata.get("FILTER_METADATA")); + assertThat(update.parsedMetadata().get("AUDIENCE_METADATA")) + .isInstanceOf(AudienceWrapper.class); } @Test @@ -2519,8 +2522,7 @@ public void processCluster_parsesAddressMetadata() throws Exception { } @Test - public void processCluster_metadataKeyCollision_resolvesToTypedMetadata() - throws ResourceInvalidException, InvalidProtocolBufferException { + public void processCluster_metadataKeyCollision_resolvesToTypedMetadata() throws Exception { MetadataRegistry metadataRegistry = MetadataRegistry.getInstance(); MetadataValueParser testParser = @@ -2575,8 +2577,7 @@ public Object parse(Any value) { } @Test - public void parseNonAggregateCluster_withHttp11ProxyTransportSocket() - throws ResourceInvalidException, InvalidProtocolBufferException { + public void parseNonAggregateCluster_withHttp11ProxyTransportSocket() throws Exception { XdsClusterResource.isEnabledXdsHttpConnect = true; Http11ProxyUpstreamTransport http11ProxyUpstreamTransport = diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java index 9f90777be3d..52953ef5407 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -291,7 +291,7 @@ static Map createMinimalLbEndpointsMap(String ser } @SuppressWarnings("unchecked") - private static ImmutableMap getWrrLbConfigAsMap() throws IOException { + static ImmutableMap getWrrLbConfigAsMap() throws IOException { String lbConfigStr = "{\"wrr_locality_experimental\" : " + "{ \"childPolicy\" : [{\"round_robin\" : {}}]}}"; From c8d1e6e39c26da2eb54386eac6eaf9e1aa8f9b9c Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Thu, 3 Apr 2025 11:22:26 +0530 Subject: [PATCH 235/591] xds: listener type validation (#11933) --- .../io/grpc/xds/EnvoyServerProtoData.java | 9 +- .../java/io/grpc/xds/XdsListenerResource.java | 13 +- .../java/io/grpc/xds/XdsNameResolver.java | 7 + .../java/io/grpc/xds/XdsServerWrapper.java | 36 ++++- .../java/io/grpc/xds/ControlPlaneRule.java | 4 + .../grpc/xds/GrpcXdsClientImplDataTest.java | 35 +++++ .../XdsClientWrapperForServerSdsTestMisc.java | 6 +- .../grpc/xds/XdsSecurityClientServerTest.java | 7 +- .../io/grpc/xds/XdsServerBuilderTest.java | 12 +- .../java/io/grpc/xds/XdsServerTestHelper.java | 20 ++- .../io/grpc/xds/XdsServerWrapperTest.java | 147 +++++++++++++++++- 11 files changed, 271 insertions(+), 25 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java index ae5b3c5b1c9..fd2a1d2a069 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java @@ -22,6 +22,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.protobuf.util.Durations; +import io.envoyproxy.envoy.config.core.v3.SocketAddress.Protocol; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; import io.grpc.Internal; import io.grpc.xds.client.EnvoyProtoData; @@ -248,13 +249,17 @@ abstract static class Listener { @Nullable abstract FilterChain defaultFilterChain(); + @Nullable + abstract Protocol protocol(); + static Listener create( String name, @Nullable String address, ImmutableList filterChains, - @Nullable FilterChain defaultFilterChain) { + @Nullable FilterChain defaultFilterChain, + @Nullable Protocol protocol) { return new AutoValue_EnvoyServerProtoData_Listener(name, address, filterChains, - defaultFilterChain); + defaultFilterChain, protocol); } } diff --git a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java index ec0cbbf243f..041b659b4c3 100644 --- a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java @@ -162,13 +162,16 @@ static EnvoyServerProtoData.Listener parseServerSideListener( } String address = null; + SocketAddress socketAddress = null; if (proto.getAddress().hasSocketAddress()) { - SocketAddress socketAddress = proto.getAddress().getSocketAddress(); + socketAddress = proto.getAddress().getSocketAddress(); address = socketAddress.getAddress(); + if (address.isEmpty()) { + throw new ResourceInvalidException("Invalid address: Empty address is not allowed."); + } switch (socketAddress.getPortSpecifierCase()) { case NAMED_PORT: - address = address + ":" + socketAddress.getNamedPort(); - break; + throw new ResourceInvalidException("NAMED_PORT is not supported in gRPC."); case PORT_VALUE: address = address + ":" + socketAddress.getPortValue(); break; @@ -209,8 +212,8 @@ static EnvoyServerProtoData.Listener parseServerSideListener( null, certProviderInstances, args); } - return EnvoyServerProtoData.Listener.create( - proto.getName(), address, filterChains.build(), defaultFilterChain); + return EnvoyServerProtoData.Listener.create(proto.getName(), address, filterChains.build(), + defaultFilterChain, socketAddress == null ? null : socketAddress.getProtocol()); } @VisibleForTesting diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 7704a4a09db..a14abf95f41 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -684,6 +684,13 @@ public void onUpdate(StatusOr updateOrStatus) { // Process Route XdsConfig update = updateOrStatus.getValue(); HttpConnectionManager httpConnectionManager = update.getListener().httpConnectionManager(); + if (httpConnectionManager == null) { + logger.log(XdsLogLevel.INFO, "API Listener: httpConnectionManager does not exist."); + updateActiveFilters(null); + cleanUpRoutes(updateOrStatus.getStatus()); + return; + } + VirtualHost virtualHost = update.getVirtualHost(); ImmutableList filterConfigs = httpConnectionManager.httpFilterConfigs(); long streamDurationNano = httpConnectionManager.httpMaxStreamDurationNano(); diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index 6625bd8178a..be64b56eef5 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -24,7 +24,10 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.net.HostAndPort; +import com.google.common.net.InetAddresses; import com.google.common.util.concurrent.SettableFuture; +import io.envoyproxy.envoy.config.core.v3.SocketAddress.Protocol; import io.grpc.Attributes; import io.grpc.InternalServerInterceptors; import io.grpc.Metadata; @@ -57,6 +60,7 @@ import io.grpc.xds.client.XdsClient.ResourceWatcher; import io.grpc.xds.internal.security.SslContextProviderSupplier; import java.io.IOException; +import java.net.InetAddress; import java.net.SocketAddress; import java.util.ArrayList; import java.util.HashMap; @@ -383,7 +387,21 @@ public void onChanged(final LdsUpdate update) { return; } logger.log(Level.FINEST, "Received Lds update {0}", update); - checkNotNull(update.listener(), "update"); + if (update.listener() == null) { + onResourceDoesNotExist("Non-API"); + return; + } + + String ldsAddress = update.listener().address(); + if (ldsAddress == null || update.listener().protocol() != Protocol.TCP + || !ipAddressesMatch(ldsAddress)) { + handleConfigNotFoundOrMismatch( + Status.UNKNOWN.withDescription( + String.format( + "Listener address mismatch: expected %s, but got %s.", + listenerAddress, ldsAddress)).asException()); + return; + } if (!pendingRds.isEmpty()) { // filter chain state has not yet been applied to filterChainSelectorManager and there // are two sets of sslContextProviderSuppliers, so we release the old ones. @@ -432,6 +450,18 @@ public void onChanged(final LdsUpdate update) { } } + private boolean ipAddressesMatch(String ldsAddress) { + HostAndPort ldsAddressHnP = HostAndPort.fromString(ldsAddress); + HostAndPort listenerAddressHnP = HostAndPort.fromString(listenerAddress); + if (!ldsAddressHnP.hasPort() || !listenerAddressHnP.hasPort() + || ldsAddressHnP.getPort() != listenerAddressHnP.getPort()) { + return false; + } + InetAddress listenerIp = InetAddresses.forString(listenerAddressHnP.getHost()); + InetAddress ldsIp = InetAddresses.forString(ldsAddressHnP.getHost()); + return listenerIp.equals(ldsIp); + } + @Override public void onResourceDoesNotExist(final String resourceName) { if (stopped) { @@ -440,7 +470,7 @@ public void onResourceDoesNotExist(final String resourceName) { StatusException statusException = Status.UNAVAILABLE.withDescription( String.format("Listener %s unavailable, xDS node ID: %s", resourceName, xdsClient.getBootstrapInfo().node().getId())).asException(); - handleConfigNotFound(statusException); + handleConfigNotFoundOrMismatch(statusException); } @Override @@ -674,7 +704,7 @@ public Listener interceptCall(ServerCall call, }; } - private void handleConfigNotFound(StatusException exception) { + private void handleConfigNotFoundOrMismatch(StatusException exception) { cleanUpRouteDiscoveryStates(); shutdownActiveFilters(); List toRelease = getSuppliersInUse(); diff --git a/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java b/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java index 39761912ea5..8c10627d153 100644 --- a/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java +++ b/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java @@ -366,10 +366,14 @@ static Listener buildServerListener() { .setFilterChainMatch(filterChainMatch) .addFilters(filter) .build(); + Address address = Address.newBuilder() + .setSocketAddress(SocketAddress.newBuilder().setAddress("0.0.0.0").setPortValue(0)) + .build(); return Listener.newBuilder() .setName(SERVER_LISTENER_TEMPLATE_NO_REPLACEMENT) .setTrafficDirection(TrafficDirection.INBOUND) .addFilterChains(filterChain) + .setAddress(address) .build(); } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index e53ed9047ca..0ea58c974bb 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -2661,6 +2661,41 @@ public void parseServerSideListener_useOriginalDst() throws ResourceInvalidExcep listener,null, filterRegistry, null, getXdsResourceTypeArgs(true)); } + @Test + public void parseServerSideListener_emptyAddress() throws ResourceInvalidException { + Listener listener = + Listener.newBuilder() + .setName("listener1") + .setTrafficDirection(TrafficDirection.INBOUND) + .setAddress(Address.newBuilder() + .setSocketAddress( + SocketAddress.newBuilder())) + .build(); + thrown.expect(ResourceInvalidException.class); + thrown.expectMessage("Invalid address: Empty address is not allowed."); + + XdsListenerResource.parseServerSideListener( + listener,null, filterRegistry, null, getXdsResourceTypeArgs(true)); + } + + @Test + public void parseServerSideListener_namedPort() throws ResourceInvalidException { + Listener listener = + Listener.newBuilder() + .setName("listener1") + .setTrafficDirection(TrafficDirection.INBOUND) + .setAddress(Address.newBuilder() + .setSocketAddress( + SocketAddress.newBuilder() + .setAddress("172.14.14.5").setNamedPort(""))) + .build(); + thrown.expect(ResourceInvalidException.class); + thrown.expectMessage("NAMED_PORT is not supported in gRPC."); + + XdsListenerResource.parseServerSideListener( + listener,null, filterRegistry, null, getXdsResourceTypeArgs(true)); + } + @Test public void parseServerSideListener_nonUniqueFilterChainMatch() throws ResourceInvalidException { Filter filter1 = buildHttpConnectionManagerFilter( diff --git a/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java b/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java index f3f4d74eb2f..ff97afe6916 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java @@ -32,6 +32,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.SettableFuture; +import io.envoyproxy.envoy.config.core.v3.SocketAddress.Protocol; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.Status; @@ -165,9 +166,10 @@ public void run() { EnvoyServerProtoData.Listener tcpListener = EnvoyServerProtoData.Listener.create( "listener1", - "10.1.2.3", + "0.0.0.0:7000", ImmutableList.of(), - null); + null, + Protocol.TCP); LdsUpdate listenerUpdate = LdsUpdate.forTcpListener(tcpListener); xdsClient.ldsWatcher.onChanged(listenerUpdate); verify(listener, timeout(5000)).onServing(); diff --git a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java index 380c0591812..23068d665bf 100644 --- a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java @@ -35,6 +35,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.SettableFuture; +import io.envoyproxy.envoy.config.core.v3.SocketAddress.Protocol; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.grpc.Attributes; import io.grpc.EquivalentAddressGroup; @@ -497,7 +498,7 @@ public void mtlsClientServer_changeServerContext_expectException() DownstreamTlsContext downstreamTlsContext = CommonTlsContextTestsUtil.buildDownstreamTlsContext( "cert-instance-name2", true, true); - EnvoyServerProtoData.Listener listener = buildListener("listener1", "0.0.0.0", + EnvoyServerProtoData.Listener listener = buildListener("listener1", "0.0.0.0:0", downstreamTlsContext, tlsContextManagerForServer); xdsClient.deliverLdsUpdate(LdsUpdate.forTcpListener(listener)); @@ -601,7 +602,7 @@ private void buildServer( tlsContextManagerForServer = new TlsContextManagerImpl(bootstrapInfoForServer); XdsServerWrapper xdsServer = (XdsServerWrapper) builder.build(); SettableFuture startFuture = startServerAsync(xdsServer); - EnvoyServerProtoData.Listener listener = buildListener("listener1", "10.1.2.3", + EnvoyServerProtoData.Listener listener = buildListener("listener1", "0.0.0.0:0", downstreamTlsContext, tlsContextManagerForServer); LdsUpdate listenerUpdate = LdsUpdate.forTcpListener(listener); xdsClient.deliverLdsUpdate(listenerUpdate); @@ -642,7 +643,7 @@ static EnvoyServerProtoData.Listener buildListener( "filter-chain-foo", filterChainMatch, httpConnectionManager, tlsContext, tlsContextManager); EnvoyServerProtoData.Listener listener = EnvoyServerProtoData.Listener.create( - name, address, ImmutableList.of(defaultFilterChain), null); + name, address, ImmutableList.of(defaultFilterChain), null, Protocol.TCP); return listener; } diff --git a/xds/src/test/java/io/grpc/xds/XdsServerBuilderTest.java b/xds/src/test/java/io/grpc/xds/XdsServerBuilderTest.java index d28c7d7c607..2c168c65869 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerBuilderTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerBuilderTest.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.xds.XdsServerTestHelper.buildTestListener; import static org.junit.Assert.fail; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; @@ -26,6 +27,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.SettableFuture; import io.grpc.BindableService; import io.grpc.InsecureServerCredentials; @@ -33,6 +35,7 @@ import io.grpc.Status; import io.grpc.StatusException; import io.grpc.testing.GrpcCleanupRule; +import io.grpc.xds.XdsListenerResource.LdsUpdate; import io.grpc.xds.XdsServerTestHelper.FakeXdsClient; import io.grpc.xds.XdsServerTestHelper.FakeXdsClientPoolFactory; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; @@ -221,10 +224,13 @@ public void xdsServer_startError() buildServer(mockXdsServingStatusListener); Future future = startServerAsync(); // create port conflict for start to fail - XdsServerTestHelper.generateListenerUpdate( - xdsClient, + EnvoyServerProtoData.Listener listener = buildTestListener( + "listener1", "0.0.0.0:" + port, ImmutableList.of(), CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"), - tlsContextManager); + null, tlsContextManager); + LdsUpdate listenerUpdate = LdsUpdate.forTcpListener(listener); + xdsClient.deliverLdsUpdate(listenerUpdate); + Throwable exception = future.get(5, TimeUnit.SECONDS); assertThat(exception).isInstanceOf(IOException.class); assertThat(exception).hasMessageThat().contains("Failed to bind"); diff --git a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java index ec9f4c54c31..b0472b4729d 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.SettableFuture; +import io.envoyproxy.envoy.config.core.v3.SocketAddress.Protocol; import io.grpc.InsecureChannelCredentials; import io.grpc.MetricRecorder; import io.grpc.internal.ObjectPool; @@ -74,7 +75,7 @@ public class XdsServerTestHelper { static void generateListenerUpdate(FakeXdsClient xdsClient, EnvoyServerProtoData.DownstreamTlsContext tlsContext, TlsContextManager tlsContextManager) { - EnvoyServerProtoData.Listener listener = buildTestListener("listener1", "10.1.2.3", + EnvoyServerProtoData.Listener listener = buildTestListener("listener1", "0.0.0.0:0", ImmutableList.of(), tlsContext, null, tlsContextManager); LdsUpdate listenerUpdate = LdsUpdate.forTcpListener(listener); xdsClient.deliverLdsUpdate(listenerUpdate); @@ -85,7 +86,8 @@ static void generateListenerUpdate( EnvoyServerProtoData.DownstreamTlsContext tlsContext, EnvoyServerProtoData.DownstreamTlsContext tlsContextForDefaultFilterChain, TlsContextManager tlsContextManager) { - EnvoyServerProtoData.Listener listener = buildTestListener("listener1", "10.1.2.3", sourcePorts, + EnvoyServerProtoData.Listener listener = buildTestListener( + "listener1", "0.0.0.0:7000", sourcePorts, tlsContext, tlsContextForDefaultFilterChain, tlsContextManager); LdsUpdate listenerUpdate = LdsUpdate.forTcpListener(listener); xdsClient.deliverLdsUpdate(listenerUpdate); @@ -130,7 +132,7 @@ static EnvoyServerProtoData.Listener buildTestListener( tlsContextForDefaultFilterChain, tlsContextManager); EnvoyServerProtoData.Listener listener = EnvoyServerProtoData.Listener.create( - name, address, ImmutableList.of(filterChain1), defaultFilterChain); + name, address, ImmutableList.of(filterChain1), defaultFilterChain, Protocol.TCP); return listener; } @@ -290,6 +292,14 @@ private String awaitLdsResource(Duration timeout) { } } + void deliverLdsUpdateWithApiListener(long httpMaxStreamDurationNano, + List virtualHosts) { + execute(() -> { + ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( + httpMaxStreamDurationNano, virtualHosts, null))); + }); + } + void deliverLdsUpdate(LdsUpdate ldsUpdate) { execute(() -> ldsWatcher.onChanged(ldsUpdate)); } @@ -297,8 +307,8 @@ void deliverLdsUpdate(LdsUpdate ldsUpdate) { void deliverLdsUpdate( List filterChains, @Nullable FilterChain defaultFilterChain) { - deliverLdsUpdate(LdsUpdate.forTcpListener(Listener.create( - "listener", "0.0.0.0:1", ImmutableList.copyOf(filterChains), defaultFilterChain))); + deliverLdsUpdate(LdsUpdate.forTcpListener(Listener.create("listener", "0.0.0.0:1", + ImmutableList.copyOf(filterChains), defaultFilterChain, Protocol.TCP))); } void deliverLdsUpdate(FilterChain filterChain, @Nullable FilterChain defaultFilterChain) { diff --git a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java index e5f0f44cbae..ce1c6b94a35 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java @@ -37,6 +37,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.net.InetAddresses; import com.google.common.util.concurrent.SettableFuture; +import io.envoyproxy.envoy.config.core.v3.SocketAddress.Protocol; import io.grpc.Attributes; import io.grpc.InsecureChannelCredentials; import io.grpc.Metadata; @@ -54,6 +55,7 @@ import io.grpc.xds.EnvoyServerProtoData.CidrRange; import io.grpc.xds.EnvoyServerProtoData.FilterChain; import io.grpc.xds.EnvoyServerProtoData.FilterChainMatch; +import io.grpc.xds.EnvoyServerProtoData.Listener; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.Filter.NamedFilterConfig; import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler.FilterChainSelector; @@ -61,6 +63,7 @@ import io.grpc.xds.VirtualHost.Route; import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher; +import io.grpc.xds.XdsListenerResource.LdsUpdate; import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import io.grpc.xds.XdsServerBuilder.XdsServingStatusListener; import io.grpc.xds.XdsServerTestHelper.FakeXdsClient; @@ -538,6 +541,146 @@ public void run() { verify(mockServer).start(); } + @Test + public void onChanged_listenerIsNull() + throws ExecutionException, InterruptedException, TimeoutException { + xdsServerWrapper = new XdsServerWrapper("10.1.2.3:1", mockBuilder, listener, + selectorManager, new FakeXdsClientPoolFactory(xdsClient), + filterRegistry, executor.getScheduledExecutorService()); + final SettableFuture start = SettableFuture.create(); + Executors.newSingleThreadExecutor().execute(new Runnable() { + @Override + public void run() { + try { + start.set(xdsServerWrapper.start()); + } catch (Exception ex) { + start.setException(ex); + } + } + }); + String ldsResource = xdsClient.ldsResource.get(5, TimeUnit.SECONDS); + assertThat(ldsResource).isEqualTo("grpc/server?udpa.resource.listening_address=10.1.2.3:1"); + VirtualHost virtualHost = + VirtualHost.create( + "virtual-host", Collections.singletonList("auth"), new ArrayList(), + ImmutableMap.of()); + + xdsClient.deliverLdsUpdateWithApiListener(0L, Arrays.asList(virtualHost)); + + verify(listener, timeout(10000)).onNotServing(any()); + } + + @Test + public void onChanged_listenerAddressMissingPort() + throws ExecutionException, InterruptedException, TimeoutException { + xdsServerWrapper = new XdsServerWrapper("10.1.2.3:1", mockBuilder, listener, + selectorManager, new FakeXdsClientPoolFactory(xdsClient), + filterRegistry, executor.getScheduledExecutorService()); + final SettableFuture start = SettableFuture.create(); + Executors.newSingleThreadExecutor().execute(new Runnable() { + @Override + public void run() { + try { + start.set(xdsServerWrapper.start()); + } catch (Exception ex) { + start.setException(ex); + } + } + }); + String ldsResource = xdsClient.ldsResource.get(5, TimeUnit.SECONDS); + assertThat(ldsResource).isEqualTo("grpc/server?udpa.resource.listening_address=10.1.2.3:1"); + VirtualHost virtualHost = + VirtualHost.create( + "virtual-host", Collections.singletonList("auth"), new ArrayList(), + ImmutableMap.of()); + HttpConnectionManager httpConnectionManager = HttpConnectionManager.forVirtualHosts( + 0L, Collections.singletonList(virtualHost), new ArrayList()); + EnvoyServerProtoData.FilterChain filterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-foo", createMatch(), httpConnectionManager, createTls(), + mock(TlsContextManager.class)); + LdsUpdate listenerUpdate = LdsUpdate.forTcpListener( + Listener.create("listener", "20.3.4.5:", + ImmutableList.copyOf(Collections.singletonList(filterChain)), null, Protocol.TCP)); + + xdsClient.deliverLdsUpdate(listenerUpdate); + + verify(listener, timeout(10000)).onNotServing(any()); + } + + @Test + public void onChanged_listenerAddressMismatch() + throws ExecutionException, InterruptedException, TimeoutException { + xdsServerWrapper = new XdsServerWrapper("10.1.2.3:1", mockBuilder, listener, + selectorManager, new FakeXdsClientPoolFactory(xdsClient), + filterRegistry, executor.getScheduledExecutorService()); + final SettableFuture start = SettableFuture.create(); + Executors.newSingleThreadExecutor().execute(new Runnable() { + @Override + public void run() { + try { + start.set(xdsServerWrapper.start()); + } catch (Exception ex) { + start.setException(ex); + } + } + }); + String ldsResource = xdsClient.ldsResource.get(5, TimeUnit.SECONDS); + assertThat(ldsResource).isEqualTo("grpc/server?udpa.resource.listening_address=10.1.2.3:1"); + VirtualHost virtualHost = + VirtualHost.create( + "virtual-host", Collections.singletonList("auth"), new ArrayList(), + ImmutableMap.of()); + HttpConnectionManager httpConnectionManager = HttpConnectionManager.forVirtualHosts( + 0L, Collections.singletonList(virtualHost), new ArrayList()); + EnvoyServerProtoData.FilterChain filterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-foo", createMatch(), httpConnectionManager, createTls(), + mock(TlsContextManager.class)); + LdsUpdate listenerUpdate = LdsUpdate.forTcpListener( + Listener.create("listener", "20.3.4.5:1", + ImmutableList.copyOf(Collections.singletonList(filterChain)), null, Protocol.TCP)); + + xdsClient.deliverLdsUpdate(listenerUpdate); + + verify(listener, timeout(10000)).onNotServing(any()); + } + + @Test + public void onChanged_listenerAddressPortMismatch() + throws ExecutionException, InterruptedException, TimeoutException { + xdsServerWrapper = new XdsServerWrapper("10.1.2.3:1", mockBuilder, listener, + selectorManager, new FakeXdsClientPoolFactory(xdsClient), + filterRegistry, executor.getScheduledExecutorService()); + final SettableFuture start = SettableFuture.create(); + Executors.newSingleThreadExecutor().execute(new Runnable() { + @Override + public void run() { + try { + start.set(xdsServerWrapper.start()); + } catch (Exception ex) { + start.setException(ex); + } + } + }); + String ldsResource = xdsClient.ldsResource.get(5, TimeUnit.SECONDS); + assertThat(ldsResource).isEqualTo("grpc/server?udpa.resource.listening_address=10.1.2.3:1"); + VirtualHost virtualHost = + VirtualHost.create( + "virtual-host", Collections.singletonList("auth"), new ArrayList(), + ImmutableMap.of()); + HttpConnectionManager httpConnectionManager = HttpConnectionManager.forVirtualHosts( + 0L, Collections.singletonList(virtualHost), new ArrayList()); + EnvoyServerProtoData.FilterChain filterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-foo", createMatch(), httpConnectionManager, createTls(), + mock(TlsContextManager.class)); + LdsUpdate listenerUpdate = LdsUpdate.forTcpListener( + Listener.create("listener", "10.1.2.3:2", + ImmutableList.copyOf(Collections.singletonList(filterChain)), null, Protocol.TCP)); + + xdsClient.deliverLdsUpdate(listenerUpdate); + + verify(listener, timeout(10000)).onNotServing(any()); + } + @Test public void discoverState_rds() throws Exception { final SettableFuture start = SettableFuture.create(); @@ -1811,7 +1954,7 @@ private static HttpConnectionManager createRds(String name) { /** * Returns the least-specific match-all Filter Chain Match. */ - private static FilterChainMatch createMatch() { + static FilterChainMatch createMatch() { return FilterChainMatch.create( 0, ImmutableList.of(), @@ -1867,7 +2010,7 @@ private static MethodDescriptor createMethod(String path) { .build(); } - private static EnvoyServerProtoData.DownstreamTlsContext createTls() { + static EnvoyServerProtoData.DownstreamTlsContext createTls() { return CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); } } From d4c46a7f1fe67bad762139a679b3597dd8c1849d Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Fri, 4 Apr 2025 05:53:08 +0000 Subject: [PATCH 236/591] refactor: prevents global stats config freeze in ConfiguratorRegistry.getConfigurators() (#11991) --- .../java/io/grpc/ConfiguratorRegistry.java | 19 ++++++++++++++----- .../io/grpc/InternalConfiguratorRegistry.java | 4 ++++ .../io/grpc/ConfiguratorRegistryTest.java | 14 ++++++-------- .../ManagedChannelImplBuilderTest.java | 9 +++------ .../grpc/internal/ServerImplBuilderTest.java | 10 +++------- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/api/src/main/java/io/grpc/ConfiguratorRegistry.java b/api/src/main/java/io/grpc/ConfiguratorRegistry.java index a0a91609dde..19d6703d308 100644 --- a/api/src/main/java/io/grpc/ConfiguratorRegistry.java +++ b/api/src/main/java/io/grpc/ConfiguratorRegistry.java @@ -33,9 +33,9 @@ final class ConfiguratorRegistry { @GuardedBy("this") private boolean wasConfiguratorsSet; @GuardedBy("this") - private boolean configFrozen; - @GuardedBy("this") private List configurators = Collections.emptyList(); + @GuardedBy("this") + private int configuratorsCallCountBeforeSet = 0; ConfiguratorRegistry() {} @@ -56,11 +56,10 @@ public static synchronized ConfiguratorRegistry getDefaultRegistry() { * @throws IllegalStateException if this method is called more than once */ public synchronized void setConfigurators(List configurators) { - if (configFrozen) { + if (wasConfiguratorsSet) { throw new IllegalStateException("Configurators are already set"); } this.configurators = Collections.unmodifiableList(new ArrayList<>(configurators)); - configFrozen = true; wasConfiguratorsSet = true; } @@ -68,10 +67,20 @@ public synchronized void setConfigurators(List configura * Returns a list of the configurators in this registry. */ public synchronized List getConfigurators() { - configFrozen = true; + if (!wasConfiguratorsSet) { + configuratorsCallCountBeforeSet++; + } return configurators; } + /** + * Returns the number of times getConfigurators() was called before + * setConfigurators() was successfully invoked. + */ + public synchronized int getConfiguratorsCallCountBeforeSet() { + return configuratorsCallCountBeforeSet; + } + public synchronized boolean wasSetConfiguratorsCalled() { return wasConfiguratorsSet; } diff --git a/api/src/main/java/io/grpc/InternalConfiguratorRegistry.java b/api/src/main/java/io/grpc/InternalConfiguratorRegistry.java index b495800ff13..f567dab74c4 100644 --- a/api/src/main/java/io/grpc/InternalConfiguratorRegistry.java +++ b/api/src/main/java/io/grpc/InternalConfiguratorRegistry.java @@ -48,4 +48,8 @@ public static void configureServerBuilder(ServerBuilder serverBuilder) { public static boolean wasSetConfiguratorsCalled() { return ConfiguratorRegistry.getDefaultRegistry().wasSetConfiguratorsCalled(); } + + public static int getConfiguratorsCallCountBeforeSet() { + return ConfiguratorRegistry.getDefaultRegistry().getConfiguratorsCallCountBeforeSet(); + } } diff --git a/api/src/test/java/io/grpc/ConfiguratorRegistryTest.java b/api/src/test/java/io/grpc/ConfiguratorRegistryTest.java index e231d13503a..457d5a36e77 100644 --- a/api/src/test/java/io/grpc/ConfiguratorRegistryTest.java +++ b/api/src/test/java/io/grpc/ConfiguratorRegistryTest.java @@ -85,14 +85,12 @@ public static final class StaticTestingClassLoaderGetBeforeSet implements Runnab @Override public void run() { assertThat(ConfiguratorRegistry.getDefaultRegistry().getConfigurators()).isEmpty(); - - try { - ConfiguratorRegistry.getDefaultRegistry() - .setConfigurators(Arrays.asList(new NoopConfigurator())); - fail("should have failed for invoking set call after get is already called"); - } catch (IllegalStateException e) { - assertThat(e).hasMessageThat().isEqualTo("Configurators are already set"); - } + NoopConfigurator noopConfigurator = new NoopConfigurator(); + ConfiguratorRegistry.getDefaultRegistry() + .setConfigurators(Arrays.asList(noopConfigurator)); + assertThat(ConfiguratorRegistry.getDefaultRegistry().getConfigurators()) + .containsExactly(noopConfigurator); + assertThat(InternalConfiguratorRegistry.getConfiguratorsCallCountBeforeSet()).isEqualTo(1); } } diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java index 861412653fb..a054e65a6e8 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java @@ -529,12 +529,9 @@ public void run() { List effectiveInterceptors = builder.getEffectiveInterceptors("unused:///"); assertThat(effectiveInterceptors).hasSize(2); - try { - InternalConfiguratorRegistry.setConfigurators(Collections.emptyList()); - fail("exception expected"); - } catch (IllegalStateException e) { - assertThat(e).hasMessageThat().contains("Configurators are already set"); - } + InternalConfiguratorRegistry.setConfigurators(Collections.emptyList()); + assertThat(InternalConfiguratorRegistry.getConfigurators()).isEmpty(); + assertThat(InternalConfiguratorRegistry.getConfiguratorsCallCountBeforeSet()).isEqualTo(1); } } diff --git a/core/src/test/java/io/grpc/internal/ServerImplBuilderTest.java b/core/src/test/java/io/grpc/internal/ServerImplBuilderTest.java index 107591038d6..7ad7f15f358 100644 --- a/core/src/test/java/io/grpc/internal/ServerImplBuilderTest.java +++ b/core/src/test/java/io/grpc/internal/ServerImplBuilderTest.java @@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; import io.grpc.InternalConfigurator; import io.grpc.InternalConfiguratorRegistry; @@ -145,12 +144,9 @@ public void run() { }); assertThat(builder.getTracerFactories()).hasSize(2); assertThat(builder.interceptors).hasSize(0); - try { - InternalConfiguratorRegistry.setConfigurators(Collections.emptyList()); - fail("exception expected"); - } catch (IllegalStateException e) { - assertThat(e).hasMessageThat().contains("Configurators are already set"); - } + InternalConfiguratorRegistry.setConfigurators(Collections.emptyList()); + assertThat(InternalConfiguratorRegistry.getConfigurators()).isEmpty(); + assertThat(InternalConfiguratorRegistry.getConfiguratorsCallCountBeforeSet()).isEqualTo(1); } } From 5ca4d852ae5a50ea8d52497996af4a6b278ccd84 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 4 Apr 2025 05:16:37 -0700 Subject: [PATCH 237/591] core: Avoid Set.removeAll() when passing a possibly-large List (#11994) See #11958 --- .../main/java/io/grpc/internal/DelayedClientTransport.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java index 8ff755af3eb..eccd8fadc8c 100644 --- a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java +++ b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java @@ -325,7 +325,11 @@ final void reprocess(@Nullable SubchannelPicker picker) { if (!hasPendingStreams()) { return; } - pendingStreams.removeAll(toRemove); + // Avoid pendingStreams.removeAll() as it can degrade to calling toRemove.contains() for each + // element in pendingStreams. + for (PendingStream stream : toRemove) { + pendingStreams.remove(stream); + } // Because delayed transport is long-lived, we take this opportunity to down-size the // hashmap. if (pendingStreams.isEmpty()) { From edc2bf7346ea88d8db773535bf62a59dbea4fb20 Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Fri, 4 Apr 2025 16:09:35 +0200 Subject: [PATCH 238/591] stub: Utility method StreamObservers.nextAndComplete() that does both onNext and onComplete (#11778) --- .../java/io/grpc/stub/StreamObservers.java | 23 +++++++++--- .../io/grpc/stub/StreamObserversTest.java | 35 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 stub/src/test/java/io/grpc/stub/StreamObserversTest.java diff --git a/stub/src/main/java/io/grpc/stub/StreamObservers.java b/stub/src/main/java/io/grpc/stub/StreamObservers.java index 2cc53ea0aa2..a421d3eca2f 100644 --- a/stub/src/main/java/io/grpc/stub/StreamObservers.java +++ b/stub/src/main/java/io/grpc/stub/StreamObservers.java @@ -23,12 +23,21 @@ /** * Utility functions for working with {@link StreamObserver} and it's common subclasses like * {@link CallStreamObserver}. - * - * @deprecated Of questionable utility and generally not used. */ -@Deprecated -@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4694") public final class StreamObservers { + // Prevent instantiation + private StreamObservers() { } + + /** + * Utility method to call {@link StreamObserver#onNext(Object)} and + * {@link StreamObserver#onCompleted()} on the specified responseObserver. + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10957") + public static void nextAndComplete(StreamObserver responseObserver, T response) { + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + /** * Copy the values of an {@link Iterator} to the target {@link CallStreamObserver} while properly * accounting for outbound flow-control. After calling this method, {@code target} should no @@ -40,7 +49,10 @@ public final class StreamObservers { * * @param source of values expressed as an {@link Iterator}. * @param target {@link CallStreamObserver} which accepts values from the source. + * @deprecated Of questionable utility and generally not used. */ + @Deprecated + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4694") public static void copyWithFlowControl(final Iterator source, final CallStreamObserver target) { Preconditions.checkNotNull(source, "source"); @@ -80,7 +92,10 @@ public void run() { * * @param source of values expressed as an {@link Iterable}. * @param target {@link CallStreamObserver} which accepts values from the source. + * @deprecated Of questionable utility and generally not used. */ + @Deprecated + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4694") public static void copyWithFlowControl(final Iterable source, CallStreamObserver target) { Preconditions.checkNotNull(source, "source"); diff --git a/stub/src/test/java/io/grpc/stub/StreamObserversTest.java b/stub/src/test/java/io/grpc/stub/StreamObserversTest.java new file mode 100644 index 00000000000..86a1aba2e76 --- /dev/null +++ b/stub/src/test/java/io/grpc/stub/StreamObserversTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.stub; + +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; + +public class StreamObserversTest { + + @Test + public void nextAndComplete() { + @SuppressWarnings("unchecked") + StreamObserver observer = Mockito.mock(StreamObserver.class); + InOrder inOrder = Mockito.inOrder(observer); + StreamObservers.nextAndComplete(observer, "TEST"); + inOrder.verify(observer).onNext("TEST"); + inOrder.verify(observer).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } +} From a13fca2bf2cddd58d917865bc0bb24036c5eb873 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Fri, 4 Apr 2025 16:38:29 +0000 Subject: [PATCH 239/591] xds: ClusterResolverLoadBalancer handle update for both resolved addresses and errors via ResolutionResult (#11997) --- .../grpc/xds/ClusterResolverLoadBalancer.java | 134 +++++++++--------- .../xds/ClusterResolverLoadBalancerTest.java | 83 +++++++++-- 2 files changed, 138 insertions(+), 79 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index c92f592ebc8..0fb7cf15909 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -33,6 +33,7 @@ import io.grpc.NameResolver; import io.grpc.NameResolver.ResolutionResult; import io.grpc.Status; +import io.grpc.StatusOr; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; import io.grpc.internal.BackoffPolicy; @@ -657,79 +658,84 @@ private class NameResolverListener extends NameResolver.Listener2 { @Override public void onResult(final ResolutionResult resolutionResult) { - class NameResolved implements Runnable { - @Override - public void run() { - if (shutdown) { - return; - } - backoffPolicy = null; // reset backoff sequence if succeeded - // Arbitrary priority notation for all DNS-resolved endpoints. - String priorityName = priorityName(name, 0); // value doesn't matter - List addresses = new ArrayList<>(); - for (EquivalentAddressGroup eag : resolutionResult.getAddresses()) { - // No weight attribute is attached, all endpoint-level LB policy should be able - // to handle such it. - String localityName = localityName(LOGICAL_DNS_CLUSTER_LOCALITY); - Attributes attr = eag.getAttributes().toBuilder() - .set(XdsAttributes.ATTR_LOCALITY, LOGICAL_DNS_CLUSTER_LOCALITY) - .set(XdsAttributes.ATTR_LOCALITY_NAME, localityName) - .set(XdsAttributes.ATTR_ADDRESS_NAME, dnsHostName) - .build(); - eag = new EquivalentAddressGroup(eag.getAddresses(), attr); - eag = AddressFilter.setPathFilter(eag, Arrays.asList(priorityName, localityName)); - addresses.add(eag); - } - PriorityChildConfig priorityChildConfig = generateDnsBasedPriorityChildConfig( - name, lrsServerInfo, maxConcurrentRequests, tlsContext, filterMetadata, - lbRegistry, Collections.emptyList()); - status = Status.OK; - resolved = true; - result = new ClusterResolutionResult(addresses, priorityName, priorityChildConfig); - handleEndpointResourceUpdate(); + syncContext.execute(() -> onResult2(resolutionResult)); + } + + @Override + public Status onResult2(final ResolutionResult resolutionResult) { + if (shutdown) { + return Status.OK; + } + // Arbitrary priority notation for all DNS-resolved endpoints. + String priorityName = priorityName(name, 0); // value doesn't matter + List addresses = new ArrayList<>(); + StatusOr> addressesOrError = + resolutionResult.getAddressesOrError(); + if (addressesOrError.hasValue()) { + backoffPolicy = null; // reset backoff sequence if succeeded + for (EquivalentAddressGroup eag : resolutionResult.getAddresses()) { + // No weight attribute is attached, all endpoint-level LB policy should be able + // to handle such it. + String localityName = localityName(LOGICAL_DNS_CLUSTER_LOCALITY); + Attributes attr = eag.getAttributes().toBuilder() + .set(XdsAttributes.ATTR_LOCALITY, LOGICAL_DNS_CLUSTER_LOCALITY) + .set(XdsAttributes.ATTR_LOCALITY_NAME, localityName) + .set(XdsAttributes.ATTR_ADDRESS_NAME, dnsHostName) + .build(); + eag = new EquivalentAddressGroup(eag.getAddresses(), attr); + eag = AddressFilter.setPathFilter(eag, Arrays.asList(priorityName, localityName)); + addresses.add(eag); } + PriorityChildConfig priorityChildConfig = generateDnsBasedPriorityChildConfig( + name, lrsServerInfo, maxConcurrentRequests, tlsContext, filterMetadata, + lbRegistry, Collections.emptyList()); + status = Status.OK; + resolved = true; + result = new ClusterResolutionResult(addresses, priorityName, priorityChildConfig); + handleEndpointResourceUpdate(); + return Status.OK; + } else { + handleErrorInSyncContext(addressesOrError.getStatus()); + return addressesOrError.getStatus(); } - - syncContext.execute(new NameResolved()); } @Override public void onError(final Status error) { - syncContext.execute(new Runnable() { - @Override - public void run() { - if (shutdown) { - return; - } - status = error; - // NameResolver.Listener API cannot distinguish between address-not-found and - // transient errors. If the error occurs in the first resolution, treat it as - // address not found. Otherwise, either there is previously resolved addresses - // previously encountered error, propagate the error to downstream/upstream and - // let downstream/upstream handle it. - if (!resolved) { - resolved = true; - handleEndpointResourceUpdate(); - } else { - handleEndpointResolutionError(); - } - if (scheduledRefresh != null && scheduledRefresh.isPending()) { - return; - } - if (backoffPolicy == null) { - backoffPolicy = backoffPolicyProvider.get(); - } - long delayNanos = backoffPolicy.nextBackoffNanos(); - logger.log(XdsLogLevel.DEBUG, + syncContext.execute(() -> handleErrorInSyncContext(error)); + } + + private void handleErrorInSyncContext(final Status error) { + if (shutdown) { + return; + } + status = error; + // NameResolver.Listener API cannot distinguish between address-not-found and + // transient errors. If the error occurs in the first resolution, treat it as + // address not found. Otherwise, either there is previously resolved addresses + // previously encountered error, propagate the error to downstream/upstream and + // let downstream/upstream handle it. + if (!resolved) { + resolved = true; + handleEndpointResourceUpdate(); + } else { + handleEndpointResolutionError(); + } + if (scheduledRefresh != null && scheduledRefresh.isPending()) { + return; + } + if (backoffPolicy == null) { + backoffPolicy = backoffPolicyProvider.get(); + } + long delayNanos = backoffPolicy.nextBackoffNanos(); + logger.log(XdsLogLevel.DEBUG, "Logical DNS resolver for cluster {0} encountered name resolution " - + "error: {1}, scheduling DNS resolution backoff for {2} ns", + + "error: {1}, scheduling DNS resolution backoff for {2} ns", name, error, delayNanos); - scheduledRefresh = + scheduledRefresh = syncContext.schedule( - new DelayedNameResolverRefresh(), delayNanos, TimeUnit.NANOSECONDS, - timeService); - } - }); + new DelayedNameResolverRefresh(), delayNanos, TimeUnit.NANOSECONDS, + timeService); } } } diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index d0176d7aa38..d701f281c01 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -200,6 +200,7 @@ public XdsClient returnObject(Object object) { private ArgumentCaptor pickerCaptor; private int xdsClientRefs; private ClusterResolverLoadBalancer loadBalancer; + private NameResolverProvider fakeNameResolverProvider; @Before public void setUp() throws URISyntaxException { @@ -216,7 +217,8 @@ public void setUp() throws URISyntaxException { .setServiceConfigParser(mock(ServiceConfigParser.class)) .setChannelLogger(mock(ChannelLogger.class)) .build(); - nsRegistry.register(new FakeNameResolverProvider()); + fakeNameResolverProvider = new FakeNameResolverProvider(false); + nsRegistry.register(fakeNameResolverProvider); when(helper.getNameResolverRegistry()).thenReturn(nsRegistry); when(helper.getNameResolverArgs()).thenReturn(args); when(helper.getSynchronizationContext()).thenReturn(syncContext); @@ -826,6 +828,17 @@ public void handleEdsResource_noHealthyEndpoint() { @Test public void onlyLogicalDnsCluster_endpointsResolved() { + do_onlyLogicalDnsCluster_endpointsResolved(); + } + + @Test + public void oldListenerCallback_onlyLogicalDnsCluster_endpointsResolved() { + nsRegistry.deregister(fakeNameResolverProvider); + nsRegistry.register(new FakeNameResolverProvider(true)); + do_onlyLogicalDnsCluster_endpointsResolved(); + } + + void do_onlyLogicalDnsCluster_endpointsResolved() { ClusterResolverConfig config = new ClusterResolverConfig( Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); @@ -854,7 +867,6 @@ public void onlyLogicalDnsCluster_endpointsResolved() { .get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME); assertThat(childBalancer.addresses.get(1).getAttributes() .get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME); - } @Test @@ -874,37 +886,48 @@ public void onlyLogicalDnsCluster_handleRefreshNameResolution() { } @Test - public void onlyLogicalDnsCluster_resolutionError_backoffAndRefresh() { + public void resolutionError_backoffAndRefresh() { + do_onlyLogicalDnsCluster_resolutionError_backoffAndRefresh(); + } + + @Test + public void oldListenerCallback_resolutionError_backoffAndRefresh() { + nsRegistry.deregister(fakeNameResolverProvider); + nsRegistry.register(new FakeNameResolverProvider(true)); + do_onlyLogicalDnsCluster_resolutionError_backoffAndRefresh(); + } + + void do_onlyLogicalDnsCluster_resolutionError_backoffAndRefresh() { InOrder inOrder = Mockito.inOrder(helper, backoffPolicyProvider, - backoffPolicy1, backoffPolicy2); + backoffPolicy1, backoffPolicy2); ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin, false); + Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin, false); deliverLbConfig(config); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); Status error = Status.UNAVAILABLE.withDescription("cannot reach DNS server"); resolver.deliverError(error); inOrder.verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); + eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); assertPicker(pickerCaptor.getValue(), error, null); assertThat(resolver.refreshCount).isEqualTo(0); inOrder.verify(backoffPolicyProvider).get(); inOrder.verify(backoffPolicy1).nextBackoffNanos(); assertThat(fakeClock.getPendingTasks()).hasSize(1); assertThat(Iterables.getOnlyElement(fakeClock.getPendingTasks()).getDelay(TimeUnit.SECONDS)) - .isEqualTo(1L); + .isEqualTo(1L); fakeClock.forwardTime(1L, TimeUnit.SECONDS); assertThat(resolver.refreshCount).isEqualTo(1); error = Status.UNKNOWN.withDescription("I am lost"); resolver.deliverError(error); inOrder.verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); + eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); inOrder.verify(backoffPolicy1).nextBackoffNanos(); assertPicker(pickerCaptor.getValue(), error, null); assertThat(fakeClock.getPendingTasks()).hasSize(1); assertThat(Iterables.getOnlyElement(fakeClock.getPendingTasks()).getDelay(TimeUnit.SECONDS)) - .isEqualTo(10L); + .isEqualTo(10L); fakeClock.forwardTime(10L, TimeUnit.SECONDS); assertThat(resolver.refreshCount).isEqualTo(2); @@ -914,7 +937,7 @@ public void onlyLogicalDnsCluster_resolutionError_backoffAndRefresh() { resolver.deliverEndpointAddresses(Arrays.asList(endpoint1, endpoint2)); assertThat(childBalancers).hasSize(1); assertAddressesEqual(Arrays.asList(endpoint1, endpoint2), - Iterables.getOnlyElement(childBalancers).addresses); + Iterables.getOnlyElement(childBalancers).addresses); assertThat(fakeClock.getPendingTasks()).isEmpty(); inOrder.verifyNoMoreInteractions(); @@ -1319,10 +1342,18 @@ void deliverError(Status error) { } private class FakeNameResolverProvider extends NameResolverProvider { + private final boolean useOldListenerCallback; + + private FakeNameResolverProvider(boolean useOldListenerCallback) { + this.useOldListenerCallback = useOldListenerCallback; + } + @Override public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) { assertThat(targetUri.getScheme()).isEqualTo("dns"); - FakeNameResolver resolver = new FakeNameResolver(targetUri); + FakeNameResolver resolver = useOldListenerCallback + ? new FakeNameResolverUsingOldListenerCallback(targetUri) + : new FakeNameResolver(targetUri); resolvers.add(resolver); return resolver; } @@ -1343,9 +1374,10 @@ protected int priority() { } } + private class FakeNameResolver extends NameResolver { private final URI targetUri; - private Listener2 listener; + protected Listener2 listener; private int refreshCount; private FakeNameResolver(URI targetUri) { @@ -1372,12 +1404,33 @@ public void shutdown() { resolvers.remove(this); } - private void deliverEndpointAddresses(List addresses) { + protected void deliverEndpointAddresses(List addresses) { + syncContext.execute(() -> { + Status ret = listener.onResult2(ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromValue(addresses)).build()); + assertThat(ret.getCode()).isEqualTo(Status.Code.OK); + }); + } + + protected void deliverError(Status error) { + syncContext.execute(() -> listener.onResult2(ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromStatus(error)).build())); + } + } + + private class FakeNameResolverUsingOldListenerCallback extends FakeNameResolver { + private FakeNameResolverUsingOldListenerCallback(URI targetUri) { + super(targetUri); + } + + @Override + protected void deliverEndpointAddresses(List addresses) { listener.onResult(ResolutionResult.newBuilder() - .setAddressesOrError(StatusOr.fromValue(addresses)).build()); + .setAddressesOrError(StatusOr.fromValue(addresses)).build()); } - private void deliverError(Status error) { + @Override + protected void deliverError(Status error) { listener.onError(error); } } From aae52de3b8fee34dfc548d36d802cbb07af0c2b6 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 4 Apr 2025 10:58:52 -0700 Subject: [PATCH 240/591] stub: Add RunWith(JUnit4) to support varied environments Some JUnit environments require the RunWith annotation. Notably Blaze/Bazel needs it. --- stub/src/test/java/io/grpc/stub/StreamObserversTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stub/src/test/java/io/grpc/stub/StreamObserversTest.java b/stub/src/test/java/io/grpc/stub/StreamObserversTest.java index 86a1aba2e76..237dd2e1434 100644 --- a/stub/src/test/java/io/grpc/stub/StreamObserversTest.java +++ b/stub/src/test/java/io/grpc/stub/StreamObserversTest.java @@ -17,9 +17,12 @@ package io.grpc.stub; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; import org.mockito.InOrder; import org.mockito.Mockito; +@RunWith(JUnit4.class) public class StreamObserversTest { @Test From 54d37839a39defddf59665962e05d863a490cfc9 Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Tue, 8 Apr 2025 05:34:57 +0800 Subject: [PATCH 241/591] stub: trailersFromThrowable() metadata should be copied (#11979) If the same exception is passed to multiple RPCs, then the results will race. Fixes #11973 --- .../main/java/io/grpc/stub/ServerCalls.java | 7 +++-- .../java/io/grpc/stub/ServerCallsTest.java | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/stub/src/main/java/io/grpc/stub/ServerCalls.java b/stub/src/main/java/io/grpc/stub/ServerCalls.java index 7990a5b34c0..9f0063713cc 100644 --- a/stub/src/main/java/io/grpc/stub/ServerCalls.java +++ b/stub/src/main/java/io/grpc/stub/ServerCalls.java @@ -382,9 +382,10 @@ public void onNext(RespT response) { @Override public void onError(Throwable t) { - Metadata metadata = Status.trailersFromThrowable(t); - if (metadata == null) { - metadata = new Metadata(); + Metadata metadata = new Metadata(); + Metadata trailers = Status.trailersFromThrowable(t); + if (trailers != null) { + metadata.merge(trailers); } call.close(Status.fromThrowable(t), metadata); aborted = true; diff --git a/stub/src/test/java/io/grpc/stub/ServerCallsTest.java b/stub/src/test/java/io/grpc/stub/ServerCallsTest.java index 1e51ac10110..6f458facc5e 100644 --- a/stub/src/test/java/io/grpc/stub/ServerCallsTest.java +++ b/stub/src/test/java/io/grpc/stub/ServerCallsTest.java @@ -555,6 +555,35 @@ public void invoke(Integer req, StreamObserver responseObserver) { listener.onHalfClose(); } + @Test + public void clientSendsOne_serverOnErrorWithTrailers_serverStreaming() { + Metadata trailers = new Metadata(); + Metadata.Key key = Metadata.Key.of("trailers-test-key1", + Metadata.ASCII_STRING_MARSHALLER); + trailers.put(key, "trailers-test-value1"); + + ServerCallRecorder serverCall = new ServerCallRecorder(SERVER_STREAMING_METHOD); + ServerCallHandler callHandler = ServerCalls.asyncServerStreamingCall( + new ServerCalls.ServerStreamingMethod() { + @Override + public void invoke(Integer req, StreamObserver responseObserver) { + responseObserver.onError( + Status.fromCode(Status.Code.INTERNAL) + .asRuntimeException(trailers) + ); + } + }); + ServerCall.Listener listener = callHandler.startCall(serverCall, new Metadata()); + serverCall.isReady = true; + serverCall.isCancelled = false; + listener.onReady(); + listener.onMessage(1); + listener.onHalfClose(); + // verify trailers key is set + assertTrue(serverCall.trailers.containsKey(key)); + assertTrue(serverCall.status.equals(Status.INTERNAL)); + } + @Test public void inprocessTransportManualFlow() throws Exception { final Semaphore semaphore = new Semaphore(1); @@ -652,6 +681,7 @@ private static class ServerCallRecorder extends ServerCall { private boolean isCancelled; private boolean isReady; private int onReadyThreshold; + private Metadata trailers; public ServerCallRecorder(MethodDescriptor methodDescriptor) { this.methodDescriptor = methodDescriptor; @@ -674,6 +704,7 @@ public void sendMessage(Integer message) { @Override public void close(Status status, Metadata trailers) { this.status = status; + this.trailers = trailers; } @Override From 2db4852e231f58e8c2faccdf7feea08077a62a87 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 7 Apr 2025 14:26:14 -0700 Subject: [PATCH 242/591] core: Loop over interceptors when computing effective interceptors A post-merge review of 8516cfef9 suggested this change and the comment had been lost in my inbox. --- .../internal/ManagedChannelImplBuilder.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java index 20f7e901ddd..fc3c7891008 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java @@ -737,18 +737,16 @@ public ManagedChannel build() { // TODO(zdapeng): FIX IT @VisibleForTesting List getEffectiveInterceptors(String computedTarget) { - List effectiveInterceptors = new ArrayList<>(this.interceptors); - for (int i = 0; i < effectiveInterceptors.size(); i++) { - if (!(effectiveInterceptors.get(i) instanceof InterceptorFactoryWrapper)) { - continue; - } - InterceptorFactory factory = - ((InterceptorFactoryWrapper) effectiveInterceptors.get(i)).factory; - ClientInterceptor interceptor = factory.newInterceptor(computedTarget); - if (interceptor == null) { - throw new NullPointerException("Factory returned null interceptor: " + factory); + List effectiveInterceptors = new ArrayList<>(this.interceptors.size()); + for (ClientInterceptor interceptor : this.interceptors) { + if (interceptor instanceof InterceptorFactoryWrapper) { + InterceptorFactory factory = ((InterceptorFactoryWrapper) interceptor).factory; + interceptor = factory.newInterceptor(computedTarget); + if (interceptor == null) { + throw new NullPointerException("Factory returned null interceptor: " + factory); + } } - effectiveInterceptors.set(i, interceptor); + effectiveInterceptors.add(interceptor); } boolean disableImplicitCensus = InternalConfiguratorRegistry.wasSetConfiguratorsCalled(); From a6aec2769e0ab4a41f6aa6b0771d0614a834dac2 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 8 Apr 2025 09:20:12 -0700 Subject: [PATCH 243/591] auth: Use pre-existing private key in test Generating a KeyPair is very expensive when running with TSAN, because TSAN keeps the JVM in interpreted mode. This speeds up the test running on my desktop from .368s to .151s; faster, but nobody cares. With TSAN, the speedup is from 150-500s to 4-6s. Within Google the test was timing out because it was taking so long. While we can increase the timeout, it seems better to speed up the test in this easy way. --- .../GoogleAuthLibraryCallCredentialsTest.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/auth/src/test/java/io/grpc/auth/GoogleAuthLibraryCallCredentialsTest.java b/auth/src/test/java/io/grpc/auth/GoogleAuthLibraryCallCredentialsTest.java index 1e8c27bca25..75026fd7c18 100644 --- a/auth/src/test/java/io/grpc/auth/GoogleAuthLibraryCallCredentialsTest.java +++ b/auth/src/test/java/io/grpc/auth/GoogleAuthLibraryCallCredentialsTest.java @@ -50,10 +50,12 @@ import io.grpc.Status; import io.grpc.internal.JsonParser; import io.grpc.testing.TestMethodDescriptors; +import io.grpc.testing.TlsTesting; +import io.grpc.util.CertificateUtils; import java.io.IOException; +import java.io.InputStream; import java.net.URI; -import java.security.KeyPair; -import java.security.KeyPairGenerator; +import java.security.PrivateKey; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -342,7 +344,10 @@ public void serviceUri() throws Exception { @Test public void serviceAccountToJwt() throws Exception { - KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + PrivateKey privateKey; + try (InputStream server1Key = TlsTesting.loadCert("server1.key")) { + privateKey = CertificateUtils.getPrivateKey(server1Key); + } HttpTransportFactory factory = Mockito.mock(HttpTransportFactory.class); Mockito.when(factory.create()).thenThrow(new AssertionError()); @@ -350,7 +355,7 @@ public void serviceAccountToJwt() throws Exception { ServiceAccountCredentials credentials = ServiceAccountCredentials.newBuilder() .setClientEmail("test-email@example.com") - .setPrivateKey(pair.getPrivate()) + .setPrivateKey(privateKey) .setPrivateKeyId("test-private-key-id") .setHttpTransportFactory(factory) .build(); @@ -390,13 +395,16 @@ public void oauthClassesNotInClassPath() throws Exception { @Test public void jwtAccessCredentialsInRequestMetadata() throws Exception { - KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + PrivateKey privateKey; + try (InputStream server1Key = TlsTesting.loadCert("server1.key")) { + privateKey = CertificateUtils.getPrivateKey(server1Key); + } ServiceAccountCredentials credentials = ServiceAccountCredentials.newBuilder() .setClientId("test-client") .setClientEmail("test-email@example.com") - .setPrivateKey(pair.getPrivate()) + .setPrivateKey(privateKey) .setPrivateKeyId("test-private-key-id") .setQuotaProjectId("test-quota-project-id") .build(); From f79ab2f16f7e037da0e8c9985e917eca552c22cb Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 9 Apr 2025 09:14:44 -0700 Subject: [PATCH 244/591] api: Remove deprecated SubchannelPicker.requestConnection() It has been deprecated since cec9ee368, six years ago. It was replaced with LoadBalancer.requestConnection(). --- api/src/main/java/io/grpc/LoadBalancer.java | 12 ------------ .../java/io/grpc/internal/ManagedChannelImpl.java | 13 +------------ .../io/grpc/internal/ManagedChannelImplTest.java | 1 - xds/src/main/java/io/grpc/xds/LazyLoadBalancer.java | 5 ----- 4 files changed, 1 insertion(+), 30 deletions(-) diff --git a/api/src/main/java/io/grpc/LoadBalancer.java b/api/src/main/java/io/grpc/LoadBalancer.java index d5f44dafa5e..53ba60fdc9c 100644 --- a/api/src/main/java/io/grpc/LoadBalancer.java +++ b/api/src/main/java/io/grpc/LoadBalancer.java @@ -452,18 +452,6 @@ public abstract static class SubchannelPicker { * @since 1.3.0 */ public abstract PickResult pickSubchannel(PickSubchannelArgs args); - - /** - * Tries to establish connections now so that the upcoming RPC may then just pick a ready - * connection without having to connect first. - * - *

    No-op if unsupported. - * - * @deprecated override {@link LoadBalancer#requestConnection} instead. - * @since 1.11.0 - */ - @Deprecated - public void requestConnection() {} } /** diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 1b51c2dbb32..c5daba7bfc8 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -228,11 +228,6 @@ public void uncaughtException(Thread t, Throwable e) { @Nullable private LbHelperImpl lbHelper; - // Must ONLY be assigned from updateSubchannelPicker(), which is called from syncContext. - // null if channel is in idle mode. - @Nullable - private volatile SubchannelPicker subchannelPicker; - // Must be accessed from the syncContext private boolean panicMode; @@ -259,8 +254,7 @@ public void uncaughtException(Thread t, Throwable e) { // Channel's shutdown process: // 1. shutdown(): stop accepting new calls from applications // 1a shutdown <- true - // 1b subchannelPicker <- null - // 1c delayedTransport.shutdown() + // 1b delayedTransport.shutdown() // 2. delayedTransport terminated: stop stream-creation functionality // 2a terminating <- true // 2b loadBalancer.shutdown() @@ -393,7 +387,6 @@ private void shutdownNameResolverAndLoadBalancer(boolean channelIsActive) { lbHelper.lb.shutdown(); lbHelper = null; } - subchannelPicker = null; } /** @@ -804,7 +797,6 @@ boolean isInPanicMode() { // Called from syncContext private void updateSubchannelPicker(SubchannelPicker newPicker) { - subchannelPicker = newPicker; delayedTransport.reprocess(newPicker); } @@ -1228,9 +1220,6 @@ final class RequestConnection implements Runnable { @Override public void run() { exitIdleMode(); - if (subchannelPicker != null) { - subchannelPicker.requestConnection(); - } if (lbHelper != null) { lbHelper.lb.requestConnection(); } diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 21ccf1095df..ac845947f1c 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -2590,7 +2590,6 @@ public void getState_withRequestConnect_IdleWithLbRunning() { assertEquals(IDLE, channel.getState(true)); verify(mockLoadBalancerProvider).newLoadBalancer(any(Helper.class)); - verify(mockPicker).requestConnection(); verify(mockLoadBalancer).requestConnection(); } diff --git a/xds/src/main/java/io/grpc/xds/LazyLoadBalancer.java b/xds/src/main/java/io/grpc/xds/LazyLoadBalancer.java index 87f1b72ca47..8ba1cb28c62 100644 --- a/xds/src/main/java/io/grpc/xds/LazyLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/LazyLoadBalancer.java @@ -107,11 +107,6 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { helper.getSynchronizationContext().execute(LazyDelegate.this::activate); return PickResult.withNoResult(); } - - @Override - public void requestConnection() { - helper.getSynchronizationContext().execute(LazyDelegate.this::requestConnection); - } } } From 65d0bb8a4d9ee111f1eeb02c43321bbb99143151 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 11 Apr 2025 08:25:21 -0700 Subject: [PATCH 245/591] xds: Enable deprecation warnings The security code referenced fields removed from gRFC A29 before it was finalized. Note that this fixes a bug in CommonTlsContextUtil where CombinedValidationContext was not checked. I believe this was the only location with such a bug as I audited all non-test usages of has/getValidationContext() and confirmed they have have a corresponding has/getCombinedValidationContext(). --- xds/build.gradle | 2 - .../grpc/xds/ClusterResolverLoadBalancer.java | 2 +- xds/src/main/java/io/grpc/xds/RbacFilter.java | 9 +- .../java/io/grpc/xds/XdsClusterResource.java | 23 +-- .../grpc/xds/XdsRouteConfigureResource.java | 3 +- .../io/grpc/xds/internal/MatcherParser.java | 17 +- .../security/CommonTlsContextUtil.java | 21 +-- .../CertProviderSslContextProvider.java | 11 -- .../security/trust/XdsX509TrustManager.java | 1 + ...rChainMatchingProtocolNegotiatorsTest.java | 3 +- .../grpc/xds/GrpcXdsClientImplDataTest.java | 146 ++++++------------ .../grpc/xds/GrpcXdsClientImplTestBase.java | 9 +- .../io/grpc/xds/GrpcXdsClientImplV3Test.java | 12 +- .../ClientSslContextProviderFactoryTest.java | 6 +- .../security/CommonTlsContextTestsUtil.java | 101 +++++------- .../CertificateProviderStoreTest.java | 3 - 16 files changed, 116 insertions(+), 253 deletions(-) diff --git a/xds/build.gradle b/xds/build.gradle index cdd4924cab3..90ba3709d14 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -133,8 +133,6 @@ tasks.named("checkstyleThirdparty").configure { tasks.named("compileJava").configure { it.options.compilerArgs += [ - // TODO: remove - "-Xlint:-deprecation", // only has AutoValue annotation processor "-Xlint:-processing", ] diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index 0fb7cf15909..080760303bf 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -673,7 +673,7 @@ public Status onResult2(final ResolutionResult resolutionResult) { resolutionResult.getAddressesOrError(); if (addressesOrError.hasValue()) { backoffPolicy = null; // reset backoff sequence if succeeded - for (EquivalentAddressGroup eag : resolutionResult.getAddresses()) { + for (EquivalentAddressGroup eag : addressesOrError.getValue()) { // No weight attribute is attached, all endpoint-level LB policy should be able // to handle such it. String localityName = localityName(LOGICAL_DNS_CLUSTER_LOCALITY); diff --git a/xds/src/main/java/io/grpc/xds/RbacFilter.java b/xds/src/main/java/io/grpc/xds/RbacFilter.java index d91884735e9..91df1e68802 100644 --- a/xds/src/main/java/io/grpc/xds/RbacFilter.java +++ b/xds/src/main/java/io/grpc/xds/RbacFilter.java @@ -276,8 +276,13 @@ private static Matcher parsePrincipal(Principal principal) { return createSourceIpMatcher(principal.getDirectRemoteIp()); case REMOTE_IP: return createSourceIpMatcher(principal.getRemoteIp()); - case SOURCE_IP: - return createSourceIpMatcher(principal.getSourceIp()); + case SOURCE_IP: { + // gRFC A41 has identical handling of source_ip as remote_ip and direct_remote_ip and + // pre-dates the deprecation. + @SuppressWarnings("deprecation") + CidrRange sourceIp = principal.getSourceIp(); + return createSourceIpMatcher(sourceIp); + } case HEADER: return parseHeaderMatcher(principal.getHeader()); case NOT_ID: diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index cfc74f3ca70..0d9274e2869 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -450,15 +450,6 @@ static void validateCommonTlsContext( throw new ResourceInvalidException( "common-tls-context with validation_context_sds_secret_config is not supported"); } - if (commonTlsContext.hasValidationContextCertificateProvider()) { - throw new ResourceInvalidException( - "common-tls-context with validation_context_certificate_provider is not supported"); - } - if (commonTlsContext.hasValidationContextCertificateProviderInstance()) { - throw new ResourceInvalidException( - "common-tls-context with validation_context_certificate_provider_instance is not" - + " supported"); - } String certInstanceName = getIdentityCertInstanceName(commonTlsContext); if (certInstanceName == null) { if (server) { @@ -473,10 +464,6 @@ static void validateCommonTlsContext( throw new ResourceInvalidException( "tls_certificate_provider_instance is unset"); } - if (commonTlsContext.hasTlsCertificateCertificateProvider()) { - throw new ResourceInvalidException( - "tls_certificate_provider_instance is unset"); - } } else if (certProviderInstances == null || !certProviderInstances.contains(certInstanceName)) { throw new ResourceInvalidException( "CertificateProvider instance name '" + certInstanceName @@ -505,7 +492,9 @@ static void validateCommonTlsContext( .getDefaultValidationContext(); } if (certificateValidationContext != null) { - if (certificateValidationContext.getMatchSubjectAltNamesCount() > 0 && server) { + @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names + int matchSubjectAltNamesCount = certificateValidationContext.getMatchSubjectAltNamesCount(); + if (matchSubjectAltNamesCount > 0 && server) { throw new ResourceInvalidException( "match_subject_alt_names only allowed in upstream_tls_context"); } @@ -536,8 +525,6 @@ static void validateCommonTlsContext( private static String getIdentityCertInstanceName(CommonTlsContext commonTlsContext) { if (commonTlsContext.hasTlsCertificateProviderInstance()) { return commonTlsContext.getTlsCertificateProviderInstance().getInstanceName(); - } else if (commonTlsContext.hasTlsCertificateCertificateProviderInstance()) { - return commonTlsContext.getTlsCertificateCertificateProviderInstance().getInstanceName(); } return null; } @@ -556,10 +543,6 @@ private static String getRootCertInstanceName(CommonTlsContext commonTlsContext) .hasCaCertificateProviderInstance()) { return combinedCertificateValidationContext.getDefaultValidationContext() .getCaCertificateProviderInstance().getInstanceName(); - } else if (combinedCertificateValidationContext - .hasValidationContextCertificateProviderInstance()) { - return combinedCertificateValidationContext - .getValidationContextCertificateProviderInstance().getInstanceName(); } } return null; diff --git a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java index 80a77cbb1d4..2ee326435c4 100644 --- a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java @@ -451,8 +451,7 @@ static StructOrError parseRouteAction( config.getHeader(); Pattern regEx = null; String regExSubstitute = null; - if (headerCfg.hasRegexRewrite() && headerCfg.getRegexRewrite().hasPattern() - && headerCfg.getRegexRewrite().getPattern().hasGoogleRe2()) { + if (headerCfg.hasRegexRewrite() && headerCfg.getRegexRewrite().hasPattern()) { regEx = Pattern.compile(headerCfg.getRegexRewrite().getPattern().getRegex()); regExSubstitute = headerCfg.getRegexRewrite().getSubstitution(); } diff --git a/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java b/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java index 39b80bbcc03..fb291efc461 100644 --- a/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java +++ b/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java @@ -26,9 +26,12 @@ public static Matchers.HeaderMatcher parseHeaderMatcher( io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto) { switch (proto.getHeaderMatchSpecifierCase()) { case EXACT_MATCH: + @SuppressWarnings("deprecation") // gRFC A63: support indefinitely + String exactMatch = proto.getExactMatch(); return Matchers.HeaderMatcher.forExactValue( - proto.getName(), proto.getExactMatch(), proto.getInvertMatch()); + proto.getName(), exactMatch, proto.getInvertMatch()); case SAFE_REGEX_MATCH: + @SuppressWarnings("deprecation") // gRFC A63: support indefinitely String rawPattern = proto.getSafeRegexMatch().getRegex(); Pattern safeRegExMatch; try { @@ -49,14 +52,20 @@ public static Matchers.HeaderMatcher parseHeaderMatcher( return Matchers.HeaderMatcher.forPresent( proto.getName(), proto.getPresentMatch(), proto.getInvertMatch()); case PREFIX_MATCH: + @SuppressWarnings("deprecation") // gRFC A63: support indefinitely + String prefixMatch = proto.getPrefixMatch(); return Matchers.HeaderMatcher.forPrefix( - proto.getName(), proto.getPrefixMatch(), proto.getInvertMatch()); + proto.getName(), prefixMatch, proto.getInvertMatch()); case SUFFIX_MATCH: + @SuppressWarnings("deprecation") // gRFC A63: support indefinitely + String suffixMatch = proto.getSuffixMatch(); return Matchers.HeaderMatcher.forSuffix( - proto.getName(), proto.getSuffixMatch(), proto.getInvertMatch()); + proto.getName(), suffixMatch, proto.getInvertMatch()); case CONTAINS_MATCH: + @SuppressWarnings("deprecation") // gRFC A63: support indefinitely + String containsMatch = proto.getContainsMatch(); return Matchers.HeaderMatcher.forContains( - proto.getName(), proto.getContainsMatch(), proto.getInvertMatch()); + proto.getName(), containsMatch, proto.getInvertMatch()); case STRING_MATCH: return Matchers.HeaderMatcher.forString( proto.getName(), parseStringMatcher(proto.getStringMatch()), proto.getInvertMatch()); diff --git a/xds/src/main/java/io/grpc/xds/internal/security/CommonTlsContextUtil.java b/xds/src/main/java/io/grpc/xds/internal/security/CommonTlsContextUtil.java index e5a8c115361..50fa18b64f9 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/CommonTlsContextUtil.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/CommonTlsContextUtil.java @@ -18,7 +18,6 @@ import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateProviderPluginInstance; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; -import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext.CombinedCertificateValidationContext; /** Class for utility functions for {@link CommonTlsContext}. */ public final class CommonTlsContextUtil { @@ -29,22 +28,8 @@ public static boolean hasCertProviderInstance(CommonTlsContext commonTlsContext) if (commonTlsContext == null) { return false; } - return hasIdentityCertificateProviderInstance(commonTlsContext) - || hasCertProviderValidationContext(commonTlsContext); - } - - private static boolean hasCertProviderValidationContext(CommonTlsContext commonTlsContext) { - if (commonTlsContext.hasCombinedValidationContext()) { - CombinedCertificateValidationContext combinedCertificateValidationContext = - commonTlsContext.getCombinedValidationContext(); - return combinedCertificateValidationContext.hasValidationContextCertificateProviderInstance(); - } - return hasValidationProviderInstance(commonTlsContext); - } - - private static boolean hasIdentityCertificateProviderInstance(CommonTlsContext commonTlsContext) { return commonTlsContext.hasTlsCertificateProviderInstance() - || commonTlsContext.hasTlsCertificateCertificateProviderInstance(); + || hasValidationProviderInstance(commonTlsContext); } private static boolean hasValidationProviderInstance(CommonTlsContext commonTlsContext) { @@ -52,7 +37,9 @@ private static boolean hasValidationProviderInstance(CommonTlsContext commonTlsC .hasCaCertificateProviderInstance()) { return true; } - return commonTlsContext.hasValidationContextCertificateProviderInstance(); + return commonTlsContext.hasCombinedValidationContext() + && commonTlsContext.getCombinedValidationContext().getDefaultValidationContext() + .hasCaCertificateProviderInstance(); } /** diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java index f9cd14f2efd..801dabeecb7 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java @@ -99,8 +99,6 @@ protected static CertificateProviderInstance getCertProviderInstance( CommonTlsContext commonTlsContext) { if (commonTlsContext.hasTlsCertificateProviderInstance()) { return CommonTlsContextUtil.convert(commonTlsContext.getTlsCertificateProviderInstance()); - } else if (commonTlsContext.hasTlsCertificateCertificateProviderInstance()) { - return commonTlsContext.getTlsCertificateCertificateProviderInstance(); } return null; } @@ -128,15 +126,6 @@ protected static CommonTlsContext.CertificateProviderInstance getRootCertProvide if (certValidationContext != null && certValidationContext.hasCaCertificateProviderInstance()) { return CommonTlsContextUtil.convert(certValidationContext.getCaCertificateProviderInstance()); } - if (commonTlsContext.hasCombinedValidationContext()) { - CommonTlsContext.CombinedCertificateValidationContext combinedValidationContext = - commonTlsContext.getCombinedValidationContext(); - if (combinedValidationContext.hasValidationContextCertificateProviderInstance()) { - return combinedValidationContext.getValidationContextCertificateProviderInstance(); - } - } else if (commonTlsContext.hasValidationContextCertificateProviderInstance()) { - return commonTlsContext.getValidationContextCertificateProviderInstance(); - } return null; } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java b/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java index dcd3f2006a1..1ecfe378d29 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java @@ -207,6 +207,7 @@ void verifySubjectAltNameInChain(X509Certificate[] peerCertChain) throws Certifi if (certContext == null) { return; } + @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names List verifyList = certContext.getMatchSubjectAltNamesList(); if (verifyList.isEmpty()) { return; diff --git a/xds/src/test/java/io/grpc/xds/FilterChainMatchingProtocolNegotiatorsTest.java b/xds/src/test/java/io/grpc/xds/FilterChainMatchingProtocolNegotiatorsTest.java index c3d006a6003..722f915dbea 100644 --- a/xds/src/test/java/io/grpc/xds/FilterChainMatchingProtocolNegotiatorsTest.java +++ b/xds/src/test/java/io/grpc/xds/FilterChainMatchingProtocolNegotiatorsTest.java @@ -1125,7 +1125,6 @@ public void filterChain_5stepMatch() throws Exception { } @Test - @SuppressWarnings("deprecation") public void filterChainMatch_unsupportedMatchers() throws Exception { EnvoyServerProtoData.DownstreamTlsContext tlsContext1 = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "ROOTCA"); @@ -1194,7 +1193,7 @@ public void filterChainMatch_unsupportedMatchers() throws Exception { assertThat(sslSet.get()).isEqualTo(defaultFilterChain.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext().getCommonTlsContext() - .getTlsCertificateCertificateProviderInstance() + .getTlsCertificateProviderInstance() .getCertificateName()).isEqualTo("CERT3"); } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 0ea58c974bb..e5502463db0 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -98,7 +98,6 @@ import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateProviderPluginInstance; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; -import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext.CertificateProviderInstance; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext.CombinedCertificateValidationContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.SdsSecretConfig; @@ -3042,35 +3041,6 @@ public void validateCommonTlsContext_validationContextSdsSecretConfig() XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); } - @Test - @SuppressWarnings("deprecation") - public void validateCommonTlsContext_validationContextCertificateProvider() - throws ResourceInvalidException { - CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() - .setValidationContextCertificateProvider( - CommonTlsContext.CertificateProvider.getDefaultInstance()) - .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( - "common-tls-context with validation_context_certificate_provider is not supported"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); - } - - @Test - @SuppressWarnings("deprecation") - public void validateCommonTlsContext_validationContextCertificateProviderInstance() - throws ResourceInvalidException { - CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() - .setValidationContextCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) - .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( - "common-tls-context with validation_context_certificate_provider_instance is not " - + "supported"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); - } - @Test public void validateCommonTlsContext_tlsCertificateProviderInstance_isRequiredForServer() throws ResourceInvalidException { @@ -3083,36 +3053,33 @@ public void validateCommonTlsContext_tlsCertificateProviderInstance_isRequiredFo } @Test - @SuppressWarnings("deprecation") public void validateCommonTlsContext_tlsNewCertificateProviderInstance() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setTlsCertificateProviderInstance( - CertificateProviderPluginInstance.newBuilder().setInstanceName("name1").build()) + CertificateProviderPluginInstance.newBuilder().setInstanceName("name1")) .build(); XdsClusterResource .validateCommonTlsContext(commonTlsContext, ImmutableSet.of("name1", "name2"), true); } @Test - @SuppressWarnings("deprecation") public void validateCommonTlsContext_tlsCertificateProviderInstance() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() - .setTlsCertificateCertificateProviderInstance( - CertificateProviderInstance.newBuilder().setInstanceName("name1").build()) + .setTlsCertificateProviderInstance( + CertificateProviderPluginInstance.newBuilder().setInstanceName("name1")) .build(); XdsClusterResource .validateCommonTlsContext(commonTlsContext, ImmutableSet.of("name1", "name2"), true); } @Test - @SuppressWarnings("deprecation") public void validateCommonTlsContext_tlsCertificateProviderInstance_absentInBootstrapFile() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() - .setTlsCertificateCertificateProviderInstance( - CertificateProviderInstance.newBuilder().setInstanceName("bad-name").build()) + .setTlsCertificateProviderInstance( + CertificateProviderPluginInstance.newBuilder().setInstanceName("bad-name")) .build(); thrown.expect(ResourceInvalidException.class); thrown.expectMessage( @@ -3122,15 +3089,14 @@ public void validateCommonTlsContext_tlsCertificateProviderInstance_absentInBoot } @Test - @SuppressWarnings("deprecation") public void validateCommonTlsContext_validationContextProviderInstance() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setCombinedValidationContext( CommonTlsContext.CombinedCertificateValidationContext.newBuilder() - .setValidationContextCertificateProviderInstance( - CertificateProviderInstance.newBuilder().setInstanceName("name1").build()) - .build()) + .setDefaultValidationContext(CertificateValidationContext.newBuilder() + .setCaCertificateProviderInstance(CertificateProviderPluginInstance.newBuilder() + .setInstanceName("name1")))) .build(); XdsClusterResource .validateCommonTlsContext(commonTlsContext, ImmutableSet.of("name1", "name2"), false); @@ -3218,15 +3184,14 @@ public void validateCommonTlsContext_validationContextSystemRootCerts() } @Test - @SuppressWarnings("deprecation") public void validateCommonTlsContext_validationContextProviderInstance_absentInBootstrapFile() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setCombinedValidationContext( CommonTlsContext.CombinedCertificateValidationContext.newBuilder() - .setValidationContextCertificateProviderInstance( - CertificateProviderInstance.newBuilder().setInstanceName("bad-name").build()) - .build()) + .setDefaultValidationContext(CertificateValidationContext.newBuilder() + .setCaCertificateProviderInstance(CertificateProviderPluginInstance.newBuilder() + .setInstanceName("bad-name")))) .build(); thrown.expect(ResourceInvalidException.class); thrown.expectMessage( @@ -3258,20 +3223,6 @@ public void validateCommonTlsContext_tlsCertificateSdsSecretConfigsCount() XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); } - @Test - @SuppressWarnings("deprecation") - public void validateCommonTlsContext_tlsCertificateCertificateProvider() - throws ResourceInvalidException { - CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() - .setTlsCertificateCertificateProvider( - CommonTlsContext.CertificateProvider.getDefaultInstance()) - .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( - "tls_certificate_provider_instance is unset"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); - } - @Test public void validateCommonTlsContext_combinedValidationContext_isRequiredForClient() throws ResourceInvalidException { @@ -3304,13 +3255,13 @@ public void validateCommonTlsContext_combinedValContextWithDefaultValContextForS CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setCombinedValidationContext( CombinedCertificateValidationContext.newBuilder() - .setValidationContextCertificateProviderInstance( - CertificateProviderInstance.getDefaultInstance()) .setDefaultValidationContext(CertificateValidationContext.newBuilder() + .setCaCertificateProviderInstance( + CertificateProviderPluginInstance.getDefaultInstance()) .addMatchSubjectAltNames(StringMatcher.newBuilder().setExact("foo.com").build()) .build())) - .setTlsCertificateCertificateProviderInstance( - CertificateProviderInstance.getDefaultInstance()) + .setTlsCertificateProviderInstance( + CertificateProviderPluginInstance.getDefaultInstance()) .build(); thrown.expect(ResourceInvalidException.class); thrown.expectMessage("match_subject_alt_names only allowed in upstream_tls_context"); @@ -3318,18 +3269,16 @@ public void validateCommonTlsContext_combinedValContextWithDefaultValContextForS } @Test - @SuppressWarnings("deprecation") public void validateCommonTlsContext_combinedValContextWithDefaultValContextVerifyCertSpki() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setCombinedValidationContext( CommonTlsContext.CombinedCertificateValidationContext.newBuilder() - .setValidationContextCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) - .setDefaultValidationContext( - CertificateValidationContext.newBuilder().addVerifyCertificateSpki("foo"))) - .setTlsCertificateCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) + .setDefaultValidationContext(CertificateValidationContext.newBuilder() + .setCaCertificateProviderInstance( + CertificateProviderPluginInstance.getDefaultInstance()) + .addVerifyCertificateSpki("foo"))) + .setTlsCertificateProviderInstance(CertificateProviderPluginInstance.getDefaultInstance()) .build(); thrown.expect(ResourceInvalidException.class); thrown.expectMessage("verify_certificate_spki in default_validation_context is not " @@ -3338,18 +3287,16 @@ public void validateCommonTlsContext_combinedValContextWithDefaultValContextVeri } @Test - @SuppressWarnings("deprecation") public void validateCommonTlsContext_combinedValContextWithDefaultValContextVerifyCertHash() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setCombinedValidationContext( CommonTlsContext.CombinedCertificateValidationContext.newBuilder() - .setValidationContextCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) - .setDefaultValidationContext( - CertificateValidationContext.newBuilder().addVerifyCertificateHash("foo"))) - .setTlsCertificateCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) + .setDefaultValidationContext(CertificateValidationContext.newBuilder() + .setCaCertificateProviderInstance( + CertificateProviderPluginInstance.getDefaultInstance()) + .addVerifyCertificateHash("foo"))) + .setTlsCertificateProviderInstance(CertificateProviderPluginInstance.getDefaultInstance()) .build(); thrown.expect(ResourceInvalidException.class); thrown.expectMessage("verify_certificate_hash in default_validation_context is not " @@ -3358,18 +3305,17 @@ public void validateCommonTlsContext_combinedValContextWithDefaultValContextVeri } @Test - @SuppressWarnings("deprecation") public void validateCommonTlsContext_combinedValContextDfltValContextRequireSignedCertTimestamp() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setCombinedValidationContext( CommonTlsContext.CombinedCertificateValidationContext.newBuilder() - .setValidationContextCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) .setDefaultValidationContext(CertificateValidationContext.newBuilder() + .setCaCertificateProviderInstance( + CertificateProviderPluginInstance.getDefaultInstance()) .setRequireSignedCertificateTimestamp(BoolValue.of(true)))) - .setTlsCertificateCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) + .setTlsCertificateProviderInstance( + CertificateProviderPluginInstance.getDefaultInstance()) .build(); thrown.expect(ResourceInvalidException.class); thrown.expectMessage( @@ -3379,18 +3325,16 @@ public void validateCommonTlsContext_combinedValContextDfltValContextRequireSign } @Test - @SuppressWarnings("deprecation") public void validateCommonTlsContext_combinedValidationContextWithDefaultValidationContextCrl() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setCombinedValidationContext( CommonTlsContext.CombinedCertificateValidationContext.newBuilder() - .setValidationContextCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) .setDefaultValidationContext(CertificateValidationContext.newBuilder() + .setCaCertificateProviderInstance( + CertificateProviderPluginInstance.getDefaultInstance()) .setCrl(DataSource.getDefaultInstance()))) - .setTlsCertificateCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) + .setTlsCertificateProviderInstance(CertificateProviderPluginInstance.getDefaultInstance()) .build(); thrown.expect(ResourceInvalidException.class); thrown.expectMessage("crl in default_validation_context is not supported"); @@ -3398,18 +3342,16 @@ public void validateCommonTlsContext_combinedValidationContextWithDefaultValidat } @Test - @SuppressWarnings("deprecation") public void validateCommonTlsContext_combinedValContextWithDfltValContextCustomValidatorConfig() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setCombinedValidationContext( CommonTlsContext.CombinedCertificateValidationContext.newBuilder() - .setValidationContextCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) .setDefaultValidationContext(CertificateValidationContext.newBuilder() + .setCaCertificateProviderInstance( + CertificateProviderPluginInstance.getDefaultInstance()) .setCustomValidatorConfig(TypedExtensionConfig.getDefaultInstance()))) - .setTlsCertificateCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) + .setTlsCertificateProviderInstance(CertificateProviderPluginInstance.getDefaultInstance()) .build(); thrown.expect(ResourceInvalidException.class); thrown.expectMessage("custom_validator_config in default_validation_context is not " @@ -3426,15 +3368,14 @@ public void validateDownstreamTlsContext_noCommonTlsContext() throws ResourceInv } @Test - @SuppressWarnings("deprecation") public void validateDownstreamTlsContext_hasRequireSni() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setCombinedValidationContext( CommonTlsContext.CombinedCertificateValidationContext.newBuilder() - .setValidationContextCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance())) - .setTlsCertificateCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) + .setDefaultValidationContext(CertificateValidationContext.newBuilder() + .setCaCertificateProviderInstance( + CertificateProviderPluginInstance.getDefaultInstance()))) + .setTlsCertificateProviderInstance(CertificateProviderPluginInstance.getDefaultInstance()) .build(); DownstreamTlsContext downstreamTlsContext = DownstreamTlsContext.newBuilder() .setCommonTlsContext(commonTlsContext) @@ -3446,15 +3387,14 @@ public void validateDownstreamTlsContext_hasRequireSni() throws ResourceInvalidE } @Test - @SuppressWarnings("deprecation") public void validateDownstreamTlsContext_hasOcspStaplePolicy() throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setCombinedValidationContext( CommonTlsContext.CombinedCertificateValidationContext.newBuilder() - .setValidationContextCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance())) - .setTlsCertificateCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.getDefaultInstance()) + .setDefaultValidationContext(CertificateValidationContext.newBuilder() + .setCaCertificateProviderInstance( + CertificateProviderPluginInstance.getDefaultInstance()))) + .setTlsCertificateProviderInstance(CertificateProviderPluginInstance.getDefaultInstance()) .build(); DownstreamTlsContext downstreamTlsContext = DownstreamTlsContext.newBuilder() .setCommonTlsContext(commonTlsContext) diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 36131464d08..369763a21b7 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -47,7 +47,6 @@ import io.envoyproxy.envoy.config.route.v3.WeightedCluster; import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateProviderPluginInstance; -import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; import io.grpc.BindableService; import io.grpc.ChannelCredentials; import io.grpc.Context; @@ -2245,7 +2244,6 @@ public void cdsResponseWithCircuitBreakers() { * CDS response containing UpstreamTlsContext for a cluster. */ @Test - @SuppressWarnings("deprecation") public void cdsResponseWithUpstreamTlsContext() { DiscoveryRpcCall call = startResourceWatcher(XdsClusterResource.getInstance(), CDS_RESOURCE, cdsResourceWatcher); @@ -2269,9 +2267,9 @@ public void cdsResponseWithUpstreamTlsContext() { verify(cdsResourceWatcher, times(1)) .onChanged(cdsUpdateCaptor.capture()); CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); - CommonTlsContext.CertificateProviderInstance certificateProviderInstance = + CertificateProviderPluginInstance certificateProviderInstance = cdsUpdate.upstreamTlsContext().getCommonTlsContext().getCombinedValidationContext() - .getValidationContextCertificateProviderInstance(); + .getDefaultValidationContext().getCaCertificateProviderInstance(); assertThat(certificateProviderInstance.getInstanceName()).isEqualTo("cert-instance-name"); assertThat(certificateProviderInstance.getCertificateName()).isEqualTo("cert1"); verifyResourceMetadataAcked(CDS, CDS_RESOURCE, clusterEds, VERSION_1, TIME_INCREMENT); @@ -2282,7 +2280,6 @@ public void cdsResponseWithUpstreamTlsContext() { * CDS response containing new UpstreamTlsContext for a cluster. */ @Test - @SuppressWarnings("deprecation") public void cdsResponseWithNewUpstreamTlsContext() { DiscoveryRpcCall call = startResourceWatcher(XdsClusterResource.getInstance(), CDS_RESOURCE, cdsResourceWatcher); @@ -2344,7 +2341,6 @@ public void cdsResponseErrorHandling_badUpstreamTlsContext() { * CDS response containing OutlierDetection for a cluster. */ @Test - @SuppressWarnings("deprecation") public void cdsResponseWithOutlierDetection() { DiscoveryRpcCall call = startResourceWatcher(XdsClusterResource.getInstance(), CDS_RESOURCE, cdsResourceWatcher); @@ -2413,7 +2409,6 @@ public void cdsResponseWithOutlierDetection() { * CDS response containing OutlierDetection for a cluster. */ @Test - @SuppressWarnings("deprecation") public void cdsResponseWithInvalidOutlierDetectionNacks() { DiscoveryRpcCall call = startResourceWatcher(XdsClusterResource.getInstance(), CDS_RESOURCE, diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplV3Test.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplV3Test.java index af34c7232d0..3966fae7f20 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplV3Test.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplV3Test.java @@ -613,18 +613,15 @@ protected Message buildLeastRequestLbConfig(int choiceCount) { } @Override - @SuppressWarnings("deprecation") protected Message buildUpstreamTlsContext(String instanceName, String certName) { CommonTlsContext.Builder commonTlsContextBuilder = CommonTlsContext.newBuilder(); if (instanceName != null && certName != null) { - CommonTlsContext.CertificateProviderInstance providerInstance = - CommonTlsContext.CertificateProviderInstance.newBuilder() - .setInstanceName(instanceName) - .setCertificateName(certName) - .build(); CommonTlsContext.CombinedCertificateValidationContext combined = CommonTlsContext.CombinedCertificateValidationContext.newBuilder() - .setValidationContextCertificateProviderInstance(providerInstance) + .setDefaultValidationContext(CertificateValidationContext.newBuilder() + .setCaCertificateProviderInstance(CertificateProviderPluginInstance.newBuilder() + .setInstanceName(instanceName) + .setCertificateName(certName))) .build(); commonTlsContextBuilder.setCombinedValidationContext(combined); } @@ -751,7 +748,6 @@ protected Message buildDropOverload(String category, int dropPerMillion) { .build(); } - @SuppressWarnings("deprecation") @Override protected FilterChain buildFilterChain( List alpn, Message tlsContext, String transportSocketName, diff --git a/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java index 9c8340123da..397fe01e0f5 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java @@ -317,7 +317,6 @@ static void verifyWatcher( .isSameInstanceAs(sslContextProvider); } - @SuppressWarnings("deprecation") static CommonTlsContext.Builder addFilenames( CommonTlsContext.Builder builder, String certChain, String privateKey, String trustCa) { TlsCertificate tlsCert = @@ -329,13 +328,10 @@ static CommonTlsContext.Builder addFilenames( CertificateValidationContext.newBuilder() .setTrustedCa(DataSource.newBuilder().setFilename(trustCa)) .build(); - CommonTlsContext.CertificateProviderInstance certificateProviderInstance = - builder.getValidationContextCertificateProviderInstance(); CommonTlsContext.CombinedCertificateValidationContext.Builder combinedBuilder = CommonTlsContext.CombinedCertificateValidationContext.newBuilder(); combinedBuilder - .setDefaultValidationContext(certContext) - .setValidationContextCertificateProviderInstance(certificateProviderInstance); + .setDefaultValidationContext(certContext); return builder .addTlsCertificates(tlsCert) .setCombinedValidationContext(combinedBuilder.build()); diff --git a/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java b/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java index a6cf2c52a15..48814dece1d 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java @@ -23,7 +23,6 @@ import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateProviderPluginInstance; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; -import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext.CertificateProviderInstance; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext.CombinedCertificateValidationContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext; @@ -63,48 +62,26 @@ public class CommonTlsContextTestsUtil { public static final String BAD_CLIENT_KEY_FILE = "badclient.key"; /** takes additional values and creates CombinedCertificateValidationContext as needed. */ - @SuppressWarnings("deprecation") - static CommonTlsContext buildCommonTlsContextWithAdditionalValues( + private static CommonTlsContext buildCommonTlsContextWithAdditionalValues( String certInstanceName, String certName, String validationContextCertInstanceName, String validationContextCertName, Iterable matchSubjectAltNames, Iterable alpnNames) { - - CommonTlsContext.Builder builder = CommonTlsContext.newBuilder(); - - CertificateProviderInstance certificateProviderInstance = CertificateProviderInstance - .newBuilder().setInstanceName(certInstanceName).setCertificateName(certName).build(); - if (certificateProviderInstance != null) { - builder.setTlsCertificateCertificateProviderInstance(certificateProviderInstance); - } - CertificateProviderInstance validationCertificateProviderInstance = - CertificateProviderInstance.newBuilder().setInstanceName(validationContextCertInstanceName) - .setCertificateName(validationContextCertName).build(); - CertificateValidationContext certValidationContext = - matchSubjectAltNames == null - ? null - : CertificateValidationContext.newBuilder() - .addAllMatchSubjectAltNames(matchSubjectAltNames) - .build(); - if (validationCertificateProviderInstance != null) { - CombinedCertificateValidationContext.Builder combinedBuilder = - CombinedCertificateValidationContext.newBuilder() - .setValidationContextCertificateProviderInstance( - validationCertificateProviderInstance); - if (certValidationContext != null) { - combinedBuilder = combinedBuilder.setDefaultValidationContext(certValidationContext); - } - builder.setCombinedValidationContext(combinedBuilder); - } else if (validationCertificateProviderInstance != null) { - builder - .setValidationContextCertificateProviderInstance(validationCertificateProviderInstance); - } else if (certValidationContext != null) { - builder.setValidationContext(certValidationContext); - } - if (alpnNames != null) { - builder.addAllAlpnProtocols(alpnNames); - } - return builder.build(); + @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names + CertificateValidationContext.Builder certificateValidationContextBuilder + = CertificateValidationContext.newBuilder() + .addAllMatchSubjectAltNames(matchSubjectAltNames); + return CommonTlsContext.newBuilder() + .setTlsCertificateProviderInstance(CertificateProviderPluginInstance.newBuilder() + .setInstanceName(certInstanceName) + .setCertificateName(certName)) + .setCombinedValidationContext(CombinedCertificateValidationContext.newBuilder() + .setDefaultValidationContext(certificateValidationContextBuilder + .setCaCertificateProviderInstance(CertificateProviderPluginInstance.newBuilder() + .setInstanceName(validationContextCertInstanceName) + .setCertificateName(validationContextCertName)))) + .addAllAlpnProtocols(alpnNames) + .build(); } /** Helper method to build DownstreamTlsContext for multiple test classes. */ @@ -152,7 +129,7 @@ public static DownstreamTlsContext buildTestDownstreamTlsContext( useSans ? Arrays.asList( StringMatcher.newBuilder() .setExact("spiffe://grpc-sds-testing.svc.id.goog/ns/default/sa/bob") - .build()) : null, + .build()) : Arrays.asList(), Arrays.asList("managed-tls")); } return buildDownstreamTlsContext(commonTlsContext, /* requireClientCert= */ false); @@ -199,7 +176,6 @@ public static X509Certificate getCertFromResourceName(String resourceName) } } - @SuppressWarnings("deprecation") private static CommonTlsContext buildCommonTlsContextForCertProviderInstance( String certInstanceName, String certName, @@ -210,10 +186,10 @@ private static CommonTlsContext buildCommonTlsContextForCertProviderInstance( CommonTlsContext.Builder builder = CommonTlsContext.newBuilder(); if (certInstanceName != null) { builder = - builder.setTlsCertificateCertificateProviderInstance( - CommonTlsContext.CertificateProviderInstance.newBuilder() - .setInstanceName(certInstanceName) - .setCertificateName(certName)); + builder.setTlsCertificateProviderInstance( + CertificateProviderPluginInstance.newBuilder() + .setInstanceName(certInstanceName) + .setCertificateName(certName)); } builder = addCertificateValidationContext( @@ -248,35 +224,28 @@ private static CommonTlsContext buildNewCommonTlsContextForCertProviderInstance( return builder.build(); } - @SuppressWarnings("deprecation") private static CommonTlsContext.Builder addCertificateValidationContext( CommonTlsContext.Builder builder, String rootInstanceName, String rootCertName, CertificateValidationContext staticCertValidationContext) { - CertificateProviderInstance providerInstance = null; - if (rootInstanceName != null) { - providerInstance = CertificateProviderInstance.newBuilder() - .setInstanceName(rootInstanceName) - .setCertificateName(rootCertName) - .build(); - } - if (providerInstance != null) { - builder = builder.setValidationContextCertificateProviderInstance(providerInstance); + if (staticCertValidationContext == null && rootInstanceName == null) { + return builder; } - CombinedCertificateValidationContext.Builder combined = - CombinedCertificateValidationContext.newBuilder(); - if (providerInstance != null) { - combined = combined.setValidationContextCertificateProviderInstance(providerInstance); - } - if (staticCertValidationContext != null) { - combined = combined.setDefaultValidationContext(staticCertValidationContext); + CertificateValidationContext.Builder contextBuilder; + if (staticCertValidationContext == null) { + contextBuilder = CertificateValidationContext.newBuilder(); + } else { + contextBuilder = staticCertValidationContext.toBuilder(); } - if (combined.hasValidationContextCertificateProviderInstance() - || combined.hasDefaultValidationContext()) { - builder = builder.setCombinedValidationContext(combined.build()); + if (rootInstanceName != null) { + contextBuilder.setCaCertificateProviderInstance(CertificateProviderPluginInstance.newBuilder() + .setInstanceName(rootInstanceName) + .setCertificateName(rootCertName)); + builder.setValidationContext(contextBuilder.build()); } - return builder; + return builder.setCombinedValidationContext(CombinedCertificateValidationContext.newBuilder() + .setDefaultValidationContext(contextBuilder)); } private static CommonTlsContext.Builder addNewCertificateValidationContext( diff --git a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertificateProviderStoreTest.java b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertificateProviderStoreTest.java index 8f77de7b5e2..c0bc095eab6 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertificateProviderStoreTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertificateProviderStoreTest.java @@ -123,7 +123,6 @@ public void notifyCertUpdatesNotSupported_expectExceptionOnSecondCall() { } @Test - @SuppressWarnings("deprecation") public void onePluginSameConfig_sameInstance() { registerPlugin("plugin1"); CertificateProvider.Watcher mockWatcher1 = mock(CertificateProvider.Watcher.class); @@ -167,7 +166,6 @@ public void onePluginSameConfig_sameInstance() { } @Test - @SuppressWarnings("deprecation") public void onePluginSameConfig_secondWatcherAfterFirstNotify() { registerPlugin("plugin1"); CertificateProvider.Watcher mockWatcher1 = mock(CertificateProvider.Watcher.class); @@ -275,7 +273,6 @@ public void twoPlugins_differentInstance() { mockWatcher1, handle1, certProviderProvider1, mockWatcher2, handle2, certProviderProvider2); } - @SuppressWarnings("deprecation") private static void checkDifferentInstances( CertificateProvider.Watcher mockWatcher1, CertificateProviderStore.Handle handle1, From 84bd01454b38815ad4b67ea2ca83708088d498f1 Mon Sep 17 00:00:00 2001 From: Kurt Alfred Kluever Date: Tue, 15 Apr 2025 11:10:10 -0700 Subject: [PATCH 246/591] context: Remove mention of "epoch" from Ticker.nanoTime() javadocs, plus other minor touchups In Java, when people hear "epoch", they think "unix epoch". cl/747082451 --- api/src/context/java/io/grpc/Deadline.java | 49 ++++++++-------------- 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/api/src/context/java/io/grpc/Deadline.java b/api/src/context/java/io/grpc/Deadline.java index 62b803267a8..92eeba5ffce 100644 --- a/api/src/context/java/io/grpc/Deadline.java +++ b/api/src/context/java/io/grpc/Deadline.java @@ -16,8 +16,10 @@ package io.grpc; -import java.util.Arrays; +import static java.util.Objects.requireNonNull; + import java.util.Locale; +import java.util.Objects; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -33,7 +35,7 @@ * passed to the various components unambiguously. */ public final class Deadline implements Comparable { - private static final SystemTicker SYSTEM_TICKER = new SystemTicker(); + private static final Ticker SYSTEM_TICKER = new SystemTicker(); // nanoTime has a range of just under 300 years. Only allow up to 100 years in the past or future // to prevent wraparound as long as process runs for less than ~100 years. private static final long MAX_OFFSET = TimeUnit.DAYS.toNanos(100 * 365); @@ -91,7 +93,7 @@ public static Deadline after(long duration, TimeUnit units) { * @since 1.24.0 */ public static Deadline after(long duration, TimeUnit units, Ticker ticker) { - checkNotNull(units, "units"); + requireNonNull(units, "units"); return new Deadline(ticker, units.toNanos(duration), true); } @@ -191,8 +193,8 @@ public long timeRemaining(TimeUnit unit) { * @return {@link ScheduledFuture} which can be used to cancel execution of the task */ public ScheduledFuture runOnExpiration(Runnable task, ScheduledExecutorService scheduler) { - checkNotNull(task, "task"); - checkNotNull(scheduler, "scheduler"); + requireNonNull(task, "task"); + requireNonNull(scheduler, "scheduler"); return scheduler.schedule(task, deadlineNanos - ticker.nanoTime(), TimeUnit.NANOSECONDS); } @@ -225,37 +227,27 @@ public String toString() { @Override public int compareTo(Deadline that) { checkTicker(that); - long diff = this.deadlineNanos - that.deadlineNanos; - if (diff < 0) { - return -1; - } else if (diff > 0) { - return 1; - } - return 0; + return Long.compare(this.deadlineNanos, that.deadlineNanos); } @Override public int hashCode() { - return Arrays.asList(this.ticker, this.deadlineNanos).hashCode(); + return Objects.hash(this.ticker, this.deadlineNanos); } @Override - public boolean equals(final Object o) { - if (o == this) { + public boolean equals(final Object object) { + if (object == this) { return true; } - if (!(o instanceof Deadline)) { - return false; - } - - final Deadline other = (Deadline) o; - if (this.ticker == null ? other.ticker != null : this.ticker != other.ticker) { + if (!(object instanceof Deadline)) { return false; } - if (this.deadlineNanos != other.deadlineNanos) { + final Deadline that = (Deadline) object; + if (this.ticker == null ? that.ticker != null : this.ticker != that.ticker) { return false; } - return true; + return this.deadlineNanos == that.deadlineNanos; } /** @@ -275,24 +267,17 @@ public boolean equals(final Object o) { * @since 1.24.0 */ public abstract static class Ticker { - /** Returns the number of nanoseconds since this source's epoch. */ + /** Returns the number of nanoseconds elapsed since this ticker's reference point in time. */ public abstract long nanoTime(); } - private static class SystemTicker extends Ticker { + private static final class SystemTicker extends Ticker { @Override public long nanoTime() { return System.nanoTime(); } } - private static T checkNotNull(T reference, Object errorMessage) { - if (reference == null) { - throw new NullPointerException(String.valueOf(errorMessage)); - } - return reference; - } - private void checkTicker(Deadline other) { if (ticker != other.ticker) { throw new AssertionError( From 7a08fdb7f94a43951da6245e100c3535d0ce7879 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Thu, 17 Apr 2025 07:26:40 +0530 Subject: [PATCH 247/591] xds: float LRU cache across interceptors (#11992) --- .../io/grpc/xds/GcpAuthenticationFilter.java | 60 ++++--- .../grpc/xds/GcpAuthenticationFilterTest.java | 146 ++++++++++++++++-- 2 files changed, 175 insertions(+), 31 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java index b5568efe400..8ec02f4f809 100644 --- a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java +++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java @@ -59,15 +59,17 @@ final class GcpAuthenticationFilter implements Filter { static final String TYPE_URL = "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig"; - + private final LruCache callCredentialsCache; final String filterInstanceName; - GcpAuthenticationFilter(String name) { + GcpAuthenticationFilter(String name, int cacheSize) { filterInstanceName = checkNotNull(name, "name"); + this.callCredentialsCache = new LruCache<>(cacheSize); } - static final class Provider implements Filter.Provider { + private final int cacheSize = 10; + @Override public String[] typeUrls() { return new String[]{TYPE_URL}; @@ -80,7 +82,7 @@ public boolean isClientFilter() { @Override public GcpAuthenticationFilter newInstance(String name) { - return new GcpAuthenticationFilter(name); + return new GcpAuthenticationFilter(name, cacheSize); } @Override @@ -101,11 +103,14 @@ public ConfigOrError parseFilterConfig(Message rawProto // Validate cache_config if (gcpAuthnProto.hasCacheConfig()) { TokenCacheConfig cacheConfig = gcpAuthnProto.getCacheConfig(); - cacheSize = cacheConfig.getCacheSize().getValue(); - if (cacheSize == 0) { - return ConfigOrError.fromError( - "cache_config.cache_size must be greater than zero"); + if (cacheConfig.hasCacheSize()) { + cacheSize = cacheConfig.getCacheSize().getValue(); + if (cacheSize == 0) { + return ConfigOrError.fromError( + "cache_config.cache_size must be greater than zero"); + } } + // LruCache's size is an int and briefly exceeds its maximum size before evicting entries cacheSize = UnsignedLongs.min(cacheSize, Integer.MAX_VALUE - 1); } @@ -127,8 +132,9 @@ public ClientInterceptor buildClientInterceptor(FilterConfig config, @Nullable FilterConfig overrideConfig, ScheduledExecutorService scheduler) { ComputeEngineCredentials credentials = ComputeEngineCredentials.create(); - LruCache callCredentialsCache = - new LruCache<>(((GcpAuthenticationConfig) config).getCacheSize()); + synchronized (callCredentialsCache) { + callCredentialsCache.resizeCache(((GcpAuthenticationConfig) config).getCacheSize()); + } return new ClientInterceptor() { @Override public ClientCall interceptCall( @@ -254,23 +260,37 @@ public void sendMessage(ReqT message) {} private static final class LruCache { - private final Map cache; + private Map cache; + private int maxSize; LruCache(int maxSize) { - this.cache = new LinkedHashMap( - maxSize, - 0.75f, - true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > maxSize; - } - }; + this.maxSize = maxSize; + this.cache = createEvictingMap(maxSize); } V getOrInsert(K key, Function create) { return cache.computeIfAbsent(key, create); } + + private void resizeCache(int newSize) { + if (newSize >= maxSize) { + maxSize = newSize; + return; + } + Map newCache = createEvictingMap(newSize); + maxSize = newSize; + newCache.putAll(cache); + cache = newCache; + } + + private Map createEvictingMap(int size) { + return new LinkedHashMap(size, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > LruCache.this.maxSize; + } + }; + } } static class AudienceMetadataParser implements MetadataValueParser { diff --git a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java index a5e142b4094..d84d8c9d768 100644 --- a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java @@ -28,11 +28,13 @@ import static io.grpc.xds.XdsTestUtils.getWrrLbConfigAsMap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.google.common.collect.ImmutableList; @@ -89,8 +91,8 @@ public class GcpAuthenticationFilterTest { @Test public void testNewFilterInstancesPerFilterName() { - assertThat(new GcpAuthenticationFilter("FILTER_INSTANCE_NAME1")) - .isNotEqualTo(new GcpAuthenticationFilter("FILTER_INSTANCE_NAME1")); + assertThat(new GcpAuthenticationFilter("FILTER_INSTANCE_NAME1", 10)) + .isNotEqualTo(new GcpAuthenticationFilter("FILTER_INSTANCE_NAME1", 10)); } @Test @@ -152,7 +154,7 @@ public void testClientInterceptor_success() throws IOException, ResourceInvalidE .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0") .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); - GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME", 10); ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); Channel mockChannel = Mockito.mock(Channel.class); @@ -181,7 +183,7 @@ public void testClientInterceptor_createsAndReusesCachedCredentials() .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0") .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); - GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME", 10); ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); Channel mockChannel = Mockito.mock(Channel.class); @@ -190,7 +192,7 @@ public void testClientInterceptor_createsAndReusesCachedCredentials() interceptor.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); interceptor.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); - verify(mockChannel, Mockito.times(2)) + verify(mockChannel, times(2)) .newCall(eq(methodDescriptor), callOptionsCaptor.capture()); CallOptions firstCapturedOptions = callOptionsCaptor.getAllValues().get(0); CallOptions secondCapturedOptions = callOptionsCaptor.getAllValues().get(1); @@ -202,7 +204,7 @@ public void testClientInterceptor_createsAndReusesCachedCredentials() @Test public void testClientInterceptor_withoutClusterSelectionKey() throws Exception { GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); - GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME", 10); ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); Channel mockChannel = mock(Channel.class); @@ -233,7 +235,7 @@ public void testClientInterceptor_clusterSelectionKeyWithoutPrefix() throws Exce Channel mockChannel = mock(Channel.class); GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); - GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME", 10); ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); interceptor.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); @@ -244,7 +246,7 @@ public void testClientInterceptor_clusterSelectionKeyWithoutPrefix() throws Exce @Test public void testClientInterceptor_xdsConfigDoesNotExist() throws Exception { GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); - GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME", 10); ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); Channel mockChannel = mock(Channel.class); @@ -274,7 +276,7 @@ public void testClientInterceptor_incorrectClusterName() throws Exception { .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster") .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); - GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME", 10); ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); Channel mockChannel = mock(Channel.class); @@ -300,7 +302,7 @@ public void testClientInterceptor_statusOrError() throws Exception { .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0") .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); - GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME", 10); ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); Channel mockChannel = mock(Channel.class); @@ -329,7 +331,7 @@ public void testClientInterceptor_notAudienceWrapper() .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0") .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); GcpAuthenticationConfig config = new GcpAuthenticationConfig(10); - GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME"); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME", 10); ClientInterceptor interceptor = filter.buildClientInterceptor(config, null, null); MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); Channel mockChannel = Mockito.mock(Channel.class); @@ -342,6 +344,115 @@ public void testClientInterceptor_notAudienceWrapper() assertThat(clientCall.error.getDescription()).contains("GCP Authn found wrong type"); } + @Test + public void testLruCacheAcrossInterceptors() throws IOException, ResourceInvalidException { + XdsConfig.XdsClusterConfig clusterConfig = new XdsConfig.XdsClusterConfig( + CLUSTER_NAME, cdsUpdate, new EndpointConfig(StatusOr.fromValue(edsUpdate))); + XdsConfig defaultXdsConfig = new XdsConfig.XdsConfigBuilder() + .setListener(ldsUpdate) + .setRoute(rdsUpdate) + .setVirtualHost(rdsUpdate.virtualHosts.get(0)) + .addCluster(CLUSTER_NAME, StatusOr.fromValue(clusterConfig)).build(); + CallOptions callOptionsWithXds = CallOptions.DEFAULT + .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0") + .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME", 2); + ClientInterceptor interceptor1 + = filter.buildClientInterceptor(new GcpAuthenticationConfig(2), null, null); + MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); + Channel mockChannel = Mockito.mock(Channel.class); + ArgumentCaptor callOptionsCaptor = ArgumentCaptor.forClass(CallOptions.class); + + interceptor1.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); + verify(mockChannel).newCall(eq(methodDescriptor), callOptionsCaptor.capture()); + CallOptions capturedOptions1 = callOptionsCaptor.getAllValues().get(0); + assertNotNull(capturedOptions1.getCredentials()); + ClientInterceptor interceptor2 + = filter.buildClientInterceptor(new GcpAuthenticationConfig(1), null, null); + interceptor2.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel); + verify(mockChannel, times(2)) + .newCall(eq(methodDescriptor), callOptionsCaptor.capture()); + CallOptions capturedOptions2 = callOptionsCaptor.getAllValues().get(1); + assertNotNull(capturedOptions2.getCredentials()); + + assertSame(capturedOptions1.getCredentials(), capturedOptions2.getCredentials()); + } + + @Test + public void testLruCacheEvictionOnResize() throws IOException, ResourceInvalidException { + XdsConfig.XdsClusterConfig clusterConfig = new XdsConfig.XdsClusterConfig( + CLUSTER_NAME, cdsUpdate, new EndpointConfig(StatusOr.fromValue(edsUpdate))); + XdsConfig defaultXdsConfig = new XdsConfig.XdsConfigBuilder() + .setListener(ldsUpdate) + .setRoute(rdsUpdate) + .setVirtualHost(rdsUpdate.virtualHosts.get(0)) + .addCluster(CLUSTER_NAME, StatusOr.fromValue(clusterConfig)).build(); + CallOptions callOptionsWithXds = CallOptions.DEFAULT + .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0") + .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); + GcpAuthenticationFilter filter = new GcpAuthenticationFilter("FILTER_INSTANCE_NAME", 2); + MethodDescriptor methodDescriptor = TestMethodDescriptors.voidMethod(); + + ClientInterceptor interceptor1 = + filter.buildClientInterceptor(new GcpAuthenticationConfig(2), null, null); + Channel mockChannel1 = Mockito.mock(Channel.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(CallOptions.class); + interceptor1.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel1); + verify(mockChannel1).newCall(eq(methodDescriptor), captor.capture()); + CallOptions options1 = captor.getValue(); + // This will recreate the cache with max size of 1 and copy the credential for audience1. + ClientInterceptor interceptor2 = + filter.buildClientInterceptor(new GcpAuthenticationConfig(1), null, null); + Channel mockChannel2 = Mockito.mock(Channel.class); + interceptor2.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel2); + verify(mockChannel2).newCall(eq(methodDescriptor), captor.capture()); + CallOptions options2 = captor.getValue(); + + assertSame(options1.getCredentials(), options2.getCredentials()); + + clusterConfig = new XdsConfig.XdsClusterConfig( + CLUSTER_NAME, getCdsUpdate2(), new EndpointConfig(StatusOr.fromValue(edsUpdate))); + defaultXdsConfig = new XdsConfig.XdsConfigBuilder() + .setListener(ldsUpdate) + .setRoute(rdsUpdate) + .setVirtualHost(rdsUpdate.virtualHosts.get(0)) + .addCluster(CLUSTER_NAME, StatusOr.fromValue(clusterConfig)).build(); + callOptionsWithXds = CallOptions.DEFAULT + .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0") + .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); + + // This will evict the credential for audience1 and add new credential for audience2 + ClientInterceptor interceptor3 = + filter.buildClientInterceptor(new GcpAuthenticationConfig(1), null, null); + Channel mockChannel3 = Mockito.mock(Channel.class); + interceptor3.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel3); + verify(mockChannel3).newCall(eq(methodDescriptor), captor.capture()); + CallOptions options3 = captor.getValue(); + + assertNotSame(options1.getCredentials(), options3.getCredentials()); + + clusterConfig = new XdsConfig.XdsClusterConfig( + CLUSTER_NAME, cdsUpdate, new EndpointConfig(StatusOr.fromValue(edsUpdate))); + defaultXdsConfig = new XdsConfig.XdsConfigBuilder() + .setListener(ldsUpdate) + .setRoute(rdsUpdate) + .setVirtualHost(rdsUpdate.virtualHosts.get(0)) + .addCluster(CLUSTER_NAME, StatusOr.fromValue(clusterConfig)).build(); + callOptionsWithXds = CallOptions.DEFAULT + .withOption(CLUSTER_SELECTION_KEY, "cluster:cluster0") + .withOption(XDS_CONFIG_CALL_OPTION_KEY, defaultXdsConfig); + + // This will create new credential for audience1 because it has been evicted + ClientInterceptor interceptor4 = + filter.buildClientInterceptor(new GcpAuthenticationConfig(1), null, null); + Channel mockChannel4 = Mockito.mock(Channel.class); + interceptor4.interceptCall(methodDescriptor, callOptionsWithXds, mockChannel4); + verify(mockChannel4).newCall(eq(methodDescriptor), captor.capture()); + CallOptions options4 = captor.getValue(); + + assertNotSame(options1.getCredentials(), options4.getCredentials()); + } + private static LdsUpdate getLdsUpdate() { Filter.NamedFilterConfig routerFilterConfig = new Filter.NamedFilterConfig( serverName, RouterFilter.ROUTER_CONFIG); @@ -384,6 +495,19 @@ private static CdsUpdate getCdsUpdate() { } } + private static CdsUpdate getCdsUpdate2() { + ImmutableMap.Builder parsedMetadata = ImmutableMap.builder(); + parsedMetadata.put("FILTER_INSTANCE_NAME", new AudienceWrapper("NEW_TEST_AUDIENCE")); + try { + CdsUpdate.Builder cdsUpdate = CdsUpdate.forEds( + CLUSTER_NAME, EDS_NAME, null, null, null, null, false) + .lbPolicyConfig(getWrrLbConfigAsMap()); + return cdsUpdate.parsedMetadata(parsedMetadata.build()).build(); + } catch (IOException ex) { + return null; + } + } + private static CdsUpdate getCdsUpdateWithIncorrectAudienceWrapper() throws IOException { ImmutableMap.Builder parsedMetadata = ImmutableMap.builder(); parsedMetadata.put("FILTER_INSTANCE_NAME", "TEST_AUDIENCE"); From 53de8a72ca989ca547c7df67d71ddc3b43f49a8e Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Thu, 17 Apr 2025 12:07:08 +0000 Subject: [PATCH 248/591] Update README etc to reference 1.72.0 (#12025) --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 756519d0ad0..d1b4d516f74 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.71.0/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.71.0/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.72.0/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.72.0/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.71.0 + 1.72.0 runtime io.grpc grpc-protobuf - 1.71.0 + 1.72.0 io.grpc grpc-stub - 1.71.0 + 1.72.0 org.apache.tomcat @@ -79,18 +79,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.71.0' -implementation 'io.grpc:grpc-protobuf:1.71.0' -implementation 'io.grpc:grpc-stub:1.71.0' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.72.0' +implementation 'io.grpc:grpc-protobuf:1.72.0' +implementation 'io.grpc:grpc-stub:1.72.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.71.0' -implementation 'io.grpc:grpc-protobuf-lite:1.71.0' -implementation 'io.grpc:grpc-stub:1.71.0' +implementation 'io.grpc:grpc-okhttp:1.72.0' +implementation 'io.grpc:grpc-protobuf-lite:1.72.0' +implementation 'io.grpc:grpc-stub:1.72.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` @@ -99,7 +99,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.71.0 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.72.0 Development snapshots are available in [Sonatypes's snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/). @@ -131,7 +131,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.71.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.72.0:exe:${os.detected.classifier} @@ -161,7 +161,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.71.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.72.0' } } generateProtoTasks { @@ -194,7 +194,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.71.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.72.0' } } generateProtoTasks { From 9619453799e98f3fb4bbbf999b1d8d09c374d687 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 21 Apr 2025 06:17:43 -0700 Subject: [PATCH 249/591] Implement grpc.lb.backend_service optional label This completes gRFC A89. 7162d2d66 and fc86084df had already implemented the LB plumbing for the optional label on RPC metrics. This observes the value in OpenTelemetry and adds it to WRR metrics as well. https://github.com/grpc/proposal/blob/master/A89-backend-service-metric-label.md --- api/src/main/java/io/grpc/NameResolver.java | 5 + .../OpenTelemetryMetricsModule.java | 19 ++- .../internal/OpenTelemetryConstants.java | 3 + .../OpenTelemetryMetricsModuleTest.java | 135 ++++++++++++++++++ .../io/grpc/xds/ClusterImplLoadBalancer.java | 5 +- .../xds/WeightedRoundRobinLoadBalancer.java | 46 ++++-- .../grpc/xds/ClusterImplLoadBalancerTest.java | 2 + .../WeightedRoundRobinLoadBalancerTest.java | 18 ++- 8 files changed, 212 insertions(+), 21 deletions(-) diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java index e8295365ca8..21064d7f115 100644 --- a/api/src/main/java/io/grpc/NameResolver.java +++ b/api/src/main/java/io/grpc/NameResolver.java @@ -275,6 +275,11 @@ public Status onResult2(ResolutionResult resolutionResult) { @Documented public @interface ResolutionResultAttr {} + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11989") + @ResolutionResultAttr + public static final Attributes.Key ATTR_BACKEND_SERVICE = + Attributes.Key.create("io.grpc.NameResolver.ATTR_BACKEND_SERVICE"); + /** * Information that a {@link Factory} uses to create a {@link NameResolver}. * diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java index b6cf09d9db6..82ad41e7b20 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java @@ -17,6 +17,7 @@ package io.grpc.opentelemetry; import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.BACKEND_SERVICE_KEY; import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.LOCALITY_KEY; import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.METHOD_KEY; import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.STATUS_KEY; @@ -70,7 +71,6 @@ */ final class OpenTelemetryMetricsModule { private static final Logger logger = Logger.getLogger(OpenTelemetryMetricsModule.class.getName()); - private static final String LOCALITY_LABEL_NAME = "grpc.lb.locality"; public static final ImmutableSet DEFAULT_PER_CALL_METRICS_SET = ImmutableSet.of( "grpc.client.attempt.started", @@ -90,6 +90,7 @@ final class OpenTelemetryMetricsModule { private final OpenTelemetryMetricsResource resource; private final Supplier stopwatchSupplier; private final boolean localityEnabled; + private final boolean backendServiceEnabled; private final ImmutableList plugins; OpenTelemetryMetricsModule(Supplier stopwatchSupplier, @@ -97,7 +98,8 @@ final class OpenTelemetryMetricsModule { List plugins) { this.resource = checkNotNull(resource, "resource"); this.stopwatchSupplier = checkNotNull(stopwatchSupplier, "stopwatchSupplier"); - this.localityEnabled = optionalLabels.contains(LOCALITY_LABEL_NAME); + this.localityEnabled = optionalLabels.contains(LOCALITY_KEY.getKey()); + this.backendServiceEnabled = optionalLabels.contains(BACKEND_SERVICE_KEY.getKey()); this.plugins = ImmutableList.copyOf(plugins); } @@ -162,6 +164,7 @@ private static final class ClientTracer extends ClientStreamTracer { volatile long outboundWireSize; volatile long inboundWireSize; volatile String locality; + volatile String backendService; long attemptNanos; Code statusCode; @@ -206,9 +209,12 @@ public void inboundWireSize(long bytes) { @Override public void addOptionalLabel(String key, String value) { - if (LOCALITY_LABEL_NAME.equals(key)) { + if ("grpc.lb.locality".equals(key)) { locality = value; } + if ("grpc.lb.backend_service".equals(key)) { + backendService = value; + } } @Override @@ -248,6 +254,13 @@ void recordFinishedAttempt() { } builder.put(LOCALITY_KEY, savedLocality); } + if (module.backendServiceEnabled) { + String savedBackendService = backendService; + if (savedBackendService == null) { + savedBackendService = ""; + } + builder.put(BACKEND_SERVICE_KEY, savedBackendService); + } for (OpenTelemetryPlugin.ClientStreamPlugin plugin : streamPlugins) { plugin.addLabels(builder); } diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java index 081e376b8c5..5214804d369 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java @@ -33,6 +33,9 @@ public final class OpenTelemetryConstants { public static final AttributeKey LOCALITY_KEY = AttributeKey.stringKey("grpc.lb.locality"); + public static final AttributeKey BACKEND_SERVICE_KEY = + AttributeKey.stringKey("grpc.lb.backend_service"); + public static final List LATENCY_BUCKETS = ImmutableList.of( 0d, 0.00001d, 0.00005d, 0.0001d, 0.0003d, 0.0006d, 0.0008d, 0.001d, 0.002d, diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java index 6003599668b..77f0268ec2f 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java @@ -51,6 +51,7 @@ import io.grpc.opentelemetry.OpenTelemetryMetricsModule.CallAttemptsTracerFactory; import io.grpc.opentelemetry.internal.OpenTelemetryConstants; import io.grpc.testing.GrpcServerRule; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule; @@ -1070,6 +1071,140 @@ public void clientLocalityMetrics_missing() { point -> point.hasAttributes(clientAttributes)))); } + @Test + public void clientBackendServiceMetrics_present() { + String target = "target:///"; + OpenTelemetryMetricsResource resource = GrpcOpenTelemetry.createMetricInstruments(testMeter, + enabledMetricsMap, disableDefaultMetrics); + OpenTelemetryMetricsModule module = new OpenTelemetryMetricsModule( + fakeClock.getStopwatchSupplier(), resource, Arrays.asList("grpc.lb.backend_service"), + emptyList()); + OpenTelemetryMetricsModule.CallAttemptsTracerFactory callAttemptsTracerFactory = + new CallAttemptsTracerFactory(module, target, method.getFullMethodName(), emptyList()); + + ClientStreamTracer tracer = + callAttemptsTracerFactory.newClientStreamTracer(STREAM_INFO, new Metadata()); + tracer.addOptionalLabel("grpc.lb.foo", "unimportant"); + tracer.addOptionalLabel("grpc.lb.backend_service", "should-be-overwritten"); + tracer.addOptionalLabel("grpc.lb.backend_service", "the-moon"); + tracer.addOptionalLabel("grpc.lb.foo", "thats-no-moon"); + tracer.streamClosed(Status.OK); + callAttemptsTracerFactory.callEnded(Status.OK); + + io.opentelemetry.api.common.Attributes attributes = io.opentelemetry.api.common.Attributes.of( + TARGET_KEY, target, + METHOD_KEY, method.getFullMethodName()); + + io.opentelemetry.api.common.Attributes clientAttributes + = io.opentelemetry.api.common.Attributes.of( + TARGET_KEY, target, + METHOD_KEY, method.getFullMethodName(), + STATUS_KEY, Status.Code.OK.toString()); + + io.opentelemetry.api.common.Attributes clientAttributesWithBackendService + = clientAttributes.toBuilder() + .put(AttributeKey.stringKey("grpc.lb.backend_service"), "the-moon") + .build(); + + assertThat(openTelemetryTesting.getMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName(CLIENT_ATTEMPT_COUNT_INSTRUMENT_NAME) + .hasLongSumSatisfying( + longSum -> longSum.hasPointsSatisfying( + point -> point.hasAttributes(attributes))), + metric -> + assertThat(metric) + .hasName(CLIENT_ATTEMPT_DURATION_INSTRUMENT_NAME) + .hasHistogramSatisfying( + histogram -> histogram.hasPointsSatisfying( + point -> point.hasAttributes(clientAttributesWithBackendService))), + metric -> + assertThat(metric) + .hasName(CLIENT_ATTEMPT_SENT_TOTAL_COMPRESSED_MESSAGE_SIZE) + .hasHistogramSatisfying( + histogram -> histogram.hasPointsSatisfying( + point -> point.hasAttributes(clientAttributesWithBackendService))), + metric -> + assertThat(metric) + .hasName(CLIENT_ATTEMPT_RECV_TOTAL_COMPRESSED_MESSAGE_SIZE) + .hasHistogramSatisfying( + histogram -> histogram.hasPointsSatisfying( + point -> point.hasAttributes(clientAttributesWithBackendService))), + metric -> + assertThat(metric) + .hasName(CLIENT_CALL_DURATION) + .hasHistogramSatisfying( + histogram -> histogram.hasPointsSatisfying( + point -> point.hasAttributes(clientAttributes)))); + } + + @Test + public void clientBackendServiceMetrics_missing() { + String target = "target:///"; + OpenTelemetryMetricsResource resource = GrpcOpenTelemetry.createMetricInstruments(testMeter, + enabledMetricsMap, disableDefaultMetrics); + OpenTelemetryMetricsModule module = new OpenTelemetryMetricsModule( + fakeClock.getStopwatchSupplier(), resource, Arrays.asList("grpc.lb.backend_service"), + emptyList()); + OpenTelemetryMetricsModule.CallAttemptsTracerFactory callAttemptsTracerFactory = + new CallAttemptsTracerFactory(module, target, method.getFullMethodName(), emptyList()); + + ClientStreamTracer tracer = + callAttemptsTracerFactory.newClientStreamTracer(STREAM_INFO, new Metadata()); + tracer.streamClosed(Status.OK); + callAttemptsTracerFactory.callEnded(Status.OK); + + io.opentelemetry.api.common.Attributes attributes = io.opentelemetry.api.common.Attributes.of( + TARGET_KEY, target, + METHOD_KEY, method.getFullMethodName()); + + io.opentelemetry.api.common.Attributes clientAttributes + = io.opentelemetry.api.common.Attributes.of( + TARGET_KEY, target, + METHOD_KEY, method.getFullMethodName(), + STATUS_KEY, Status.Code.OK.toString()); + + io.opentelemetry.api.common.Attributes clientAttributesWithBackendService + = clientAttributes.toBuilder() + .put(AttributeKey.stringKey("grpc.lb.backend_service"), "") + .build(); + + assertThat(openTelemetryTesting.getMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> + assertThat(metric) + .hasName(CLIENT_ATTEMPT_COUNT_INSTRUMENT_NAME) + .hasLongSumSatisfying( + longSum -> longSum.hasPointsSatisfying( + point -> point.hasAttributes(attributes))), + metric -> + assertThat(metric) + .hasName(CLIENT_ATTEMPT_DURATION_INSTRUMENT_NAME) + .hasHistogramSatisfying( + histogram -> histogram.hasPointsSatisfying( + point -> point.hasAttributes(clientAttributesWithBackendService))), + metric -> + assertThat(metric) + .hasName(CLIENT_ATTEMPT_SENT_TOTAL_COMPRESSED_MESSAGE_SIZE) + .hasHistogramSatisfying( + histogram -> histogram.hasPointsSatisfying( + point -> point.hasAttributes(clientAttributesWithBackendService))), + metric -> + assertThat(metric) + .hasName(CLIENT_ATTEMPT_RECV_TOTAL_COMPRESSED_MESSAGE_SIZE) + .hasHistogramSatisfying( + histogram -> histogram.hasPointsSatisfying( + point -> point.hasAttributes(clientAttributesWithBackendService))), + metric -> + assertThat(metric) + .hasName(CLIENT_CALL_DURATION) + .hasHistogramSatisfying( + histogram -> histogram.hasPointsSatisfying( + point -> point.hasAttributes(clientAttributes)))); + } + @Test public void serverBasicMetrics() { OpenTelemetryMetricsResource resource = GrpcOpenTelemetry.createMetricInstruments(testMeter, diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index fd4f49fbb83..034cdee0815 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -32,6 +32,7 @@ import io.grpc.InternalLogId; import io.grpc.LoadBalancer; import io.grpc.Metadata; +import io.grpc.NameResolver; import io.grpc.Status; import io.grpc.internal.ForwardingClientStreamTracer; import io.grpc.internal.GrpcUtil; @@ -150,7 +151,9 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { childSwitchLb.handleResolvedAddresses( resolvedAddresses.toBuilder() - .setAttributes(attributes) + .setAttributes(attributes.toBuilder() + .set(NameResolver.ATTR_BACKEND_SERVICE, cluster) + .build()) .setLoadBalancingPolicyConfig(config.childConfig) .build()); return Status.OK; diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index b7b4fd51afe..c3d36f7cdc8 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -102,32 +102,44 @@ final class WeightedRoundRobinLoadBalancer extends MultiChildLoadBalancer { private final long infTime; private final Ticker ticker; private String locality = ""; + private String backendService = ""; private SubchannelPicker currentPicker = new FixedResultPicker(PickResult.withNoResult()); // The metric instruments are only registered once and shared by all instances of this LB. static { MetricInstrumentRegistry metricInstrumentRegistry = MetricInstrumentRegistry.getDefaultRegistry(); - RR_FALLBACK_COUNTER = metricInstrumentRegistry.registerLongCounter("grpc.lb.wrr.rr_fallback", + RR_FALLBACK_COUNTER = metricInstrumentRegistry.registerLongCounter( + "grpc.lb.wrr.rr_fallback", "EXPERIMENTAL. Number of scheduler updates in which there were not enough endpoints " + "with valid weight, which caused the WRR policy to fall back to RR behavior", - "{update}", Lists.newArrayList("grpc.target"), Lists.newArrayList("grpc.lb.locality"), + "{update}", + Lists.newArrayList("grpc.target"), + Lists.newArrayList("grpc.lb.locality", "grpc.lb.backend_service"), false); ENDPOINT_WEIGHT_NOT_YET_USEABLE_COUNTER = metricInstrumentRegistry.registerLongCounter( - "grpc.lb.wrr.endpoint_weight_not_yet_usable", "EXPERIMENTAL. Number of endpoints " - + "from each scheduler update that don't yet have usable weight information", - "{endpoint}", Lists.newArrayList("grpc.target"), Lists.newArrayList("grpc.lb.locality"), + "grpc.lb.wrr.endpoint_weight_not_yet_usable", + "EXPERIMENTAL. Number of endpoints from each scheduler update that don't yet have usable " + + "weight information", + "{endpoint}", + Lists.newArrayList("grpc.target"), + Lists.newArrayList("grpc.lb.locality", "grpc.lb.backend_service"), false); ENDPOINT_WEIGHT_STALE_COUNTER = metricInstrumentRegistry.registerLongCounter( "grpc.lb.wrr.endpoint_weight_stale", "EXPERIMENTAL. Number of endpoints from each scheduler update whose latest weight is " - + "older than the expiration period", "{endpoint}", Lists.newArrayList("grpc.target"), - Lists.newArrayList("grpc.lb.locality"), false); + + "older than the expiration period", + "{endpoint}", + Lists.newArrayList("grpc.target"), + Lists.newArrayList("grpc.lb.locality", "grpc.lb.backend_service"), + false); ENDPOINT_WEIGHTS_HISTOGRAM = metricInstrumentRegistry.registerDoubleHistogram( "grpc.lb.wrr.endpoint_weights", "EXPERIMENTAL. The histogram buckets will be endpoint weight ranges.", - "{weight}", Lists.newArrayList(), Lists.newArrayList("grpc.target"), - Lists.newArrayList("grpc.lb.locality"), + "{weight}", + Lists.newArrayList(), + Lists.newArrayList("grpc.target"), + Lists.newArrayList("grpc.lb.locality", "grpc.lb.backend_service"), false); } @@ -168,6 +180,13 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { } else { this.locality = ""; } + String backendService + = resolvedAddresses.getAttributes().get(NameResolver.ATTR_BACKEND_SERVICE); + if (backendService != null) { + this.backendService = backendService; + } else { + this.backendService = ""; + } config = (WeightedRoundRobinLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); @@ -232,7 +251,7 @@ private void updateWeight(WeightedRoundRobinPicker picker) { helper.getMetricRecorder() .recordDoubleHistogram(ENDPOINT_WEIGHTS_HISTOGRAM, newWeight, ImmutableList.of(helper.getChannelTarget()), - ImmutableList.of(locality)); + ImmutableList.of(locality, backendService)); newWeights[i] = newWeight > 0 ? (float) newWeight : 0.0f; } @@ -240,18 +259,19 @@ private void updateWeight(WeightedRoundRobinPicker picker) { helper.getMetricRecorder() .addLongCounter(ENDPOINT_WEIGHT_STALE_COUNTER, staleEndpoints.get(), ImmutableList.of(helper.getChannelTarget()), - ImmutableList.of(locality)); + ImmutableList.of(locality, backendService)); } if (notYetUsableEndpoints.get() > 0) { helper.getMetricRecorder() .addLongCounter(ENDPOINT_WEIGHT_NOT_YET_USEABLE_COUNTER, notYetUsableEndpoints.get(), - ImmutableList.of(helper.getChannelTarget()), ImmutableList.of(locality)); + ImmutableList.of(helper.getChannelTarget()), + ImmutableList.of(locality, backendService)); } boolean weightsEffective = picker.updateWeight(newWeights); if (!weightsEffective) { helper.getMetricRecorder() .addLongCounter(RR_FALLBACK_COUNTER, 1, ImmutableList.of(helper.getChannelTarget()), - ImmutableList.of(locality)); + ImmutableList.of(locality, backendService)); } } diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index 09a1abb36e0..7df0630b779 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -50,6 +50,7 @@ import io.grpc.LoadBalancerRegistry; import io.grpc.ManagedChannel; import io.grpc.Metadata; +import io.grpc.NameResolver; import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.SynchronizationContext; @@ -198,6 +199,7 @@ public void handleResolvedAddresses_propagateToChildPolicy() { assertThat(childBalancer.config).isSameInstanceAs(weightedTargetConfig); assertThat(childBalancer.attributes.get(XdsAttributes.XDS_CLIENT_POOL)) .isSameInstanceAs(xdsClientPool); + assertThat(childBalancer.attributes.get(NameResolver.ATTR_BACKEND_SERVICE)).isEqualTo(CLUSTER); } /** diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 9ab783223d9..bf23c65def4 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -58,6 +58,7 @@ import io.grpc.Metadata; import io.grpc.MetricRecorder; import io.grpc.MetricSink; +import io.grpc.NameResolver; import io.grpc.NoopMetricSink; import io.grpc.ServerCall; import io.grpc.ServerServiceDefinition; @@ -161,6 +162,7 @@ public void uncaughtException(Thread t, Throwable e) { private String channelTarget = "channel-target"; private String locality = "locality"; + private String backendService = "the-backend-service"; public WeightedRoundRobinLoadBalancerTest() { testHelperInstance = new TestHelper(); @@ -1119,7 +1121,9 @@ public void removingAddressShutsdownSubchannel() { public void metrics() { // Give WRR some valid addresses to work with. Attributes attributesWithLocality = Attributes.newBuilder() - .set(WeightedTargetLoadBalancer.CHILD_NAME, locality).build(); + .set(WeightedTargetLoadBalancer.CHILD_NAME, locality) + .set(NameResolver.ATTR_BACKEND_SERVICE, backendService) + .build(); syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() .setAddresses(servers).setLoadBalancingPolicyConfig(weightedConfig) .setAttributes(attributesWithLocality).build())); @@ -1269,7 +1273,7 @@ public void metricWithRealChannel() throws Exception { argThat((instr) -> instr.getName().equals("grpc.lb.wrr.rr_fallback")), eq(1L), eq(Arrays.asList("directaddress:///wrr-metrics")), - eq(Arrays.asList(""))); + eq(Arrays.asList("", ""))); } // Verifies that the MetricRecorder has been called to record a long counter value of 1 for the @@ -1281,7 +1285,10 @@ private void verifyLongCounterRecord(String name, int times, long value) { public boolean matches(LongCounterMetricInstrument longCounterInstrument) { return longCounterInstrument.getName().equals(name); } - }), eq(value), eq(Lists.newArrayList(channelTarget)), eq(Lists.newArrayList(locality))); + }), + eq(value), + eq(Lists.newArrayList(channelTarget)), + eq(Lists.newArrayList(locality, backendService))); } // Verifies that the MetricRecorder has been called to record a given double histogram value the @@ -1293,7 +1300,10 @@ private void verifyDoubleHistogramRecord(String name, int times, double value) { public boolean matches(DoubleHistogramMetricInstrument doubleHistogramInstrument) { return doubleHistogramInstrument.getName().equals(name); } - }), eq(value), eq(Lists.newArrayList(channelTarget)), eq(Lists.newArrayList(locality))); + }), + eq(value), + eq(Lists.newArrayList(channelTarget)), + eq(Lists.newArrayList(locality, backendService))); } private int getNumFilteredPendingTasks() { From 6cd007d0d0603187224231079107ee79e2b0a539 Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:04:51 +0000 Subject: [PATCH 250/591] xds: add the missing xds.authority metric (#12018) This completes the [XDS client metrics](https://github.com/grpc/proposal/blob/master/A78-grpc-metrics-wrr-pf-xds.md#xdsclient) by adding the remaining grpc.xds.authority metric. --- .../grpc/xds/XdsClientMetricReporterImpl.java | 36 ++++++++--- .../java/io/grpc/xds/client/XdsClient.java | 17 ++++++ .../io/grpc/xds/client/XdsClientImpl.java | 19 +----- .../xds/XdsClientMetricReporterImplTest.java | 59 +++++++++++-------- 4 files changed, 80 insertions(+), 51 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java b/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java index 0b592eb019e..5cfba11c065 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java +++ b/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java @@ -90,7 +90,7 @@ final class XdsClientMetricReporterImpl implements XdsClientMetricReporter { Arrays.asList("grpc.target", "grpc.xds.server"), Collections.emptyList(), false); RESOURCES_GAUGE = metricInstrumentRegistry.registerLongGauge("grpc.xds_client.resources", "EXPERIMENTAL. Number of xDS resources.", "{resource}", - Arrays.asList("grpc.target", "grpc.xds.cache_state", + Arrays.asList("grpc.target", "grpc.xds.authority", "grpc.xds.cache_state", "grpc.xds.resource_type"), Collections.emptyList(), false); } @@ -161,15 +161,32 @@ private void computeAndReportResourceCounts( for (Map.Entry, Map> metadataByTypeEntry : metadataByType.entrySet()) { XdsResourceType type = metadataByTypeEntry.getKey(); + Map resources = metadataByTypeEntry.getValue(); - Map resourceCountsByState = new HashMap<>(); - for (ResourceMetadata metadata : metadataByTypeEntry.getValue().values()) { + Map> resourceCountsByAuthorityAndState = new HashMap<>(); + for (Map.Entry resourceEntry : resources.entrySet()) { + String resourceName = resourceEntry.getKey(); + ResourceMetadata metadata = resourceEntry.getValue(); + String authority = XdsClient.getAuthorityFromResourceName(resourceName); String cacheState = cacheStateFromResourceStatus(metadata.getStatus(), metadata.isCached()); - resourceCountsByState.compute(cacheState, (k, v) -> (v == null) ? 1 : v + 1); + resourceCountsByAuthorityAndState + .computeIfAbsent(authority, k -> new HashMap<>()) + .merge(cacheState, 1L, Long::sum); } - resourceCountsByState.forEach((cacheState, count) -> - callback.reportResourceCountGauge(count, cacheState, type.typeUrl())); + // Report metrics + for (Map.Entry> authorityEntry + : resourceCountsByAuthorityAndState.entrySet()) { + String authority = authorityEntry.getKey(); + Map stateCounts = authorityEntry.getValue(); + + for (Map.Entry stateEntry : stateCounts.entrySet()) { + String cacheState = stateEntry.getKey(); + Long count = stateEntry.getValue(); + + callback.reportResourceCountGauge(count, authority, cacheState, type.typeUrl()); + } + } } } @@ -199,11 +216,12 @@ static final class MetricReporterCallback implements ServerConnectionCallback { this.target = target; } - // TODO(dnvindhya): include the "authority" label once xds.authority is available. - void reportResourceCountGauge(long resourceCount, String cacheState, + void reportResourceCountGauge(long resourceCount, String authority, String cacheState, String resourceType) { + // authority = #old, for non-xdstp resource names recorder.recordLongGauge(RESOURCES_GAUGE, resourceCount, - Arrays.asList(target, cacheState, resourceType), Collections.emptyList()); + Arrays.asList(target, authority == null ? "#old" : authority, cacheState, resourceType), + Collections.emptyList()); } @Override diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClient.java b/xds/src/main/java/io/grpc/xds/client/XdsClient.java index 1b53f6778c7..edbb0b2d74c 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClient.java @@ -118,6 +118,23 @@ public static String percentEncodePath(String input) { return Joiner.on('/').join(encodedSegs); } + /** + * Returns the authority from the resource name. + */ + public static String getAuthorityFromResourceName(String resourceNames) { + String authority; + if (resourceNames.startsWith(XDSTP_SCHEME)) { + URI uri = URI.create(resourceNames); + authority = uri.getAuthority(); + if (authority == null) { + authority = ""; + } + } else { + authority = null; + } + return authority; + } + public interface ResourceUpdate {} /** diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index 034779ed023..4de8ead7c0a 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static io.grpc.xds.client.Bootstrapper.XDSTP_SCHEME; import static io.grpc.xds.client.XdsResourceType.ParsedResource; import static io.grpc.xds.client.XdsResourceType.ValidatedResourceUpdate; @@ -43,7 +42,6 @@ import io.grpc.xds.client.XdsClient.ResourceStore; import io.grpc.xds.client.XdsLogger.XdsLogLevel; import java.io.IOException; -import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -530,21 +528,6 @@ public Map getServerLrsClientMap() { return ImmutableMap.copyOf(serverLrsClientMap); } - private String getAuthority(String resource) { - String authority; - if (resource.startsWith(XDSTP_SCHEME)) { - URI uri = URI.create(resource); - authority = uri.getAuthority(); - if (authority == null) { - authority = ""; - } - } else { - authority = null; - } - - return authority; - } - @Nullable private ImmutableList getServerInfos(String authority) { if (authority != null) { @@ -698,7 +681,7 @@ private final class ResourceSubscriber { syncContext.throwIfNotInThisSynchronizationContext(); this.type = type; this.resource = resource; - this.authority = getAuthority(resource); + this.authority = getAuthorityFromResourceName(resource); if (getServerInfos(authority) == null) { this.errorDescription = "Wrong configuration: xds server does not exist for resource " + resource; diff --git a/xds/src/test/java/io/grpc/xds/XdsClientMetricReporterImplTest.java b/xds/src/test/java/io/grpc/xds/XdsClientMetricReporterImplTest.java index df5ab87a1c0..509a0025b7b 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientMetricReporterImplTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientMetricReporterImplTest.java @@ -76,6 +76,7 @@ public class XdsClientMetricReporterImplTest { private static final String target = "test-target"; + private static final String authority = "test-authority"; private static final String server = "trafficdirector.googleapis.com"; private static final String resourceTypeUrl = "resourceTypeUrl.googleapis.com/envoy.config.cluster.v3.Cluster"; @@ -101,7 +102,6 @@ public void setUp() { @Test public void reportResourceUpdates() { - // TODO(dnvindhya): add the "authority" label once available. reporter.reportResourceUpdates(10, 5, server, resourceTypeUrl); verify(mockMetricRecorder).addLongCounter( eqMetricInstrumentName("grpc.xds_client.resource_updates_valid"), eq((long) 10), @@ -175,8 +175,8 @@ public void setXdsClient_reportCallbackMetrics_resourceCountsFails() { public void metricGauges() { SettableFuture future = SettableFuture.create(); future.set(null); - when(mockXdsClient.getSubscribedResourcesMetadataSnapshot()).thenReturn(Futures.immediateFuture( - ImmutableMap.of())); + when(mockXdsClient.getSubscribedResourcesMetadataSnapshot()) + .thenReturn(Futures.immediateFuture(ImmutableMap.of())); when(mockXdsClient.reportServerConnections(any(ServerConnectionCallback.class))) .thenReturn(future); reporter.setXdsClient(mockXdsClient); @@ -199,13 +199,15 @@ public void metricGauges() { // Verify that reportResourceCounts and reportServerConnections were called // with the captured callback - callback.reportResourceCountGauge(10, "acked", resourceTypeUrl); + callback.reportResourceCountGauge(10, "MrPotatoHead", + "acked", resourceTypeUrl); inOrder.verify(mockBatchRecorder) .recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), eq(10L), any(), any()); callback.reportServerConnectionGauge(true, "xdsServer"); inOrder.verify(mockBatchRecorder) - .recordLongGauge(eqMetricInstrumentName("grpc.xds_client.connected"), eq(1L), any(), any()); + .recordLongGauge(eqMetricInstrumentName("grpc.xds_client.connected"), + eq(1L), any(), any()); inOrder.verifyNoMoreInteractions(); } @@ -222,10 +224,10 @@ public void metricReporterCallback() { eq(Lists.newArrayList())); String cacheState = "requested"; - callback.reportResourceCountGauge(10, cacheState, resourceTypeUrl); + callback.reportResourceCountGauge(10, authority, cacheState, resourceTypeUrl); verify(mockBatchRecorder, times(1)).recordLongGauge( eqMetricInstrumentName("grpc.xds_client.resources"), eq(10L), - eq(Arrays.asList(target, cacheState, resourceTypeUrl)), + eq(Arrays.asList(target, authority, cacheState, resourceTypeUrl)), eq(Collections.emptyList())); } @@ -236,32 +238,31 @@ public void reportCallbackMetrics_computeAndReportResourceCounts() { XdsResourceType routeConfigResource = XdsRouteConfigureResource.getInstance(); XdsResourceType clusterResource = XdsClusterResource.getInstance(); - Any rawListener = - Any.pack(Listener.newBuilder().setName("listener.googleapis.com").build()); + Any rawListener = Any.pack(Listener.newBuilder().setName("listener.googleapis.com").build()); long nanosLastUpdate = 1577923199_606042047L; Map ldsResourceMetadataMap = new HashMap<>(); - ldsResourceMetadataMap.put("resource1", + ldsResourceMetadataMap.put("xdstp://authority1", ResourceMetadata.newResourceMetadataRequested()); - ResourceMetadata ackedLdsResource = ResourceMetadata.newResourceMetadataAcked(rawListener, "42", - nanosLastUpdate); + ResourceMetadata ackedLdsResource = + ResourceMetadata.newResourceMetadataAcked(rawListener, "42", nanosLastUpdate); ldsResourceMetadataMap.put("resource2", ackedLdsResource); ldsResourceMetadataMap.put("resource3", ResourceMetadata.newResourceMetadataAcked(rawListener, "43", nanosLastUpdate)); - ldsResourceMetadataMap.put("resource4", - ResourceMetadata.newResourceMetadataNacked(ackedLdsResource, "44", nanosLastUpdate, - "nacked after previous ack", true)); + ldsResourceMetadataMap.put("xdstp:/no_authority", + ResourceMetadata.newResourceMetadataNacked(ackedLdsResource, "44", + nanosLastUpdate, "nacked after previous ack", true)); Map rdsResourceMetadataMap = new HashMap<>(); ResourceMetadata requestedRdsResourceMetadata = ResourceMetadata.newResourceMetadataRequested(); - rdsResourceMetadataMap.put("resource5", + rdsResourceMetadataMap.put("xdstp://authority5", ResourceMetadata.newResourceMetadataNacked(requestedRdsResourceMetadata, "24", nanosLastUpdate, "nacked after request", false)); - rdsResourceMetadataMap.put("resource6", + rdsResourceMetadataMap.put("xdstp://authority6", ResourceMetadata.newResourceMetadataDoesNotExist()); Map cdsResourceMetadataMap = new HashMap<>(); - cdsResourceMetadataMap.put("resource7", ResourceMetadata.newResourceMetadataUnknown()); + cdsResourceMetadataMap.put("xdstp://authority7", ResourceMetadata.newResourceMetadataUnknown()); metadataByType.put(listenerResource, ldsResourceMetadataMap); metadataByType.put(routeConfigResource, rdsResourceMetadataMap); @@ -281,24 +282,34 @@ public void reportCallbackMetrics_computeAndReportResourceCounts() { // LDS resource requested verify(mockBatchRecorder).recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), - eq(1L), eq(Arrays.asList(target, "requested", listenerResource.typeUrl())), any()); + eq(1L), + eq(Arrays.asList(target, "authority1", "requested", listenerResource.typeUrl())), any()); // LDS resources acked + // authority = #old, for non-xdstp resource names verify(mockBatchRecorder).recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), - eq(2L), eq(Arrays.asList(target, "acked", listenerResource.typeUrl())), any()); + eq(2L), + eq(Arrays.asList(target, "#old", "acked", listenerResource.typeUrl())), any()); // LDS resource nacked but cached + // "" for missing authority in the resource name verify(mockBatchRecorder).recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), - eq(1L), eq(Arrays.asList(target, "nacked_but_cached", listenerResource.typeUrl())), any()); + eq(1L), + eq(Arrays.asList(target, "", "nacked_but_cached", listenerResource.typeUrl())), any()); // RDS resource nacked verify(mockBatchRecorder).recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), - eq(1L), eq(Arrays.asList(target, "nacked", routeConfigResource.typeUrl())), any()); + eq(1L), + eq(Arrays.asList(target, "authority5", "nacked", routeConfigResource.typeUrl())), any()); // RDS resource does not exist verify(mockBatchRecorder).recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), - eq(1L), eq(Arrays.asList(target, "does_not_exist", routeConfigResource.typeUrl())), any()); + eq(1L), + eq(Arrays.asList(target, "authority6", "does_not_exist", routeConfigResource.typeUrl())), + any()); // CDS resource unknown verify(mockBatchRecorder).recordLongGauge(eqMetricInstrumentName("grpc.xds_client.resources"), - eq(1L), eq(Arrays.asList(target, "unknown", clusterResource.typeUrl())), any()); + eq(1L), + eq(Arrays.asList(target, "authority7", "unknown", clusterResource.typeUrl())), + any()); verifyNoMoreInteractions(mockBatchRecorder); } From 7952afdd561e6a3f1b9130e753512261476b60f1 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Wed, 23 Apr 2025 13:12:24 +0000 Subject: [PATCH 251/591] Add some documentation to StatusOr.equals regarding how underlying statuses are compared, to avoid any confusion, as suggested in issue #11949. (#12036) Add some documentation to StatusOr.equals regarding how underlying statuses are compared, to avoid any confusion, as suggested in issue #11949. --- api/src/main/java/io/grpc/StatusOr.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/src/main/java/io/grpc/StatusOr.java b/api/src/main/java/io/grpc/StatusOr.java index 8a88d9e62d0..0f1e9da3c75 100644 --- a/api/src/main/java/io/grpc/StatusOr.java +++ b/api/src/main/java/io/grpc/StatusOr.java @@ -66,6 +66,14 @@ public Status getStatus() { return status == null ? Status.OK : status; } + /** + * Note that StatusOr containing statuses, the equality comparision is delegated to + * {@link Status#equals} which just does a reference equality check because equality on + * Statuses is not well defined. + * Instead, do comparison based on their Code with {@link Status#getCode}. The description and + * cause of the Status are unlikely to be stable, and additional fields may be added to Status + * in the future. + */ @Override public boolean equals(Object other) { if (!(other instanceof StatusOr)) { From 25199e9df9b7f3f0db664883abb103a6abbfe120 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 23 Apr 2025 09:18:08 -0700 Subject: [PATCH 252/591] xds: XdsDepManager should ignore updates after shutdown This prevents a NPE and subsequent channel panic when trying to build a config (because there are no watchers, so waitingOnResource==false) without any listener and route. ``` java.lang.NullPointerException: Cannot invoke "io.grpc.xds.XdsDependencyManager$RdsUpdateSupplier.getRdsUpdate()" because "routeSource" is null at io.grpc.xds.XdsDependencyManager.buildUpdate(XdsDependencyManager.java:295) at io.grpc.xds.XdsDependencyManager.maybePublishConfig(XdsDependencyManager.java:266) at io.grpc.xds.XdsDependencyManager$EdsWatcher.onChanged(XdsDependencyManager.java:899) at io.grpc.xds.XdsDependencyManager$EdsWatcher.onChanged(XdsDependencyManager.java:888) at io.grpc.xds.client.XdsClientImpl$ResourceSubscriber.notifyWatcher(XdsClientImpl.java:929) at io.grpc.xds.client.XdsClientImpl$ResourceSubscriber.lambda$onData$0(XdsClientImpl.java:837) at io.grpc.SynchronizationContext.drain(SynchronizationContext.java:96) ``` I think this fully-fixes the problem today, but not tomorrow. subscribeToCluster() is racy as well, but not yet used. This was noticed when idleTimeout was firing, with some other code calling getState(true) to wake the channel back up. That may have made this panic more visible than it would be otherwise, but that has not been investigated. b/412474567 --- .../io/grpc/xds/XdsDependencyManager.java | 16 ++ .../io/grpc/xds/XdsDependencyManagerTest.java | 137 +++++++++++++++++- 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index 8cd3119727d..d804954ecf9 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -199,6 +199,7 @@ private void shutdownWatchersForType(TypeWatchers for (Map.Entry> watcherEntry : watchers.watchers.entrySet()) { xdsClient.cancelXdsResourceWatch(watchers.resourceType, watcherEntry.getKey(), watcherEntry.getValue()); + watcherEntry.getValue().cancelled = true; } } @@ -591,6 +592,9 @@ private XdsWatcherBase(XdsResourceType type, String resourceName) { @Override public void onError(Status error) { checkNotNull(error, "error"); + if (cancelled) { + return; + } // Don't update configuration on error, if we've already received configuration if (!hasDataValue()) { setDataAsStatus(Status.UNAVAILABLE.withDescription( @@ -659,6 +663,9 @@ private LdsWatcher(String resourceName) { @Override public void onChanged(XdsListenerResource.LdsUpdate update) { checkNotNull(update, "update"); + if (cancelled) { + return; + } HttpConnectionManager httpConnectionManager = update.httpConnectionManager(); List virtualHosts; @@ -787,6 +794,9 @@ public RdsWatcher(String resourceName) { @Override public void onChanged(RdsUpdate update) { checkNotNull(update, "update"); + if (cancelled) { + return; + } List oldVirtualHosts = hasDataValue() ? getData().getValue().virtualHosts : Collections.emptyList(); @@ -815,6 +825,9 @@ private class CdsWatcher extends XdsWatcherBase { @Override public void onChanged(XdsClusterResource.CdsUpdate update) { checkNotNull(update, "update"); + if (cancelled) { + return; + } switch (update.clusterType()) { case EDS: setData(update); @@ -895,6 +908,9 @@ private EdsWatcher(String resourceName, CdsWatcher parentContext) { @Override public void onChanged(XdsEndpointResource.EdsUpdate update) { + if (cancelled) { + return; + } setData(checkNotNull(update, "update")); maybePublishConfig(); } diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index 2af04a3aedf..1f3d8511ecc 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -41,6 +41,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.Message; import io.envoyproxy.envoy.config.cluster.v3.Cluster; import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; @@ -65,7 +66,7 @@ import io.grpc.xds.XdsConfig.XdsClusterConfig; import io.grpc.xds.XdsEndpointResource.EdsUpdate; import io.grpc.xds.client.CommonBootstrapperTestUtils; -import io.grpc.xds.client.XdsClientImpl; +import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsClientMetricReporter; import io.grpc.xds.client.XdsTransportFactory; import java.io.Closeable; @@ -115,7 +116,7 @@ public class XdsDependencyManagerTest { }); private ManagedChannel channel; - private XdsClientImpl xdsClient; + private XdsClient xdsClient; private XdsDependencyManager xdsDependencyManager; private TestWatcher xdsConfigWatcher; private Server xdsServer; @@ -715,6 +716,138 @@ public void testCdsError() throws IOException { assertThat(status.getDescription()).contains(XdsTestUtils.CLUSTER_NAME); } + @Test + public void ldsUpdateAfterShutdown() { + XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS", + ENDPOINT_HOSTNAME, ENDPOINT_PORT); + + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + + verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + + @SuppressWarnings("unchecked") + XdsClient.ResourceWatcher resourceWatcher = + mock(XdsClient.ResourceWatcher.class); + xdsClient.watchXdsResource( + XdsListenerResource.getInstance(), + serverName, + resourceWatcher, + MoreExecutors.directExecutor()); + verify(resourceWatcher, timeout(5000)).onChanged(any()); + + syncContext.execute(() -> { + // Shutdown before any updates. This will unsubscribe from XdsClient, but only after this + // Runnable returns + xdsDependencyManager.shutdown(); + + XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS2", "CDS", "EDS", + ENDPOINT_HOSTNAME, ENDPOINT_PORT); + verify(resourceWatcher, timeout(5000).times(2)).onChanged(any()); + xdsClient.cancelXdsResourceWatch( + XdsListenerResource.getInstance(), serverName, resourceWatcher); + }); + } + + @Test + public void rdsUpdateAfterShutdown() { + XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS", + ENDPOINT_HOSTNAME, ENDPOINT_PORT); + + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + + verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + + @SuppressWarnings("unchecked") + XdsClient.ResourceWatcher resourceWatcher = + mock(XdsClient.ResourceWatcher.class); + xdsClient.watchXdsResource( + XdsRouteConfigureResource.getInstance(), + "RDS", + resourceWatcher, + MoreExecutors.directExecutor()); + verify(resourceWatcher, timeout(5000)).onChanged(any()); + + syncContext.execute(() -> { + // Shutdown before any updates. This will unsubscribe from XdsClient, but only after this + // Runnable returns + xdsDependencyManager.shutdown(); + + XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS2", "EDS", + ENDPOINT_HOSTNAME, ENDPOINT_PORT); + verify(resourceWatcher, timeout(5000).times(2)).onChanged(any()); + xdsClient.cancelXdsResourceWatch( + XdsRouteConfigureResource.getInstance(), serverName, resourceWatcher); + }); + } + + @Test + public void cdsUpdateAfterShutdown() { + XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS", + ENDPOINT_HOSTNAME, ENDPOINT_PORT); + + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + + verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + + @SuppressWarnings("unchecked") + XdsClient.ResourceWatcher resourceWatcher = + mock(XdsClient.ResourceWatcher.class); + xdsClient.watchXdsResource( + XdsClusterResource.getInstance(), + "CDS", + resourceWatcher, + MoreExecutors.directExecutor()); + verify(resourceWatcher, timeout(5000)).onChanged(any()); + + syncContext.execute(() -> { + // Shutdown before any updates. This will unsubscribe from XdsClient, but only after this + // Runnable returns + xdsDependencyManager.shutdown(); + + XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS2", + ENDPOINT_HOSTNAME, ENDPOINT_PORT); + verify(resourceWatcher, timeout(5000).times(2)).onChanged(any()); + xdsClient.cancelXdsResourceWatch( + XdsClusterResource.getInstance(), serverName, resourceWatcher); + }); + } + + @Test + public void edsUpdateAfterShutdown() { + XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS", + ENDPOINT_HOSTNAME, ENDPOINT_PORT); + + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + + verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + + @SuppressWarnings("unchecked") + XdsClient.ResourceWatcher resourceWatcher = + mock(XdsClient.ResourceWatcher.class); + xdsClient.watchXdsResource( + XdsEndpointResource.getInstance(), + "EDS", + resourceWatcher, + MoreExecutors.directExecutor()); + verify(resourceWatcher, timeout(5000)).onChanged(any()); + + syncContext.execute(() -> { + // Shutdown before any updates. This will unsubscribe from XdsClient, but only after this + // Runnable returns + xdsDependencyManager.shutdown(); + + XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS", + ENDPOINT_HOSTNAME + "2", ENDPOINT_PORT); + verify(resourceWatcher, timeout(5000).times(2)).onChanged(any()); + xdsClient.cancelXdsResourceWatch( + XdsEndpointResource.getInstance(), serverName, resourceWatcher); + }); + } + private Listener buildInlineClientListener(String rdsName, String clusterName) { return XdsTestUtils.buildInlineClientListener(rdsName, clusterName, serverName); } From 12aaf88d86390b79e95cba3f4768f5a950807bda Mon Sep 17 00:00:00 2001 From: Kim Jin Young Date: Tue, 6 May 2025 02:02:31 +0900 Subject: [PATCH 253/591] Fix comment's typo (#12045) --- api/src/main/java/io/grpc/InternalConfigSelector.java | 2 +- core/src/main/java/io/grpc/internal/RetriableStream.java | 2 +- xds/src/main/java/io/grpc/xds/package-info.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/io/grpc/InternalConfigSelector.java b/api/src/main/java/io/grpc/InternalConfigSelector.java index 38856f440b4..a63009361d4 100644 --- a/api/src/main/java/io/grpc/InternalConfigSelector.java +++ b/api/src/main/java/io/grpc/InternalConfigSelector.java @@ -35,7 +35,7 @@ public abstract class InternalConfigSelector { = Attributes.Key.create("internal:io.grpc.config-selector"); // Use PickSubchannelArgs for SelectConfigArgs for now. May change over time. - /** Selects the config for an PRC. */ + /** Selects the config for an RPC. */ public abstract Result selectConfig(LoadBalancer.PickSubchannelArgs args); public static final class Result { diff --git a/core/src/main/java/io/grpc/internal/RetriableStream.java b/core/src/main/java/io/grpc/internal/RetriableStream.java index 7765408a627..6d33c2eb3e7 100644 --- a/core/src/main/java/io/grpc/internal/RetriableStream.java +++ b/core/src/main/java/io/grpc/internal/RetriableStream.java @@ -382,7 +382,7 @@ public void runWith(Substream substream) { } } - /** Starts the first PRC attempt. */ + /** Starts the first RPC attempt. */ @Override public final void start(ClientStreamListener listener) { masterListener = listener; diff --git a/xds/src/main/java/io/grpc/xds/package-info.java b/xds/src/main/java/io/grpc/xds/package-info.java index 74fa88cfe38..9cc15cd5449 100644 --- a/xds/src/main/java/io/grpc/xds/package-info.java +++ b/xds/src/main/java/io/grpc/xds/package-info.java @@ -15,7 +15,7 @@ */ /** - * Library for gPRC proxyless service mesh using Envoy xDS protocol. + * Library for gRPC proxyless service mesh using Envoy xDS protocol. * *

    The package currently includes a name resolver plugin and a family of load balancer plugins. * A gRPC channel for a target with {@code "xds:"} scheme will load the plugins and a From be0247f501f5f96ad83b4ab6013ab1e69aba591d Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 5 May 2025 13:16:28 -0700 Subject: [PATCH 254/591] Bump com.google.protobuf gradle plugin to 0.9.5 The plugin now outputs to "generated/sources". The IDE configuration explicitly adding the folders to the source sets hasn't been needed for some years. --- README.md | 4 ++-- build.gradle | 2 +- compiler/build.gradle | 4 ++-- examples/android/clientcache/build.gradle | 2 +- examples/android/helloworld/build.gradle | 2 +- examples/android/routeguide/build.gradle | 2 +- examples/android/strictmode/build.gradle | 2 +- examples/build.gradle | 12 +----------- examples/example-alts/build.gradle | 12 +----------- examples/example-debug/build.gradle | 2 +- examples/example-dualstack/build.gradle | 2 +- examples/example-gauth/build.gradle | 12 +----------- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-jwt-auth/build.gradle | 12 +----------- examples/example-oauth/build.gradle | 12 +----------- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 12 +----------- examples/example-tls/build.gradle | 12 +----------- examples/example-xds/build.gradle | 2 +- protobuf-lite/build.gradle | 2 +- settings.gradle | 2 +- 25 files changed, 27 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index d1b4d516f74..30e26bc9955 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ For non-Android protobuf-based codegen integrated with the Gradle build system, you can use [protobuf-gradle-plugin][]: ```gradle plugins { - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' } protobuf { @@ -185,7 +185,7 @@ use protobuf-gradle-plugin but specify the 'lite' options: ```gradle plugins { - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' } protobuf { diff --git a/build.gradle b/build.gradle index 42e16fbfef9..0788895cdfa 100644 --- a/build.gradle +++ b/build.gradle @@ -151,7 +151,7 @@ subprojects { appendToProperty( it.options.errorprone.excludedPaths, ".*/src/generated/[^/]+/java/.*" + - "|.*/build/generated/source/proto/[^/]+/java/.*", + "|.*/build/generated/sources/proto/[^/]+/java/.*", "|") } } diff --git a/compiler/build.gradle b/compiler/build.gradle index 3c8e9358401..3d31d602642 100644 --- a/compiler/build.gradle +++ b/compiler/build.gradle @@ -152,7 +152,7 @@ dependencies { } tasks.named("compileTestJava").configure { - options.errorprone.excludedPaths = ".*/build/generated/source/proto/.*" + options.errorprone.excludedPaths = ".*/build/generated/sources/proto/.*" } tasks.named("compileTestLiteJava").configure { @@ -160,7 +160,7 @@ tasks.named("compileTestLiteJava").configure { options.compilerArgs += [ "-Xlint:-cast" ] - options.errorprone.excludedPaths = ".*/build/generated/source/proto/.*" + options.errorprone.excludedPaths = ".*/build/generated/sources/proto/.*" } tasks.named("checkstyleTestLite").configure { diff --git a/examples/android/clientcache/build.gradle b/examples/android/clientcache/build.gradle index 67d25905bbc..6db6a9bced1 100644 --- a/examples/android/clientcache/build.gradle +++ b/examples/android/clientcache/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:7.4.0' - classpath "com.google.protobuf:protobuf-gradle-plugin:0.9.4" + classpath "com.google.protobuf:protobuf-gradle-plugin:0.9.5" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/examples/android/helloworld/build.gradle b/examples/android/helloworld/build.gradle index 67d25905bbc..6db6a9bced1 100644 --- a/examples/android/helloworld/build.gradle +++ b/examples/android/helloworld/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:7.4.0' - classpath "com.google.protobuf:protobuf-gradle-plugin:0.9.4" + classpath "com.google.protobuf:protobuf-gradle-plugin:0.9.5" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/examples/android/routeguide/build.gradle b/examples/android/routeguide/build.gradle index fd058a5d68e..8fc1d293228 100644 --- a/examples/android/routeguide/build.gradle +++ b/examples/android/routeguide/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:7.4.0' - classpath "com.google.protobuf:protobuf-gradle-plugin:0.9.4" + classpath "com.google.protobuf:protobuf-gradle-plugin:0.9.5" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/examples/android/strictmode/build.gradle b/examples/android/strictmode/build.gradle index 67d25905bbc..6db6a9bced1 100644 --- a/examples/android/strictmode/build.gradle +++ b/examples/android/strictmode/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:7.4.0' - classpath "com.google.protobuf:protobuf-gradle-plugin:0.9.4" + classpath "com.google.protobuf:protobuf-gradle-plugin:0.9.5" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/examples/build.gradle b/examples/build.gradle index 206fd38e0e3..7d0fc0c715a 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -1,7 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' } @@ -52,16 +52,6 @@ protobuf { } } -// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code. -sourceSets { - main { - java { - srcDirs 'build/generated/source/proto/main/grpc' - srcDirs 'build/generated/source/proto/main/java' - } - } -} - startScripts.enabled = false // Creates start scripts for a class name and adds it to the distribution. The diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index c7d804973a5..3b76948b5c7 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -1,7 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' } @@ -40,16 +40,6 @@ protobuf { } } -// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code. -sourceSets { - main { - java { - srcDirs 'build/generated/source/proto/main/grpc' - srcDirs 'build/generated/source/proto/main/java' - } - } -} - startScripts.enabled = false diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index c2449833cdc..69b17859d75 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -2,7 +2,7 @@ plugins { id 'application' // Provide convenience executables for trying out the examples. id 'java' - id "com.google.protobuf" version "0.9.4" + id "com.google.protobuf" version "0.9.5" // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index 7a37c46b536..f769894cd9b 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -2,7 +2,7 @@ plugins { id 'application' // Provide convenience executables for trying out the examples. id 'java' - id "com.google.protobuf" version "0.9.4" + id "com.google.protobuf" version "0.9.5" // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 404cab907de..75c64019dde 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -1,7 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' } @@ -46,16 +46,6 @@ protobuf { } } -// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code. -sourceSets { - main { - java { - srcDirs 'build/generated/source/proto/main/grpc' - srcDirs 'build/generated/source/proto/main/java' - } - } -} - startScripts.enabled = false task googleAuthClient(type: CreateStartScripts) { diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index dcb1f254263..aca88c4003e 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -1,7 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' id 'java' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 673d3f4461d..98c3e478f66 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -1,7 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' id 'java' diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index a0b9153785e..232146c9e38 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -2,7 +2,7 @@ plugins { id 'application' // Provide convenience executables for trying out the examples. id 'java' - id "com.google.protobuf" version "0.9.4" + id "com.google.protobuf" version "0.9.5" id 'com.google.cloud.tools.jib' version '3.4.4' // For releasing to Docker Hub } diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index 0d6d095bf1d..204dcf90941 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -1,7 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' } @@ -51,16 +51,6 @@ protobuf { } } -// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code. -sourceSets { - main { - java { - srcDirs 'build/generated/source/proto/main/grpc' - srcDirs 'build/generated/source/proto/main/java' - } - } -} - startScripts.enabled = false task hellowWorldJwtAuthServer(type: CreateStartScripts) { diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 9e40c3e33f9..1bffa4272c3 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -1,7 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' } @@ -51,16 +51,6 @@ protobuf { } } -// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code. -sourceSets { - main { - java { - srcDirs 'build/generated/source/proto/main/grpc' - srcDirs 'build/generated/source/proto/main/java' - } - } -} - startScripts.enabled = false task hellowWorldOauthServer(type: CreateStartScripts) { diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index 5f98b32be60..715c3b95569 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -1,7 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' } diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index edb28e1573b..fb3475b1d60 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -1,6 +1,6 @@ plugins { id 'application' // Provide convenience executables for trying out the examples. - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' id 'java' diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index 18157b0eed1..98e1e4a4a39 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -1,6 +1,6 @@ plugins { id 'application' // Provide convenience executables for trying out the examples. - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' id 'java' diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index e66fda59e4e..dae75299e25 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' id 'war' @@ -34,13 +34,3 @@ protobuf { all()*.plugins { grpc {} } } } - -// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code. -sourceSets { - main { - java { - srcDirs 'build/generated/source/proto/main/grpc' - srcDirs 'build/generated/source/proto/main/java' - } - } -} diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index 9603e04e417..c75c9d0f9a0 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -1,7 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' } @@ -41,16 +41,6 @@ protobuf { } } -// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code. -sourceSets { - main { - java { - srcDirs 'build/generated/source/proto/main/grpc' - srcDirs 'build/generated/source/proto/main/java' - } - } -} - startScripts.enabled = false task helloWorldTlsServer(type: CreateStartScripts) { diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index 1e55f182d3a..ae67659a7fa 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -1,6 +1,6 @@ plugins { id 'application' // Provide convenience executables for trying out the examples. - id 'com.google.protobuf' version '0.9.4' + id 'com.google.protobuf' version '0.9.5' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' id 'java' diff --git a/protobuf-lite/build.gradle b/protobuf-lite/build.gradle index 16b622535e8..c1e5b51ae35 100644 --- a/protobuf-lite/build.gradle +++ b/protobuf-lite/build.gradle @@ -39,7 +39,7 @@ tasks.named("compileTestJava").configure { options.compilerArgs += [ "-Xlint:-cast" ] - options.errorprone.excludedPaths = ".*/build/generated/source/proto/.*" + options.errorprone.excludedPaths = ".*/build/generated/sources/proto/.*" } protobuf { diff --git a/settings.gradle b/settings.gradle index 96cc173635d..e30849dbe2b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,7 +13,7 @@ pluginManagement { // https://github.com/google/osdetector-gradle-plugin/tags id "com.google.osdetector" version "1.7.3" // https://github.com/google/protobuf-gradle-plugin/releases - id "com.google.protobuf" version "0.9.4" + id "com.google.protobuf" version "0.9.5" // https://github.com/GradleUp/shadow/releases // 8.3.2+ requires Java 11+ // 8.3.1 breaks apache imports for netty/shaded, fixed in 8.3.2 From 3f5fdf12663dac0d221e6b9fb4b0dfed7486a6a9 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 6 May 2025 21:44:48 -0700 Subject: [PATCH 255/591] core: Remove unnecessary SuppressWarnings("deprecation") (#12052) --- core/src/main/java/io/grpc/internal/ManagedChannelImpl.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index c5daba7bfc8..976587a6db9 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -1212,7 +1212,6 @@ private void handleInternalSubchannelState(ConnectivityStateInfo newState) { } @Override - @SuppressWarnings("deprecation") public ConnectivityState getState(boolean requestConnection) { ConnectivityState savedChannelState = channelStateManager.getState(); if (requestConnection && savedChannelState == IDLE) { @@ -1558,7 +1557,6 @@ protected ManagedChannelBuilder delegate() { checkState(!terminated, "Channel is terminated"); - @SuppressWarnings("deprecation") ResolvingOobChannelBuilder builder = new ResolvingOobChannelBuilder(); return builder From 80cc988b3c2ed8961d0df9df0af13e297c522084 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 7 May 2025 23:04:16 -0700 Subject: [PATCH 256/591] xds: Use acceptResolvedAddresses() for WeightedTarget children (#12053) Convert the tests to use acceptResolvedAddresses() as well. --- .../grpc/xds/WeightedTargetLoadBalancer.java | 8 +++-- .../xds/WeightedTargetLoadBalancerTest.java | 30 ++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancer.java index 0a11f118057..ce7427e5c2c 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancer.java @@ -87,8 +87,9 @@ public Status acceptResolvedAddressesInternal(ResolvedAddresses resolvedAddresse } } targets = newTargets; + Status status = Status.OK; for (String targetName : targets.keySet()) { - childBalancers.get(targetName).handleResolvedAddresses( + Status newStatus = childBalancers.get(targetName).acceptResolvedAddresses( resolvedAddresses.toBuilder() .setAddresses(AddressFilter.filter(resolvedAddresses.getAddresses(), targetName)) .setLoadBalancingPolicyConfig(targets.get(targetName).childConfig) @@ -96,6 +97,9 @@ public Status acceptResolvedAddressesInternal(ResolvedAddresses resolvedAddresse .set(CHILD_NAME, targetName) .build()) .build()); + if (!newStatus.isOk()) { + status = newStatus; + } } // Cleanup removed targets. @@ -108,7 +112,7 @@ public Status acceptResolvedAddressesInternal(ResolvedAddresses resolvedAddresse childBalancers.keySet().retainAll(targets.keySet()); childHelpers.keySet().retainAll(targets.keySet()); updateOverallBalancingState(); - return Status.OK; + return status; } @Override diff --git a/xds/src/test/java/io/grpc/xds/WeightedTargetLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedTargetLoadBalancerTest.java index cc6cb98412c..55ff0cd8078 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedTargetLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedTargetLoadBalancerTest.java @@ -113,6 +113,7 @@ public String getPolicyName() { public LoadBalancer newLoadBalancer(Helper helper) { childHelpers.add(helper); LoadBalancer childBalancer = mock(LoadBalancer.class); + when(childBalancer.acceptResolvedAddresses(any())).thenReturn(Status.OK); childBalancers.add(childBalancer); fooLbCreated++; return childBalancer; @@ -139,6 +140,7 @@ public String getPolicyName() { public LoadBalancer newLoadBalancer(Helper helper) { childHelpers.add(helper); LoadBalancer childBalancer = mock(LoadBalancer.class); + when(childBalancer.acceptResolvedAddresses(any())).thenReturn(Status.OK); childBalancers.add(childBalancer); barLbCreated++; return childBalancer; @@ -180,7 +182,7 @@ public void tearDown() { } @Test - public void handleResolvedAddresses() { + public void acceptResolvedAddresses() { ArgumentCaptor resolvedAddressesCaptor = ArgumentCaptor.forClass(ResolvedAddresses.class); Attributes.Key fakeKey = Attributes.Key.create("fake_key"); @@ -203,12 +205,13 @@ public void handleResolvedAddresses() { eag2 = AddressFilter.setPathFilter(eag2, ImmutableList.of("target2")); EquivalentAddressGroup eag3 = new EquivalentAddressGroup(socketAddresses[3]); eag3 = AddressFilter.setPathFilter(eag3, ImmutableList.of("target3")); - weightedTargetLb.handleResolvedAddresses( + Status status = weightedTargetLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of(eag0, eag1, eag2, eag3)) .setAttributes(Attributes.newBuilder().set(fakeKey, fakeValue).build()) .setLoadBalancingPolicyConfig(new WeightedTargetConfig(targets)) .build()); + assertThat(status.isOk()).isTrue(); verify(helper).updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult())); assertThat(childBalancers).hasSize(4); assertThat(childHelpers).hasSize(4); @@ -216,7 +219,7 @@ public void handleResolvedAddresses() { assertThat(barLbCreated).isEqualTo(2); for (int i = 0; i < childBalancers.size(); i++) { - verify(childBalancers.get(i)).handleResolvedAddresses(resolvedAddressesCaptor.capture()); + verify(childBalancers.get(i)).acceptResolvedAddresses(resolvedAddressesCaptor.capture()); ResolvedAddresses resolvedAddresses = resolvedAddressesCaptor.getValue(); assertThat(resolvedAddresses.getLoadBalancingPolicyConfig()).isEqualTo(configs[i]); assertThat(resolvedAddresses.getAttributes().get(fakeKey)).isEqualTo(fakeValue); @@ -226,6 +229,11 @@ public void handleResolvedAddresses() { .containsExactly(socketAddresses[i]); } + // Even when a child return an error from the update, the other children should still receive + // their updates. + Status acceptReturnStatus = Status.UNAVAILABLE.withDescription("Didn't like something"); + when(childBalancers.get(2).acceptResolvedAddresses(any())).thenReturn(acceptReturnStatus); + // Update new weighted target config for a typical workflow. // target0 removed. target1, target2, target3 changed weight and config. target4 added. int[] newWeights = new int[]{11, 22, 33, 44}; @@ -243,11 +251,12 @@ public void handleResolvedAddresses() { "target4", new WeightedPolicySelection( newWeights[3], newChildConfig(fooLbProvider, newConfigs[3]))); - weightedTargetLb.handleResolvedAddresses( + status = weightedTargetLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(new WeightedTargetConfig(newTargets)) .build()); + assertThat(status.getCode()).isEqualTo(acceptReturnStatus.getCode()); verify(helper, atLeast(2)) .updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult())); assertThat(childBalancers).hasSize(5); @@ -258,7 +267,7 @@ public void handleResolvedAddresses() { verify(childBalancers.get(0)).shutdown(); for (int i = 1; i < childBalancers.size(); i++) { verify(childBalancers.get(i), atLeastOnce()) - .handleResolvedAddresses(resolvedAddressesCaptor.capture()); + .acceptResolvedAddresses(resolvedAddressesCaptor.capture()); assertThat(resolvedAddressesCaptor.getValue().getLoadBalancingPolicyConfig()) .isEqualTo(newConfigs[i - 1]); } @@ -286,7 +295,7 @@ public void handleNameResolutionError() { "target2", weightedLbConfig2, // {foo, 40, config3} "target3", weightedLbConfig3); - weightedTargetLb.handleResolvedAddresses( + weightedTargetLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(new WeightedTargetConfig(targets)) @@ -313,7 +322,7 @@ public void balancingStateUpdatedFromChildBalancers() { "target2", weightedLbConfig2, // {foo, 40, config3} "target3", weightedLbConfig3); - weightedTargetLb.handleResolvedAddresses( + weightedTargetLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(new WeightedTargetConfig(targets)) @@ -395,7 +404,7 @@ public void raceBetweenShutdownAndChildLbBalancingStateUpdate() { Map targets = ImmutableMap.of( "target0", weightedLbConfig0, "target1", weightedLbConfig1); - weightedTargetLb.handleResolvedAddresses( + weightedTargetLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(new WeightedTargetConfig(targets)) @@ -421,7 +430,7 @@ public void noDuplicateOverallBalancingStateUpdate() { weights[0], newChildConfig(fakeLbProvider, configs[0])), "target3", new WeightedPolicySelection( weights[3], newChildConfig(fakeLbProvider, configs[3]))); - weightedTargetLb.handleResolvedAddresses( + weightedTargetLb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(new WeightedTargetConfig(targets)) @@ -470,9 +479,10 @@ static class FakeLoadBalancer extends LoadBalancer { } @Override - public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { + public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { helper.updateBalancingState( TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(Status.INTERNAL))); + return Status.OK; } @Override From 64fe061ccd0b8e57fdb54b983d58f235d568c55a Mon Sep 17 00:00:00 2001 From: John Cormie Date: Fri, 9 May 2025 14:52:12 -0700 Subject: [PATCH 257/591] Simplify RobolectricBinderSecurityTest (#12058) - Use INSTRUMENTATION_TEST @LooperMode to avoid custom Executors and idleLoopers() toil. - No android.app.Service is actually needed with Robolectric. --- .../binder/RobolectricBinderSecurityTest.java | 198 +++++------------- 1 file changed, 57 insertions(+), 141 deletions(-) diff --git a/binder/src/test/java/io/grpc/binder/RobolectricBinderSecurityTest.java b/binder/src/test/java/io/grpc/binder/RobolectricBinderSecurityTest.java index ab81fc6b6d0..16f06ad81c9 100644 --- a/binder/src/test/java/io/grpc/binder/RobolectricBinderSecurityTest.java +++ b/binder/src/test/java/io/grpc/binder/RobolectricBinderSecurityTest.java @@ -22,11 +22,6 @@ import static org.robolectric.Shadows.shadowOf; import android.app.Application; -import android.content.ComponentName; -import android.content.Intent; -import android.os.IBinder; -import android.os.Looper; -import androidx.lifecycle.LifecycleService; import androidx.test.core.app.ApplicationProvider; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -42,90 +37,117 @@ import io.grpc.ServerServiceDefinition; import io.grpc.Status; import io.grpc.StatusRuntimeException; -import io.grpc.binder.internal.MainThreadScheduledExecutorService; import io.grpc.protobuf.lite.ProtoLiteUtils; import io.grpc.stub.ClientCalls; import io.grpc.stub.ServerCalls; import java.io.IOException; import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; -import javax.annotation.Nullable; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; -import org.robolectric.android.controller.ServiceController; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; @RunWith(RobolectricTestRunner.class) +@LooperMode(Mode.INSTRUMENTATION_TEST) public final class RobolectricBinderSecurityTest { private static final String SERVICE_NAME = "fake_service"; private static final String FULL_METHOD_NAME = "fake_service/fake_method"; private final Application context = ApplicationProvider.getApplicationContext(); - private ServiceController controller; - private SomeService service; + private final ArrayBlockingQueue> statusesToSet = + new ArrayBlockingQueue<>(128); private ManagedChannel channel; + private Server server; @Before public void setUp() { - controller = Robolectric.buildService(SomeService.class); - service = controller.create().get(); + AndroidComponentAddress listenAddress = + AndroidComponentAddress.forRemoteComponent(context.getPackageName(), "HostService"); + + MethodDescriptor methodDesc = getMethodDescriptor(); + ServerCallHandler callHandler = + ServerCalls.asyncUnaryCall( + (req, respObserver) -> { + respObserver.onNext(req); + respObserver.onCompleted(); + }); + ServerMethodDefinition methodDef = + ServerMethodDefinition.create(methodDesc, callHandler); + ServerServiceDefinition def = + ServerServiceDefinition.builder(SERVICE_NAME).addMethod(methodDef).build(); + + IBinderReceiver binderReceiver = new IBinderReceiver(); + server = + BinderServerBuilder.forAddress(listenAddress, binderReceiver) + .addService(def) + .securityPolicy( + ServerSecurityPolicy.newBuilder() + .servicePolicy( + SERVICE_NAME, + new AsyncSecurityPolicy() { + @Override + public ListenableFuture checkAuthorizationAsync(int uid) { + SettableFuture status = SettableFuture.create(); + statusesToSet.add(status); + return status; + } + }) + .build()) + .build(); + try { + server.start(); + } catch (IOException e) { + throw new IllegalStateException(e); + } - AndroidComponentAddress listenAddress = AndroidComponentAddress.forContext(service); - ScheduledExecutorService executor = service.getExecutor(); + shadowOf(context) + .setComponentNameAndServiceForBindServiceForIntent( + listenAddress.asBindIntent(), + listenAddress.getComponent(), + checkNotNull(binderReceiver.get())); channel = BinderChannelBuilder.forAddress(listenAddress, context) - .executor(executor) - .scheduledExecutorService(executor) - .offloadExecutor(executor) .build(); - idleLoopers(); } @After public void tearDown() { channel.shutdownNow(); - controller.destroy(); + server.shutdownNow(); } @Test public void testAsyncServerSecurityPolicy_failed_returnsFailureStatus() throws Exception { ListenableFuture status = makeCall(); - service.setSecurityPolicyStatusWhenReady(Status.ALREADY_EXISTS); - idleLoopers(); + statusesToSet.take().set(Status.ALREADY_EXISTS); - assertThat(Futures.getDone(status).getCode()).isEqualTo(Status.Code.ALREADY_EXISTS); + assertThat(status.get().getCode()).isEqualTo(Status.Code.ALREADY_EXISTS); } @Test public void testAsyncServerSecurityPolicy_failedFuture_failsWithCodeInternal() throws Exception { ListenableFuture status = makeCall(); - service.setSecurityPolicyFailed(new IllegalStateException("oops")); - idleLoopers(); + statusesToSet.take().setException(new IllegalStateException("oops")); - assertThat(Futures.getDone(status).getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(status.get().getCode()).isEqualTo(Status.Code.INTERNAL); } @Test public void testAsyncServerSecurityPolicy_allowed_returnsOkStatus() throws Exception { ListenableFuture status = makeCall(); - service.setSecurityPolicyStatusWhenReady(Status.OK); - idleLoopers(); + statusesToSet.take().set(Status.OK); - assertThat(Futures.getDone(status).getCode()).isEqualTo(Status.Code.OK); + assertThat(status.get().getCode()).isEqualTo(Status.Code.OK); } private ListenableFuture makeCall() { - ClientCall call = - channel.newCall( - getMethodDescriptor(), CallOptions.DEFAULT.withExecutor(service.getExecutor())); + ClientCall call = channel.newCall(getMethodDescriptor(), CallOptions.DEFAULT); ListenableFuture responseFuture = ClientCalls.futureUnaryCall(call, Empty.getDefaultInstance()); - idleLoopers(); - return Futures.catching( Futures.transform(responseFuture, unused -> Status.OK, directExecutor()), StatusRuntimeException.class, @@ -133,10 +155,6 @@ private ListenableFuture makeCall() { directExecutor()); } - private static void idleLoopers() { - shadowOf(Looper.getMainLooper()).idle(); - } - private static MethodDescriptor getMethodDescriptor() { MethodDescriptor.Marshaller marshaller = ProtoLiteUtils.marshaller(Empty.getDefaultInstance()); @@ -147,106 +165,4 @@ private static MethodDescriptor getMethodDescriptor() { .setSampledToLocalTracing(true) .build(); } - - private static class SomeService extends LifecycleService { - - private final IBinderReceiver binderReceiver = new IBinderReceiver(); - private final ArrayBlockingQueue> statusesToSet = - new ArrayBlockingQueue<>(128); - private Server server; - private final ScheduledExecutorService scheduledExecutorService = - new MainThreadScheduledExecutorService(); - - @Override - public void onCreate() { - super.onCreate(); - - MethodDescriptor methodDesc = getMethodDescriptor(); - ServerCallHandler callHandler = - ServerCalls.asyncUnaryCall( - (req, respObserver) -> { - respObserver.onNext(req); - respObserver.onCompleted(); - }); - ServerMethodDefinition methodDef = - ServerMethodDefinition.create(methodDesc, callHandler); - ServerServiceDefinition def = - ServerServiceDefinition.builder(SERVICE_NAME).addMethod(methodDef).build(); - - server = - BinderServerBuilder.forAddress(AndroidComponentAddress.forContext(this), binderReceiver) - .addService(def) - .securityPolicy( - ServerSecurityPolicy.newBuilder() - .servicePolicy( - SERVICE_NAME, - new AsyncSecurityPolicy() { - @Override - public ListenableFuture checkAuthorizationAsync(int uid) { - return Futures.submitAsync( - () -> { - SettableFuture status = SettableFuture.create(); - statusesToSet.add(status); - return status; - }, - getExecutor()); - } - }) - .build()) - .executor(getExecutor()) - .scheduledExecutorService(getExecutor()) - .build(); - try { - server.start(); - } catch (IOException e) { - throw new IllegalStateException(e); - } - - Application context = ApplicationProvider.getApplicationContext(); - ComponentName componentName = new ComponentName(context, SomeService.class); - shadowOf(context) - .setComponentNameAndServiceForBindService( - componentName, checkNotNull(binderReceiver.get())); - } - - /** - * Returns an {@link ScheduledExecutorService} under which all of the gRPC computations run. The - * execution of any pending tasks on this executor can be triggered via {@link #idleLoopers()}. - */ - ScheduledExecutorService getExecutor() { - return scheduledExecutorService; - } - - void setSecurityPolicyStatusWhenReady(Status status) { - getNextEnqueuedStatus().set(status); - } - - void setSecurityPolicyFailed(Exception e) { - getNextEnqueuedStatus().setException(e); - } - - private SettableFuture getNextEnqueuedStatus() { - @Nullable SettableFuture future = statusesToSet.poll(); - while (future == null) { - // Keep idling until the future is available. - idleLoopers(); - future = statusesToSet.poll(); - } - return checkNotNull(future); - } - - @Override - public IBinder onBind(Intent intent) { - super.onBind(intent); - return checkNotNull(binderReceiver.get()); - } - - @Override - public void onDestroy() { - super.onDestroy(); - server.shutdownNow(); - } - - /** A future representing a task submitted to a {@link Handler}. */ - } } From 454f1c5c6acc46e3d93ddf434a4e77943f5f52ec Mon Sep 17 00:00:00 2001 From: John Cormie Date: Fri, 9 May 2025 14:57:59 -0700 Subject: [PATCH 258/591] binder: Create a Robolectric version of BinderTransportTest (#12057) This version runs way faster than BinderTransportTest and doesn't require an actual Android device/emulator. It'll allow future tests to simulate things that are difficult/impossible on real Android, at the price of some realism. --- .../RobolectricBinderTransportTest.java | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java diff --git a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java new file mode 100644 index 00000000000..7d336067842 --- /dev/null +++ b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.binder.internal; + +import static org.robolectric.Shadows.shadowOf; + +import android.app.Application; +import android.content.Intent; +import androidx.test.core.app.ApplicationProvider; +import io.grpc.ServerStreamTracer; +import io.grpc.binder.AndroidComponentAddress; +import io.grpc.internal.AbstractTransportTest; +import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; +import io.grpc.internal.GrpcUtil; +import io.grpc.internal.InternalServer; +import io.grpc.internal.ManagedClientTransport; +import io.grpc.internal.ObjectPool; +import io.grpc.internal.SharedResourcePool; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; + +/** + * All of the AbstractTransportTest cases applied to {@link BinderTransport} running in a + * Robolectric environment. + * + *

    Runs much faster than BinderTransportTest and doesn't require an Android device/emulator. + * Somewhat less realistic but allows simulating behavior that would be difficult or impossible with + * real Android. + * + *

    NB: Unlike most robolectric tests, we run in {@link LooperMode.Mode#INSTRUMENTATION_TEST}, + * meaning test cases don't run on the main thread. This supports the AbstractTransportTest approach + * where the test thread frequently blocks waiting for transport state changes to take effect. + */ +@RunWith(RobolectricTestRunner.class) +@LooperMode(Mode.INSTRUMENTATION_TEST) +public final class RobolectricBinderTransportTest extends AbstractTransportTest { + + private final Application application = ApplicationProvider.getApplicationContext(); + private final ObjectPool executorServicePool = + SharedResourcePool.forResource(GrpcUtil.TIMER_SERVICE); + private final ObjectPool offloadExecutorPool = + SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR); + private final ObjectPool serverExecutorPool = + SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR); + + private int nextServerAddress; + + @Override + protected InternalServer newServer(List streamTracerFactories) { + AndroidComponentAddress listenAddr = AndroidComponentAddress.forBindIntent( + new Intent() + .setClassName(application.getPackageName(), "HostService") + .setAction("io.grpc.action.BIND." + nextServerAddress++)); + + BinderServer binderServer = + new BinderServer.Builder() + .setListenAddress(listenAddr) + .setExecutorPool(serverExecutorPool) + .setExecutorServicePool(executorServicePool) + .setStreamTracerFactories(streamTracerFactories) + .build(); + + shadowOf(application) + .setComponentNameAndServiceForBindServiceForIntent( + listenAddr.asBindIntent(), listenAddr.getComponent(), binderServer.getHostBinder()); + return binderServer; + } + + @Override + protected InternalServer newServer( + int port, List streamTracerFactories) { + if (port > 0) { + // TODO: TCP ports have no place in an *abstract* transport test. Replace with SocketAddress. + throw new UnsupportedOperationException(); + } + return newServer(streamTracerFactories); + } + + @Override + protected ManagedClientTransport newClientTransport(InternalServer server) { + BinderClientTransportFactory.Builder builder = + new BinderClientTransportFactory.Builder() + .setSourceContext(application) + .setScheduledExecutorPool(executorServicePool) + .setOffloadExecutorPool(offloadExecutorPool); + + ClientTransportOptions options = new ClientTransportOptions(); + options.setEagAttributes(eagAttrs()); + options.setChannelLogger(transportLogger()); + + return new BinderTransport.BinderClientTransport( + builder.buildClientTransportFactory(), + (AndroidComponentAddress) server.getListenSocketAddress(), + options); + } + + @Override + protected String testAuthority(InternalServer server) { + return ((AndroidComponentAddress) server.getListenSocketAddress()).getAuthority(); + } + + @Test + @Ignore("See BinderTransportTest#socketStats.") + @Override + public void socketStats() {} + + @Test + @Ignore("See BinderTransportTest#flowControlPushBack") + @Override + public void flowControlPushBack() {} + + @Test + @Ignore("See BinderTransportTest#serverAlreadyListening") + @Override + public void serverAlreadyListening() {} +} From 6baac45bd238f6a4cdc4275840808c52a9162679 Mon Sep 17 00:00:00 2001 From: vinodhabib <47808007+vinodhabib@users.noreply.github.com> Date: Mon, 12 May 2025 07:14:14 +0000 Subject: [PATCH 259/591] xds: Fix pretty-print of Cluster with WrrLocality and LB policies (#12037) --- xds/src/main/java/io/grpc/xds/MessagePrinter.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/MessagePrinter.java b/xds/src/main/java/io/grpc/xds/MessagePrinter.java index 5927bfd517e..db15e961204 100644 --- a/xds/src/main/java/io/grpc/xds/MessagePrinter.java +++ b/xds/src/main/java/io/grpc/xds/MessagePrinter.java @@ -16,6 +16,7 @@ package io.grpc.xds; +import com.github.xds.type.v3.TypedStruct; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; @@ -32,6 +33,8 @@ import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBACPerRoute; import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager; +import io.envoyproxy.envoy.extensions.load_balancing_policies.round_robin.v3.RoundRobin; +import io.envoyproxy.envoy.extensions.load_balancing_policies.wrr_locality.v3.WrrLocality; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext; import io.grpc.xds.client.MessagePrettyPrinter; @@ -65,7 +68,10 @@ private static JsonFormat.Printer newPrinter() { .add(RouteConfiguration.getDescriptor()) .add(Cluster.getDescriptor()) .add(ClusterConfig.getDescriptor()) - .add(ClusterLoadAssignment.getDescriptor()); + .add(ClusterLoadAssignment.getDescriptor()) + .add(WrrLocality.getDescriptor()) + .add(TypedStruct.getDescriptor()) + .add(RoundRobin.getDescriptor()); try { @SuppressWarnings("unchecked") Class routeLookupClusterSpecifierClass = From 59adeb9d472953f1be9a25dfad91652cdc688624 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 13 May 2025 17:15:36 +0530 Subject: [PATCH 260/591] Start 1.74.0 development cycle (#12061) --- MODULE.bazel | 2 +- build.gradle | 2 +- .../src/test/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/test/golden/TestService.java.txt | 2 +- core/src/main/java/io/grpc/internal/GrpcUtil.java | 2 +- examples/MODULE.bazel | 2 +- examples/android/clientcache/app/build.gradle | 10 +++++----- examples/android/helloworld/app/build.gradle | 8 ++++---- examples/android/routeguide/app/build.gradle | 8 ++++---- examples/android/strictmode/app/build.gradle | 8 ++++---- examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 4 ++-- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 4 ++-- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 4 ++-- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 4 ++-- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 4 ++-- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- 34 files changed, 55 insertions(+), 55 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index d69bb1927aa..7fa3eab395d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "grpc-java", compatibility_level = 0, repo_name = "io_grpc_grpc_java", - version = "1.73.0-SNAPSHOT", # CURRENT_GRPC_VERSION + version = "1.74.0-SNAPSHOT", # CURRENT_GRPC_VERSION ) # GRPC_DEPS_START diff --git a/build.gradle b/build.gradle index 0788895cdfa..089c78639b4 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.73.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.74.0-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index a870a5a82e1..712be32132e 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.73.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.74.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index 1f62ca26718..e3c759639ec 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.73.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.74.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index 15fcdfb6300..1434afa884c 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -219,7 +219,7 @@ public byte[] parseAsciiString(byte[] serialized) { public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); - public static final String IMPLEMENTATION_VERSION = "1.73.0-SNAPSHOT"; // CURRENT_GRPC_VERSION + public static final String IMPLEMENTATION_VERSION = "1.74.0-SNAPSHOT"; // CURRENT_GRPC_VERSION /** * The default timeout in nanos for a keepalive ping request. diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index b960555e8c5..dd0ae167171 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,5 +1,5 @@ bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") -bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.73.0-SNAPSHOT") # CURRENT_GRPC_VERSION +bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.74.0-SNAPSHOT") # CURRENT_GRPC_VERSION bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") bazel_dep(name = "rules_jvm_external", version = "6.0") diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index 670193167fe..1530cfb01d6 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -34,7 +34,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -54,12 +54,12 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.5' - testImplementation 'io.grpc:grpc-testing:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + testImplementation 'io.grpc:grpc-testing:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index 81f79c67440..66ef6bd9a14 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index 12e6430d2ad..7c0527237e1 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index f37e970ed7b..a48ad08e4db 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -33,7 +33,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -53,8 +53,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/build.gradle b/examples/build.gradle index 7d0fc0c715a..b6ea92e6c50 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index 3b76948b5c7..b66f3d3ab76 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index 69b17859d75..ec027796185 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index ce46b13c019..477dfe56a64 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -6,13 +6,13 @@ jar - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT example-debug https://github.com/grpc/grpc-java UTF-8 - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index f769894cd9b..9793567f10c 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index 4af44beae46..b556e95d900 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -6,13 +6,13 @@ jar - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT example-dualstack https://github.com/grpc/grpc-java UTF-8 - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 75c64019dde..037758b14c6 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index b8f8d4d930e..b6e1d64a745 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,13 +6,13 @@ jar - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index aca88c4003e..b6e3d1307ab 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 98c3e478f66..bf4c43627da 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 232146c9e38..050cb9592fb 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index f0aab1c8edc..b6217aad6dd 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,13 +6,13 @@ jar - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index 204dcf90941..e2e63521ee4 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index cc7171910df..1b5d4fa6ec7 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,13 +7,13 @@ jar - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 1bffa4272c3..abefaa35bea 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index 36cf63dd602..837dcede0b6 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -7,13 +7,13 @@ jar - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT example-oauth https://github.com/grpc/grpc-java UTF-8 - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index 715c3b95569..d49c52fb32f 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index fb3475b1d60..71baca108b1 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index 98e1e4a4a39..5d1c01a2ce0 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index dae75299e25..96f3e12ab93 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -15,7 +15,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index c75c9d0f9a0..44a85d9bfac 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index c8b87f54cd0..c14f88e0f9b 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,13 +6,13 @@ jar - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT example-tls https://github.com/grpc/grpc-java UTF-8 - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index ae67659a7fa..66f42841fbf 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.73.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/pom.xml b/examples/pom.xml index ff32936f2a2..1584416760e 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,13 +6,13 @@ jar - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT examples https://github.com/grpc/grpc-java UTF-8 - 1.73.0-SNAPSHOT + 1.74.0-SNAPSHOT 3.25.5 3.25.5 From b08976148634102719c620766d8cc858c6836730 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 14 May 2025 00:53:37 -0700 Subject: [PATCH 261/591] xds: Enable least request by default (#12054) Enable least request by default. It has seen good testing by users and behaved as expected. Fixes #11996 --- xds/src/main/java/io/grpc/xds/XdsClusterResource.java | 3 ++- xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index 0d9274e2869..3f2b2d8fd7e 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -60,7 +60,8 @@ class XdsClusterResource extends XdsResourceType { static boolean enableLeastRequest = !Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST")) ? Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST")) - : Boolean.parseBoolean(System.getProperty("io.grpc.xds.experimentalEnableLeastRequest")); + : Boolean.parseBoolean( + System.getProperty("io.grpc.xds.experimentalEnableLeastRequest", "true")); @VisibleForTesting public static boolean enableSystemRootCerts = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_SYSTEM_ROOT_CERTS", false); diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index e5502463db0..dd6f2fd9243 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -186,7 +186,6 @@ public void setUp() { originalEnableRouteLookup = XdsRouteConfigureResource.enableRouteLookup; originalEnableLeastRequest = XdsClusterResource.enableLeastRequest; originalEnableUseSystemRootCerts = XdsClusterResource.enableSystemRootCerts; - assertThat(originalEnableLeastRequest).isFalse(); } @After From b88536a17d320065a972858e17e44e1e5c656a0f Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Thu, 15 May 2025 05:29:52 +0000 Subject: [PATCH 262/591] kokoro: add cloud run tests config (#12065) kokoro: add cloud run tests config --- buildscripts/kokoro/psm-cloud-run.cfg | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 buildscripts/kokoro/psm-cloud-run.cfg diff --git a/buildscripts/kokoro/psm-cloud-run.cfg b/buildscripts/kokoro/psm-cloud-run.cfg new file mode 100644 index 00000000000..1f2d6da208f --- /dev/null +++ b/buildscripts/kokoro/psm-cloud-run.cfg @@ -0,0 +1,17 @@ +# Config file for internal CI + +# Location of the continuous shell script in repository. +build_file: "grpc-java/buildscripts/kokoro/psm-interop-test-java.sh" +timeout_mins: 240 + +action { + define_artifacts { + regex: "artifacts/**/*sponge_log.xml" + regex: "artifacts/**/*.log" + strip_prefix: "artifacts" + } +} +env_vars { + key: "PSM_TEST_SUITE" + value: "cloud_run" +} From 480640dc2ff7851ec6c9a4c7d929e3e383d16563 Mon Sep 17 00:00:00 2001 From: Gregory Cooke Date: Mon, 19 May 2025 11:30:10 -0400 Subject: [PATCH 263/591] alts: add experimental keepalive (#12076) --- .../io/grpc/alts/HandshakerServiceChannel.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java b/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java index 8e8d175b7af..fa1d4f7fa1c 100644 --- a/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java +++ b/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java @@ -21,6 +21,7 @@ import io.grpc.ClientCall; import io.grpc.ManagedChannel; import io.grpc.MethodDescriptor; +import io.grpc.internal.GrpcUtil; import io.grpc.internal.SharedResourceHolder.Resource; import io.grpc.netty.NettyChannelBuilder; import io.netty.channel.EventLoopGroup; @@ -45,6 +46,9 @@ static Resource getHandshakerChannelForTesting(String handshakerAddress return new ChannelResource(handshakerAddress); } + private static final boolean EXPERIMENTAL_ALTS_HANDSHAKER_KEEPALIVE_PARAMS = + GrpcUtil.getFlag("GRPC_EXPERIMENTAL_ALTS_HANDSHAKER_KEEPALIVE_PARAMS", false); + private static class ChannelResource implements Resource { private final String target; @@ -57,12 +61,16 @@ public Channel create() { /* Use its own event loop thread pool to avoid blocking. */ EventLoopGroup eventGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("handshaker pool", true)); - ManagedChannel channel = NettyChannelBuilder.forTarget(target) + NettyChannelBuilder channelBuilder = + NettyChannelBuilder.forTarget(target) .channelType(NioSocketChannel.class, InetSocketAddress.class) .directExecutor() .eventLoopGroup(eventGroup) - .usePlaintext() - .build(); + .usePlaintext(); + if (EXPERIMENTAL_ALTS_HANDSHAKER_KEEPALIVE_PARAMS) { + channelBuilder.keepAliveTime(10, TimeUnit.MINUTES).keepAliveTimeout(10, TimeUnit.SECONDS); + } + ManagedChannel channel = channelBuilder.build(); return new EventLoopHoldingChannel(channel, eventGroup); } From 2fb09578a8a8ca4b0000f6f8be8c3a22460d9019 Mon Sep 17 00:00:00 2001 From: Luwei Ge Date: Mon, 19 May 2025 12:50:25 -0700 Subject: [PATCH 264/591] xds: enableSpiffe also checks the new env var per gRFC-A87 --- .../certprovider/FileWatcherCertificateProviderProvider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderProvider.java index 53864ae33f1..e4871dc4c84 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/FileWatcherCertificateProviderProvider.java @@ -36,9 +36,10 @@ */ public final class FileWatcherCertificateProviderProvider implements CertificateProviderProvider { + // TODO(lwge): Remove the old env var check once it's confirmed to be unused. @VisibleForTesting public static boolean enableSpiffe = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_SPIFFE_TRUST_BUNDLE_MAP", - false); + false) || GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_MTLS_SPIFFE", false); private static final String CERT_FILE_KEY = "certificate_file"; private static final String KEY_FILE_KEY = "private_key_file"; private static final String ROOT_FILE_KEY = "ca_certificate_file"; From f8700a13ad1b7624cc370081164bb161a6d89112 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 21 May 2025 10:19:30 -0700 Subject: [PATCH 265/591] compiler: Default to @generated=omit (#12080) After many years of issue 9179 being open, there's been nothing to show that we need the javax.annotations.Generated annotation. Most tools use file paths and a few check for annotations with "Generated" in the name. ErrorProne has a few that check for javax.annotations.Generated, but only UnnecessarilyFullyQualified looks like it'd be a problem and it is disabled by default. We're not getting any more information, no users have reported issues with `@generated=omit`, and the existing dependency is annoying users, so just drop it. Given we will still retain the GrpcGenerated annotation, it seems highly likely things are already okay. Even if there are problems they would probably be addressed by adding a io.grpc.stub.annotations.Generated annotation or small tweaks. In the short-term, (non-Bazel) users can use `@generated=javax`, but long-term we could consider removing the option assuming we've resolved any outstanding issues. We will want to update the examples and the README to remove the org.apache.tomcat:annotations-api dependency after the next release. Fixes #9179 --- BUILD.bazel | 3 --- MODULE.bazel | 1 - .../io/grpc/alts/internal/HandshakerServiceGrpc.java | 3 --- .../integration/LoadBalancerStatsServiceGrpc.java | 3 --- .../io/grpc/testing/integration/MetricsServiceGrpc.java | 3 --- .../grpc/testing/integration/ReconnectServiceGrpc.java | 3 --- .../io/grpc/testing/integration/TestServiceGrpc.java | 3 --- .../testing/integration/UnimplementedServiceGrpc.java | 3 --- .../integration/XdsUpdateClientConfigureServiceGrpc.java | 3 --- .../testing/integration/XdsUpdateHealthServiceGrpc.java | 3 --- .../integration/LoadBalancerStatsServiceGrpc.java | 3 --- .../io/grpc/testing/integration/MetricsServiceGrpc.java | 3 --- .../grpc/testing/integration/ReconnectServiceGrpc.java | 3 --- .../io/grpc/testing/integration/TestServiceGrpc.java | 3 --- .../testing/integration/UnimplementedServiceGrpc.java | 3 --- .../integration/XdsUpdateClientConfigureServiceGrpc.java | 3 --- .../testing/integration/XdsUpdateHealthServiceGrpc.java | 3 --- .../io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java | 3 --- .../benchmarks/proto/ReportQpsScenarioServiceGrpc.java | 3 --- .../grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java | 3 --- compiler/BUILD.bazel | 2 -- compiler/build.gradle | 7 +++++-- compiler/src/java_plugin/cpp/java_plugin.cpp | 2 +- .../main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java | 3 --- .../integration/LoadBalancerStatsServiceGrpc.java | 3 --- .../io/grpc/testing/integration/MetricsServiceGrpc.java | 3 --- .../grpc/testing/integration/ReconnectServiceGrpc.java | 3 --- .../io/grpc/testing/integration/TestServiceGrpc.java | 3 --- .../testing/integration/UnimplementedServiceGrpc.java | 3 --- .../integration/XdsUpdateClientConfigureServiceGrpc.java | 3 --- .../testing/integration/XdsUpdateHealthServiceGrpc.java | 3 --- .../main/grpc/io/istio/test/EchoTestServiceGrpc.java | 3 --- repositories.bzl | 1 - .../grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java | 3 --- .../main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java | 3 --- .../main/grpc/io/grpc/health/v1/HealthGrpc.java | 3 --- .../grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java | 3 --- .../io/grpc/reflection/v1alpha/ServerReflectionGrpc.java | 3 --- .../reflection/testing/AnotherDynamicServiceGrpc.java | 3 --- .../testing/AnotherReflectableServiceGrpc.java | 3 --- .../io/grpc/reflection/testing/DynamicServiceGrpc.java | 3 --- .../grpc/reflection/testing/ReflectableServiceGrpc.java | 3 --- stub/BUILD.bazel | 9 --------- .../grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java | 3 --- .../github/xds/service/orca/v3/OpenRcaServiceGrpc.java | 3 --- .../discovery/v3/AggregatedDiscoveryServiceGrpc.java | 3 --- .../service/load_stats/v3/LoadReportingServiceGrpc.java | 3 --- .../rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java | 3 --- .../status/v3/ClientStatusDiscoveryServiceGrpc.java | 3 --- 49 files changed, 6 insertions(+), 145 deletions(-) diff --git a/BUILD.bazel b/BUILD.bazel index b6d0838bf87..8350ed9aa39 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -33,7 +33,6 @@ java_library( "//api", "//protobuf", "//stub", - "//stub:javax_annotation", "@com_google_protobuf//:protobuf_java", artifact("com.google.code.findbugs:jsr305"), artifact("com.google.guava:guava"), @@ -47,7 +46,6 @@ java_library( "//api", "//protobuf-lite", "//stub", - "//stub:javax_annotation", artifact("com.google.code.findbugs:jsr305"), artifact("com.google.guava:guava"), ], @@ -67,6 +65,5 @@ java_library( visibility = ["//:__subpackages__"], exports = [ artifact("com.google.auto.value:auto-value-annotations"), - artifact("org.apache.tomcat:annotations-api"), # @Generated for Java 9+ ], ) diff --git a/MODULE.bazel b/MODULE.bazel index 7fa3eab395d..83aa6c1b026 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -41,7 +41,6 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", "junit:junit:4.13.2", - "org.apache.tomcat:annotations-api:6.0.53", "org.checkerframework:checker-qual:3.12.0", "org.codehaus.mojo:animal-sniffer-annotations:1.24", ] diff --git a/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java b/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java index 91e88f331d7..07e4256eb75 100644 --- a/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java +++ b/alts/src/generated/main/grpc/io/grpc/alts/internal/HandshakerServiceGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/gcp/handshaker.proto") @io.grpc.stub.annotations.GrpcGenerated public final class HandshakerServiceGrpc { diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java index 120033a8051..42934e94c5b 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java @@ -7,9 +7,6 @@ * A service used to obtain stats for verifying LB behavior. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class LoadBalancerStatsServiceGrpc { diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java index 489838ddc6c..6c2166468f6 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/metrics.proto") @io.grpc.stub.annotations.GrpcGenerated public final class MetricsServiceGrpc { diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java index e0ea29e42e7..07ce250bc4b 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java @@ -7,9 +7,6 @@ * A service used to control reconnect server. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class ReconnectServiceGrpc { diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java index a0f44f46473..4593215b601 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java @@ -8,9 +8,6 @@ * performance with various types of payload. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java index f758c2d0840..d9ef0e6ddd9 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java @@ -8,9 +8,6 @@ * that case. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class UnimplementedServiceGrpc { diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java index 5fa43e4721a..8ae0c2f93a4 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java @@ -7,9 +7,6 @@ * A service to dynamically update the configuration of an xDS test client. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class XdsUpdateClientConfigureServiceGrpc { diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java index 2492ec0f90b..5b950c73c12 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java @@ -7,9 +7,6 @@ * A service to remotely control health status of an xDS test server. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class XdsUpdateHealthServiceGrpc { diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java index 120033a8051..42934e94c5b 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java @@ -7,9 +7,6 @@ * A service used to obtain stats for verifying LB behavior. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class LoadBalancerStatsServiceGrpc { diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java index 489838ddc6c..6c2166468f6 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/metrics.proto") @io.grpc.stub.annotations.GrpcGenerated public final class MetricsServiceGrpc { diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java index e0ea29e42e7..07ce250bc4b 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java @@ -7,9 +7,6 @@ * A service used to control reconnect server. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class ReconnectServiceGrpc { diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java index a0f44f46473..4593215b601 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java @@ -8,9 +8,6 @@ * performance with various types of payload. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java index f758c2d0840..d9ef0e6ddd9 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java @@ -8,9 +8,6 @@ * that case. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class UnimplementedServiceGrpc { diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java index 5fa43e4721a..8ae0c2f93a4 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java @@ -7,9 +7,6 @@ * A service to dynamically update the configuration of an xDS test client. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class XdsUpdateClientConfigureServiceGrpc { diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java index 2492ec0f90b..5b950c73c12 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java @@ -7,9 +7,6 @@ * A service to remotely control health status of an xDS test server. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class XdsUpdateHealthServiceGrpc { diff --git a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java index 242d4551d6e..15b9a67918b 100644 --- a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java +++ b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/services.proto") @io.grpc.stub.annotations.GrpcGenerated public final class BenchmarkServiceGrpc { diff --git a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java index 8f466185ea0..8fe5f926d99 100644 --- a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java +++ b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/services.proto") @io.grpc.stub.annotations.GrpcGenerated public final class ReportQpsScenarioServiceGrpc { diff --git a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java index 11859482972..bf9649e8377 100644 --- a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java +++ b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/services.proto") @io.grpc.stub.annotations.GrpcGenerated public final class WorkerServiceGrpc { diff --git a/compiler/BUILD.bazel b/compiler/BUILD.bazel index 753f485074e..6f66164f155 100644 --- a/compiler/BUILD.bazel +++ b/compiler/BUILD.bazel @@ -22,7 +22,6 @@ java_library( "//api", "//protobuf", "//stub", - "//stub:javax_annotation", artifact("com.google.code.findbugs:jsr305"), artifact("com.google.guava:guava"), "@com_google_protobuf//:protobuf_java", @@ -35,7 +34,6 @@ java_library( "//api", "//protobuf-lite", "//stub", - "//stub:javax_annotation", artifact("com.google.code.findbugs:jsr305"), artifact("com.google.guava:guava"), ], diff --git a/compiler/build.gradle b/compiler/build.gradle index 3d31d602642..9fee2545f0c 100644 --- a/compiler/build.gradle +++ b/compiler/build.gradle @@ -184,7 +184,11 @@ protobuf { inputs.file javaPluginPath } ofSourceSet('test').configureEach { - plugins { grpc {} } + plugins { + grpc { + option '@generated=javax' + } + } } ofSourceSet('testLite').configureEach { builtins { @@ -193,7 +197,6 @@ protobuf { plugins { grpc { option 'lite' - option '@generated=omit' } } } diff --git a/compiler/src/java_plugin/cpp/java_plugin.cpp b/compiler/src/java_plugin/cpp/java_plugin.cpp index 6b7cc03d486..a595a6a6896 100644 --- a/compiler/src/java_plugin/cpp/java_plugin.cpp +++ b/compiler/src/java_plugin/cpp/java_plugin.cpp @@ -80,7 +80,7 @@ class JavaGrpcGenerator : public protobuf::compiler::CodeGenerator { java_grpc_generator::ProtoFlavor flavor = java_grpc_generator::ProtoFlavor::NORMAL; java_grpc_generator::GeneratedAnnotation generated_annotation = - java_grpc_generator::GeneratedAnnotation::JAVAX; + java_grpc_generator::GeneratedAnnotation::OMIT; bool disable_version = false; for (size_t i = 0; i < options.size(); i++) { diff --git a/grpclb/src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java b/grpclb/src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java index 2a81dfe4ee5..b730eff7b37 100644 --- a/grpclb/src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java +++ b/grpclb/src/generated/main/grpc/io/grpc/lb/v1/LoadBalancerGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/lb/v1/load_balancer.proto") @io.grpc.stub.annotations.GrpcGenerated public final class LoadBalancerGrpc { diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java index f060a1308d8..766d63a51dc 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java @@ -7,9 +7,6 @@ * A service used to obtain stats for verifying LB behavior. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class LoadBalancerStatsServiceGrpc { diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java index 3104f7b263e..8693f1086bb 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/metrics.proto") @io.grpc.stub.annotations.GrpcGenerated public final class MetricsServiceGrpc { diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java index 39df95f4d90..164c92295cc 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java @@ -7,9 +7,6 @@ * A service used to control reconnect server. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class ReconnectServiceGrpc { diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java index 2b519686d85..b8c220925e1 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java @@ -8,9 +8,6 @@ * performance with various types of payload. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java index 400ac6dc4a3..fe6c7c7d1c6 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java @@ -8,9 +8,6 @@ * that case. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class UnimplementedServiceGrpc { diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java index 9cc13780723..9a628ea1b1a 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java @@ -7,9 +7,6 @@ * A service to dynamically update the configuration of an xDS test client. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class XdsUpdateClientConfigureServiceGrpc { diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java index 8243ba713fa..5582c60d9cc 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java @@ -7,9 +7,6 @@ * A service to remotely control health status of an xDS test server. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/testing/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class XdsUpdateHealthServiceGrpc { diff --git a/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java b/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java index 3cb27ac1067..c6947a075d5 100644 --- a/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java +++ b/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: test/echo/proto/echo.proto") @io.grpc.stub.annotations.GrpcGenerated public final class EchoTestServiceGrpc { diff --git a/repositories.bzl b/repositories.bzl index d55ff07e7e6..12a8ec7a6f1 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -45,7 +45,6 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", "junit:junit:4.13.2", - "org.apache.tomcat:annotations-api:6.0.53", "org.checkerframework:checker-qual:3.12.0", "org.codehaus.mojo:animal-sniffer-annotations:1.24", ] diff --git a/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java b/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java index 558a223173c..39e9d0f4144 100644 --- a/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java +++ b/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/lookup/v1/rls.proto") @io.grpc.stub.annotations.GrpcGenerated public final class RouteLookupServiceGrpc { diff --git a/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java b/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java index 7dd74034efe..f839f11cfe5 100644 --- a/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java +++ b/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java @@ -8,9 +8,6 @@ * information. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/channelz/v1/channelz.proto") @io.grpc.stub.annotations.GrpcGenerated public final class ChannelzGrpc { diff --git a/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java b/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java index 9786392cfe6..feb5932b0d9 100644 --- a/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java +++ b/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/health/v1/health.proto") @io.grpc.stub.annotations.GrpcGenerated public final class HealthGrpc { diff --git a/services/src/generated/main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java b/services/src/generated/main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java index 19bf6ed90b3..04f8dea3ace 100644 --- a/services/src/generated/main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java +++ b/services/src/generated/main/grpc/io/grpc/reflection/v1/ServerReflectionGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/reflection/v1/reflection.proto") @io.grpc.stub.annotations.GrpcGenerated public final class ServerReflectionGrpc { diff --git a/services/src/generated/main/grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java b/services/src/generated/main/grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java index 152a8f7c81d..3cbb3a1d1b9 100644 --- a/services/src/generated/main/grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java +++ b/services/src/generated/main/grpc/io/grpc/reflection/v1alpha/ServerReflectionGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: grpc/reflection/v1alpha/reflection.proto") @io.grpc.stub.annotations.GrpcGenerated public final class ServerReflectionGrpc { diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java index a53d199ef52..5b7aaa1e4ec 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java @@ -7,9 +7,6 @@ * AnotherDynamicService * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: io/grpc/reflection/testing/dynamic_reflection_test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class AnotherDynamicServiceGrpc { diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java index a22138ecb03..f8b8d58e621 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: io/grpc/reflection/testing/reflection_test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class AnotherReflectableServiceGrpc { diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java index 6e9dfac72db..81e60438518 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java @@ -7,9 +7,6 @@ * A DynamicService * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: io/grpc/reflection/testing/dynamic_reflection_test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class DynamicServiceGrpc { diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java index aeefc77aff8..5f620038b2c 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: io/grpc/reflection/testing/reflection_test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class ReflectableServiceGrpc { diff --git a/stub/BUILD.bazel b/stub/BUILD.bazel index 572ea681ef3..692ebd6059f 100644 --- a/stub/BUILD.bazel +++ b/stub/BUILD.bazel @@ -15,12 +15,3 @@ java_library( artifact("org.codehaus.mojo:animal-sniffer-annotations"), ], ) - -# javax.annotation.Generated is not included in the default root modules in 9, -# see: http://openjdk.java.net/jeps/320. -java_library( - name = "javax_annotation", - neverlink = 1, # @Generated is source-retention - visibility = ["//visibility:public"], - exports = [artifact("org.apache.tomcat:annotations-api")], -) diff --git a/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java b/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java index 8b33691535d..cccc4eb8f8a 100644 --- a/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java +++ b/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java @@ -7,9 +7,6 @@ * A simple service for test. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: io/grpc/testing/protobuf/simpleservice.proto") @io.grpc.stub.annotations.GrpcGenerated public final class SimpleServiceGrpc { diff --git a/xds/src/generated/thirdparty/grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java b/xds/src/generated/thirdparty/grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java index cb129fd31ba..e0e28ad4072 100644 --- a/xds/src/generated/thirdparty/grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/com/github/xds/service/orca/v3/OpenRcaServiceGrpc.java @@ -14,9 +14,6 @@ * a new call to change backend reporting frequency. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: xds/service/orca/v3/orca.proto") @io.grpc.stub.annotations.GrpcGenerated public final class OpenRcaServiceGrpc { diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java index b93a5df2b71..94b2fd86b96 100644 --- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java @@ -12,9 +12,6 @@ * 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") @io.grpc.stub.annotations.GrpcGenerated public final class AggregatedDiscoveryServiceGrpc { diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/load_stats/v3/LoadReportingServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/load_stats/v3/LoadReportingServiceGrpc.java index 3bce13a7cf5..4f12405be87 100644 --- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/load_stats/v3/LoadReportingServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/load_stats/v3/LoadReportingServiceGrpc.java @@ -4,9 +4,6 @@ /** */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: envoy/service/load_stats/v3/lrs.proto") @io.grpc.stub.annotations.GrpcGenerated public final class LoadReportingServiceGrpc { diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java index f05d38290dd..3f17bb54566 100644 --- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/rate_limit_quota/v3/RateLimitQuotaServiceGrpc.java @@ -7,9 +7,6 @@ * Defines the Rate Limit Quota Service (RLQS). * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: envoy/service/rate_limit_quota/v3/rlqs.proto") @io.grpc.stub.annotations.GrpcGenerated public final class RateLimitQuotaServiceGrpc { diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java index 1a3240875cc..775fa0c1e3e 100644 --- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java @@ -9,9 +9,6 @@ * also be used to get the current xDS states directly from the client. * */ -@javax.annotation.Generated( - value = "by gRPC proto compiler", - comments = "Source: envoy/service/status/v3/csds.proto") @io.grpc.stub.annotations.GrpcGenerated public final class ClientStatusDiscoveryServiceGrpc { From 9d439d4a447e16fb6240abbd223d232f2bc875cf Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Thu, 22 May 2025 16:22:41 +0530 Subject: [PATCH 266/591] xds: Add GcpAuthenticationFilter to FilterRegistry (#12075) --- xds/src/main/java/io/grpc/xds/FilterRegistry.java | 7 +++++++ .../java/io/grpc/xds/GcpAuthenticationFilter.java | 5 +++++ .../io/grpc/xds/GcpAuthenticationFilterTest.java | 6 ++++++ .../java/io/grpc/xds/GrpcXdsClientImplDataTest.java | 13 +++++++++---- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/FilterRegistry.java b/xds/src/main/java/io/grpc/xds/FilterRegistry.java index 426c6d1b3f6..1fbccea8000 100644 --- a/xds/src/main/java/io/grpc/xds/FilterRegistry.java +++ b/xds/src/main/java/io/grpc/xds/FilterRegistry.java @@ -17,6 +17,7 @@ package io.grpc.xds; import com.google.common.annotations.VisibleForTesting; +import io.grpc.internal.GrpcUtil; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; @@ -32,12 +33,18 @@ final class FilterRegistry { private FilterRegistry() {} + static boolean isEnabledGcpAuthnFilter = + GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_GCP_AUTHENTICATION_FILTER", false); + static synchronized FilterRegistry getDefaultRegistry() { if (instance == null) { instance = newRegistry().register( new FaultFilter.Provider(), new RouterFilter.Provider(), new RbacFilter.Provider()); + if (isEnabledGcpAuthnFilter) { + instance.register(new GcpAuthenticationFilter.Provider()); + } } return instance; } diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java index 8ec02f4f809..dc133eaaf1a 100644 --- a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java +++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.xds.FilterRegistry.isEnabledGcpAuthnFilter; import static io.grpc.xds.XdsNameResolver.CLUSTER_SELECTION_KEY; import static io.grpc.xds.XdsNameResolver.XDS_CONFIG_CALL_OPTION_KEY; @@ -312,6 +313,10 @@ public String getTypeUrl() { public AudienceWrapper parse(Any any) throws ResourceInvalidException { Audience audience; try { + if (!isEnabledGcpAuthnFilter) { + throw new InvalidProtocolBufferException("Environment variable for GCP Authentication " + + "Filter is Not Set"); + } audience = any.unpack(Audience.class); } catch (InvalidProtocolBufferException ex) { throw new ResourceInvalidException("Invalid Resource in address proto", ex); diff --git a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java index d84d8c9d768..cebf739d417 100644 --- a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java @@ -73,6 +73,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -89,6 +90,11 @@ public class GcpAuthenticationFilterTest { private static final RdsUpdate rdsUpdate = getRdsUpdate(); private static final CdsUpdate cdsUpdate = getCdsUpdate(); + @Before + public void setUp() { + System.setProperty("GRPC_EXPERIMENTAL_XDS_GCP_AUTHENTICATION_FILTER", "true"); + } + @Test public void testNewFilterInstancesPerFilterName() { assertThat(new GcpAuthenticationFilter("FILTER_INSTANCE_NAME1", 10)) diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index dd6f2fd9243..48d78e8555e 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -2417,6 +2417,7 @@ public Object parse(Any value) { @Test public void processCluster_parsesAudienceMetadata() throws Exception { + FilterRegistry.isEnabledGcpAuthnFilter = true; MetadataRegistry.getInstance(); Audience audience = Audience.newBuilder() @@ -2460,10 +2461,14 @@ public void processCluster_parsesAudienceMetadata() throws Exception { "FILTER_METADATA", ImmutableMap.of( "key1", "value1", "key2", 42.0)); - assertThat(update.parsedMetadata().get("FILTER_METADATA")) - .isEqualTo(expectedParsedMetadata.get("FILTER_METADATA")); - assertThat(update.parsedMetadata().get("AUDIENCE_METADATA")) - .isInstanceOf(AudienceWrapper.class); + try { + assertThat(update.parsedMetadata().get("FILTER_METADATA")) + .isEqualTo(expectedParsedMetadata.get("FILTER_METADATA")); + assertThat(update.parsedMetadata().get("AUDIENCE_METADATA")) + .isInstanceOf(AudienceWrapper.class); + } finally { + FilterRegistry.isEnabledGcpAuthnFilter = false; + } } @Test From 9406d3b2a05dbfb95519cd3a2e43238b48010c05 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 22 May 2025 14:49:12 -0700 Subject: [PATCH 267/591] Rename PSM interop fallback test suite to light --- buildscripts/kokoro/{psm-fallback.cfg => psm-light.cfg} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename buildscripts/kokoro/{psm-fallback.cfg => psm-light.cfg} (94%) diff --git a/buildscripts/kokoro/psm-fallback.cfg b/buildscripts/kokoro/psm-light.cfg similarity index 94% rename from buildscripts/kokoro/psm-fallback.cfg rename to buildscripts/kokoro/psm-light.cfg index 7335d1d9fd9..decd179efa3 100644 --- a/buildscripts/kokoro/psm-fallback.cfg +++ b/buildscripts/kokoro/psm-light.cfg @@ -13,5 +13,5 @@ action { } env_vars { key: "PSM_TEST_SUITE" - value: "fallback" + value: "light" } From 46485c8b62778f1b581019ef19e67dd8de937ca3 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Fri, 23 May 2025 06:54:14 +0000 Subject: [PATCH 268/591] Upgrade Protobuf C++ to 22.5 (#11961) --- COMPILING.md | 4 +- buildscripts/grpc-java-artifacts/Dockerfile | 6 +- .../Dockerfile.multiarch.base | 6 ++ .../Dockerfile.ubuntu2004.base | 6 ++ buildscripts/kokoro/android-interop.sh | 4 +- buildscripts/kokoro/android.sh | 14 ++-- buildscripts/kokoro/unix.sh | 6 +- buildscripts/kokoro/windows.cfg | 2 +- buildscripts/kokoro/windows32.bat | 39 +++++++++-- buildscripts/kokoro/windows64.bat | 39 +++++++++-- buildscripts/make_dependencies.bat | 41 ++++++++---- buildscripts/make_dependencies.sh | 64 +++++++++++++------ buildscripts/toolchain.cmake | 9 +++ compiler/build.gradle | 24 ++++--- compiler/check-artifact.sh | 2 +- .../test/java/io/grpc/okhttp/UtilsTest.java | 6 +- 16 files changed, 206 insertions(+), 66 deletions(-) create mode 100644 buildscripts/toolchain.cmake diff --git a/COMPILING.md b/COMPILING.md index de3cbb026c1..b7df1319beb 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -44,11 +44,11 @@ This section is only necessary if you are making changes to the code generation. Most users only need to use `skipCodegen=true` as discussed above. ### Build Protobuf -The codegen plugin is C++ code and requires protobuf 21.7 or later. +The codegen plugin is C++ code and requires protobuf 22.5 or later. For Linux, Mac and MinGW: ``` -$ PROTOBUF_VERSION=21.7 +$ PROTOBUF_VERSION=22.5 $ curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOBUF_VERSION/protobuf-all-$PROTOBUF_VERSION.tar.gz $ tar xzf protobuf-all-$PROTOBUF_VERSION.tar.gz $ cd protobuf-$PROTOBUF_VERSION diff --git a/buildscripts/grpc-java-artifacts/Dockerfile b/buildscripts/grpc-java-artifacts/Dockerfile index 736babe9d8e..0cc5634b9df 100644 --- a/buildscripts/grpc-java-artifacts/Dockerfile +++ b/buildscripts/grpc-java-artifacts/Dockerfile @@ -27,7 +27,11 @@ RUN mkdir -p "$ANDROID_HOME/cmdline-tools" && \ mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest" && \ yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses +RUN curl -Ls https://github.com/Kitware/CMake/releases/download/v3.26.3/cmake-3.26.3-linux-x86_64.tar.gz | \ + tar xz -C /var/local + # Install Maven RUN curl -Ls https://dlcdn.apache.org/maven/maven-3/3.8.8/binaries/apache-maven-3.8.8-bin.tar.gz | \ tar xz -C /var/local -ENV PATH /var/local/apache-maven-3.8.8/bin:$PATH +ENV PATH /var/local/cmake-3.26.3-linux-x86_64/bin:/var/local/apache-maven-3.8.8/bin:$PATH + diff --git a/buildscripts/grpc-java-artifacts/Dockerfile.multiarch.base b/buildscripts/grpc-java-artifacts/Dockerfile.multiarch.base index 8f7cfae2f52..da2c46904ca 100644 --- a/buildscripts/grpc-java-artifacts/Dockerfile.multiarch.base +++ b/buildscripts/grpc-java-artifacts/Dockerfile.multiarch.base @@ -10,5 +10,11 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ g++-aarch64-linux-gnu \ g++-powerpc64le-linux-gnu \ openjdk-8-jdk \ + pkg-config \ && \ rm -rf /var/lib/apt/lists/* + +RUN curl -Ls https://github.com/Kitware/CMake/releases/download/v3.26.3/cmake-3.26.3-linux-x86_64.tar.gz | \ + tar xz -C /var/local +ENV PATH /var/local/cmake-3.26.3-linux-x86_64/bin:$PATH + diff --git a/buildscripts/grpc-java-artifacts/Dockerfile.ubuntu2004.base b/buildscripts/grpc-java-artifacts/Dockerfile.ubuntu2004.base index 2d11d76c373..e987fb3e684 100644 --- a/buildscripts/grpc-java-artifacts/Dockerfile.ubuntu2004.base +++ b/buildscripts/grpc-java-artifacts/Dockerfile.ubuntu2004.base @@ -9,5 +9,11 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ curl \ g++-s390x-linux-gnu \ openjdk-8-jdk \ + pkg-config \ && \ rm -rf /var/lib/apt/lists/* + +RUN curl -Ls https://github.com/Kitware/CMake/releases/download/v3.26.3/cmake-3.26.3-linux-x86_64.tar.gz | \ + tar xz -C /var/local +ENV PATH /var/local/cmake-3.26.3-linux-x86_64/bin:$PATH + diff --git a/buildscripts/kokoro/android-interop.sh b/buildscripts/kokoro/android-interop.sh index b4adc8bed43..f987aea85f6 100755 --- a/buildscripts/kokoro/android-interop.sh +++ b/buildscripts/kokoro/android-interop.sh @@ -7,8 +7,8 @@ set -exu -o pipefail cd github/grpc-java -export LDFLAGS=-L/tmp/protobuf/lib -export CXXFLAGS=-I/tmp/protobuf/include +export LDFLAGS="$(PKG_CONFIG_PATH=/tmp/protobuf/lib/pkgconfig pkg-config --libs protobuf)" +export CXXFLAGS="$(PKG_CONFIG_PATH=/tmp/protobuf/lib/pkgconfig pkg-config --cflags protobuf)" export LD_LIBRARY_PATH=/tmp/protobuf/lib export OS_NAME=$(uname) diff --git a/buildscripts/kokoro/android.sh b/buildscripts/kokoro/android.sh index 13983e747b7..5af55b2f551 100755 --- a/buildscripts/kokoro/android.sh +++ b/buildscripts/kokoro/android.sh @@ -9,9 +9,6 @@ BASE_DIR="$(pwd)" cd "$BASE_DIR/github/grpc-java" -export LDFLAGS=-L/tmp/protobuf/lib -export CXXFLAGS=-I/tmp/protobuf/include -export LD_LIBRARY_PATH=/tmp/protobuf/lib export OS_NAME=$(uname) cat <> gradle.properties @@ -30,10 +27,18 @@ unzip -qd "${ANDROID_HOME}/cmdline-tools" cmdline.zip rm cmdline.zip mv "${ANDROID_HOME}/cmdline-tools/cmdline-tools" "${ANDROID_HOME}/cmdline-tools/latest" (yes || true) | "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses - +curl -Ls https://github.com/Kitware/CMake/releases/download/v3.26.3/cmake-3.26.3-linux-x86_64.tar.gz | \ + tar xz -C /tmp +export PATH=/tmp/cmake-3.26.3-linux-x86_64/bin:$PATH + # Proto deps buildscripts/make_dependencies.sh +sudo apt-get update && sudo apt-get install pkg-config +export LDFLAGS="$(PKG_CONFIG_PATH=/tmp/protobuf/lib/pkgconfig pkg-config --libs protobuf)" +export CXXFLAGS="$(PKG_CONFIG_PATH=/tmp/protobuf/lib/pkgconfig pkg-config --cflags protobuf)" +export LD_LIBRARY_PATH=/tmp/protobuf/lib + # Build Android with Java 11, this adds it to the PATH sudo update-java-alternatives --set java-1.11.0-openjdk-amd64 # Unset any existing JAVA_HOME env var to stop Gradle from using it @@ -98,6 +103,7 @@ cd $BASE_DIR/github/grpc-java ./gradlew clean git checkout HEAD^ ./gradlew --stop # use a new daemon to build the previous commit +GRADLE_FLAGS="${GRADLE_FLAGS} -PskipCodegen=true" # skip codegen for build from previous commit since it wasn't built with --std=c++14 when making this change ./gradlew publishToMavenLocal $GRADLE_FLAGS cd examples/android/helloworld/ ../../gradlew build $GRADLE_FLAGS diff --git a/buildscripts/kokoro/unix.sh b/buildscripts/kokoro/unix.sh index 1b88b56ab40..e65825cac01 100755 --- a/buildscripts/kokoro/unix.sh +++ b/buildscripts/kokoro/unix.sh @@ -51,9 +51,9 @@ fi export GRADLE_OPTS="-Dorg.gradle.jvmargs='-Xmx1g'" # Make protobuf discoverable by :grpc-compiler -export LD_LIBRARY_PATH=/tmp/protobuf/lib -export LDFLAGS=-L/tmp/protobuf/lib -export CXXFLAGS="-I/tmp/protobuf/include" +export LDFLAGS="$(PKG_CONFIG_PATH=/tmp/protobuf/lib/pkgconfig pkg-config --libs protobuf)" +export CXXFLAGS="$(PKG_CONFIG_PATH=/tmp/protobuf/lib/pkgconfig pkg-config --cflags protobuf)" +export LIBRARY_PATH=/tmp/protobuf/lib ./gradlew grpc-compiler:clean $GRADLE_FLAGS diff --git a/buildscripts/kokoro/windows.cfg b/buildscripts/kokoro/windows.cfg index bdfaa38904f..ec0a3c9ae34 100644 --- a/buildscripts/kokoro/windows.cfg +++ b/buildscripts/kokoro/windows.cfg @@ -2,7 +2,7 @@ # Location of the continuous shell script in repository. build_file: "grpc-java/buildscripts/kokoro/windows.bat" -timeout_mins: 45 +timeout_mins: 90 # We always build mvn artifacts. action { diff --git a/buildscripts/kokoro/windows32.bat b/buildscripts/kokoro/windows32.bat index ffd4d3b99a6..f87899d0ab8 100644 --- a/buildscripts/kokoro/windows32.bat +++ b/buildscripts/kokoro/windows32.bat @@ -15,19 +15,21 @@ set ESCWORKSPACE=%WORKSPACE:\=\\% @rem Clear JAVA_HOME to prevent a different Java version from being used set JAVA_HOME= -set PATH=C:\Program Files\OpenJDK\openjdk-11.0.12_7\bin;%PATH% mkdir grpc-java-helper32 cd grpc-java-helper32 -call "%VS140COMNTOOLS%\vsvars32.bat" || exit /b 1 +call "%VS170COMNTOOLS%\..\..\VC\Auxiliary\Build\vcvars32.bat" || exit /b 1 call "%WORKSPACE%\buildscripts\make_dependencies.bat" || exit /b 1 cd "%WORKSPACE%" SET TARGET_ARCH=x86_32 SET FAIL_ON_WARNINGS=true -SET VC_PROTOBUF_LIBS=%ESCWORKSPACE%\\grpc-java-helper32\\protobuf-%PROTOBUF_VER%\\build\\Release -SET VC_PROTOBUF_INCLUDE=%ESCWORKSPACE%\\grpc-java-helper32\\protobuf-%PROTOBUF_VER%\\build\\include +SET PROTOBUF_VER=22.5 +SET PKG_CONFIG_PATH=%ESCWORKSPACE%\\grpc-java-helper32\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\lib\\pkgconfig +SET VC_PROTOBUF_LIBS=/LIBPATH:%ESCWORKSPACE%\\grpc-java-helper32\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\lib +SET VC_PROTOBUF_INCLUDE=%ESCWORKSPACE%\\grpc-java-helper32\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\include +call :Get_Libs SET GRADLE_FLAGS=-PtargetArch=%TARGET_ARCH% -PfailOnWarnings=%FAIL_ON_WARNINGS% -PvcProtobufLibs=%VC_PROTOBUF_LIBS% -PvcProtobufInclude=%VC_PROTOBUF_INCLUDE% -PskipAndroid=true SET GRADLE_OPTS="-Dorg.gradle.jvmargs='-Xmx1g'" @@ -50,3 +52,32 @@ IF NOT %GRADLEEXIT% == 0 ( cmd.exe /C "%WORKSPACE%\gradlew.bat --stop" cmd.exe /C "%WORKSPACE%\gradlew.bat %GRADLE_FLAGS% -Dorg.gradle.parallel=false -PrepositoryDir=%WORKSPACE%\artifacts clean grpc-compiler:build grpc-compiler:publish" || exit /b 1 + +goto :eof +:Get_Libs +SetLocal EnableDelayedExpansion +set "libs_list=" +for /f "tokens=*" %%a in ('pkg-config --libs protobuf') do ( + for %%b in (%%a) do ( + set lib=%%b + set libfirst2char=!lib:~0,2! + if !libfirst2char!==-l ( + @rem remove the leading -l + set lib=!lib:~2! + @rem remove spaces + set lib=!lib: =! + @rem Because protobuf is specified as libprotobuf and elsewhere + if !lib! NEQ protobuf ( + set lib=!lib!.lib + if "!libs_list!"=="" ( + set libs_list=!lib! + ) else ( + set libs_list=!libs_list!,!lib! + ) + ) + ) + ) +) +EndLocal & set "VC_PROTOBUF_LIBS=%VC_PROTOBUF_LIBS%,%libs_list%" +exit /b 0 + diff --git a/buildscripts/kokoro/windows64.bat b/buildscripts/kokoro/windows64.bat index 8542f1c0536..0a99f47dd3d 100644 --- a/buildscripts/kokoro/windows64.bat +++ b/buildscripts/kokoro/windows64.bat @@ -14,19 +14,21 @@ set ESCWORKSPACE=%WORKSPACE:\=\\% @rem Clear JAVA_HOME to prevent a different Java version from being used set JAVA_HOME= -set PATH=C:\Program Files\OpenJDK\openjdk-11.0.12_7\bin;%PATH% mkdir grpc-java-helper64 cd grpc-java-helper64 -call "%VS140COMNTOOLS%\..\..\VC\bin\amd64\vcvars64.bat" || exit /b 1 +call "%VS170COMNTOOLS%\..\..\VC\Auxiliary\Build\vcvars64.bat" || exit /b 1 call "%WORKSPACE%\buildscripts\make_dependencies.bat" || exit /b 1 cd "%WORKSPACE%" SET TARGET_ARCH=x86_64 SET FAIL_ON_WARNINGS=true -SET VC_PROTOBUF_LIBS=%ESCWORKSPACE%\\grpc-java-helper64\\protobuf-%PROTOBUF_VER%\\build\\Release -SET VC_PROTOBUF_INCLUDE=%ESCWORKSPACE%\\grpc-java-helper64\\protobuf-%PROTOBUF_VER%\\build\\include +SET PROTOBUF_VER=22.5 +SET PKG_CONFIG_PATH=%ESCWORKSPACE%\\grpc-java-helper64\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\lib\\pkgconfig +SET VC_PROTOBUF_LIBS=/LIBPATH:%ESCWORKSPACE%\\grpc-java-helper64\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\lib +SET VC_PROTOBUF_INCLUDE=%ESCWORKSPACE%\\grpc-java-helper64\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\include +call :Get_Libs SET GRADLE_FLAGS=-PtargetArch=%TARGET_ARCH% -PfailOnWarnings=%FAIL_ON_WARNINGS% -PvcProtobufLibs=%VC_PROTOBUF_LIBS% -PvcProtobufInclude=%VC_PROTOBUF_INCLUDE% -PskipAndroid=true SET GRADLE_OPTS="-Dorg.gradle.jvmargs='-Xmx1g'" @@ -34,3 +36,32 @@ SET GRADLE_OPTS="-Dorg.gradle.jvmargs='-Xmx1g'" cmd.exe /C "%WORKSPACE%\gradlew.bat --stop" cmd.exe /C "%WORKSPACE%\gradlew.bat %GRADLE_FLAGS% -Dorg.gradle.parallel=false -PrepositoryDir=%WORKSPACE%\artifacts grpc-compiler:clean grpc-compiler:build grpc-compiler:publish" || exit /b 1 + +goto :eof +:Get_Libs +SetLocal EnableDelayedExpansion +set "libs_list=" +for /f "tokens=*" %%a in ('pkg-config --libs protobuf') do ( + for %%b in (%%a) do ( + set lib=%%b + set libfirst2char=!lib:~0,2! + if !libfirst2char!==-l ( + @rem remove the leading -l + set lib=!lib:~2! + @rem remove spaces + set lib=!lib: =! + @rem Because protobuf is specified as libprotobuf and elsewhere + if !lib! NEQ protobuf ( + set lib=!lib!.lib + if "!libs_list!"=="" ( + set libs_list=!lib! + ) else ( + set libs_list=!libs_list!,!lib! + ) + ) + ) + ) +) +EndLocal & set "VC_PROTOBUF_LIBS=%VC_PROTOBUF_LIBS%,%libs_list%" +exit /b 0 + diff --git a/buildscripts/make_dependencies.bat b/buildscripts/make_dependencies.bat index 2bbfd394d46..dce08ef7624 100644 --- a/buildscripts/make_dependencies.bat +++ b/buildscripts/make_dependencies.bat @@ -1,12 +1,16 @@ -set PROTOBUF_VER=21.7 -set CMAKE_NAME=cmake-3.3.2-win32-x86 +choco install -y pkgconfiglite +choco install -y openjdk --version=17.0 +set PATH=%PATH%;"c:\Program Files\OpenJDK\jdk-17\bin" +set PROTOBUF_VER=22.5 +set ABSL_VERSION=20230125.4 +set CMAKE_NAME=cmake-3.26.3-windows-x86_64 if not exist "protobuf-%PROTOBUF_VER%\build\Release\" ( call :installProto || exit /b 1 ) echo Compile gRPC-Java with something like: -echo -PtargetArch=x86_32 -PvcProtobufLibs=%cd%\protobuf-%PROTOBUF_VER%\build\Release -PvcProtobufInclude=%cd%\protobuf-%PROTOBUF_VER%\build\include +echo -PtargetArch=x86_32 -PvcProtobufLibPath=%cd%\protobuf-%PROTOBUF_VER%\build\protobuf-%PROTOBUF_VER%\lib -PvcProtobufInclude=%cd%\protobuf-%PROTOBUF_VER%\build\protobuf-%PROTOBUF_VER%\include -PvcProtobufLibs=insert-list-of-libs-from-pkg-config-output-here goto :eof @@ -20,25 +24,35 @@ if not exist "%CMAKE_NAME%" ( set PATH=%PATH%;%cd%\%CMAKE_NAME%\bin :hasCmake @rem GitHub requires TLSv1.2, and for whatever reason our powershell doesn't have it enabled -powershell -command "$ErrorActionPreference = 'stop'; & { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; iwr https://github.com/google/protobuf/archive/v%PROTOBUF_VER%.zip -OutFile protobuf.zip }" || exit /b 1 +powershell -command "$ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'stop'; & { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; iwr https://github.com/google/protobuf/releases/download/v%PROTOBUF_VER%/protobuf-%PROTOBUF_VER%.zip -OutFile protobuf.zip }" || exit /b 1 powershell -command "$ErrorActionPreference = 'stop'; & { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('protobuf.zip', '.') }" || exit /b 1 del protobuf.zip +powershell -command "$ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'stop'; & { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; iwr https://github.com/abseil/abseil-cpp/archive/refs/tags/%ABSL_VERSION%.zip -OutFile absl.zip }" || exit /b 1 +powershell -command "$ErrorActionPreference = 'stop'; & { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('absl.zip', '.') }" || exit /b 1 +del absl.zip +rmdir protobuf-%PROTOBUF_VER%\third_party\abseil-cpp +move abseil-cpp-%ABSL_VERSION% protobuf-%PROTOBUF_VER%\third_party\abseil-cpp mkdir protobuf-%PROTOBUF_VER%\build pushd protobuf-%PROTOBUF_VER%\build -@rem Workaround https://github.com/protocolbuffers/protobuf/issues/10174 -powershell -command "(Get-Content ..\cmake\extract_includes.bat.in) -replace '\.\.\\', '' | Out-File -encoding ascii ..\cmake\extract_includes.bat.in" @rem cmake does not detect x86_64 from the vcvars64.bat variables. -@rem If vcvars64.bat has set PLATFORM to X64, then inform cmake to use the Win64 version of VS -if "%PLATFORM%" == "X64" ( - @rem Note the space - SET CMAKE_VSARCH= Win64 +@rem If vcvars64.bat has set PLATFORM to X64, then inform cmake to use the Win64 version of VS, likewise for x32 +if "%PLATFORM%" == "x64" ( + SET CMAKE_VSARCH=-A x64 +) else if "%PLATFORM%" == "x86" ( + @rem -A x86 doesn't work: https://github.com/microsoft/vcpkg/issues/15465 + SET CMAKE_VSARCH=-DCMAKE_GENERATOR_PLATFORM=WIN32 ) else ( SET CMAKE_VSARCH= ) -cmake -Dprotobuf_BUILD_TESTS=OFF -G "Visual Studio %VisualStudioVersion:~0,2%%CMAKE_VSARCH%" .. || exit /b 1 -msbuild /maxcpucount /p:Configuration=Release /verbosity:minimal libprotoc.vcxproj || exit /b 1 -call extract_includes.bat || exit /b 1 +for /f "tokens=4 delims=\" %%a in ("%VCINSTALLDIR%") do ( + SET VC_YEAR=%%a +) +for /f "tokens=1 delims=." %%a in ("%VisualStudioVersion%") do ( + SET visual_studio_major_version=%%a +) +cmake -Dprotobuf_BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX=%cd%\protobuf-%PROTOBUF_VER% -DCMAKE_PREFIX_PATH=%cd%\protobuf-%PROTOBUF_VER% -G "Visual Studio %visual_studio_major_version% %VC_YEAR%" %CMAKE_VSARCH% .. || exit /b 1 +cmake --build . --config Release --target install || exit /b 1 popd goto :eof @@ -49,3 +63,4 @@ powershell -command "$ErrorActionPreference = 'stop'; & { iwr https://cmake.org/ powershell -command "$ErrorActionPreference = 'stop'; & { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('cmake.zip', '.') }" || exit /b 1 del cmake.zip goto :eof + diff --git a/buildscripts/make_dependencies.sh b/buildscripts/make_dependencies.sh index 3d02a72f4eb..e5d2450afe7 100755 --- a/buildscripts/make_dependencies.sh +++ b/buildscripts/make_dependencies.sh @@ -3,13 +3,17 @@ # Build protoc set -evux -o pipefail -PROTOBUF_VERSION=21.7 +PROTOBUF_VERSION=22.5 +ABSL_VERSION=20230125.4 +CMAKE_VERSION=3.26.3 # ARCH is x86_64 bit unless otherwise specified. ARCH="${ARCH:-x86_64}" DOWNLOAD_DIR=/tmp/source INSTALL_DIR="/tmp/protobuf-cache/$PROTOBUF_VERSION/$(uname -s)-$ARCH" +BUILDSCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)" mkdir -p $DOWNLOAD_DIR +cd "$DOWNLOAD_DIR" # Start with a sane default NUM_CPU=4 @@ -26,27 +30,46 @@ if [ -f ${INSTALL_DIR}/bin/protoc ]; then echo "Not building protobuf. Already built" # TODO(ejona): swap to `brew install --devel protobuf` once it is up-to-date else - if [[ ! -d "$DOWNLOAD_DIR"/protobuf-"${PROTOBUF_VERSION}" ]]; then - curl -Ls https://github.com/google/protobuf/releases/download/v${PROTOBUF_VERSION}/protobuf-all-${PROTOBUF_VERSION}.tar.gz | tar xz -C $DOWNLOAD_DIR + if [[ ! -d "protobuf-${PROTOBUF_VERSION}" ]]; then + curl -Ls "https://github.com/google/protobuf/releases/download/v${PROTOBUF_VERSION}/protobuf-${PROTOBUF_VERSION}.tar.gz" | tar xz + curl -Ls "https://github.com/abseil/abseil-cpp/archive/refs/tags/${ABSL_VERSION}.tar.gz" | tar xz + rmdir "protobuf-$PROTOBUF_VERSION/third_party/abseil-cpp" + mv "abseil-cpp-$ABSL_VERSION" "protobuf-$PROTOBUF_VERSION/third_party/abseil-cpp" fi - pushd $DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION} + # the same source dir is used for 32 and 64 bit builds, so we need to clean stale data first + rm -rf "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" + mkdir "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" + pushd "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" # install here so we don't need sudo if [[ "$ARCH" == x86* ]]; then - ./configure CFLAGS=-m${ARCH#*_} CXXFLAGS=-m${ARCH#*_} --disable-shared \ - --prefix="$INSTALL_DIR" - elif [[ "$ARCH" == aarch* ]]; then - ./configure --disable-shared --host=aarch64-linux-gnu --prefix="$INSTALL_DIR" - elif [[ "$ARCH" == ppc* ]]; then - ./configure --disable-shared --host=powerpc64le-linux-gnu --prefix="$INSTALL_DIR" - elif [[ "$ARCH" == s390* ]]; then - ./configure --disable-shared --host=s390x-linux-gnu --prefix="$INSTALL_DIR" - elif [[ "$ARCH" == loongarch* ]]; then - ./configure --disable-shared --host=loongarch64-unknown-linux-gnu --prefix="$INSTALL_DIR" + CFLAGS=-m${ARCH#*_} CXXFLAGS=-m${ARCH#*_} cmake .. \ + -DCMAKE_CXX_STANDARD=14 -Dprotobuf_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" -DABSL_INTERNAL_AT_LEAST_CXX17=0 \ + -B. || exit 1 + else + if [[ "$ARCH" == aarch_64 ]]; then + GCC_ARCH=aarch64-linux-gnu + elif [[ "$ARCH" == ppcle_64 ]]; then + GCC_ARCH=powerpc64le-linux-gnu + elif [[ "$ARCH" == s390_64 ]]; then + GCC_ARCH=s390x-linux-gnu + elif [[ "$ARCH" == loongarch_64 ]]; then + GCC_ARCH=loongarch64-unknown-linux-gnu + else + echo "Unknown architecture: $ARCH" + exit 1 + fi + cmake .. \ + -DCMAKE_CXX_STANDARD=14 -Dprotobuf_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" -DABSL_INTERNAL_AT_LEAST_CXX17=0 \ + -Dcrosscompile_ARCH="$GCC_ARCH" \ + -DCMAKE_TOOLCHAIN_FILE=$BUILDSCRIPTS_DIR/toolchain.cmake \ + -B. || exit 1 fi - # the same source dir is used for 32 and 64 bit builds, so we need to clean stale data first - make clean - make V=0 -j$NUM_CPU - make install + export CMAKE_BUILD_PARALLEL_LEVEL="$NUM_CPU" + cmake --build . || exit 1 + cmake --install . || exit 1 + [ -d "$INSTALL_DIR/lib64" ] && mv "$INSTALL_DIR/lib64" "$INSTALL_DIR/lib" popd fi @@ -60,7 +83,8 @@ ln -s "$INSTALL_DIR" /tmp/protobuf cat < linker.args.add(lib) } } } } @@ -242,9 +245,10 @@ def checkArtifacts = tasks.register("checkArtifacts") { if (ret.exitValue != 0) { throw new GradleException("dumpbin exited with " + ret.exitValue) } - def dlls = os.toString() =~ /Image has the following dependencies:\s+(.*)\s+Summary/ - if (dlls[0][1] != "KERNEL32.dll") { - throw new Exception("unexpected dll deps: " + dlls[0][1]); + def dlls_match_results = os.toString() =~ /Image has the following dependencies:([\S\s]*)Summary/ + def dlls = dlls_match_results[0][1].trim().split("\\s+").sort() + if (dlls != ["KERNEL32.dll", "dbghelp.dll"]) { + throw new Exception("unexpected dll deps: " + dlls); } os.reset() ret = exec { diff --git a/compiler/check-artifact.sh b/compiler/check-artifact.sh index 4d0c2fa6286..12d7709a2a8 100755 --- a/compiler/check-artifact.sh +++ b/compiler/check-artifact.sh @@ -114,7 +114,7 @@ checkDependencies () white_list="KERNEL32\.dll\|msvcrt\.dll\|USER32\.dll" elif [[ "$OS" == linux ]]; then dump_cmd='objdump -x '"$1"' | grep "NEEDED"' - white_list="libpthread\.so\.0\|libstdc++\.so\.6\|libc\.so\.6" + white_list="libpthread\.so\.0\|libstdc++\.so\.6\|libc\.so\.6\|librt\.so\.1\|libm\.so\.6" if [[ "$ARCH" == x86_32 ]]; then white_list="${white_list}\|libm\.so\.6" elif [[ "$ARCH" == x86_64 ]]; then diff --git a/okhttp/src/test/java/io/grpc/okhttp/UtilsTest.java b/okhttp/src/test/java/io/grpc/okhttp/UtilsTest.java index 895ba7ff7c7..ede8511ee70 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/UtilsTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/UtilsTest.java @@ -25,6 +25,7 @@ import io.grpc.okhttp.internal.TlsVersion; import java.net.Socket; import java.util.List; +import java.util.Locale; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -95,6 +96,9 @@ public void getSocketOptions() throws Exception { assertEquals("5000", socketOptions.others.get("SO_SNDBUF")); assertEquals("true", socketOptions.others.get("SO_KEEPALIVE")); assertEquals("true", socketOptions.others.get("SO_OOBINLINE")); - assertEquals("8", socketOptions.others.get("IP_TOS")); + String osName = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); + if (!osName.startsWith("windows")) { + assertEquals("8", socketOptions.others.get("IP_TOS")); + } } } From d124007ff4e4229d0a8d3096d13c7e82aa524d84 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sun, 25 May 2025 20:12:58 -0700 Subject: [PATCH 269/591] kokoro: Don't run grpc codegen in android-interop (#12098) android-interop has been failing to build since 46485c8 because it didn't have cmake installed and defined LDFLAGS/CXXFLAGS with pkg-config before make_dependencies.sh had been run. Android-interop didn't verify the codegen is up-to-date. Building the codegen was just a relic from when android was its own separate gradle build. Avoiding codegen means we don't have to compile absl/protobuf and have a C++ toolchain. --- buildscripts/kokoro/android-interop.sh | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/buildscripts/kokoro/android-interop.sh b/buildscripts/kokoro/android-interop.sh index f987aea85f6..877311daca5 100755 --- a/buildscripts/kokoro/android-interop.sh +++ b/buildscripts/kokoro/android-interop.sh @@ -2,16 +2,8 @@ set -exu -o pipefail -# Install gRPC and codegen for the Android interop app -# (a composite gradle build can't find protoc-gen-grpc-java) - cd github/grpc-java -export LDFLAGS="$(PKG_CONFIG_PATH=/tmp/protobuf/lib/pkgconfig pkg-config --libs protobuf)" -export CXXFLAGS="$(PKG_CONFIG_PATH=/tmp/protobuf/lib/pkgconfig pkg-config --cflags protobuf)" -export LD_LIBRARY_PATH=/tmp/protobuf/lib -export OS_NAME=$(uname) - export ANDROID_HOME=/tmp/Android/Sdk mkdir -p "${ANDROID_HOME}/cmdline-tools" curl -Ls -o cmdline.zip \ @@ -21,15 +13,12 @@ rm cmdline.zip mv "${ANDROID_HOME}/cmdline-tools/cmdline-tools" "${ANDROID_HOME}/cmdline-tools/latest" (yes || true) | "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses -# Proto deps -buildscripts/make_dependencies.sh - # Build Android with Java 11, this adds it to the PATH sudo update-java-alternatives --set java-1.11.0-openjdk-amd64 # Unset any existing JAVA_HOME env var to stop Gradle from using it unset JAVA_HOME -GRADLE_FLAGS="-Pandroid.useAndroidX=true -Dorg.gradle.jvmargs=-Xmx1024m" +GRADLE_FLAGS="-Pandroid.useAndroidX=true -Dorg.gradle.jvmargs=-Xmx1024m -PskipCodegen=true" ./gradlew $GRADLE_FLAGS :grpc-android-interop-testing:assembleDebug ./gradlew $GRADLE_FLAGS :grpc-android-interop-testing:assembleDebugAndroidTest From 83538cdae3142be122496fbaa80440f39d715a47 Mon Sep 17 00:00:00 2001 From: Sangamesh Date: Tue, 27 May 2025 17:52:47 +0000 Subject: [PATCH 270/591] cronet: Delete TODO for User-Agent on CronetEngine gRPC doesn't create the CronetEngine, so even though streaming is observing the CronetEngine's User-Agent, we don't have control of that. In addition, CronetEngines are commonly shared between gRPC and normal HTTP traffic, so we don't actually expect users to set gRPC in engine's user agent. The existing behavior seems to be working as well as feasible. Fixes #11582 --- cronet/src/main/java/io/grpc/cronet/CronetClientStream.java | 1 - 1 file changed, 1 deletion(-) diff --git a/cronet/src/main/java/io/grpc/cronet/CronetClientStream.java b/cronet/src/main/java/io/grpc/cronet/CronetClientStream.java index 95adb65ec40..fcba49a7ae1 100644 --- a/cronet/src/main/java/io/grpc/cronet/CronetClientStream.java +++ b/cronet/src/main/java/io/grpc/cronet/CronetClientStream.java @@ -362,7 +362,6 @@ private static boolean isApplicationHeader(String key) { private void setGrpcHeaders(BidirectionalStream.Builder builder) { // Psuedo-headers are set by cronet. // All non-pseudo headers must come after pseudo headers. - // TODO(ericgribkoff): remove this and set it on CronetEngine after crbug.com/588204 gets fixed. builder.addHeader(USER_AGENT_KEY.name(), userAgent); builder.addHeader(CONTENT_TYPE_KEY.name(), GrpcUtil.CONTENT_TYPE_GRPC); builder.addHeader("te", GrpcUtil.TE_TRAILERS); From 22cf7cf2ac454843b20993fd0b58918563a51a83 Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Fri, 30 May 2025 07:25:37 +0200 Subject: [PATCH 271/591] tests: Replace usages of deprecated junit ExpectedException with assertThrows (#12103) --- .../grpc/netty/NettyChannelBuilderTest.java | 92 ++-- .../io/grpc/netty/NettyServerBuilderTest.java | 117 +++-- .../grpc/netty/ProtocolNegotiatorsTest.java | 36 +- .../grpc/okhttp/OkHttpChannelBuilderTest.java | 32 +- .../okhttp/OkHttpProtocolNegotiatorTest.java | 30 +- .../test/java/io/grpc/okhttp/UtilsTest.java | 14 +- .../protobuf/lite/ProtoLiteUtilsTest.java | 13 +- .../io/grpc/testing/GrpcCleanupRuleTest.java | 21 +- .../util/GracefulSwitchLoadBalancerTest.java | 11 +- .../io/grpc/xds/GrpcBootstrapperImplTest.java | 41 +- .../grpc/xds/GrpcXdsClientImplDataTest.java | 413 +++++++++--------- .../xds/PriorityLoadBalancerProviderTest.java | 13 +- .../xds/SharedXdsClientPoolProviderTest.java | 11 +- .../io/grpc/xds/WeightedRandomPickerTest.java | 19 +- 14 files changed, 407 insertions(+), 456 deletions(-) diff --git a/netty/src/test/java/io/grpc/netty/NettyChannelBuilderTest.java b/netty/src/test/java/io/grpc/netty/NettyChannelBuilderTest.java index 5789d275c07..95d54d13b82 100644 --- a/netty/src/test/java/io/grpc/netty/NettyChannelBuilderTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyChannelBuilderTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -39,17 +40,13 @@ import java.net.SocketAddress; import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLException; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class NettyChannelBuilderTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); private final SslContext noSslContext = null; private void shutdown(ManagedChannel mc) throws Exception { @@ -107,10 +104,9 @@ private void overrideAuthorityIsReadableHelper(NettyChannelBuilder builder, public void failOverrideInvalidAuthority() { NettyChannelBuilder builder = new NettyChannelBuilder(getTestSocketAddress()); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid authority:"); - - builder.overrideAuthority("[invalidauthority"); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.overrideAuthority("[invalidauthority")); + assertThat(e).hasMessageThat().isEqualTo("Invalid authority: [invalidauthority"); } @Test @@ -128,20 +124,18 @@ public void enableCheckAuthorityFailOverrideInvalidAuthority() { NettyChannelBuilder builder = new NettyChannelBuilder(getTestSocketAddress()) .disableCheckAuthority() .enableCheckAuthority(); - - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid authority:"); - builder.overrideAuthority("[invalidauthority"); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.overrideAuthority("[invalidauthority")); + assertThat(e).hasMessageThat().isEqualTo("Invalid authority: [invalidauthority"); } @Test public void failInvalidAuthority() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid host or port"); - @SuppressWarnings("AddressSelection") // We actually expect zero addresses! - Object unused = - NettyChannelBuilder.forAddress(new InetSocketAddress("invalid_authority", 1234)); + InetSocketAddress address = new InetSocketAddress("invalid_authority", 1234); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> NettyChannelBuilder.forAddress(address)); + assertThat(e).hasMessageThat().isEqualTo("Invalid host or port: invalid_authority 1234"); } @Test @@ -155,10 +149,10 @@ public void failIfSslContextIsNotClient() { SslContext sslContext = mock(SslContext.class); NettyChannelBuilder builder = new NettyChannelBuilder(getTestSocketAddress()); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Server SSL context can not be used for client channel"); - - builder.sslContext(sslContext); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.sslContext(sslContext)); + assertThat(e).hasMessageThat() + .isEqualTo("Server SSL context can not be used for client channel"); } @Test @@ -166,10 +160,10 @@ public void failNegotiationTypeWithChannelCredentials_target() { NettyChannelBuilder builder = NettyChannelBuilder.forTarget( "fakeTarget", InsecureChannelCredentials.create()); - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Cannot change security when using ChannelCredentials"); - - builder.negotiationType(NegotiationType.TLS); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> builder.negotiationType(NegotiationType.TLS)); + assertThat(e).hasMessageThat() + .isEqualTo("Cannot change security when using ChannelCredentials"); } @Test @@ -177,10 +171,10 @@ public void failNegotiationTypeWithChannelCredentials_socketAddress() { NettyChannelBuilder builder = NettyChannelBuilder.forAddress( getTestSocketAddress(), InsecureChannelCredentials.create()); - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Cannot change security when using ChannelCredentials"); - - builder.negotiationType(NegotiationType.TLS); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> builder.negotiationType(NegotiationType.TLS)); + assertThat(e).hasMessageThat() + .isEqualTo("Cannot change security when using ChannelCredentials"); } @Test @@ -205,10 +199,9 @@ public void createProtocolNegotiatorByType_plaintextUpgrade() { @Test public void createProtocolNegotiatorByType_tlsWithNoContext() { - thrown.expect(NullPointerException.class); - NettyChannelBuilder.createProtocolNegotiatorByType( - NegotiationType.TLS, - noSslContext, null); + assertThrows(NullPointerException.class, + () -> NettyChannelBuilder.createProtocolNegotiatorByType( + NegotiationType.TLS, noSslContext, null)); } @Test @@ -245,38 +238,40 @@ public void createProtocolNegotiatorByType_tlsWithAuthorityFallback() throws SSL public void negativeKeepAliveTime() { NettyChannelBuilder builder = NettyChannelBuilder.forTarget("fakeTarget"); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("keepalive time must be positive"); - builder.keepAliveTime(-1L, TimeUnit.HOURS); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.keepAliveTime(-1L, TimeUnit.HOURS)); + assertThat(e).hasMessageThat().isEqualTo("keepalive time must be positive"); } @Test public void negativeKeepAliveTimeout() { NettyChannelBuilder builder = NettyChannelBuilder.forTarget("fakeTarget"); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("keepalive timeout must be positive"); - builder.keepAliveTimeout(-1L, TimeUnit.HOURS); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.keepAliveTimeout(-1L, TimeUnit.HOURS)); + assertThat(e).hasMessageThat().isEqualTo("keepalive timeout must be positive"); } @Test public void assertEventLoopAndChannelType_onlyGroupProvided() { NettyChannelBuilder builder = NettyChannelBuilder.forTarget("fakeTarget"); builder.eventLoopGroup(mock(EventLoopGroup.class)); - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Both EventLoopGroup and ChannelType should be provided"); - builder.assertEventLoopAndChannelType(); + IllegalStateException e = assertThrows(IllegalStateException.class, + builder::assertEventLoopAndChannelType); + assertThat(e).hasMessageThat() + .isEqualTo("Both EventLoopGroup and ChannelType should be provided or neither should be"); } @Test public void assertEventLoopAndChannelType_onlyTypeProvided() { NettyChannelBuilder builder = NettyChannelBuilder.forTarget("fakeTarget"); builder.channelType(LocalChannel.class, LocalAddress.class); - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Both EventLoopGroup and ChannelType should be provided"); - builder.assertEventLoopAndChannelType(); + IllegalStateException e = assertThrows(IllegalStateException.class, + builder::assertEventLoopAndChannelType); + assertThat(e).hasMessageThat() + .isEqualTo("Both EventLoopGroup and ChannelType should be provided or neither should be"); } @Test @@ -288,10 +283,11 @@ public Channel newChannel() { return null; } }); - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Both EventLoopGroup and ChannelType should be provided"); - builder.assertEventLoopAndChannelType(); + IllegalStateException e = assertThrows(IllegalStateException.class, + builder::assertEventLoopAndChannelType); + assertThat(e).hasMessageThat() + .isEqualTo("Both EventLoopGroup and ChannelType should be provided or neither should be"); } @Test diff --git a/netty/src/test/java/io/grpc/netty/NettyServerBuilderTest.java b/netty/src/test/java/io/grpc/netty/NettyServerBuilderTest.java index 48af23b78a8..797cfa95c0e 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerBuilderTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerBuilderTest.java @@ -16,20 +16,19 @@ package io.grpc.netty; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; -import com.google.common.truth.Truth; import io.grpc.ServerStreamTracer; import io.netty.channel.EventLoopGroup; import io.netty.channel.local.LocalServerChannel; import io.netty.handler.ssl.SslContext; import java.net.InetSocketAddress; import java.util.concurrent.TimeUnit; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -39,9 +38,6 @@ @RunWith(JUnit4.class) public class NettyServerBuilderTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); - private NettyServerBuilder builder = NettyServerBuilder.forPort(8080); @Test @@ -50,7 +46,7 @@ public void addMultipleListenAddresses() { NettyServer server = builder.buildTransportServers(ImmutableList.of()); - Truth.assertThat(server.getListenSocketAddresses()).hasSize(2); + assertThat(server.getListenSocketAddresses()).hasSize(2); } @Test @@ -63,121 +59,112 @@ public void failIfSslContextIsNotServer() { SslContext sslContext = mock(SslContext.class); when(sslContext.isClient()).thenReturn(true); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Client SSL context can not be used for server"); - builder.sslContext(sslContext); + IllegalArgumentException e = assertThrows( + IllegalArgumentException.class, () -> builder.sslContext(sslContext)); + assertThat(e).hasMessageThat().isEqualTo("Client SSL context can not be used for server"); } @Test public void failIfKeepAliveTimeNegative() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("keepalive time must be positive"); - - builder.keepAliveTime(-10L, TimeUnit.HOURS); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.keepAliveTime(-10L, TimeUnit.HOURS)); + assertThat(e).hasMessageThat().isEqualTo("keepalive time must be positive:-10"); } @Test public void failIfKeepAliveTimeoutNegative() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("keepalive timeout must be positive"); - - builder.keepAliveTimeout(-10L, TimeUnit.HOURS); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.keepAliveTimeout(-10L, TimeUnit.HOURS)); + assertThat(e).hasMessageThat().isEqualTo("keepalive timeout must be positive: -10"); } @Test public void failIfMaxConcurrentCallsPerConnectionNegative() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("max must be positive"); - - builder.maxConcurrentCallsPerConnection(0); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.maxConcurrentCallsPerConnection(0)); + assertThat(e).hasMessageThat().isEqualTo("max must be positive: 0"); } @Test public void failIfMaxInboundMetadataSizeNonPositive() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("maxInboundMetadataSize must be positive"); - - builder.maxInboundMetadataSize(0); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.maxInboundMetadataSize(0)); + assertThat(e).hasMessageThat().isEqualTo("maxInboundMetadataSize must be positive: 0"); } @Test public void failIfSoftInboundMetadataSizeNonPositive() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("softLimitHeaderListSize must be positive"); - - builder.maxInboundMetadataSize(0, 100); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.maxInboundMetadataSize(0, 100)); + assertThat(e).hasMessageThat().isEqualTo("softLimitHeaderListSize must be positive: 0"); } @Test public void failIfMaxInboundMetadataSizeSmallerThanSoft() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("must be greater than softLimitHeaderListSize"); - - builder.maxInboundMetadataSize(100, 80); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.maxInboundMetadataSize(100, 80)); + assertThat(e).hasMessageThat().isEqualTo("maxInboundMetadataSize: 80 " + + "must be greater than softLimitHeaderListSize: 100"); } @Test public void failIfMaxConnectionIdleNegative() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("max connection idle must be positive"); - - builder.maxConnectionIdle(-1, TimeUnit.HOURS); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.maxConnectionIdle(-1, TimeUnit.HOURS)); + assertThat(e).hasMessageThat().isEqualTo("max connection idle must be positive: -1"); } @Test public void failIfMaxConnectionAgeNegative() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("max connection age must be positive"); - - builder.maxConnectionAge(-1, TimeUnit.HOURS); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.maxConnectionAge(-1, TimeUnit.HOURS)); + assertThat(e).hasMessageThat().isEqualTo("max connection age must be positive: -1"); } @Test public void failIfMaxConnectionAgeGraceNegative() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("max connection age grace must be non-negative"); - - builder.maxConnectionAgeGrace(-1, TimeUnit.HOURS); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.maxConnectionAgeGrace(-1, TimeUnit.HOURS)); + assertThat(e).hasMessageThat().isEqualTo("max connection age grace must be non-negative: -1"); } @Test public void failIfPermitKeepAliveTimeNegative() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("permit keepalive time must be non-negative"); - - builder.permitKeepAliveTime(-1, TimeUnit.HOURS); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.permitKeepAliveTime(-1, TimeUnit.HOURS)); + assertThat(e).hasMessageThat().isEqualTo("permit keepalive time must be non-negative: -1"); } @Test public void assertEventLoopsAndChannelType_onlyBossGroupProvided() { EventLoopGroup mockEventLoopGroup = mock(EventLoopGroup.class); builder.bossEventLoopGroup(mockEventLoopGroup); - thrown.expect(IllegalStateException.class); - thrown.expectMessage( - "All of BossEventLoopGroup, WorkerEventLoopGroup and ChannelType should be provided"); - - builder.assertEventLoopsAndChannelType(); + IllegalStateException e = assertThrows(IllegalStateException.class, + builder::assertEventLoopsAndChannelType); + assertThat(e).hasMessageThat().isEqualTo( + "All of BossEventLoopGroup, WorkerEventLoopGroup and ChannelType should be provided " + + "or neither should be"); } @Test public void assertEventLoopsAndChannelType_onlyWorkerGroupProvided() { EventLoopGroup mockEventLoopGroup = mock(EventLoopGroup.class); builder.workerEventLoopGroup(mockEventLoopGroup); - thrown.expect(IllegalStateException.class); - thrown.expectMessage( - "All of BossEventLoopGroup, WorkerEventLoopGroup and ChannelType should be provided"); - - builder.assertEventLoopsAndChannelType(); + IllegalStateException e = assertThrows(IllegalStateException.class, + builder::assertEventLoopsAndChannelType); + assertThat(e).hasMessageThat().isEqualTo( + "All of BossEventLoopGroup, WorkerEventLoopGroup and ChannelType should be provided " + + "or neither should be"); } @Test public void assertEventLoopsAndChannelType_onlyTypeProvided() { builder.channelType(LocalServerChannel.class); - thrown.expect(IllegalStateException.class); - thrown.expectMessage( - "All of BossEventLoopGroup, WorkerEventLoopGroup and ChannelType should be provided"); - - builder.assertEventLoopsAndChannelType(); + IllegalStateException e = assertThrows(IllegalStateException.class, + builder::assertEventLoopsAndChannelType); + assertThat(e).hasMessageThat().isEqualTo( + "All of BossEventLoopGroup, WorkerEventLoopGroup and ChannelType should be provided " + + "or neither should be"); } @Test diff --git a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java index 4829bcc7419..638fe960a32 100644 --- a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java +++ b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -146,7 +147,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.DisableOnDebug; -import org.junit.rules.ExpectedException; import org.junit.rules.TestRule; import org.junit.rules.Timeout; import org.junit.runner.RunWith; @@ -174,8 +174,6 @@ public static void loadCerts() throws Exception { private static final int TIMEOUT_SECONDS = 60; @Rule public final TestRule globalTimeout = new DisableOnDebug(Timeout.seconds(TIMEOUT_SECONDS)); - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); private final EventLoopGroup group = new DefaultEventLoop(); private Channel chan; @@ -714,11 +712,10 @@ public void handlerAdded(ChannelHandlerContext ctx) throws Exception { } @Test - public void tlsHandler_failsOnNullEngine() throws Exception { - thrown.expect(NullPointerException.class); - thrown.expectMessage("ssl"); - - Object unused = ProtocolNegotiators.serverTls(null); + public void tlsHandler_failsOnNullEngine() { + NullPointerException e = assertThrows(NullPointerException.class, + () -> ProtocolNegotiators.serverTls(null)); + assertThat(e).hasMessageThat().isEqualTo("sslContext"); } @@ -1058,9 +1055,8 @@ public boolean isLoggable(LogRecord record) { @Test public void tls_failsOnNullSslContext() { - thrown.expect(NullPointerException.class); - - Object unused = ProtocolNegotiators.tls(null, null); + assertThrows(NullPointerException.class, + () -> ProtocolNegotiators.tls(null, null)); } @Test @@ -1090,17 +1086,16 @@ public void tls_invalidHost() throws SSLException { } @Test - public void httpProxy_nullAddressNpe() throws Exception { - thrown.expect(NullPointerException.class); - Object unused = - ProtocolNegotiators.httpProxy(null, "user", "pass", ProtocolNegotiators.plaintext()); + public void httpProxy_nullAddressNpe() { + assertThrows(NullPointerException.class, + () -> ProtocolNegotiators.httpProxy(null, "user", "pass", ProtocolNegotiators.plaintext())); } @Test - public void httpProxy_nullNegotiatorNpe() throws Exception { - thrown.expect(NullPointerException.class); - Object unused = ProtocolNegotiators.httpProxy( - InetSocketAddress.createUnresolved("localhost", 80), "user", "pass", null); + public void httpProxy_nullNegotiatorNpe() { + assertThrows(NullPointerException.class, + () -> ProtocolNegotiators.httpProxy( + InetSocketAddress.createUnresolved("localhost", 80), "user", "pass", null)); } @Test @@ -1218,9 +1213,8 @@ public void httpProxy_500() throws Exception { assertFalse(negotiationFuture.isDone()); String response = "HTTP/1.1 500 OMG\r\nContent-Length: 4\r\n\r\noops"; serverContext.writeAndFlush(bb(response, serverContext.channel())).sync(); - thrown.expect(ProxyConnectException.class); try { - negotiationFuture.sync(); + assertThrows(ProxyConnectException.class, negotiationFuture::sync); } finally { channel.close(); } diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpChannelBuilderTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpChannelBuilderTest.java index c86e80656e3..89d37536b70 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpChannelBuilderTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpChannelBuilderTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import com.google.common.util.concurrent.SettableFuture; @@ -57,7 +58,6 @@ import javax.security.auth.x500.X500Principal; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -67,8 +67,6 @@ @RunWith(JUnit4.class) public class OkHttpChannelBuilderTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); @Rule public final GrpcCleanupRule grpcCleanupRule = new GrpcCleanupRule(); @Test @@ -100,10 +98,9 @@ private void overrideAuthorityIsReadableHelper(OkHttpChannelBuilder builder, @Test public void failOverrideInvalidAuthority() { OkHttpChannelBuilder builder = OkHttpChannelBuilder.forAddress("good", 1234); - - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid authority:"); - builder.overrideAuthority("[invalidauthority"); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.overrideAuthority("[invalidauthority")); + assertThat(e).hasMessageThat().isEqualTo("Invalid authority: [invalidauthority"); } @Test @@ -119,17 +116,16 @@ public void enableCheckAuthorityFailOverrideInvalidAuthority() { .disableCheckAuthority() .enableCheckAuthority(); - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid authority:"); - builder.overrideAuthority("[invalidauthority"); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.overrideAuthority("[invalidauthority")); + assertThat(e).hasMessageThat().isEqualTo("Invalid authority: [invalidauthority"); } @Test public void failInvalidAuthority() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid host or port"); - - OkHttpChannelBuilder.forAddress("invalid_authority", 1234); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> OkHttpChannelBuilder.forAddress("invalid_authority", 1234)); + assertThat(e.getMessage()).isEqualTo("Invalid host or port: invalid_authority 1234"); } @Test @@ -396,10 +392,10 @@ public ChannelCredentials withoutBearerTokens() { @Test public void failForUsingClearTextSpecDirectly() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("plaintext ConnectionSpec is not accepted"); - - OkHttpChannelBuilder.forAddress("host", 1234).connectionSpec(ConnectionSpec.CLEARTEXT); + OkHttpChannelBuilder builder = OkHttpChannelBuilder.forAddress("host", 1234); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.connectionSpec(ConnectionSpec.CLEARTEXT)); + assertThat(e).hasMessageThat().isEqualTo("plaintext ConnectionSpec is not accepted"); } @Test diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpProtocolNegotiatorTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpProtocolNegotiatorTest.java index cc9f30862af..4353dc2597b 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpProtocolNegotiatorTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpProtocolNegotiatorTest.java @@ -16,10 +16,12 @@ package io.grpc.okhttp; +import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -37,9 +39,7 @@ import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; -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.ArgumentMatchers; @@ -49,9 +49,6 @@ */ @RunWith(JUnit4.class) public class OkHttpProtocolNegotiatorTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); - private final SSLSocket sock = mock(SSLSocket.class); private final Platform platform = mock(Platform.class); @@ -118,21 +115,19 @@ public void negotiate_handshakeFails() throws IOException { OkHttpProtocolNegotiator negotiator = OkHttpProtocolNegotiator.get(); doReturn(parameters).when(sock).getSSLParameters(); doThrow(new IOException()).when(sock).startHandshake(); - thrown.expect(IOException.class); - - negotiator.negotiate(sock, "hostname", ImmutableList.of(Protocol.HTTP_2)); + assertThrows(IOException.class, + () -> negotiator.negotiate(sock, "hostname", ImmutableList.of(Protocol.HTTP_2))); } @Test - public void negotiate_noSelectedProtocol() throws Exception { + public void negotiate_noSelectedProtocol() { Platform platform = mock(Platform.class); OkHttpProtocolNegotiator negotiator = new OkHttpProtocolNegotiator(platform); - thrown.expect(RuntimeException.class); - thrown.expectMessage("TLS ALPN negotiation failed"); - - negotiator.negotiate(sock, "hostname", ImmutableList.of(Protocol.HTTP_2)); + RuntimeException e = assertThrows(RuntimeException.class, + () -> negotiator.negotiate(sock, "hostname", ImmutableList.of(Protocol.HTTP_2))); + assertThat(e).hasMessageThat().isEqualTo("TLS ALPN negotiation failed with protocols: [h2]"); } @Test @@ -150,7 +145,7 @@ public void negotiate_success() throws Exception { // Checks that the super class is properly invoked. @Test - public void negotiate_android_handshakeFails() throws Exception { + public void negotiate_android_handshakeFails() { when(platform.getTlsExtensionType()).thenReturn(TlsExtensionType.ALPN_AND_NPN); AndroidNegotiator negotiator = new AndroidNegotiator(platform); @@ -161,10 +156,9 @@ public void startHandshake() throws IOException { } }; - thrown.expect(IOException.class); - thrown.expectMessage("expected"); - - negotiator.negotiate(androidSock, "hostname", ImmutableList.of(Protocol.HTTP_2)); + IOException e = assertThrows(IOException.class, + () -> negotiator.negotiate(androidSock, "hostname", ImmutableList.of(Protocol.HTTP_2))); + assertThat(e).hasMessageThat().isEqualTo("expected"); } @VisibleForTesting diff --git a/okhttp/src/test/java/io/grpc/okhttp/UtilsTest.java b/okhttp/src/test/java/io/grpc/okhttp/UtilsTest.java index ede8511ee70..1c97e027b4a 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/UtilsTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/UtilsTest.java @@ -16,7 +16,9 @@ package io.grpc.okhttp; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import io.grpc.InternalChannelz.SocketOptions; @@ -26,9 +28,7 @@ import java.net.Socket; import java.util.List; import java.util.Locale; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -38,16 +38,12 @@ @RunWith(JUnit4.class) public class UtilsTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); - @Test public void convertSpecRejectsPlaintext() { com.squareup.okhttp.ConnectionSpec plaintext = com.squareup.okhttp.ConnectionSpec.CLEARTEXT; - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("plaintext ConnectionSpec is not accepted"); - Utils.convertSpec(plaintext); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> Utils.convertSpec(plaintext)); + assertThat(e).hasMessageThat().isEqualTo("plaintext ConnectionSpec is not accepted"); } @Test diff --git a/protobuf-lite/src/test/java/io/grpc/protobuf/lite/ProtoLiteUtilsTest.java b/protobuf-lite/src/test/java/io/grpc/protobuf/lite/ProtoLiteUtilsTest.java index 5c25cb3b309..204264b016d 100644 --- a/protobuf-lite/src/test/java/io/grpc/protobuf/lite/ProtoLiteUtilsTest.java +++ b/protobuf-lite/src/test/java/io/grpc/protobuf/lite/ProtoLiteUtilsTest.java @@ -16,6 +16,7 @@ package io.grpc.protobuf.lite; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -43,9 +44,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Arrays; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -53,9 +52,6 @@ @RunWith(JUnit4.class) public class ProtoLiteUtilsTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); - private final Marshaller marshaller = ProtoLiteUtils.marshaller(Type.getDefaultInstance()); private Type proto = Type.newBuilder().setName("name").build(); @@ -214,10 +210,9 @@ public void metadataMarshaller_invalid() { @Test public void extensionRegistry_notNull() { - thrown.expect(NullPointerException.class); - thrown.expectMessage("newRegistry"); - - ProtoLiteUtils.setExtensionRegistry(null); + NullPointerException e = assertThrows(NullPointerException.class, + () -> ProtoLiteUtils.setExtensionRegistry(null)); + assertThat(e).hasMessageThat().isEqualTo("newRegistry"); } @Test diff --git a/testing/src/test/java/io/grpc/testing/GrpcCleanupRuleTest.java b/testing/src/test/java/io/grpc/testing/GrpcCleanupRuleTest.java index a5a6783d53f..8eb3edd3825 100644 --- a/testing/src/test/java/io/grpc/testing/GrpcCleanupRuleTest.java +++ b/testing/src/test/java/io/grpc/testing/GrpcCleanupRuleTest.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.any; @@ -35,9 +36,7 @@ import io.grpc.internal.FakeClock; import io.grpc.testing.GrpcCleanupRule.Resource; import java.util.concurrent.TimeUnit; -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.junit.runners.model.MultipleFailureException; @@ -51,10 +50,6 @@ public class GrpcCleanupRuleTest { public static final FakeClock fakeClock = new FakeClock(); - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public ExpectedException thrown = ExpectedException.none(); - @Test public void registerChannelReturnSameChannel() { ManagedChannel channel = mock(ManagedChannel.class); @@ -72,10 +67,9 @@ public void registerNullChannelThrowsNpe() { ManagedChannel channel = null; GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); - thrown.expect(NullPointerException.class); - thrown.expectMessage("channel"); - - grpcCleanup.register(channel); + NullPointerException e = assertThrows(NullPointerException.class, + () -> grpcCleanup.register(channel)); + assertThat(e).hasMessageThat().isEqualTo("channel"); } @Test @@ -83,10 +77,9 @@ public void registerNullServerThrowsNpe() { Server server = null; GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); - thrown.expect(NullPointerException.class); - thrown.expectMessage("server"); - - grpcCleanup.register(server); + NullPointerException e = assertThrows(NullPointerException.class, + () -> grpcCleanup.register(server)); + assertThat(e).hasMessageThat().isEqualTo("server"); } @Test diff --git a/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java b/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java index 843e16194c5..8f87dab5da6 100644 --- a/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java @@ -21,6 +21,7 @@ import static io.grpc.ConnectivityState.IDLE; import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; @@ -53,9 +54,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -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; @@ -68,10 +67,6 @@ public class GracefulSwitchLoadBalancerTest { private static final Object FAKE_CONFIG = new Object(); - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); - private final Map balancers = new HashMap<>(); private final Map helpers = new HashMap<>(); private final Helper mockHelper = mock(Helper.class); @@ -102,8 +97,8 @@ public void handleSubchannelState_shouldThrow() { .build())); Subchannel subchannel = mock(Subchannel.class); ConnectivityStateInfo connectivityStateInfo = ConnectivityStateInfo.forNonError(READY); - thrown.expect(UnsupportedOperationException.class); - gracefulSwitchLb.handleSubchannelState(subchannel, connectivityStateInfo); + assertThrows(UnsupportedOperationException.class, + () -> gracefulSwitchLb.handleSubchannelState(subchannel, connectivityStateInfo)); } @Test diff --git a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java index 192d88177eb..3f93cc6f191 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verifyNoInteractions; @@ -40,10 +41,9 @@ import java.util.List; import java.util.Map; import org.junit.After; +import org.junit.Assert; 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; @@ -53,9 +53,6 @@ public class GrpcBootstrapperImplTest { private static final String BOOTSTRAP_FILE_PATH = "/fake/fs/path/bootstrap.json"; private static final String SERVER_URI = "trafficdirector.googleapis.com:443"; - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); private final GrpcBootstrapperImpl bootstrapper = new GrpcBootstrapperImpl(); private String originalBootstrapPathFromEnvVar; @@ -236,7 +233,7 @@ public void parseBootstrap_IgnoreIrrelevantFields() throws XdsInitializationExce } @Test - public void parseBootstrap_missingServerChannelCreds() throws XdsInitializationException { + public void parseBootstrap_missingServerChannelCreds() { String rawData = "{\n" + " \"xds_servers\": [\n" + " {\n" @@ -246,13 +243,14 @@ public void parseBootstrap_missingServerChannelCreds() throws XdsInitializationE + "}"; bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); - thrown.expect(XdsInitializationException.class); - thrown.expectMessage("Invalid bootstrap: server " + SERVER_URI + " 'channel_creds' required"); - bootstrapper.bootstrap(); + XdsInitializationException e = Assert.assertThrows(XdsInitializationException.class, + bootstrapper::bootstrap); + assertThat(e).hasMessageThat() + .isEqualTo("Invalid bootstrap: server " + SERVER_URI + " 'channel_creds' required"); } @Test - public void parseBootstrap_unsupportedServerChannelCreds() throws XdsInitializationException { + public void parseBootstrap_unsupportedServerChannelCreds() { String rawData = "{\n" + " \"xds_servers\": [\n" + " {\n" @@ -265,9 +263,10 @@ public void parseBootstrap_unsupportedServerChannelCreds() throws XdsInitializat + "}"; bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); - thrown.expect(XdsInitializationException.class); - thrown.expectMessage("Server " + SERVER_URI + ": no supported channel credentials found"); - bootstrapper.bootstrap(); + XdsInitializationException e = assertThrows(XdsInitializationException.class, + bootstrapper::bootstrap); + assertThat(e).hasMessageThat() + .isEqualTo("Server " + SERVER_URI + ": no supported channel credentials found"); } @Test @@ -294,7 +293,7 @@ public void parseBootstrap_useFirstSupportedChannelCredentials() } @Test - public void parseBootstrap_noXdsServers() throws XdsInitializationException { + public void parseBootstrap_noXdsServers() { String rawData = "{\n" + " \"node\": {\n" + " \"id\": \"ENVOY_NODE_ID\",\n" @@ -312,9 +311,10 @@ public void parseBootstrap_noXdsServers() throws XdsInitializationException { + "}"; bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); - thrown.expect(XdsInitializationException.class); - thrown.expectMessage("Invalid bootstrap: 'xds_servers' does not exist."); - bootstrapper.bootstrap(); + XdsInitializationException e = assertThrows(XdsInitializationException.class, + bootstrapper::bootstrap); + assertThat(e).hasMessageThat() + .isEqualTo("Invalid bootstrap: 'xds_servers' does not exist."); } @Test @@ -343,8 +343,9 @@ public void parseBootstrap_serverWithoutServerUri() throws XdsInitializationExce + "}"; bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); - thrown.expectMessage("Invalid bootstrap: missing 'server_uri'"); - bootstrapper.bootstrap(); + XdsInitializationException e = assertThrows(XdsInitializationException.class, + bootstrapper::bootstrap); + assertThat(e).hasMessageThat().isEqualTo("Invalid bootstrap: missing 'server_uri'"); } @Test @@ -870,7 +871,7 @@ public void parseAuthorities() throws Exception { } @Test - public void badFederationConfig() throws Exception { + public void badFederationConfig() { String rawData = "{\n" + " \"authorities\": {\n" + " \"a.com\": {\n" diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 48d78e8555e..e129e2644e9 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -20,6 +20,7 @@ import static io.envoyproxy.envoy.config.route.v3.RouteAction.ClusterSpecifierCase.CLUSTER_SPECIFIER_PLUGIN; import static io.grpc.xds.XdsClusterResource.TRANSPORT_SOCKET_NAME_HTTP11_PROXY; import static io.grpc.xds.XdsEndpointResource.GRPC_EXPERIMENTAL_XDS_DUALSTACK_ENDPOINTS; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import com.github.udpa.udpa.type.v1.TypedStruct; @@ -154,9 +155,7 @@ import java.util.concurrent.TimeUnit; 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; @@ -173,9 +172,6 @@ public class GrpcXdsClientImplDataTest { private static final String GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE = "GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE"; - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); private final FilterRegistry filterRegistry = FilterRegistry.getDefaultRegistry(); private boolean originalEnableRouteLookup; private boolean originalEnableLeastRequest; @@ -1572,11 +1568,12 @@ public void parseHttpConnectionManager_xffNumTrustedHopsUnsupported() throws ResourceInvalidException { @SuppressWarnings("deprecation") HttpConnectionManager hcm = HttpConnectionManager.newBuilder().setXffNumTrustedHops(2).build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("HttpConnectionManager with xff_num_trusted_hops unsupported"); - XdsListenerResource.parseHttpConnectionManager( - hcm, filterRegistry, - true /* does not matter */, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, + () -> XdsListenerResource.parseHttpConnectionManager( + hcm, filterRegistry, + true /* does not matter */, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("HttpConnectionManager with xff_num_trusted_hops unsupported"); } @Test @@ -1586,12 +1583,13 @@ public void parseHttpConnectionManager_OriginalIpDetectionExtensionsMustEmpty() HttpConnectionManager hcm = HttpConnectionManager.newBuilder() .addOriginalIpDetectionExtensions(TypedExtensionConfig.newBuilder().build()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("HttpConnectionManager with original_ip_detection_extensions unsupported"); - XdsListenerResource.parseHttpConnectionManager( - hcm, filterRegistry, false, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseHttpConnectionManager( + hcm, filterRegistry, false, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("HttpConnectionManager with original_ip_detection_extensions unsupported"); } - + @Test public void parseHttpConnectionManager_missingRdsAndInlinedRouteConfiguration() throws ResourceInvalidException { @@ -1604,11 +1602,12 @@ public void parseHttpConnectionManager_missingRdsAndInlinedRouteConfiguration() HttpFilter.newBuilder().setName("terminal").setTypedConfig( Any.pack(Router.newBuilder().build())).setIsOptional(true)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("HttpConnectionManager neither has inlined route_config nor RDS"); - XdsListenerResource.parseHttpConnectionManager( - hcm, filterRegistry, - true /* does not matter */, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseHttpConnectionManager( + hcm, filterRegistry, + true /* does not matter */, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("HttpConnectionManager neither has inlined route_config nor RDS"); } @Test @@ -1623,11 +1622,12 @@ public void parseHttpConnectionManager_duplicateHttpFilters() throws ResourceInv HttpFilter.newBuilder().setName("terminal").setTypedConfig( Any.pack(Router.newBuilder().build())).setIsOptional(true)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("HttpConnectionManager contains duplicate HttpFilter: envoy.filter.foo"); - XdsListenerResource.parseHttpConnectionManager( - hcm, filterRegistry, - true /* does not matter */, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseHttpConnectionManager( + hcm, filterRegistry, + true /* does not matter */, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("HttpConnectionManager contains duplicate HttpFilter: envoy.filter.foo"); } @Test @@ -1641,11 +1641,12 @@ public void parseHttpConnectionManager_lastNotTerminal() throws ResourceInvalidE HttpFilter.newBuilder().setName("envoy.filter.bar").setIsOptional(true) .setTypedConfig(Any.pack(HTTPFault.newBuilder().build()))) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("The last HttpFilter must be a terminal filter: envoy.filter.bar"); - XdsListenerResource.parseHttpConnectionManager( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true /* does not matter */, getXdsResourceTypeArgs(true)); + true /* does not matter */, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("The last HttpFilter must be a terminal filter: envoy.filter.bar"); } @Test @@ -1659,11 +1660,12 @@ public void parseHttpConnectionManager_terminalNotLast() throws ResourceInvalidE .addHttpFilters( HttpFilter.newBuilder().setName("envoy.filter.foo").setIsOptional(true)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("A terminal HttpFilter must be the last filter: terminal"); - XdsListenerResource.parseHttpConnectionManager( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true, getXdsResourceTypeArgs(true)); + true, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("A terminal HttpFilter must be the last filter: terminal"); } @Test @@ -1675,11 +1677,12 @@ public void parseHttpConnectionManager_unknownFilters() throws ResourceInvalidEx .addHttpFilters( HttpFilter.newBuilder().setName("envoy.filter.bar").setIsOptional(true)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("The last HttpFilter must be a terminal filter: envoy.filter.bar"); - XdsListenerResource.parseHttpConnectionManager( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true /* does not matter */, getXdsResourceTypeArgs(true)); + true /* does not matter */, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("The last HttpFilter must be a terminal filter: envoy.filter.bar"); } @Test @@ -1687,11 +1690,12 @@ public void parseHttpConnectionManager_emptyFilters() throws ResourceInvalidExce HttpConnectionManager hcm = HttpConnectionManager.newBuilder() .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("Missing HttpFilter in HttpConnectionManager."); - XdsListenerResource.parseHttpConnectionManager( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseHttpConnectionManager( hcm, filterRegistry, - true /* does not matter */, getXdsResourceTypeArgs(true)); + true /* does not matter */, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("Missing HttpFilter in HttpConnectionManager."); } @Test @@ -1815,12 +1819,12 @@ public void parseHttpConnectionManager_duplicatePluginName() throws Exception { Any.pack(Router.newBuilder().build())).setIsOptional(true)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("Multiple ClusterSpecifierPlugins with the same name: rls-plugin-1"); - - XdsListenerResource.parseHttpConnectionManager( - hcm, filterRegistry, - true /* does not matter */, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseHttpConnectionManager( + hcm, filterRegistry, + true /* does not matter */, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("Multiple ClusterSpecifierPlugins with the same name: rls-plugin-1"); } @Test @@ -1867,12 +1871,12 @@ public void parseHttpConnectionManager_pluginNameNotFound() throws Exception { Any.pack(Router.newBuilder().build())).setIsOptional(true)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("ClusterSpecifierPlugin for [invalid-plugin-name] not found"); - - XdsListenerResource.parseHttpConnectionManager( - hcm, filterRegistry, - true /* does not matter */, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseHttpConnectionManager( + hcm, filterRegistry, + true /* does not matter */, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .contains("ClusterSpecifierPlugin for [invalid-plugin-name] not found"); } @@ -2001,12 +2005,12 @@ public void parseHttpConnectionManager_validateRdsConfigSource() throws Exceptio HttpFilter.newBuilder().setName("terminal").setTypedConfig( Any.pack(Router.newBuilder().build())).setIsOptional(true)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseHttpConnectionManager( + hcm3, filterRegistry, + true /* does not matter */, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat().isEqualTo( "HttpConnectionManager contains invalid RDS: must specify ADS or self ConfigSource"); - XdsListenerResource.parseHttpConnectionManager( - hcm3, filterRegistry, - true /* does not matter */, getXdsResourceTypeArgs(true)); } @Test @@ -2096,11 +2100,10 @@ public void parseClusterSpecifierPlugin_unregisteredPlugin() throws Exception { .setTypedConfig(Any.pack(StringValue.of("unregistered")))) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsRouteConfigureResource.parseClusterSpecifierPlugin(pluginProto, registry)); + assertThat(e).hasMessageThat().isEqualTo( "Unsupported ClusterSpecifierPlugin type: type.googleapis.com/google.protobuf.StringValue"); - - XdsRouteConfigureResource.parseClusterSpecifierPlugin(pluginProto, registry); } @Test @@ -2297,11 +2300,11 @@ public void parseCluster_transportSocketMatches_exception() throws ResourceInval Cluster.TransportSocketMatch.newBuilder().setName("match1").build()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.processCluster(cluster, null, LRS_SERVER_INFO, + LoadBalancerRegistry.getDefaultRegistry())); + assertThat(e).hasMessageThat().isEqualTo( "Cluster cluster-foo.googleapis.com: transport-socket-matches not supported."); - XdsClusterResource.processCluster(cluster, null, LRS_SERVER_INFO, - LoadBalancerRegistry.getDefaultRegistry()); } @Test @@ -2346,12 +2349,12 @@ public void parseCluster_validateEdsSourceConfig() throws ResourceInvalidExcepti .setLbPolicy(LbPolicy.ROUND_ROBIN) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.processCluster(cluster3, null, LRS_SERVER_INFO, + LoadBalancerRegistry.getDefaultRegistry())); + assertThat(e).hasMessageThat().isEqualTo( "Cluster cluster-foo.googleapis.com: field eds_cluster_config must be set to indicate to" + " use EDS over ADS or self ConfigSource"); - XdsClusterResource.processCluster(cluster3, null, LRS_SERVER_INFO, - LoadBalancerRegistry.getDefaultRegistry()); } @Test @@ -2620,10 +2623,11 @@ public void parseServerSideListener_invalidTrafficDirection() throws ResourceInv .setName("listener1") .setTrafficDirection(TrafficDirection.OUTBOUND) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("Listener listener1 with invalid traffic direction: OUTBOUND"); - XdsListenerResource.parseServerSideListener( - listener, null, filterRegistry, null, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseServerSideListener( + listener, null, filterRegistry, null, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("Listener listener1 with invalid traffic direction: OUTBOUND"); } @Test @@ -2644,10 +2648,11 @@ public void parseServerSideListener_listenerFiltersPresent() throws ResourceInva .setTrafficDirection(TrafficDirection.INBOUND) .addListenerFilters(ListenerFilter.newBuilder().build()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("Listener listener1 cannot have listener_filters"); - XdsListenerResource.parseServerSideListener( - listener, null, filterRegistry, null, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseServerSideListener(listener, null, filterRegistry, null, + getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("Listener listener1 cannot have listener_filters"); } @Test @@ -2658,10 +2663,11 @@ public void parseServerSideListener_useOriginalDst() throws ResourceInvalidExcep .setTrafficDirection(TrafficDirection.INBOUND) .setUseOriginalDst(BoolValue.of(true)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("Listener listener1 cannot have use_original_dst set to true"); - XdsListenerResource.parseServerSideListener( - listener,null, filterRegistry, null, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseServerSideListener(listener, null, filterRegistry, null, + getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("Listener listener1 cannot have use_original_dst set to true"); } @Test @@ -2674,11 +2680,10 @@ public void parseServerSideListener_emptyAddress() throws ResourceInvalidExcepti .setSocketAddress( SocketAddress.newBuilder())) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("Invalid address: Empty address is not allowed."); - - XdsListenerResource.parseServerSideListener( - listener,null, filterRegistry, null, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseServerSideListener( + listener, null, filterRegistry, null, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat().isEqualTo("Invalid address: Empty address is not allowed."); } @Test @@ -2692,11 +2697,10 @@ public void parseServerSideListener_namedPort() throws ResourceInvalidException SocketAddress.newBuilder() .setAddress("172.14.14.5").setNamedPort(""))) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("NAMED_PORT is not supported in gRPC."); - - XdsListenerResource.parseServerSideListener( - listener,null, filterRegistry, null, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseServerSideListener( + listener, null, filterRegistry, null, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat().isEqualTo("NAMED_PORT is not supported in gRPC."); } @Test @@ -2742,10 +2746,11 @@ public void parseServerSideListener_nonUniqueFilterChainMatch() throws ResourceI .setTrafficDirection(TrafficDirection.INBOUND) .addAllFilterChains(Arrays.asList(filterChain1, filterChain2)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("FilterChainMatch must be unique. Found duplicate:"); - XdsListenerResource.parseServerSideListener( - listener, null, filterRegistry, null, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseServerSideListener( + listener, null, filterRegistry, null, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .startsWith("FilterChainMatch must be unique. Found duplicate:"); } @Test @@ -2791,10 +2796,11 @@ public void parseServerSideListener_nonUniqueFilterChainMatch_sameFilter() .setTrafficDirection(TrafficDirection.INBOUND) .addAllFilterChains(Arrays.asList(filterChain1, filterChain2)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("FilterChainMatch must be unique. Found duplicate:"); - XdsListenerResource.parseServerSideListener( - listener,null, filterRegistry, null, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseServerSideListener( + listener, null, filterRegistry, null, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .startsWith("FilterChainMatch must be unique. Found duplicate:"); } @Test @@ -2854,12 +2860,12 @@ public void parseFilterChain_noHcm() throws ResourceInvalidException { .setFilterChainMatch(FilterChainMatch.getDefaultInstance()) .setTransportSocket(TransportSocket.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseFilterChain( + filterChain, "filter-chain-foo", null, filterRegistry, null, null, + getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat().isEqualTo( "FilterChain filter-chain-foo should contain exact one HttpConnectionManager filter"); - XdsListenerResource.parseFilterChain( - filterChain, "filter-chain-foo", null, filterRegistry, null, null, - getXdsResourceTypeArgs(true)); } @Test @@ -2873,12 +2879,12 @@ public void parseFilterChain_duplicateFilter() throws ResourceInvalidException { .setTransportSocket(TransportSocket.getDefaultInstance()) .addAllFilters(Arrays.asList(filter, filter)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseFilterChain( + filterChain, "filter-chain-foo", null, filterRegistry, null, null, + getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat().isEqualTo( "FilterChain filter-chain-foo should contain exact one HttpConnectionManager filter"); - XdsListenerResource.parseFilterChain( - filterChain, "filter-chain-foo", null, filterRegistry, null, null, - getXdsResourceTypeArgs(true)); } @Test @@ -2891,13 +2897,13 @@ public void parseFilterChain_filterMissingTypedConfig() throws ResourceInvalidEx .setTransportSocket(TransportSocket.getDefaultInstance()) .addFilters(filter) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseFilterChain( + filterChain, "filter-chain-foo", null, filterRegistry, null, null, + getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat().isEqualTo( "FilterChain filter-chain-foo contains filter envoy.http_connection_manager " + "without typed_config"); - XdsListenerResource.parseFilterChain( - filterChain, "filter-chain-foo", null, filterRegistry, null, null, - getXdsResourceTypeArgs(true)); } @Test @@ -2914,13 +2920,13 @@ public void parseFilterChain_unsupportedFilter() throws ResourceInvalidException .setTransportSocket(TransportSocket.getDefaultInstance()) .addFilters(filter) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseFilterChain( + filterChain, "filter-chain-foo", null, filterRegistry, null, null, + getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat().isEqualTo( "FilterChain filter-chain-foo contains filter unsupported with unsupported " + "typed_config type unsupported-type-url"); - XdsListenerResource.parseFilterChain( - filterChain, "filter-chain-foo", null, filterRegistry, null, null, - getXdsResourceTypeArgs(true)); } @Test @@ -2996,53 +3002,55 @@ public void parseFilterChain_duplicateName() throws ResourceInvalidException { .setTrafficDirection(TrafficDirection.INBOUND) .addAllFilterChains(Arrays.asList(filterChain0, filterChain1)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("Filter chain names must be unique. Found duplicate: filter_chain"); - XdsListenerResource.parseServerSideListener( - listenerProto, null, filterRegistry, null, getXdsResourceTypeArgs(true)); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.parseServerSideListener( + listenerProto, null, filterRegistry, null, getXdsResourceTypeArgs(true))); + assertThat(e).hasMessageThat() + .isEqualTo("Filter chain names must be unique. Found duplicate: filter_chain"); } @Test - public void validateCommonTlsContext_tlsParams() throws ResourceInvalidException { + public void validateCommonTlsContext_tlsParams() { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setTlsParams(TlsParameters.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("common-tls-context with tls_params is not supported"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false)); + assertThat(e).hasMessageThat().isEqualTo("common-tls-context with tls_params is not supported"); } @Test - public void validateCommonTlsContext_customHandshaker() throws ResourceInvalidException { + public void validateCommonTlsContext_customHandshaker() { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setCustomHandshaker(TypedExtensionConfig.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("common-tls-context with custom_handshaker is not supported"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false)); + assertThat(e).hasMessageThat().isEqualTo( + "common-tls-context with custom_handshaker is not supported"); } @Test - public void validateCommonTlsContext_validationContext() throws ResourceInvalidException { + public void validateCommonTlsContext_validationContext() { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setValidationContext(CertificateValidationContext.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("ca_certificate_provider_instance or system_root_certs is required " - + "in upstream-tls-context"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false)); + assertThat(e).hasMessageThat().isEqualTo( + "ca_certificate_provider_instance or system_root_certs is required " + + "in upstream-tls-context"); } @Test - public void validateCommonTlsContext_validationContextSdsSecretConfig() - throws ResourceInvalidException { + public void validateCommonTlsContext_validationContextSdsSecretConfig() { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .setValidationContextSdsSecretConfig(SdsSecretConfig.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false)); + assertThat(e).hasMessageThat().isEqualTo( "common-tls-context with validation_context_sds_secret_config is not supported"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); } @Test @@ -3050,10 +3058,10 @@ public void validateCommonTlsContext_tlsCertificateProviderInstance_isRequiredFo throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, true)); + assertThat(e).hasMessageThat().isEqualTo( "tls_certificate_provider_instance is required in downstream-tls-context"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, true); } @Test @@ -3085,11 +3093,11 @@ public void validateCommonTlsContext_tlsCertificateProviderInstance_absentInBoot .setTlsCertificateProviderInstance( CertificateProviderPluginInstance.newBuilder().setInstanceName("bad-name")) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, + ImmutableSet.of("name1", "name2"), true)); + assertThat(e).hasMessageThat().isEqualTo( "CertificateProvider instance name 'bad-name' not defined in the bootstrap file."); - XdsClusterResource - .validateCommonTlsContext(commonTlsContext, ImmutableSet.of("name1", "name2"), true); } @Test @@ -3197,11 +3205,11 @@ public void validateCommonTlsContext_validationContextProviderInstance_absentInB .setCaCertificateProviderInstance(CertificateProviderPluginInstance.newBuilder() .setInstanceName("bad-name")))) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, + ImmutableSet.of("name1", "name2"), false)); + assertThat(e).hasMessageThat().isEqualTo( "ca_certificate_provider_instance name 'bad-name' not defined in the bootstrap file."); - XdsClusterResource - .validateCommonTlsContext(commonTlsContext, ImmutableSet.of("name1", "name2"), false); } @@ -3210,9 +3218,9 @@ public void validateCommonTlsContext_tlsCertificatesCount() throws ResourceInval CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .addTlsCertificates(TlsCertificate.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("tls_certificate_provider_instance is unset"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false)); + assertThat(e).hasMessageThat().isEqualTo("tls_certificate_provider_instance is unset"); } @Test @@ -3221,10 +3229,10 @@ public void validateCommonTlsContext_tlsCertificateSdsSecretConfigsCount() CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .addTlsCertificateSdsSecretConfigs(SdsSecretConfig.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false)); + assertThat(e).hasMessageThat().isEqualTo( "tls_certificate_provider_instance is unset"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); } @Test @@ -3232,10 +3240,11 @@ public void validateCommonTlsContext_combinedValidationContext_isRequiredForClie throws ResourceInvalidException { CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("ca_certificate_provider_instance or system_root_certs is required " - + "in upstream-tls-context"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false)); + assertThat(e).hasMessageThat().isEqualTo( + "ca_certificate_provider_instance or system_root_certs is required " + + "in upstream-tls-context"); } @Test @@ -3245,11 +3254,11 @@ public void validateCommonTlsContext_combinedValidationContextWithoutCertProvide .setCombinedValidationContext( CommonTlsContext.CombinedCertificateValidationContext.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false)); + assertThat(e).hasMessageThat().isEqualTo( "ca_certificate_provider_instance or system_root_certs is required in " + "upstream-tls-context"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, null, false); } @Test @@ -3267,9 +3276,10 @@ public void validateCommonTlsContext_combinedValContextWithDefaultValContextForS .setTlsCertificateProviderInstance( CertificateProviderPluginInstance.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("match_subject_alt_names only allowed in upstream_tls_context"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, ImmutableSet.of(""), true); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, ImmutableSet.of(""), true)); + assertThat(e).hasMessageThat().isEqualTo( + "match_subject_alt_names only allowed in upstream_tls_context"); } @Test @@ -3284,10 +3294,10 @@ public void validateCommonTlsContext_combinedValContextWithDefaultValContextVeri .addVerifyCertificateSpki("foo"))) .setTlsCertificateProviderInstance(CertificateProviderPluginInstance.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("verify_certificate_spki in default_validation_context is not " - + "supported"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, ImmutableSet.of(""), false); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, ImmutableSet.of(""), false)); + assertThat(e).hasMessageThat().isEqualTo( + "verify_certificate_spki in default_validation_context is not supported"); } @Test @@ -3302,10 +3312,10 @@ public void validateCommonTlsContext_combinedValContextWithDefaultValContextVeri .addVerifyCertificateHash("foo"))) .setTlsCertificateProviderInstance(CertificateProviderPluginInstance.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("verify_certificate_hash in default_validation_context is not " - + "supported"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, ImmutableSet.of(""), false); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, ImmutableSet.of(""), false)); + assertThat(e).hasMessageThat().isEqualTo( + "verify_certificate_hash in default_validation_context is not supported"); } @Test @@ -3321,11 +3331,11 @@ public void validateCommonTlsContext_combinedValContextDfltValContextRequireSign .setTlsCertificateProviderInstance( CertificateProviderPluginInstance.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, ImmutableSet.of(""), false)); + assertThat(e).hasMessageThat().isEqualTo( "require_signed_certificate_timestamp in default_validation_context is not " + "supported"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, ImmutableSet.of(""), false); } @Test @@ -3340,9 +3350,9 @@ public void validateCommonTlsContext_combinedValidationContextWithDefaultValidat .setCrl(DataSource.getDefaultInstance()))) .setTlsCertificateProviderInstance(CertificateProviderPluginInstance.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("crl in default_validation_context is not supported"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, ImmutableSet.of(""), false); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, ImmutableSet.of(""), false)); + assertThat(e).hasMessageThat().isEqualTo("crl in default_validation_context is not supported"); } @Test @@ -3357,18 +3367,19 @@ public void validateCommonTlsContext_combinedValContextWithDfltValContextCustomV .setCustomValidatorConfig(TypedExtensionConfig.getDefaultInstance()))) .setTlsCertificateProviderInstance(CertificateProviderPluginInstance.getDefaultInstance()) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("custom_validator_config in default_validation_context is not " - + "supported"); - XdsClusterResource.validateCommonTlsContext(commonTlsContext, ImmutableSet.of(""), false); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateCommonTlsContext(commonTlsContext, ImmutableSet.of(""), false)); + assertThat(e).hasMessageThat().isEqualTo( + "custom_validator_config in default_validation_context is not supported"); } @Test public void validateDownstreamTlsContext_noCommonTlsContext() throws ResourceInvalidException { DownstreamTlsContext downstreamTlsContext = DownstreamTlsContext.getDefaultInstance(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("common-tls-context is required in downstream-tls-context"); - XdsListenerResource.validateDownstreamTlsContext(downstreamTlsContext, null); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.validateDownstreamTlsContext(downstreamTlsContext, null)); + assertThat(e).hasMessageThat().isEqualTo( + "common-tls-context is required in downstream-tls-context"); } @Test @@ -3385,9 +3396,11 @@ public void validateDownstreamTlsContext_hasRequireSni() throws ResourceInvalidE .setCommonTlsContext(commonTlsContext) .setRequireSni(BoolValue.of(true)) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("downstream-tls-context with require-sni is not supported"); - XdsListenerResource.validateDownstreamTlsContext(downstreamTlsContext, ImmutableSet.of("")); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.validateDownstreamTlsContext(downstreamTlsContext, + ImmutableSet.of(""))); + assertThat(e).hasMessageThat().isEqualTo( + "downstream-tls-context with require-sni is not supported"); } @Test @@ -3404,18 +3417,20 @@ public void validateDownstreamTlsContext_hasOcspStaplePolicy() throws ResourceIn .setCommonTlsContext(commonTlsContext) .setOcspStaplePolicy(DownstreamTlsContext.OcspStaplePolicy.STRICT_STAPLING) .build(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage( + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsListenerResource.validateDownstreamTlsContext(downstreamTlsContext, + ImmutableSet.of(""))); + assertThat(e).hasMessageThat().isEqualTo( "downstream-tls-context with ocsp_staple_policy value STRICT_STAPLING is not supported"); - XdsListenerResource.validateDownstreamTlsContext(downstreamTlsContext, ImmutableSet.of("")); } @Test public void validateUpstreamTlsContext_noCommonTlsContext() throws ResourceInvalidException { UpstreamTlsContext upstreamTlsContext = UpstreamTlsContext.getDefaultInstance(); - thrown.expect(ResourceInvalidException.class); - thrown.expectMessage("common-tls-context is required in upstream-tls-context"); - XdsClusterResource.validateUpstreamTlsContext(upstreamTlsContext, null); + ResourceInvalidException e = assertThrows(ResourceInvalidException.class, () -> + XdsClusterResource.validateUpstreamTlsContext(upstreamTlsContext, null)); + assertThat(e).hasMessageThat().isEqualTo( + "common-tls-context is required in upstream-tls-context"); } @Test diff --git a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerProviderTest.java b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerProviderTest.java index 9f0b5f9578e..37ea24b2aa9 100644 --- a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerProviderTest.java @@ -16,6 +16,7 @@ package io.grpc.xds; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import com.google.common.collect.ImmutableList; @@ -26,17 +27,13 @@ import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; import java.util.List; import java.util.Map; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link PriorityLoadBalancerProvider}. */ @RunWith(JUnit4.class) public class PriorityLoadBalancerProviderTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule public final ExpectedException thrown = ExpectedException.none(); @SuppressWarnings("ExpectedExceptionChecker") @Test @@ -48,8 +45,8 @@ public void priorityLbConfig_emptyPriorities() { newChildConfig(mock(LoadBalancerProvider.class), null), true)); List priorities = ImmutableList.of(); - thrown.expect(IllegalArgumentException.class); - new PriorityLbConfig(childConfigs, priorities); + assertThrows(IllegalArgumentException.class, + () -> new PriorityLbConfig(childConfigs, priorities)); } @SuppressWarnings("ExpectedExceptionChecker") @@ -62,8 +59,8 @@ public void priorityLbConfig_missingChildConfig() { newChildConfig(mock(LoadBalancerProvider.class), null), true)); List priorities = ImmutableList.of("p0", "p1"); - thrown.expect(IllegalArgumentException.class); - new PriorityLbConfig(childConfigs, priorities); + assertThrows(IllegalArgumentException.class, + () -> new PriorityLbConfig(childConfigs, priorities)); } private Object newChildConfig(LoadBalancerProvider provider, Object config) { diff --git a/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java b/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java index 86e4fc83a8c..24f1750d5a8 100644 --- a/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -50,7 +51,6 @@ import java.util.concurrent.TimeUnit; 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.Mock; @@ -64,9 +64,6 @@ public class SharedXdsClientPoolProviderTest { private static final String SERVER_URI = "trafficdirector.googleapis.com"; @Rule public final MockitoRule mocks = MockitoJUnit.rule(); - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); private final Node node = Node.newBuilder().setId("SharedXdsClientPoolProviderTest").build(); private final MetricRecorder metricRecorder = new MetricRecorder() {}; private static final String DUMMY_TARGET = "dummy"; @@ -83,9 +80,9 @@ public void noServer() throws XdsInitializationException { BootstrapInfo.builder().servers(Collections.emptyList()).node(node).build(); when(bootstrapper.bootstrap()).thenReturn(bootstrapInfo); SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); - thrown.expect(XdsInitializationException.class); - thrown.expectMessage("No xDS server provided"); - provider.getOrCreate(DUMMY_TARGET, metricRecorder); + XdsInitializationException e = assertThrows(XdsInitializationException.class, + () -> provider.getOrCreate(DUMMY_TARGET, metricRecorder)); + assertThat(e).hasMessageThat().isEqualTo("No xDS server provided"); assertThat(provider.get(DUMMY_TARGET)).isNull(); } diff --git a/xds/src/test/java/io/grpc/xds/WeightedRandomPickerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRandomPickerTest.java index d6240fb09bb..691615762bf 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRandomPickerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRandomPickerTest.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import io.grpc.LoadBalancer.PickResult; @@ -30,7 +31,6 @@ import java.util.List; 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.Mock; @@ -42,9 +42,6 @@ */ @RunWith(JUnit4.class) public class WeightedRandomPickerTest { - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public final ExpectedException thrown = ExpectedException.none(); @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -128,20 +125,18 @@ public long nextLong(long bound) { public void emptyList() { List emptyList = new ArrayList<>(); - thrown.expect(IllegalArgumentException.class); - new WeightedRandomPicker(emptyList); + assertThrows(IllegalArgumentException.class, () -> new WeightedRandomPicker(emptyList)); } @Test public void negativeWeight() { - thrown.expect(IllegalArgumentException.class); - new WeightedChildPicker(-1, childPicker0); + assertThrows(IllegalArgumentException.class, () -> new WeightedChildPicker(-1, childPicker0)); } @Test public void overWeightSingle() { - thrown.expect(IllegalArgumentException.class); - new WeightedChildPicker(Integer.MAX_VALUE * 3L, childPicker0); + assertThrows(IllegalArgumentException.class, + () -> new WeightedChildPicker(Integer.MAX_VALUE * 3L, childPicker0)); } @Test @@ -152,8 +147,8 @@ public void overWeightAggregate() { new WeightedChildPicker(Integer.MAX_VALUE, childPicker1), new WeightedChildPicker(10, childPicker2)); - thrown.expect(IllegalArgumentException.class); - new WeightedRandomPicker(weightedChildPickers, fakeRandom); + assertThrows(IllegalArgumentException.class, + () -> new WeightedRandomPicker(weightedChildPickers, fakeRandom)); } @Test From 142e378cea0aa90aae36fec55f90c30ede95f965 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 30 May 2025 09:00:27 -0700 Subject: [PATCH 272/591] xds: Improve shutdown handling of XdsDepManager The most important change here is to handle subscribeToCluster() calls after shutdown(), and preventing the internal state from being heavily confused as the assumption is there are no watchers after shutdown(). ClusterSubscription.closed isn't strictly necessary, but I don't want the code to depend on double-deregistration being safe. maybePublishConfig() isn't being called after shutdown(), but adding the protection avoids a class of bugs that would cause channel panic. --- .../io/grpc/xds/XdsDependencyManager.java | 29 ++++++++++++++----- .../io/grpc/xds/XdsDependencyManagerTest.java | 16 ++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index d804954ecf9..1ba4963186e 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -93,13 +93,15 @@ public static String toContextStr(String typeName, String resourceName) { @Override public Closeable subscribeToCluster(String clusterName) { - checkNotNull(clusterName, "clusterName"); ClusterSubscription subscription = new ClusterSubscription(clusterName); syncContext.execute(() -> { + if (getWatchers(XdsListenerResource.getInstance()).isEmpty()) { + subscription.closed = true; + return; // shutdown() called + } addClusterWatcher(clusterName, subscription, 1); - maybePublishConfig(); }); return subscription; @@ -207,10 +209,14 @@ private void releaseSubscription(ClusterSubscription subscription) { checkNotNull(subscription, "subscription"); String clusterName = subscription.getClusterName(); syncContext.execute(() -> { + if (subscription.closed) { + return; + } + subscription.closed = true; XdsWatcherBase cdsWatcher = resourceWatchers.get(CLUSTER_RESOURCE).watchers.get(clusterName); if (cdsWatcher == null) { - return; // already released while waiting for the syncContext + return; // shutdown() called } cancelClusterWatcherTree((CdsWatcher) cdsWatcher, subscription); maybePublishConfig(); @@ -257,6 +263,9 @@ private void cancelClusterWatcherTree(CdsWatcher root, Object parentContext) { */ private void maybePublishConfig() { syncContext.throwIfNotInThisSynchronizationContext(); + if (getWatchers(XdsListenerResource.getInstance()).isEmpty()) { + return; // shutdown() called + } boolean waitingOnResource = resourceWatchers.values().stream() .flatMap(typeWatchers -> typeWatchers.watchers.values().stream()) .anyMatch(XdsWatcherBase::missingResult); @@ -293,6 +302,11 @@ StatusOr buildUpdate() { routeSource = ((LdsWatcher) ldsWatcher).getRouteSource(); } + if (routeSource == null) { + return StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( + "Bug: No route source found for listener " + dataPlaneAuthority)); + } + StatusOr statusOrRdsUpdate = routeSource.getRdsUpdate(); if (!statusOrRdsUpdate.hasValue()) { return StatusOr.fromStatus(statusOrRdsUpdate.getStatus()); @@ -557,14 +571,15 @@ public interface XdsConfigWatcher { void onUpdate(StatusOr config); } - private class ClusterSubscription implements Closeable { - String clusterName; + private final class ClusterSubscription implements Closeable { + private final String clusterName; + boolean closed; // Accessed from syncContext public ClusterSubscription(String clusterName) { - this.clusterName = clusterName; + this.clusterName = checkNotNull(clusterName, "clusterName"); } - public String getClusterName() { + String getClusterName() { return clusterName; } diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index 1f3d8511ecc..8a24d21f77a 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -848,6 +848,22 @@ public void edsUpdateAfterShutdown() { }); } + @Test + public void subscribeToClusterAfterShutdown() throws Exception { + XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS", + ENDPOINT_HOSTNAME, ENDPOINT_PORT); + + InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + inOrder.verify(xdsConfigWatcher).onUpdate(any()); + xdsDependencyManager.shutdown(); + + Closeable subscription = xdsDependencyManager.subscribeToCluster("CDS"); + inOrder.verify(xdsConfigWatcher, never()).onUpdate(any()); + subscription.close(); + } + private Listener buildInlineClientListener(String rdsName, String clusterName) { return XdsTestUtils.buildInlineClientListener(rdsName, clusterName, serverName); } From 8044a56ad2d6b3256ca72c21c10261ad77544ffb Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sun, 1 Jun 2025 23:55:27 -0700 Subject: [PATCH 273/591] xds: Remove timeouts from XdsDepManagerTest (#12114) The tests are using FakeClock and inprocess transport with direct executor, so all operations should run in the test thread. --- .../io/grpc/xds/XdsDependencyManagerTest.java | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index 8a24d21f77a..542471e2734 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -36,7 +36,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.google.common.collect.ImmutableMap; @@ -196,7 +196,7 @@ public void verify_basic_config() { xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig)); + verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig)); testWatcher.verifyStats(1, 0); } @@ -206,13 +206,13 @@ public void verify_config_update() { serverName, serverName, nameResolverArgs, scheduler); InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig)); + inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig)); testWatcher.verifyStats(1, 0); assertThat(testWatcher.lastConfig).isEqualTo(defaultXdsConfig); XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS2", "CDS2", "EDS2", ENDPOINT_HOSTNAME + "2", ENDPOINT_PORT + 2); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(ArgumentMatchers.notNull()); + inOrder.verify(xdsConfigWatcher).onUpdate(ArgumentMatchers.notNull()); testWatcher.verifyStats(2, 0); assertThat(testWatcher.lastConfig).isNotEqualTo(defaultXdsConfig); } @@ -222,7 +222,7 @@ public void verify_simple_aggregate() { InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig)); + inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig)); List childNames = Arrays.asList("clusterC", "clusterB", "clusterA"); String rootName = "root_c"; @@ -233,7 +233,7 @@ public void verify_simple_aggregate() { ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig)); XdsTestUtils.setAggregateCdsConfig(controlPlaneService, serverName, rootName, childNames); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + inOrder.verify(xdsConfigWatcher).onUpdate(any()); Map> lastConfigClusters = testWatcher.lastConfig.getClusters(); @@ -281,13 +281,13 @@ public void testComplexRegisteredAggregate() throws IOException { xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + inOrder.verify(xdsConfigWatcher).onUpdate(any()); Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + inOrder.verify(xdsConfigWatcher).onUpdate(any()); Closeable subscription2 = xdsDependencyManager.subscribeToCluster(rootName2); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); testWatcher.verifyStats(3, 0); ImmutableSet.Builder builder = ImmutableSet.builder(); Set expectedClusters = builder.add(rootName1).add(rootName2).add(CLUSTER_NAME) @@ -297,7 +297,7 @@ public void testComplexRegisteredAggregate() throws IOException { // Close 1 subscription shouldn't affect the other or RDS subscriptions subscription1.close(); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); builder = ImmutableSet.builder(); Set expectedClusters2 = builder.add(rootName2).add(CLUSTER_NAME).addAll(childNames2).build(); @@ -305,7 +305,7 @@ public void testComplexRegisteredAggregate() throws IOException { .isEqualTo(expectedClusters2); subscription2.close(); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig)); + inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig)); } @Test @@ -313,7 +313,7 @@ public void testDelayedSubscription() { InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig)); + inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig)); String rootName1 = "root_c"; @@ -362,7 +362,7 @@ public void testMissingCdsAndEds() { serverName, serverName, nameResolverArgs, scheduler); fakeClock.forwardTime(16, TimeUnit.SECONDS); - verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); List> returnedClusters = new ArrayList<>(); for (String childName : childNames) { @@ -395,7 +395,7 @@ public void testMissingLds() { serverName, ldsName, nameResolverArgs, scheduler); fakeClock.forwardTime(16, TimeUnit.SECONDS); - verify(xdsConfigWatcher, timeout(1000)).onUpdate( + verify(xdsConfigWatcher).onUpdate( argThat(StatusOrMatcher.hasStatus(statusHasCode(Status.Code.UNAVAILABLE) .andDescriptionContains(ldsName)))); @@ -411,7 +411,7 @@ public void testTcpListenerErrors() { serverName, serverName, nameResolverArgs, scheduler); fakeClock.forwardTime(16, TimeUnit.SECONDS); - verify(xdsConfigWatcher, timeout(1000)).onUpdate( + verify(xdsConfigWatcher).onUpdate( argThat(StatusOrMatcher.hasStatus( statusHasCode(Status.Code.UNAVAILABLE).andDescriptionContains("Not an API listener")))); @@ -429,7 +429,7 @@ public void testMissingRds() { serverName, serverName, nameResolverArgs, scheduler); fakeClock.forwardTime(16, TimeUnit.SECONDS); - verify(xdsConfigWatcher, timeout(1000)).onUpdate( + verify(xdsConfigWatcher).onUpdate( argThat(StatusOrMatcher.hasStatus(statusHasCode(Status.Code.UNAVAILABLE) .andDescriptionContains(rdsName)))); @@ -446,7 +446,7 @@ public void testUpdateToMissingVirtualHost() { serverName, serverName, nameResolverArgs, scheduler); // Update with a config that has a virtual host that doesn't match the server name - verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); assertThat(xdsUpdateCaptor.getValue().getStatus().getDescription()) .contains("Failed to find virtual host matching hostname: " + serverName); @@ -461,7 +461,7 @@ public void testCorruptLds() { xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, ldsResourceName, nameResolverArgs, scheduler); - verify(xdsConfigWatcher, timeout(1000)).onUpdate( + verify(xdsConfigWatcher).onUpdate( argThat(StatusOrMatcher.hasStatus( statusHasCode(Status.Code.UNAVAILABLE).andDescriptionContains(ldsResourceName)))); @@ -474,14 +474,14 @@ public void testChangeRdsName_fromLds() { InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig)); + inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig)); String newRdsName = "newRdsName1"; Listener clientListener = buildInlineClientListener(newRdsName, CLUSTER_NAME); controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, ImmutableMap.of(serverName, clientListener)); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); assertThat(xdsUpdateCaptor.getValue().getValue()).isNotEqualTo(defaultXdsConfig); assertThat(xdsUpdateCaptor.getValue().getValue().getVirtualHost().name()).isEqualTo(newRdsName); } @@ -530,7 +530,7 @@ public void testMultipleParentsInCdsTree() throws IOException { InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); XdsConfig initialConfig = xdsUpdateCaptor.getValue().getValue(); // Make sure that adding subscriptions that rds points at doesn't change the config @@ -551,12 +551,12 @@ public void testMultipleParentsInCdsTree() throws IOException { XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "clusterA11"); controlPlaneService.setXdsConfig( ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, newRouteConfig)); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet().size()).isEqualTo(4); // Now that it is released, we should only have A11 rootSub.close(); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet()) .containsExactly("clusterA11"); } @@ -591,7 +591,7 @@ public void testMultipleCdsReferToSameEds() { // Start the actual test xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); XdsConfig initialConfig = xdsUpdateCaptor.getValue().getValue(); assertThat(initialConfig.getClusters().keySet()) .containsExactly("root", "clusterA", "clusterB"); @@ -643,7 +643,7 @@ public void testChangeRdsName_FromLds_complexTree() { Listener clientListener = buildInlineClientListener(newRdsName, "root"); controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, ImmutableMap.of(serverName, clientListener)); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); XdsConfig config = xdsUpdateCaptor.getValue().getValue(); assertThat(config.getVirtualHost().name()).isEqualTo(newRdsName); assertThat(config.getClusters().size()).isEqualTo(4); @@ -655,7 +655,7 @@ public void testChangeAggCluster() { xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + inOrder.verify(xdsConfigWatcher).onUpdate(any()); // Setup initial config A -> A1 -> (A11, A12) Cluster rootCluster = @@ -699,7 +699,7 @@ public void testChangeAggCluster() { // Verify that the config is updated as expected ClusterNameMatcher nameMatcher = new ClusterNameMatcher(Arrays.asList("root", "clusterA21", "clusterA22")); - inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(argThat(nameMatcher)); + inOrder.verify(xdsConfigWatcher).onUpdate(argThat(nameMatcher)); } @Test @@ -710,7 +710,7 @@ public void testCdsError() throws IOException { xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture()); + verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); Status status = xdsUpdateCaptor.getValue().getValue() .getClusters().get(CLUSTER_NAME).getStatus(); assertThat(status.getDescription()).contains(XdsTestUtils.CLUSTER_NAME); @@ -724,7 +724,7 @@ public void ldsUpdateAfterShutdown() { xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + verify(xdsConfigWatcher).onUpdate(any()); @SuppressWarnings("unchecked") XdsClient.ResourceWatcher resourceWatcher = @@ -734,7 +734,7 @@ public void ldsUpdateAfterShutdown() { serverName, resourceWatcher, MoreExecutors.directExecutor()); - verify(resourceWatcher, timeout(5000)).onChanged(any()); + verify(resourceWatcher).onChanged(any()); syncContext.execute(() -> { // Shutdown before any updates. This will unsubscribe from XdsClient, but only after this @@ -743,7 +743,7 @@ public void ldsUpdateAfterShutdown() { XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS2", "CDS", "EDS", ENDPOINT_HOSTNAME, ENDPOINT_PORT); - verify(resourceWatcher, timeout(5000).times(2)).onChanged(any()); + verify(resourceWatcher, times(2)).onChanged(any()); xdsClient.cancelXdsResourceWatch( XdsListenerResource.getInstance(), serverName, resourceWatcher); }); @@ -757,7 +757,7 @@ public void rdsUpdateAfterShutdown() { xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + verify(xdsConfigWatcher).onUpdate(any()); @SuppressWarnings("unchecked") XdsClient.ResourceWatcher resourceWatcher = @@ -767,7 +767,7 @@ public void rdsUpdateAfterShutdown() { "RDS", resourceWatcher, MoreExecutors.directExecutor()); - verify(resourceWatcher, timeout(5000)).onChanged(any()); + verify(resourceWatcher).onChanged(any()); syncContext.execute(() -> { // Shutdown before any updates. This will unsubscribe from XdsClient, but only after this @@ -776,7 +776,7 @@ public void rdsUpdateAfterShutdown() { XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS2", "EDS", ENDPOINT_HOSTNAME, ENDPOINT_PORT); - verify(resourceWatcher, timeout(5000).times(2)).onChanged(any()); + verify(resourceWatcher, times(2)).onChanged(any()); xdsClient.cancelXdsResourceWatch( XdsRouteConfigureResource.getInstance(), serverName, resourceWatcher); }); @@ -790,7 +790,7 @@ public void cdsUpdateAfterShutdown() { xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + verify(xdsConfigWatcher).onUpdate(any()); @SuppressWarnings("unchecked") XdsClient.ResourceWatcher resourceWatcher = @@ -800,7 +800,7 @@ public void cdsUpdateAfterShutdown() { "CDS", resourceWatcher, MoreExecutors.directExecutor()); - verify(resourceWatcher, timeout(5000)).onChanged(any()); + verify(resourceWatcher).onChanged(any()); syncContext.execute(() -> { // Shutdown before any updates. This will unsubscribe from XdsClient, but only after this @@ -809,7 +809,7 @@ public void cdsUpdateAfterShutdown() { XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS2", ENDPOINT_HOSTNAME, ENDPOINT_PORT); - verify(resourceWatcher, timeout(5000).times(2)).onChanged(any()); + verify(resourceWatcher, times(2)).onChanged(any()); xdsClient.cancelXdsResourceWatch( XdsClusterResource.getInstance(), serverName, resourceWatcher); }); @@ -823,7 +823,7 @@ public void edsUpdateAfterShutdown() { xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); - verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); + verify(xdsConfigWatcher).onUpdate(any()); @SuppressWarnings("unchecked") XdsClient.ResourceWatcher resourceWatcher = @@ -833,7 +833,7 @@ public void edsUpdateAfterShutdown() { "EDS", resourceWatcher, MoreExecutors.directExecutor()); - verify(resourceWatcher, timeout(5000)).onChanged(any()); + verify(resourceWatcher).onChanged(any()); syncContext.execute(() -> { // Shutdown before any updates. This will unsubscribe from XdsClient, but only after this @@ -842,7 +842,7 @@ public void edsUpdateAfterShutdown() { XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS", ENDPOINT_HOSTNAME + "2", ENDPOINT_PORT); - verify(resourceWatcher, timeout(5000).times(2)).onChanged(any()); + verify(resourceWatcher, times(2)).onChanged(any()); xdsClient.cancelXdsResourceWatch( XdsEndpointResource.getInstance(), serverName, resourceWatcher); }); From 379fbaa380422c24ba0ce6e2f012fe90b33027d8 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Mon, 2 Jun 2025 15:37:11 +0530 Subject: [PATCH 274/591] Update README etc to reference 1.73.0 (#12108) --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 30e26bc9955..012eab498e6 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.72.0/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.72.0/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.73.0/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.73.0/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.72.0 + 1.73.0 runtime io.grpc grpc-protobuf - 1.72.0 + 1.73.0 io.grpc grpc-stub - 1.72.0 + 1.73.0 org.apache.tomcat @@ -79,18 +79,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.72.0' -implementation 'io.grpc:grpc-protobuf:1.72.0' -implementation 'io.grpc:grpc-stub:1.72.0' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.73.0' +implementation 'io.grpc:grpc-protobuf:1.73.0' +implementation 'io.grpc:grpc-stub:1.73.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.72.0' -implementation 'io.grpc:grpc-protobuf-lite:1.72.0' -implementation 'io.grpc:grpc-stub:1.72.0' +implementation 'io.grpc:grpc-okhttp:1.73.0' +implementation 'io.grpc:grpc-protobuf-lite:1.73.0' +implementation 'io.grpc:grpc-stub:1.73.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` @@ -99,7 +99,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.72.0 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.73.0 Development snapshots are available in [Sonatypes's snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/). @@ -131,7 +131,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.72.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.73.0:exe:${os.detected.classifier} @@ -161,7 +161,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.72.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.73.0' } } generateProtoTasks { @@ -194,7 +194,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.72.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.73.0' } } generateProtoTasks { From 6bad6005924d0e665adcd959109f8b7ad45c3a7c Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 2 Jun 2025 06:42:31 -0700 Subject: [PATCH 275/591] xds: Use getWatchers more often in XdsDepManager This provides better type and missing-map handling. Note that getWatchers() now implicitly creates the map if it doesn't exist, instead of just returning an empty map. That makes it a bit easier to use and more importantly avoids accidents where a bug tries to modify the immutable map. --- .../io/grpc/xds/XdsDependencyManager.java | 73 +++++++------------ 1 file changed, 25 insertions(+), 48 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index 1ba4963186e..0c3e110119c 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -112,14 +112,7 @@ private void addWatcher(XdsWatcherBase watcher) { XdsResourceType type = watcher.type; String resourceName = watcher.resourceName; - @SuppressWarnings("unchecked") - TypeWatchers typeWatchers = (TypeWatchers)resourceWatchers.get(type); - if (typeWatchers == null) { - typeWatchers = new TypeWatchers<>(type); - resourceWatchers.put(type, typeWatchers); - } - - typeWatchers.add(resourceName, watcher); + getWatchers(type).put(resourceName, watcher); xdsClient.watchXdsResource(type, resourceName, watcher, syncContext); } @@ -158,16 +151,12 @@ private void cancelWatcher(XdsWatcherBase watcher) XdsResourceType type = watcher.type; String resourceName = watcher.resourceName; - @SuppressWarnings("unchecked") - TypeWatchers typeWatchers = (TypeWatchers)resourceWatchers.get(type); - if (typeWatchers == null) { - logger.log(DEBUG, "Trying to cancel watcher {0}, but type not watched", watcher); + if (getWatchers(type).remove(resourceName) == null) { + logger.log(DEBUG, "Trying to cancel watcher {0}, but it isn't watched", watcher); return; } - typeWatchers.watchers.remove(resourceName); xdsClient.cancelXdsResourceWatch(type, resourceName, watcher); - } private static void throwIfParentContextsNotEmpty(XdsWatcherBase watcher) { @@ -213,8 +202,8 @@ private void releaseSubscription(ClusterSubscription subscription) { return; } subscription.closed = true; - XdsWatcherBase cdsWatcher = - resourceWatchers.get(CLUSTER_RESOURCE).watchers.get(clusterName); + XdsWatcherBase cdsWatcher + = getWatchers(CLUSTER_RESOURCE).get(clusterName); if (cdsWatcher == null) { return; // shutdown() called } @@ -236,14 +225,12 @@ private void cancelClusterWatcherTree(CdsWatcher root, Object parentContext) { switch (cdsUpdate.clusterType()) { case EDS: String edsServiceName = root.getEdsServiceName(); - EdsWatcher edsWatcher = - (EdsWatcher) resourceWatchers.get(ENDPOINT_RESOURCE).watchers.get(edsServiceName); + EdsWatcher edsWatcher = (EdsWatcher) getWatchers(ENDPOINT_RESOURCE).get(edsServiceName); cancelEdsWatcher(edsWatcher, root); break; case AGGREGATE: for (String cluster : cdsUpdate.prioritizedClusterNames()) { - CdsWatcher clusterWatcher = - (CdsWatcher) resourceWatchers.get(CLUSTER_RESOURCE).watchers.get(cluster); + CdsWatcher clusterWatcher = (CdsWatcher) getWatchers(CLUSTER_RESOURCE).get(cluster); if (clusterWatcher != null) { cancelClusterWatcherTree(clusterWatcher, root); } @@ -348,7 +335,8 @@ private Map> getWatchers( XdsResourceType resourceType) { TypeWatchers typeWatchers = resourceWatchers.get(resourceType); if (typeWatchers == null) { - return Collections.emptyMap(); + typeWatchers = new TypeWatchers(resourceType); + resourceWatchers.put(resourceType, typeWatchers); } assert typeWatchers.resourceType == resourceType; @SuppressWarnings("unchecked") @@ -470,25 +458,22 @@ public String toString() { // Returns true if the watcher was added, false if it already exists private boolean addEdsWatcher(String edsServiceName, CdsWatcher parentContext) { - TypeWatchers typeWatchers = resourceWatchers.get(XdsEndpointResource.getInstance()); - if (typeWatchers == null || !typeWatchers.watchers.containsKey(edsServiceName)) { - addWatcher(new EdsWatcher(edsServiceName, parentContext)); - return true; + EdsWatcher watcher + = (EdsWatcher) getWatchers(XdsEndpointResource.getInstance()).get(edsServiceName); + if (watcher != null) { + watcher.addParentContext(parentContext); // Is a set, so don't need to check for existence + return false; } - EdsWatcher watcher = (EdsWatcher) typeWatchers.watchers.get(edsServiceName); - watcher.addParentContext(parentContext); // Is a set, so don't need to check for existence - return false; + addWatcher(new EdsWatcher(edsServiceName, parentContext)); + return true; } private void addClusterWatcher(String clusterName, Object parentContext, int depth) { - TypeWatchers clusterWatchers = resourceWatchers.get(CLUSTER_RESOURCE); - if (clusterWatchers != null) { - CdsWatcher watcher = (CdsWatcher) clusterWatchers.watchers.get(clusterName); - if (watcher != null) { - watcher.parentContexts.put(parentContext, depth); - return; - } + CdsWatcher watcher = (CdsWatcher) getWatchers(CLUSTER_RESOURCE).get(clusterName); + if (watcher != null) { + watcher.parentContexts.put(parentContext, depth); + return; } addWatcher(new CdsWatcher(clusterName, parentContext, depth)); @@ -546,7 +531,7 @@ private static Set getClusterNamesFromVirtualHost(VirtualHost virtualHos } private CdsWatcher getCluster(String clusterName) { - return (CdsWatcher) resourceWatchers.get(CLUSTER_RESOURCE).watchers.get(clusterName); + return (CdsWatcher) getWatchers(CLUSTER_RESOURCE).get(clusterName); } private static class TypeWatchers { @@ -557,10 +542,6 @@ private static class TypeWatchers { TypeWatchers(XdsResourceType resourceType) { this.resourceType = resourceType; } - - public void add(String resourceName, XdsWatcherBase watcher) { - watchers.put(resourceName, watcher); - } } public interface XdsConfigWatcher { @@ -738,11 +719,11 @@ private void cleanUpRdsWatcher() { logger.log(XdsLogger.XdsLogLevel.DEBUG, "Stop watching RDS resource {0}", rdsName); // Cleanup clusters (as appropriate) that had the old rds watcher as a parent - if (!oldRdsWatcher.hasDataValue() || resourceWatchers.get(CLUSTER_RESOURCE) == null) { + if (!oldRdsWatcher.hasDataValue()) { return; } - for (XdsWatcherBase watcher : - resourceWatchers.get(CLUSTER_RESOURCE).watchers.values()) { + for (XdsWatcherBase watcher : + getWatchers(CLUSTER_RESOURCE).values()) { cancelCdsWatcher((CdsWatcher) watcher, oldRdsWatcher); } } @@ -752,11 +733,7 @@ private RdsWatcher getRdsWatcher() { if (rdsName == null) { return null; } - TypeWatchers watchers = resourceWatchers.get(XdsRouteConfigureResource.getInstance()); - if (watchers == null) { - return null; - } - return (RdsWatcher) watchers.watchers.get(rdsName); + return (RdsWatcher) getWatchers(XdsRouteConfigureResource.getInstance()).get(rdsName); } public RdsUpdateSupplier getRouteSource() { From 48d08e643eb872647990f6881abe8e5d00b0a307 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 2 Jun 2025 23:22:38 -0700 Subject: [PATCH 276/591] xds: Remove EDS maybePublishConfig() avoidance in XdsDepManager (#12121) The optimization makes the code more complicated. Yes, we know that maybePublishConfig() will do no work because of an outstanding watch, but we don't do this for other new watchers created and doing so would just make the code more bug-prone. This removes a difference in how different watcher types are handled. --- .../io/grpc/xds/XdsDependencyManager.java | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index 0c3e110119c..d786e525c0c 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -456,17 +456,15 @@ public String toString() { return logId.toString(); } - // Returns true if the watcher was added, false if it already exists - private boolean addEdsWatcher(String edsServiceName, CdsWatcher parentContext) { + private void addEdsWatcher(String edsServiceName, CdsWatcher parentContext) { EdsWatcher watcher = (EdsWatcher) getWatchers(XdsEndpointResource.getInstance()).get(edsServiceName); if (watcher != null) { watcher.addParentContext(parentContext); // Is a set, so don't need to check for existence - return false; + return; } addWatcher(new EdsWatcher(edsServiceName, parentContext)); - return true; } private void addClusterWatcher(String clusterName, Object parentContext, int depth) { @@ -823,13 +821,10 @@ public void onChanged(XdsClusterResource.CdsUpdate update) { switch (update.clusterType()) { case EDS: setData(update); - if (!addEdsWatcher(getEdsServiceName(), this)) { - maybePublishConfig(); - } + addEdsWatcher(getEdsServiceName(), this); break; case LOGICAL_DNS: setData(update); - maybePublishConfig(); // no eds needed break; case AGGREGATE: @@ -856,27 +851,20 @@ public void onChanged(XdsClusterResource.CdsUpdate update) { setData(update); Set addedClusters = Sets.difference(newNames, oldNames); addedClusters.forEach((cluster) -> addClusterWatcher(cluster, parentContext, depth)); - - if (addedClusters.isEmpty()) { - maybePublishConfig(); - } - } else { // data was set to error status above - maybePublishConfig(); } } else if (depth <= MAX_CLUSTER_RECURSION_DEPTH) { setData(update); update.prioritizedClusterNames() .forEach(name -> addClusterWatcher(name, parentContext, depth)); - maybePublishConfig(); } break; default: Status error = Status.UNAVAILABLE.withDescription( "aggregate cluster graph exceeds max depth at " + resourceName() + nodeInfo()); setDataAsStatus(error); - maybePublishConfig(); } + maybePublishConfig(); } public String getEdsServiceName() { From efe9ccc22caf88dfa1746e8edb840748e00cbaa4 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 2 Jun 2025 23:32:02 -0700 Subject: [PATCH 277/591] xds: Non-SOTW resources need onError() callbacks, too (#12122) SOTW is unique in that it can become absent after being found. But if we NACK when initially loading the resource, we don't want to delay, depend on the resource timeout, and then give a poor error. This was noticed while adding the EDS restriction that address is not a hostname and some tests started hanging instead of failing quickly. --- .../main/java/io/grpc/xds/client/XdsClientImpl.java | 11 +++++------ .../java/io/grpc/xds/GrpcXdsClientImplTestBase.java | 2 ++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index 4de8ead7c0a..2b25d4db977 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -592,12 +592,6 @@ private void handleResourceUpdate( subscriber.onRejected(args.versionInfo, updateTime, errorDetail); } - // Nothing else to do for incremental ADS resources. - if (!xdsResourceType.isFullStateOfTheWorld()) { - continue; - } - - // Handle State of the World ADS: invalid resources. if (invalidResources.contains(resourceName)) { // The resource is missing. Reuse the cached resource if possible. if (subscriber.data == null) { @@ -607,6 +601,11 @@ private void handleResourceUpdate( continue; } + // Nothing else to do for incremental ADS resources. + if (!xdsResourceType.isFullStateOfTheWorld()) { + continue; + } + // For State of the World services, notify watchers when their watched resource is missing // from the ADS update. Note that we can only do this if the resource update is coming from // the same xDS server that the ResourceSubscriber is subscribed to. diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 369763a21b7..006440be6c1 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -3270,6 +3270,8 @@ public void edsDuplicateLocalityInTheSamePriority() { + "locality:Locality{region=region2, zone=zone2, subZone=subzone2} for priority:1"; call.verifyRequestNack(EDS, EDS_RESOURCE, "", "0001", NODE, ImmutableList.of( errorMsg)); + verify(edsResourceWatcher).onError(errorCaptor.capture()); + assertThat(errorCaptor.getValue().getDescription()).contains(errorMsg); } @Test From 482dc5c1c3c7ff94f96d1d2d7edc1fe3b166630e Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 5 Jun 2025 03:07:49 -0700 Subject: [PATCH 278/591] xds: Don't allow hostnames in address field (#12123) * xds: Don't allow hostnames in address field gRFC A27 specifies they must be IPv4 or IPv6 addresses. Certainly doing a DNS lookup hidden inside the config object is asking for trouble. The tests were accidentally doing a lot of failing DNS requests greatly slowing them down. On my desktop, which made the problem most obvious with five search paths in /etc/resolv.conf, :grpc-xds:test decreased from 66s to 29s. The majority of that is XdsDependencyManagerTest which went from 33s to .1s, as it generated a UUID for the in-process transport each test and then used it as a hostname, which defeated Java's DNS (negative) cache. The slowness was noticed because XdsDependencyManagerTest should have run quickly as a single thread without I/O, but was particularly slow on my desktop. The cleanup caused me to audit serverName and the weird places it went. I think some of them were tricks for XdsClientFallbackTest to squirrel away something distinguishing, although reusing the serverName is asking for confusion as is including the tricks in "shared" utilities. XdsClientFallbackTest does have some non-trivial changes, but this seems to fix some pre-existing bugs in the tests. * Add failing hostname unit test --- xds/src/main/java/io/grpc/xds/Endpoints.java | 4 +++- .../java/io/grpc/xds/XdsEndpointResource.java | 13 +++++++--- .../java/io/grpc/xds/ControlPlaneRule.java | 23 +++++++----------- .../grpc/xds/GcpAuthenticationFilterTest.java | 8 +++---- .../grpc/xds/GrpcXdsClientImplDataTest.java | 24 +++++++++++++++++++ .../io/grpc/xds/XdsClientFallbackTest.java | 8 +++---- .../io/grpc/xds/XdsDependencyManagerTest.java | 14 +++++------ .../java/io/grpc/xds/XdsNameResolverTest.java | 2 +- .../test/java/io/grpc/xds/XdsTestUtils.java | 20 ++++++++-------- 9 files changed, 71 insertions(+), 45 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/Endpoints.java b/xds/src/main/java/io/grpc/xds/Endpoints.java index b0d97d42c11..dcb72f3e90d 100644 --- a/xds/src/main/java/io/grpc/xds/Endpoints.java +++ b/xds/src/main/java/io/grpc/xds/Endpoints.java @@ -22,6 +22,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.net.InetAddresses; import io.grpc.EquivalentAddressGroup; import java.net.InetSocketAddress; import java.util.List; @@ -78,7 +79,8 @@ static LbEndpoint create(EquivalentAddressGroup eag, int loadBalancingWeight, @VisibleForTesting static LbEndpoint create(String address, int port, int loadBalancingWeight, boolean isHealthy, String hostname, ImmutableMap endpointMetadata) { - return LbEndpoint.create(new EquivalentAddressGroup(new InetSocketAddress(address, port)), + return LbEndpoint.create( + new EquivalentAddressGroup(new InetSocketAddress(InetAddresses.forString(address), port)), loadBalancingWeight, isHealthy, hostname, endpointMetadata); } } diff --git a/xds/src/main/java/io/grpc/xds/XdsEndpointResource.java b/xds/src/main/java/io/grpc/xds/XdsEndpointResource.java index 11111fa51ca..9ad75595ea6 100644 --- a/xds/src/main/java/io/grpc/xds/XdsEndpointResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsEndpointResource.java @@ -40,6 +40,7 @@ import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsClient.ResourceUpdate; import io.grpc.xds.client.XdsResourceType; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collections; @@ -245,10 +246,16 @@ static StructOrError parseLocalityLbEndpoints( proto.getPriority(), localityMetadata)); } - private static InetSocketAddress getInetSocketAddress(Address address) { + private static InetSocketAddress getInetSocketAddress(Address address) + throws ResourceInvalidException { io.envoyproxy.envoy.config.core.v3.SocketAddress socketAddress = address.getSocketAddress(); - - return new InetSocketAddress(socketAddress.getAddress(), socketAddress.getPortValue()); + InetAddress parsedAddress; + try { + parsedAddress = InetAddresses.forString(socketAddress.getAddress()); + } catch (IllegalArgumentException ex) { + throw new ResourceInvalidException("Address is not an IP", ex); + } + return new InetSocketAddress(parsedAddress, socketAddress.getPortValue()); } static final class EdsUpdate implements ResourceUpdate { diff --git a/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java b/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java index 8c10627d153..11ea957ae35 100644 --- a/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java +++ b/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java @@ -266,17 +266,17 @@ static Cluster buildCluster(String clusterName, String edsName) { /** * Builds a new default EDS configuration. */ - static ClusterLoadAssignment buildClusterLoadAssignment(String hostName, String endpointHostname, - int port) { - return buildClusterLoadAssignment(hostName, endpointHostname, port, EDS_NAME); + static ClusterLoadAssignment buildClusterLoadAssignment( + String hostAddress, String endpointHostname, int port) { + return buildClusterLoadAssignment(hostAddress, endpointHostname, port, EDS_NAME); } - static ClusterLoadAssignment buildClusterLoadAssignment(String hostName, String endpointHostname, - int port, String edsName) { + static ClusterLoadAssignment buildClusterLoadAssignment( + String hostAddress, String endpointHostname, int port, String edsName) { Address address = Address.newBuilder() .setSocketAddress( - SocketAddress.newBuilder().setAddress(hostName).setPortValue(port).build()).build(); + SocketAddress.newBuilder().setAddress(hostAddress).setPortValue(port).build()).build(); LocalityLbEndpoints endpoints = LocalityLbEndpoints.newBuilder() .setLoadBalancingWeight(UInt32Value.of(10)) .setPriority(0) @@ -297,17 +297,12 @@ static ClusterLoadAssignment buildClusterLoadAssignment(String hostName, String * Builds a new client listener. */ static Listener buildClientListener(String name) { - return buildClientListener(name, "terminal-filter"); + return buildClientListener(name, RDS_NAME); } - - static Listener buildClientListener(String name, String identifier) { - return buildClientListener(name, identifier, RDS_NAME); - } - - static Listener buildClientListener(String name, String identifier, String rdsName) { + static Listener buildClientListener(String name, String rdsName) { HttpFilter httpFilter = HttpFilter.newBuilder() - .setName(identifier) + .setName("terminal-filter") .setTypedConfig(Any.pack(Router.newBuilder().build())) .setIsOptional(true) .build(); diff --git a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java index cebf739d417..1d6c97d81e6 100644 --- a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java @@ -53,7 +53,6 @@ import io.grpc.MethodDescriptor; import io.grpc.Status; import io.grpc.StatusOr; -import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.testing.TestMethodDescriptors; import io.grpc.xds.Endpoints.LbEndpoint; import io.grpc.xds.Endpoints.LocalityLbEndpoints; @@ -84,7 +83,6 @@ public class GcpAuthenticationFilterTest { private static final GcpAuthenticationFilter.Provider FILTER_PROVIDER = new GcpAuthenticationFilter.Provider(); - private static final String serverName = InProcessServerBuilder.generateName(); private static final LdsUpdate ldsUpdate = getLdsUpdate(); private static final EdsUpdate edsUpdate = getEdsUpdate(); private static final RdsUpdate rdsUpdate = getRdsUpdate(); @@ -461,7 +459,7 @@ public void testLruCacheEvictionOnResize() throws IOException, ResourceInvalidEx private static LdsUpdate getLdsUpdate() { Filter.NamedFilterConfig routerFilterConfig = new Filter.NamedFilterConfig( - serverName, RouterFilter.ROUTER_CONFIG); + "router", RouterFilter.ROUTER_CONFIG); HttpConnectionManager httpConnectionManager = HttpConnectionManager.forRdsName( 0L, RDS_NAME, Collections.singletonList(routerFilterConfig)); return XdsListenerResource.LdsUpdate.forApiListener(httpConnectionManager); @@ -469,7 +467,7 @@ private static LdsUpdate getLdsUpdate() { private static RdsUpdate getRdsUpdate() { RouteConfiguration routeConfiguration = - buildRouteConfiguration(serverName, RDS_NAME, CLUSTER_NAME); + buildRouteConfiguration("my-server", RDS_NAME, CLUSTER_NAME); XdsResourceType.Args args = new XdsResourceType.Args(null, "0", "0", null, null, null); try { return XdsRouteConfigureResource.getInstance().doParse(args, routeConfiguration); @@ -481,7 +479,7 @@ private static RdsUpdate getRdsUpdate() { private static EdsUpdate getEdsUpdate() { Map lbEndpointsMap = new HashMap<>(); LbEndpoint lbEndpoint = LbEndpoint.create( - serverName, ENDPOINT_PORT, 0, true, ENDPOINT_HOSTNAME, ImmutableMap.of()); + "127.0.0.5", ENDPOINT_PORT, 0, true, ENDPOINT_HOSTNAME, ImmutableMap.of()); lbEndpointsMap.put( Locality.create("", "", ""), LocalityLbEndpoints.create(ImmutableList.of(lbEndpoint), 10, 0, ImmutableMap.of())); diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index e129e2644e9..6167f491930 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -1077,6 +1077,30 @@ public void parseLocalityLbEndpoints_withHealthyEndpoints() throws ResourceInval 100, 1, ImmutableMap.of())); } + @Test + public void parseLocalityLbEndpoints_onlyPermitIp() { + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto = + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints.newBuilder() + .setLocality(Locality.newBuilder() + .setRegion("region-foo").setZone("zone-foo").setSubZone("subZone-foo")) + .setLoadBalancingWeight(UInt32Value.newBuilder().setValue(100)) // locality weight + .setPriority(1) + .addLbEndpoints(io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint.newBuilder() + .setEndpoint(Endpoint.newBuilder() + .setAddress(Address.newBuilder() + .setSocketAddress( + SocketAddress.newBuilder() + .setAddress("example.com").setPortValue(8888)))) + .setHealthStatus(io.envoyproxy.envoy.config.core.v3.HealthStatus.HEALTHY) + .setLoadBalancingWeight(UInt32Value.newBuilder().setValue(20))) // endpoint weight + .build(); + ResourceInvalidException ex = assertThrows( + ResourceInvalidException.class, + () -> XdsEndpointResource.parseLocalityLbEndpoints(proto)); + assertThat(ex.getMessage()).contains("IP"); + assertThat(ex.getMessage()).contains("example.com"); + } + @Test public void parseLocalityLbEndpoints_treatUnknownHealthAsHealthy() throws ResourceInvalidException { diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java index 036b9f6f55d..1e7ce6dc2a2 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java @@ -84,10 +84,10 @@ public class XdsClientFallbackTest { private static final String FALLBACK_EDS_NAME = "fallback-" + EDS_NAME; private static final HttpConnectionManager MAIN_HTTP_CONNECTION_MANAGER = HttpConnectionManager.forRdsName(0, RDS_NAME, ImmutableList.of( - new Filter.NamedFilterConfig(MAIN_SERVER, RouterFilter.ROUTER_CONFIG))); + new Filter.NamedFilterConfig("terminal-filter", RouterFilter.ROUTER_CONFIG))); private static final HttpConnectionManager FALLBACK_HTTP_CONNECTION_MANAGER = - HttpConnectionManager.forRdsName(0, RDS_NAME, ImmutableList.of( - new Filter.NamedFilterConfig(FALLBACK_SERVER, RouterFilter.ROUTER_CONFIG))); + HttpConnectionManager.forRdsName(0, FALLBACK_RDS_NAME, ImmutableList.of( + new Filter.NamedFilterConfig("terminal-filter", RouterFilter.ROUTER_CONFIG))); private ObjectPool xdsClientPool; private XdsClient xdsClient; private boolean originalEnableXdsFallback; @@ -201,7 +201,7 @@ private static void setAdsConfig(ControlPlaneRule controlPlane, String serverNam String edsName = isMainServer ? EDS_NAME : FALLBACK_EDS_NAME; controlPlane.setLdsConfig(ControlPlaneRule.buildServerListener(), - ControlPlaneRule.buildClientListener(MAIN_SERVER, serverName)); + ControlPlaneRule.buildClientListener(MAIN_SERVER, rdsName)); controlPlane.setRdsConfig(rdsName, XdsTestUtils.buildRouteConfiguration(MAIN_SERVER, rdsName, clusterName)); diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index 542471e2734..14f554412a8 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -122,7 +122,7 @@ public class XdsDependencyManagerTest { private Server xdsServer; private final FakeClock fakeClock = new FakeClock(); - private final String serverName = InProcessServerBuilder.generateName(); + private final String serverName = "the-service-name"; private final Queue loadReportCalls = new ArrayDeque<>(); private final AtomicBoolean adsEnded = new AtomicBoolean(true); private final AtomicBoolean lrsEnded = new AtomicBoolean(true); @@ -153,7 +153,7 @@ public class XdsDependencyManagerTest { @Before public void setUp() throws Exception { xdsServer = cleanupRule.register(InProcessServerBuilder - .forName(serverName) + .forName("control-plane") .addService(controlPlaneService) .addService(lrsService) .directExecutor() @@ -163,7 +163,7 @@ public void setUp() throws Exception { XdsTestUtils.setAdsConfig(controlPlaneService, serverName); channel = cleanupRule.register( - InProcessChannelBuilder.forName(serverName).directExecutor().build()); + InProcessChannelBuilder.forName("control-plane").directExecutor().build()); XdsTransportFactory xdsTransportFactory = ignore -> new GrpcXdsTransportFactory.GrpcXdsTransport(channel); @@ -351,10 +351,10 @@ public void testMissingCdsAndEds() { // Update config so that one of the 2 "valid" clusters has an EDS resource, the other does not // and there is an EDS that doesn't have matching clusters ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( - serverName, ENDPOINT_HOSTNAME, ENDPOINT_PORT, XdsTestUtils.EDS_NAME + 0); + "127.0.1.1", ENDPOINT_HOSTNAME, ENDPOINT_PORT, XdsTestUtils.EDS_NAME + 0); edsMap.put(XdsTestUtils.EDS_NAME + 0, clusterLoadAssignment); clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( - serverName, ENDPOINT_HOSTNAME, ENDPOINT_PORT, "garbageEds"); + "127.0.1.2", ENDPOINT_HOSTNAME, ENDPOINT_PORT, "garbageEds"); edsMap.put("garbageEds", clusterLoadAssignment); controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); @@ -421,7 +421,7 @@ public void testTcpListenerErrors() { @Test public void testMissingRds() { String rdsName = "badRdsName"; - Listener clientListener = ControlPlaneRule.buildClientListener(serverName, serverName, rdsName); + Listener clientListener = ControlPlaneRule.buildClientListener(serverName, rdsName); controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, ImmutableMap.of(serverName, clientListener)); @@ -578,7 +578,7 @@ public void testMultipleCdsReferToSameEds() { Map edsMap = new HashMap<>(); ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( - serverName, ENDPOINT_HOSTNAME, ENDPOINT_PORT, edsName); + "127.0.1.4", ENDPOINT_HOSTNAME, ENDPOINT_PORT, edsName); edsMap.put(edsName, clusterLoadAssignment); RouteConfiguration routeConfig = diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 622084d4306..ab966ce0025 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -1228,7 +1228,7 @@ private static void createAndDeliverClusterUpdates( .roundRobinLbPolicy(); xdsClient.deliverCdsUpdate(clusterName, forEds.build()); EdsUpdate edsUpdate = new EdsUpdate(clusterName, - XdsTestUtils.createMinimalLbEndpointsMap("host"), Collections.emptyList()); + XdsTestUtils.createMinimalLbEndpointsMap("127.0.0.3"), Collections.emptyList()); xdsClient.deliverEdsUpdate(clusterName, edsUpdate); } } diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java index 52953ef5407..ec1ccd7c6d0 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -136,7 +136,7 @@ static void setAdsConfig(XdsTestControlPlaneService service, String serverName, int endpointPort) { Listener serverListener = ControlPlaneRule.buildServerListener(); - Listener clientListener = ControlPlaneRule.buildClientListener(serverName, serverName, rdsName); + Listener clientListener = ControlPlaneRule.buildClientListener(serverName, rdsName); service.setXdsConfig(ADS_TYPE_URL_LDS, ImmutableMap.of(SERVER_LISTENER, serverListener, serverName, clientListener)); @@ -148,7 +148,7 @@ static void setAdsConfig(XdsTestControlPlaneService service, String serverName, service.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of(clusterName, cluster)); ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( - serverName, endpointHostname, endpointPort, edsName); + "127.0.0.11", endpointHostname, endpointPort, edsName); service.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of(edsName, clusterLoadAssignment)); @@ -186,7 +186,7 @@ static void setAggregateCdsConfig(XdsTestControlPlaneService service, String ser Map edsMap = new HashMap<>(); for (String child : children) { ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( - serverName, ENDPOINT_HOSTNAME, ENDPOINT_PORT, getEdsNameForCluster(child)); + "127.0.0.16", ENDPOINT_HOSTNAME, ENDPOINT_PORT, getEdsNameForCluster(child)); edsMap.put(getEdsNameForCluster(child), clusterLoadAssignment); } service.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); @@ -225,7 +225,7 @@ static void addAggregateToExistingConfig(XdsTestControlPlaneService service, Str continue; } ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( - child, ENDPOINT_HOSTNAME, ENDPOINT_PORT, getEdsNameForCluster(child)); + "127.0.0.15", ENDPOINT_HOSTNAME, ENDPOINT_PORT, getEdsNameForCluster(child)); edsMap.put(getEdsNameForCluster(child), clusterLoadAssignment); } service.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); @@ -236,7 +236,7 @@ static XdsConfig getDefaultXdsConfig(String serverHostName) XdsConfig.XdsConfigBuilder builder = new XdsConfig.XdsConfigBuilder(); Filter.NamedFilterConfig routerFilterConfig = new Filter.NamedFilterConfig( - serverHostName, RouterFilter.ROUTER_CONFIG); + "terminal-filter", RouterFilter.ROUTER_CONFIG); HttpConnectionManager httpConnectionManager = HttpConnectionManager.forRdsName( 0L, RDS_NAME, Collections.singletonList(routerFilterConfig)); @@ -257,7 +257,7 @@ static XdsConfig getDefaultXdsConfig(String serverHostName) // Need to create endpoints to create locality endpoints map to create edsUpdate Map lbEndpointsMap = new HashMap<>(); LbEndpoint lbEndpoint = LbEndpoint.create( - serverHostName, ENDPOINT_PORT, 0, true, ENDPOINT_HOSTNAME, ImmutableMap.of()); + "127.0.0.11", ENDPOINT_PORT, 0, true, ENDPOINT_HOSTNAME, ImmutableMap.of()); lbEndpointsMap.put( Locality.create("", "", ""), LocalityLbEndpoints.create(ImmutableList.of(lbEndpoint), 10, 0, ImmutableMap.of())); @@ -280,10 +280,10 @@ static XdsConfig getDefaultXdsConfig(String serverHostName) return builder.build(); } - static Map createMinimalLbEndpointsMap(String serverHostName) { + static Map createMinimalLbEndpointsMap(String serverAddress) { Map lbEndpointsMap = new HashMap<>(); LbEndpoint lbEndpoint = LbEndpoint.create( - serverHostName, ENDPOINT_PORT, 0, true, ENDPOINT_HOSTNAME, ImmutableMap.of()); + serverAddress, ENDPOINT_PORT, 0, true, ENDPOINT_HOSTNAME, ImmutableMap.of()); lbEndpointsMap.put( Locality.create("", "", ""), LocalityLbEndpoints.create(ImmutableList.of(lbEndpoint), 10, 0, ImmutableMap.of())); @@ -338,7 +338,7 @@ static void addEdsClusters(Map clusterMap, Map clusterMap.put(clusterName, cluster); ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment( - clusterName, ENDPOINT_HOSTNAME, ENDPOINT_PORT, edsName); + "127.0.0.13", ENDPOINT_HOSTNAME, ENDPOINT_PORT, edsName); edsMap.put(edsName, clusterLoadAssignment); } } @@ -346,7 +346,7 @@ static void addEdsClusters(Map clusterMap, Map static Listener buildInlineClientListener(String rdsName, String clusterName, String serverName) { HttpFilter httpFilter = HttpFilter.newBuilder() - .setName(serverName) + .setName("terminal-filter") .setTypedConfig(Any.pack(Router.newBuilder().build())) .setIsOptional(true) .build(); From 4c7399910246b4111940faf9c3a277b87e559b8a Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 5 Jun 2025 03:09:51 -0700 Subject: [PATCH 279/591] Move all test helper classes out of AbstractTransportTest so they can be used elsewhere (#12125) --- .../internal/BinderServerTransportTest.java | 38 +- .../grpc/internal/AbstractTransportTest.java | 360 ++++-------------- .../internal/ClientStreamListenerBase.java | 119 ++++++ .../io/grpc/internal/MockServerListener.java | 78 ++++ .../internal/MockServerTransportListener.java | 93 +++++ .../internal/ServerStreamListenerBase.java | 95 +++++ .../inprocess/InProcessTransportTest.java | 4 + 7 files changed, 471 insertions(+), 316 deletions(-) create mode 100644 core/src/testFixtures/java/io/grpc/internal/ClientStreamListenerBase.java create mode 100644 core/src/testFixtures/java/io/grpc/internal/MockServerListener.java create mode 100644 core/src/testFixtures/java/io/grpc/internal/MockServerTransportListener.java create mode 100644 core/src/testFixtures/java/io/grpc/internal/ServerStreamListenerBase.java diff --git a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java index d3c13d6c89e..d47106d1d35 100644 --- a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java @@ -16,7 +16,6 @@ package io.grpc.binder.internal; -import static com.google.common.base.Preconditions.checkState; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -29,11 +28,9 @@ import android.os.Parcel; import com.google.common.collect.ImmutableList; import io.grpc.Attributes; -import io.grpc.Metadata; import io.grpc.Status; import io.grpc.internal.FixedObjectPool; -import io.grpc.internal.ServerStream; -import io.grpc.internal.ServerTransportListener; +import io.grpc.internal.MockServerTransportListener; import java.util.concurrent.ScheduledExecutorService; import org.junit.Before; import org.junit.Rule; @@ -55,7 +52,7 @@ public final class BinderServerTransportTest { @Rule public MockitoRule mocks = MockitoJUnit.rule(); private final ScheduledExecutorService executorService = new MainThreadScheduledExecutorService(); - private final TestTransportListener transportListener = new TestTransportListener(); + private MockServerTransportListener transportListener; @Mock IBinder mockBinder; @@ -70,6 +67,7 @@ public void setUp() throws Exception { ImmutableList.of(), OneWayBinderProxy.IDENTITY_DECORATOR, mockBinder); + transportListener = new MockServerTransportListener(transport); } @Test @@ -82,34 +80,6 @@ public void testSetupTransactionFailureCausesMultipleShutdowns_b153460678() thro transport.shutdownNow(Status.UNKNOWN.withDescription("reasons")); shadowOf(Looper.getMainLooper()).idle(); - assertThat(transportListener.terminated).isTrue(); - } - - private static final class TestTransportListener implements ServerTransportListener { - - public boolean ready; - public boolean terminated; - - /** - * Called when a new stream was created by the remote client. - * - * @param stream the newly created stream. - * @param method the fully qualified method name being called on the server. - * @param headers containing metadata for the call. - */ - @Override - public void streamCreated(ServerStream stream, String method, Metadata headers) {} - - @Override - public Attributes transportReady(Attributes attributes) { - ready = true; - return attributes; - } - - @Override - public void transportTerminated() { - checkState(!terminated, "Terminated twice"); - terminated = true; - } + assertThat(transportListener.isTerminated()).isTrue(); } } diff --git a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java index 1f4c2b41f15..32c3bff74e9 100644 --- a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java +++ b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java @@ -58,6 +58,7 @@ import io.grpc.MethodDescriptor; import io.grpc.ServerStreamTracer; import io.grpc.Status; +import io.grpc.internal.MockServerTransportListener.StreamCreation; import io.grpc.internal.testing.TestClientStreamTracer; import io.grpc.internal.testing.TestServerStreamTracer; import java.io.ByteArrayInputStream; @@ -69,10 +70,8 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.junit.After; @@ -296,8 +295,8 @@ public void frameAfterRstStreamShouldNotBreakClientChannel() throws Exception { serverStreamCreation.stream.flush(); assertEquals( - Status.CANCELLED, clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + Status.CANCELLED, clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); ClientStreamListener mockClientStreamListener2 = mock(ClientStreamListener.class); @@ -469,7 +468,7 @@ public void openStreamPreventsTermination() throws Exception { // the stream still functions. serverStream.writeHeaders(new Metadata(), true); clientStream.halfClose(); - assertNotNull(clientStreamListener.headers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitHeaders(TIMEOUT_MS, TimeUnit.MILLISECONDS)); assertTrue(serverStreamListener.awaitHalfClosed(TIMEOUT_MS, TimeUnit.MILLISECONDS)); verify(mockClientTransportListener, never()).transportTerminated(); @@ -511,9 +510,9 @@ public void shutdownNowKillsClientStream() throws Exception { assertTrue(serverTransportListener.waitForTermination(TIMEOUT_MS, TimeUnit.MILLISECONDS)); assertTrue(serverTransportListener.isTerminated()); - assertEquals(status, clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - Status serverStatus = serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertEquals(status, clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + Status serverStatus = serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertFalse(serverStatus.isOk()); assertTrue(clientStreamTracer1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); assertNull(clientStreamTracer1.getInboundTrailers()); @@ -550,9 +549,9 @@ public void shutdownNowKillsServerStream() throws Exception { assertTrue(serverTransportListener.waitForTermination(TIMEOUT_MS, TimeUnit.MILLISECONDS)); assertTrue(serverTransportListener.isTerminated()); - Status clientStreamStatus = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + Status clientStreamStatus = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertFalse(clientStreamStatus.isOk()); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); assertTrue(clientStreamTracer1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); assertNull(clientStreamTracer1.getInboundTrailers()); assertStatusEquals(clientStreamStatus, clientStreamTracer1.getStatus()); @@ -562,7 +561,7 @@ public void shutdownNowKillsServerStream() throws Exception { // Generally will be same status provided to shutdownNow, but InProcessTransport can't // differentiate between client and server shutdownNow. The status is not really used on // server-side, so we don't care much. - assertNotNull(serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); } @Test @@ -643,8 +642,8 @@ public void newStream_duringShutdown() throws Exception { ClientStreamListenerBase clientStreamListener2 = new ClientStreamListenerBase(); stream2.start(clientStreamListener2); Status clientStreamStatus2 = - clientStreamListener2.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertNotNull(clientStreamListener2.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + clientStreamListener2.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertNotNull(clientStreamListener2.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); assertCodeEquals(Status.UNAVAILABLE, clientStreamStatus2); assertNull(clientStreamTracer2.getInboundTrailers()); assertSame(clientStreamStatus2, clientStreamTracer2.getStatus()); @@ -658,8 +657,8 @@ public void newStream_duringShutdown() throws Exception { StreamCreation serverStreamCreation = serverTransportListener.takeStreamOrFail(20 * TIMEOUT_MS, TimeUnit.MILLISECONDS); serverStreamCreation.stream.close(Status.OK, new Metadata()); - assertCodeEquals(Status.OK, clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertCodeEquals(Status.OK, clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); } @Test @@ -679,8 +678,8 @@ public void newStream_afterTermination() throws Exception { ClientStreamListenerBase clientStreamListener = new ClientStreamListenerBase(); stream.start(clientStreamListener); assertEquals( - shutdownReason, clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + shutdownReason, clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); verify(mockClientTransportListener, never()).transportInUse(anyBoolean()); assertNull(clientStreamTracer1.getInboundTrailers()); assertSame(shutdownReason, clientStreamTracer1.getStatus()); @@ -788,6 +787,17 @@ public void transportInUse_clientCancel() throws Exception { @Test public void basicStream() throws Exception { + serverListener = + new MockServerListener( + transport -> + new MockServerTransportListener(transport) { + @Override + public Attributes transportReady(Attributes attributes) { + return super.transportReady(attributes).toBuilder() + .set(ADDITIONAL_TRANSPORT_ATTR_KEY, "additional attribute value") + .build(); + } + }); InOrder serverInOrder = inOrder(serverStreamTracerFactory); server.start(serverListener); client = newClientTransport(server); @@ -881,7 +891,7 @@ public void basicStream() throws Exception { Metadata serverHeadersCopy = new Metadata(); serverHeadersCopy.merge(serverHeaders); serverStream.writeHeaders(serverHeaders, true); - Metadata headers = clientStreamListener.headers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + Metadata headers = clientStreamListener.awaitHeaders(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertNotNull(headers); assertAsciiMetadataValuesEqual(serverHeadersCopy.getAll(asciiKey), headers.getAll(asciiKey)); assertEquals( @@ -926,11 +936,11 @@ public void basicStream() throws Exception { serverStream.close(status, trailers); assertNull(serverStreamTracer1.nextInboundEvent()); assertNull(serverStreamTracer1.nextOutboundEvent()); - assertCodeEquals(Status.OK, serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertCodeEquals(Status.OK, serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); assertSame(status, serverStreamTracer1.getStatus()); - Status clientStreamStatus = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + Status clientStreamStatus = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); Metadata clientStreamTrailers = - clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertSame(clientStreamTrailers, clientStreamTracer1.getInboundTrailers()); assertSame(clientStreamStatus, clientStreamTracer1.getStatus()); assertNull(clientStreamTracer1.nextInboundEvent()); @@ -999,14 +1009,14 @@ public void zeroMessageStream() throws Exception { assertTrue(serverStreamListener.awaitHalfClosed(TIMEOUT_MS, TimeUnit.MILLISECONDS)); serverStream.writeHeaders(new Metadata(), true); - assertNotNull(clientStreamListener.headers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitHeaders(TIMEOUT_MS, TimeUnit.MILLISECONDS)); Status status = Status.OK.withDescription("Nice talking to you"); serverStream.close(status, new Metadata()); - assertCodeEquals(Status.OK, serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - Status clientStreamStatus = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertCodeEquals(Status.OK, serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + Status clientStreamStatus = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); Metadata clientStreamTrailers = - clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertNotNull(clientStreamTrailers); assertEquals(status.getCode(), clientStreamStatus.getCode()); assertEquals(status.getDescription(), clientStreamStatus.getDescription()); @@ -1036,15 +1046,15 @@ public void earlyServerClose_withServerHeaders() throws Exception { ServerStreamListenerBase serverStreamListener = serverStreamCreation.listener; serverStream.writeHeaders(new Metadata(), true); - assertNotNull(clientStreamListener.headers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitHeaders(TIMEOUT_MS, TimeUnit.MILLISECONDS)); Status strippedStatus = Status.OK.withDescription("Hello. Goodbye."); Status status = strippedStatus.withCause(new Exception()); serverStream.close(status, new Metadata()); - assertCodeEquals(Status.OK, serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - Status clientStreamStatus = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertCodeEquals(Status.OK, serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + Status clientStreamStatus = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); Metadata clientStreamTrailers = - clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertNotNull(clientStreamTrailers); checkClientStatus(status, clientStreamStatus); assertTrue(clientStreamTracer1.getOutboundHeaders()); @@ -1080,10 +1090,10 @@ public void earlyServerClose_noServerHeaders() throws Exception { trailers.put(asciiKey, "dupvalue"); trailers.put(binaryKey, "äbinarytrailers"); serverStream.close(status, trailers); - assertCodeEquals(Status.OK, serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - Status clientStreamStatus = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertCodeEquals(Status.OK, serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + Status clientStreamStatus = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); Metadata clientStreamTrailers = - clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS); checkClientStatus(status, clientStreamStatus); assertEquals( Lists.newArrayList(trailers.getAll(asciiKey)), @@ -1118,10 +1128,10 @@ public void earlyServerClose_serverFailure() throws Exception { Status strippedStatus = Status.INTERNAL.withDescription("I'm not listening"); Status status = strippedStatus.withCause(new Exception()); serverStream.close(status, new Metadata()); - assertCodeEquals(Status.OK, serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - Status clientStreamStatus = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertCodeEquals(Status.OK, serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + Status clientStreamStatus = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); Metadata clientStreamTrailers = - clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertNotNull(clientStreamTrailers); checkClientStatus(status, clientStreamStatus); assertTrue(clientStreamTracer1.getOutboundHeaders()); @@ -1161,10 +1171,10 @@ public void closed( Status strippedStatus = Status.INTERNAL.withDescription("I'm not listening"); Status status = strippedStatus.withCause(new Exception()); serverStream.close(status, new Metadata()); - assertCodeEquals(Status.OK, serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - Status clientStreamStatus = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertCodeEquals(Status.OK, serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + Status clientStreamStatus = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); Metadata clientStreamTrailers = - clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertNotNull(clientStreamTrailers); checkClientStatus(status, clientStreamStatus); assertTrue(clientStreamTracer1.getOutboundHeaders()); @@ -1192,9 +1202,9 @@ public void clientCancel() throws Exception { Status status = Status.CANCELLED.withDescription("Nevermind").withCause(new Exception()); clientStream.cancel(status); - assertEquals(status, clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - Status serverStatus = serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertEquals(status, clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + Status serverStatus = serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertNotEquals(Status.Code.OK, serverStatus.getCode()); // Cause should not be transmitted between client and server by default assertNull(serverStatus.getCause()); @@ -1306,9 +1316,9 @@ public void serverCancel() throws Exception { Status status = Status.DEADLINE_EXCEEDED.withDescription("It was bound to happen") .withCause(new Exception()); serverStream.cancel(status); - assertEquals(status, serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - Status clientStreamStatus = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(status, serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + Status clientStreamStatus = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); // Presently we can't sent much back to the client in this case. Verify that is the current // behavior for consistency between transports. assertCodeEquals(Status.CANCELLED, clientStreamStatus); @@ -1452,7 +1462,7 @@ public void flowControlPushBack() throws Exception { serverStream.close(status, new Metadata()); doPingPong(serverListener); try { - clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); fail("Expected TimeoutException"); } catch (TimeoutException expectedException) { } @@ -1460,9 +1470,9 @@ public void flowControlPushBack() throws Exception { clientStream.request(1); clientReceived += verifyMessageCountAndClose(clientStreamListener.messageQueue, 1); assertEquals(serverSent + 6, clientReceived); - assertCodeEquals(Status.OK, serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - Status clientStreamStatus = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertCodeEquals(Status.OK, serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + Status clientStreamStatus = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); assertEquals(status.getCode(), clientStreamStatus.getCode()); assertEquals(status.getDescription(), clientStreamStatus.getDescription()); } @@ -1518,9 +1528,9 @@ public void flowControlDoesNotDeadlockLargeMessage() throws Exception { serverStream.close(status, new Metadata()); doPingPong(serverListener); clientStream.request(1); - assertCodeEquals(Status.OK, serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - Status clientStreamStatus = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertCodeEquals(Status.OK, serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + Status clientStreamStatus = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); assertEquals(status.getCode(), clientStreamStatus.getCode()); assertEquals(status.getDescription(), clientStreamStatus.getDescription()); } @@ -1588,8 +1598,8 @@ public void interactionsAfterServerStreamCloseAreNoops() throws Exception { // setup clientStream.request(1); server.stream.close(Status.INTERNAL, new Metadata()); - assertNotNull(clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); // Ensure that for a closed ServerStream, interactions are noops server.stream.writeHeaders(new Metadata(), true); @@ -1621,7 +1631,7 @@ public void interactionsAfterClientStreamCancelAreNoops() throws Exception { // setup server.stream.request(1); clientStream.cancel(Status.UNKNOWN); - assertNotNull(server.listener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(server.listener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); // Ensure that for a cancelled ClientStream, interactions are noops clientStream.writeMessage(methodDescriptor.streamRequest("request")); @@ -1744,9 +1754,8 @@ public void transportTracer_server_streamEnded_ok() throws Exception { clientStream.halfClose(); serverStream.close(Status.OK, new Metadata()); // do not validate stats until close() has been called on client - assertNotNull(clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - + assertNotNull(clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); TransportStats serverAfter = getTransportStats(serverTransportListener.transport); assertEquals(1, serverAfter.streamsSucceeded); @@ -1783,9 +1792,8 @@ public void transportTracer_server_streamEnded_nonOk() throws Exception { serverStream.close(Status.UNKNOWN, new Metadata()); // do not validate stats until close() has been called on client - assertNotNull(clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - + assertNotNull(clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); TransportStats serverAfter = getTransportStats(serverTransportListener.transport); assertEquals(1, serverAfter.streamsFailed); @@ -1823,7 +1831,7 @@ public void transportTracer_client_streamEnded_nonOk() throws Exception { clientStream.cancel(Status.UNKNOWN); // do not validate stats until close() has been called on server - assertNotNull(serverStreamCreation.listener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(serverStreamCreation.listener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); TransportStats serverAfter = getTransportStats(serverTransportListener.transport); assertEquals(1, serverAfter.streamsFailed); @@ -1980,7 +1988,7 @@ public void serverChecksInboundMetadataSize() throws Exception { // Server shouldn't have created a stream, so nothing to clean up on server-side // If this times out, the server probably isn't noticing the metadata size - Status status = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + Status status = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); List codeOptions = Arrays.asList( Status.Code.UNKNOWN, Status.Code.RESOURCE_EXHAUSTED, Status.Code.INTERNAL); if (!codeOptions.contains(status.getCode())) { @@ -2021,13 +2029,13 @@ public void clientChecksInboundMetadataSize_header() throws Exception { serverStreamCreation.stream.writeMessage(methodDescriptor.streamResponse("response")); serverStreamCreation.stream.close(Status.OK, new Metadata()); - Status status = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + Status status = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); List codeOptions = Arrays.asList( Status.Code.UNKNOWN, Status.Code.RESOURCE_EXHAUSTED, Status.Code.INTERNAL); if (!codeOptions.contains(status.getCode())) { fail("Status code was not expected: " + status); } - assertFalse(clientStreamListener.headers.isDone()); + assertFalse(clientStreamListener.hasHeaders()); } /** This assumes the client limits metadata size to GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE. */ @@ -2066,13 +2074,13 @@ public void clientChecksInboundMetadataSize_trailer() throws Exception { serverStreamCreation.stream.writeMessage(methodDescriptor.streamResponse("response")); serverStreamCreation.stream.close(Status.OK, tooLargeMetadata); - Status status = clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + Status status = clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); List codeOptions = Arrays.asList( Status.Code.UNKNOWN, Status.Code.RESOURCE_EXHAUSTED, Status.Code.INTERNAL); if (!codeOptions.contains(status.getCode())) { fail("Status code was not expected: " + status); } - Metadata metadata = clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + Metadata metadata = clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertNull(metadata.get(tellTaleKey)); } @@ -2100,9 +2108,9 @@ methodDescriptor, new Metadata(), callOptions, ServerStreamListenerBase serverStreamListener = serverStreamCreation.listener; serverStream.close(Status.OK, new Metadata()); - assertNotNull(clientStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - assertNotNull(clientStreamListener.trailers.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - assertNotNull(serverStreamListener.status.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(clientStreamListener.awaitTrailers(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertNotNull(serverStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS)); client.shutdown(Status.UNAVAILABLE); } @@ -2147,7 +2155,7 @@ private static void checkClientStatus(Status expectedStatus, Status clientStream assertNull(clientStreamStatus.getCause()); } - private static boolean waitForFuture(Future future, long timeout, TimeUnit unit) + static boolean waitForFuture(Future future, long timeout, TimeUnit unit) throws InterruptedException { try { future.get(timeout, unit); @@ -2183,218 +2191,6 @@ public void streamCreated(Attributes transportAttrs, Metadata metadata) { } } - public static class MockServerListener implements ServerListener { - public final BlockingQueue listeners - = new LinkedBlockingQueue<>(); - private final SettableFuture shutdown = SettableFuture.create(); - - @Override - public ServerTransportListener transportCreated(ServerTransport transport) { - MockServerTransportListener listener = new MockServerTransportListener(transport); - listeners.add(listener); - return listener; - } - - @Override - public void serverShutdown() { - assertTrue(shutdown.set(null)); - } - - public boolean waitForShutdown(long timeout, TimeUnit unit) throws InterruptedException { - return waitForFuture(shutdown, timeout, unit); - } - - public MockServerTransportListener takeListenerOrFail(long timeout, TimeUnit unit) - throws InterruptedException { - MockServerTransportListener listener = listeners.poll(timeout, unit); - if (listener == null) { - fail("Timed out waiting for server transport"); - } - return listener; - } - } - - public static class MockServerTransportListener implements ServerTransportListener { - public final ServerTransport transport; - public final BlockingQueue streams = new LinkedBlockingQueue<>(); - private final SettableFuture terminated = SettableFuture.create(); - - public MockServerTransportListener(ServerTransport transport) { - this.transport = transport; - } - - @Override - public void streamCreated(ServerStream stream, String method, Metadata headers) { - ServerStreamListenerBase listener = new ServerStreamListenerBase(); - streams.add(new StreamCreation(stream, method, headers, listener)); - stream.setListener(listener); - } - - @Override - public Attributes transportReady(Attributes attributes) { - assertFalse(terminated.isDone()); - return Attributes.newBuilder() - .setAll(attributes) - .set(ADDITIONAL_TRANSPORT_ATTR_KEY, "additional attribute value") - .build(); - } - - @Override - public void transportTerminated() { - assertTrue(terminated.set(null)); - } - - public boolean waitForTermination(long timeout, TimeUnit unit) throws InterruptedException { - return waitForFuture(terminated, timeout, unit); - } - - public boolean isTerminated() { - return terminated.isDone(); - } - - public StreamCreation takeStreamOrFail(long timeout, TimeUnit unit) - throws InterruptedException { - StreamCreation stream = streams.poll(timeout, unit); - if (stream == null) { - fail("Timed out waiting for server stream"); - } - return stream; - } - } - - public static class ServerStreamListenerBase implements ServerStreamListener { - public final BlockingQueue messageQueue = new LinkedBlockingQueue<>(); - // Would have used Void instead of Object, but null elements are not allowed - private final BlockingQueue readyQueue = new LinkedBlockingQueue<>(); - private final CountDownLatch halfClosedLatch = new CountDownLatch(1); - private final SettableFuture status = SettableFuture.create(); - - private boolean awaitOnReady(int timeout, TimeUnit unit) throws Exception { - return readyQueue.poll(timeout, unit) != null; - } - - private boolean awaitOnReadyAndDrain(int timeout, TimeUnit unit) throws Exception { - if (!awaitOnReady(timeout, unit)) { - return false; - } - // Throw the rest away - readyQueue.drainTo(Lists.newArrayList()); - return true; - } - - private boolean awaitHalfClosed(int timeout, TimeUnit unit) throws Exception { - return halfClosedLatch.await(timeout, unit); - } - - @Override - public void messagesAvailable(MessageProducer producer) { - if (status.isDone()) { - fail("messagesAvailable invoked after closed"); - } - InputStream message; - while ((message = producer.next()) != null) { - messageQueue.add(message); - } - } - - @Override - public void onReady() { - if (status.isDone()) { - fail("onReady invoked after closed"); - } - readyQueue.add(new Object()); - } - - @Override - public void halfClosed() { - if (status.isDone()) { - fail("halfClosed invoked after closed"); - } - halfClosedLatch.countDown(); - } - - @Override - public void closed(Status status) { - if (this.status.isDone()) { - fail("closed invoked more than once"); - } - this.status.set(status); - } - } - - public static class ClientStreamListenerBase implements ClientStreamListener { - public final BlockingQueue messageQueue = new LinkedBlockingQueue<>(); - // Would have used Void instead of Object, but null elements are not allowed - private final BlockingQueue readyQueue = new LinkedBlockingQueue<>(); - private final SettableFuture headers = SettableFuture.create(); - private final SettableFuture trailers = SettableFuture.create(); - private final SettableFuture status = SettableFuture.create(); - - private boolean awaitOnReady(int timeout, TimeUnit unit) throws Exception { - return readyQueue.poll(timeout, unit) != null; - } - - private boolean awaitOnReadyAndDrain(int timeout, TimeUnit unit) throws Exception { - if (!awaitOnReady(timeout, unit)) { - return false; - } - // Throw the rest away - readyQueue.drainTo(Lists.newArrayList()); - return true; - } - - @Override - public void messagesAvailable(MessageProducer producer) { - if (status.isDone()) { - fail("messagesAvailable invoked after closed"); - } - InputStream message; - while ((message = producer.next()) != null) { - messageQueue.add(message); - } - } - - @Override - public void onReady() { - if (status.isDone()) { - fail("onReady invoked after closed"); - } - readyQueue.add(new Object()); - } - - @Override - public void headersRead(Metadata headers) { - if (status.isDone()) { - fail("headersRead invoked after closed"); - } - this.headers.set(headers); - } - - @Override - public void closed(Status status, RpcProgress rpcProgress, Metadata trailers) { - if (this.status.isDone()) { - fail("headersRead invoked after closed"); - } - this.status.set(status); - this.trailers.set(trailers); - } - } - - public static class StreamCreation { - public final ServerStream stream; - public final String method; - public final Metadata headers; - public final ServerStreamListenerBase listener; - - public StreamCreation( - ServerStream stream, String method, Metadata headers, ServerStreamListenerBase listener) { - this.stream = stream; - this.method = method; - this.headers = headers; - this.listener = listener; - } - } - private static class StringMarshaller implements MethodDescriptor.Marshaller { public static final StringMarshaller INSTANCE = new StringMarshaller(); diff --git a/core/src/testFixtures/java/io/grpc/internal/ClientStreamListenerBase.java b/core/src/testFixtures/java/io/grpc/internal/ClientStreamListenerBase.java new file mode 100644 index 00000000000..97186400cb2 --- /dev/null +++ b/core/src/testFixtures/java/io/grpc/internal/ClientStreamListenerBase.java @@ -0,0 +1,119 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import static org.junit.Assert.fail; + +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.SettableFuture; +import io.grpc.Metadata; +import io.grpc.Status; +import java.io.InputStream; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +public class ClientStreamListenerBase implements ClientStreamListener { + public final BlockingQueue messageQueue = new LinkedBlockingQueue<>(); + // Would have used Void instead of Object, but null elements are not allowed + private final BlockingQueue readyQueue = new LinkedBlockingQueue<>(); + private final SettableFuture headers = SettableFuture.create(); + private final SettableFuture trailers = SettableFuture.create(); + private final SettableFuture status = SettableFuture.create(); + + /** + * Returns the stream's status or throws {@link java.util.concurrent.TimeoutException} if it isn't + * closed before the timeout. + */ + public Status awaitClose(int timeout, TimeUnit unit) throws Exception { + return status.get(timeout, unit); + } + + /** + * Returns response headers from the server or throws {@link + * java.util.concurrent.TimeoutException} if they aren't delivered before the timeout. + * + *

    Callers must not modify the returned object. + */ + public Metadata awaitHeaders(int timeout, TimeUnit unit) throws Exception { + return headers.get(timeout, unit); + } + + /** + * Returns response trailers from the server or throws {@link + * java.util.concurrent.TimeoutException} if they aren't delivered before the timeout. + * + *

    Callers must not modify the returned object. + */ + public Metadata awaitTrailers(int timeout, TimeUnit unit) throws Exception { + return trailers.get(timeout, unit); + } + + public boolean awaitOnReady(int timeout, TimeUnit unit) throws Exception { + return readyQueue.poll(timeout, unit) != null; + } + + public boolean awaitOnReadyAndDrain(int timeout, TimeUnit unit) throws Exception { + if (!awaitOnReady(timeout, unit)) { + return false; + } + // Throw the rest away + readyQueue.drainTo(Lists.newArrayList()); + return true; + } + + @Override + public void messagesAvailable(MessageProducer producer) { + if (status.isDone()) { + fail("messagesAvailable invoked after closed"); + } + InputStream message; + while ((message = producer.next()) != null) { + messageQueue.add(message); + } + } + + @Override + public void onReady() { + if (status.isDone()) { + fail("onReady invoked after closed"); + } + readyQueue.add(new Object()); + } + + @Override + public void headersRead(Metadata headers) { + if (status.isDone()) { + fail("headersRead invoked after closed"); + } + this.headers.set(headers); + } + + @Override + public void closed(Status status, RpcProgress rpcProgress, Metadata trailers) { + if (this.status.isDone()) { + fail("headersRead invoked after closed"); + } + this.status.set(status); + this.trailers.set(trailers); + } + + /** Returns true iff response headers have been received from the server. */ + public boolean hasHeaders() { + return headers.isDone(); + } +} diff --git a/core/src/testFixtures/java/io/grpc/internal/MockServerListener.java b/core/src/testFixtures/java/io/grpc/internal/MockServerListener.java new file mode 100644 index 00000000000..0c33b98cf1c --- /dev/null +++ b/core/src/testFixtures/java/io/grpc/internal/MockServerListener.java @@ -0,0 +1,78 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.util.concurrent.SettableFuture; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * A {@link ServerListener} that helps you write blocking unit tests. + * + *

    TODO: Rename, since this is not actually a mock: + * https://testing.googleblog.com/2013/07/testing-on-toilet-know-your-test-doubles.html + */ +public class MockServerListener implements ServerListener { + private final BlockingQueue listeners = new LinkedBlockingQueue<>(); + private final SettableFuture shutdown = SettableFuture.create(); + private final ServerTransportListenerFactory serverTransportListenerFactory; + + /** + * Lets you customize the {@link MockServerTransportListener} installed on newly created + * {@link ServerTransport}s. + */ + public interface ServerTransportListenerFactory { + MockServerTransportListener create(ServerTransport transport); + } + + public MockServerListener(ServerTransportListenerFactory serverTransportListenerFactory) { + this.serverTransportListenerFactory = serverTransportListenerFactory; + } + + public MockServerListener() { + this(MockServerTransportListener::new); + } + + @Override + public ServerTransportListener transportCreated(ServerTransport transport) { + MockServerTransportListener listener = serverTransportListenerFactory.create(transport); + listeners.add(listener); + return listener; + } + + @Override + public void serverShutdown() { + assertTrue(shutdown.set(null)); + } + + public boolean waitForShutdown(long timeout, TimeUnit unit) throws InterruptedException { + return AbstractTransportTest.waitForFuture(shutdown, timeout, unit); + } + + public MockServerTransportListener takeListenerOrFail(long timeout, TimeUnit unit) + throws InterruptedException { + MockServerTransportListener listener = listeners.poll(timeout, unit); + if (listener == null) { + fail("Timed out waiting for server transport"); + } + return listener; + } +} diff --git a/core/src/testFixtures/java/io/grpc/internal/MockServerTransportListener.java b/core/src/testFixtures/java/io/grpc/internal/MockServerTransportListener.java new file mode 100644 index 00000000000..e6c4e2f578e --- /dev/null +++ b/core/src/testFixtures/java/io/grpc/internal/MockServerTransportListener.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.util.concurrent.SettableFuture; +import io.grpc.Attributes; +import io.grpc.Metadata; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * A {@link ServerTransportListener} that helps you write blocking unit tests. + * + *

    TODO: Rename, since this is not actually a mock: + * https://testing.googleblog.com/2013/07/testing-on-toilet-know-your-test-doubles.html + */ +public class MockServerTransportListener implements ServerTransportListener { + public final ServerTransport transport; + private final BlockingQueue streams = new LinkedBlockingQueue<>(); + private final SettableFuture terminated = SettableFuture.create(); + + public MockServerTransportListener(ServerTransport transport) { + this.transport = transport; + } + + @Override + public void streamCreated(ServerStream stream, String method, Metadata headers) { + ServerStreamListenerBase listener = new ServerStreamListenerBase(); + streams.add(new StreamCreation(stream, method, headers, listener)); + stream.setListener(listener); + } + + @Override + public Attributes transportReady(Attributes attributes) { + assertFalse(terminated.isDone()); + return attributes; + } + + @Override + public void transportTerminated() { + assertTrue(terminated.set(null)); + } + + public boolean waitForTermination(long timeout, TimeUnit unit) throws InterruptedException { + return AbstractTransportTest.waitForFuture(terminated, timeout, unit); + } + + public boolean isTerminated() { + return terminated.isDone(); + } + + public StreamCreation takeStreamOrFail(long timeout, TimeUnit unit) throws InterruptedException { + StreamCreation stream = streams.poll(timeout, unit); + if (stream == null) { + fail("Timed out waiting for server stream"); + } + return stream; + } + + public static class StreamCreation { + public final ServerStream stream; + public final String method; + public final Metadata headers; + public final ServerStreamListenerBase listener; + + public StreamCreation( + ServerStream stream, String method, Metadata headers, ServerStreamListenerBase listener) { + this.stream = stream; + this.method = method; + this.headers = headers; + this.listener = listener; + } + } +} diff --git a/core/src/testFixtures/java/io/grpc/internal/ServerStreamListenerBase.java b/core/src/testFixtures/java/io/grpc/internal/ServerStreamListenerBase.java new file mode 100644 index 00000000000..b4ded80e5b2 --- /dev/null +++ b/core/src/testFixtures/java/io/grpc/internal/ServerStreamListenerBase.java @@ -0,0 +1,95 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import static org.junit.Assert.fail; + +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.SettableFuture; +import io.grpc.Status; +import java.io.InputStream; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * A {@link ServerStreamListener} that helps you write blocking unit tests. + */ +public class ServerStreamListenerBase implements ServerStreamListener { + public final BlockingQueue messageQueue = new LinkedBlockingQueue<>(); + // Would have used Void instead of Object, but null elements are not allowed + private final BlockingQueue readyQueue = new LinkedBlockingQueue<>(); + private final CountDownLatch halfClosedLatch = new CountDownLatch(1); + private final SettableFuture status = SettableFuture.create(); + + public boolean awaitOnReady(int timeout, TimeUnit unit) throws Exception { + return readyQueue.poll(timeout, unit) != null; + } + + public boolean awaitOnReadyAndDrain(int timeout, TimeUnit unit) throws Exception { + if (!awaitOnReady(timeout, unit)) { + return false; + } + // Throw the rest away + readyQueue.drainTo(Lists.newArrayList()); + return true; + } + + public boolean awaitHalfClosed(int timeout, TimeUnit unit) throws Exception { + return halfClosedLatch.await(timeout, unit); + } + + public Status awaitClose(int timeout, TimeUnit unit) throws Exception { + return status.get(timeout, unit); + } + + @Override + public void messagesAvailable(MessageProducer producer) { + if (status.isDone()) { + fail("messagesAvailable invoked after closed"); + } + InputStream message; + while ((message = producer.next()) != null) { + messageQueue.add(message); + } + } + + @Override + public void onReady() { + if (status.isDone()) { + fail("onReady invoked after closed"); + } + readyQueue.add(new Object()); + } + + @Override + public void halfClosed() { + if (status.isDone()) { + fail("halfClosed invoked after closed"); + } + halfClosedLatch.countDown(); + } + + @Override + public void closed(Status status) { + if (this.status.isDone()) { + fail("closed invoked more than once"); + } + this.status.set(status); + } +} diff --git a/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java b/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java index 3ed8dd24ca9..d2220e05114 100644 --- a/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java +++ b/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java @@ -36,10 +36,14 @@ import io.grpc.StatusRuntimeException; import io.grpc.internal.AbstractTransportTest; import io.grpc.internal.ClientStream; +import io.grpc.internal.ClientStreamListenerBase; import io.grpc.internal.GrpcUtil; import io.grpc.internal.InternalServer; import io.grpc.internal.ManagedClientTransport; +import io.grpc.internal.MockServerTransportListener; +import io.grpc.internal.MockServerTransportListener.StreamCreation; import io.grpc.internal.ServerStream; +import io.grpc.internal.ServerStreamListenerBase; import io.grpc.internal.testing.TestStreamTracer; import io.grpc.stub.ClientCalls; import io.grpc.testing.GrpcCleanupRule; From 4cd78810865fa8703b8a69753cf8d85291e34c6b Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 29 May 2025 13:12:48 -0700 Subject: [PATCH 280/591] xds: Fix XdsDepManager aggregate cluster child ordering and loop detection The children of aggregate clusters have a priority order, so we can't ever throw them in an ordinary set for later iteration. This now detects recusion limits only after subscribing, but that matches our existing behavior in CdsLoadBalancer2. We don't get much value detecting the limit before subscribing and doing so makes watcher types more different. Loops are still a bit broken as they won't be unwatched when orphaned, as they will form a reference loop. In CdsLoadBalancer2, duplicate clusters had duplicate watchers so there was single-ownership and reference cycles couldn't form. Fixing that is a bigger change. Intermediate aggregate clusters are now included in XdsConfig, just for simplicity. It doesn't hurt anything whether they are present or missing. but it required updates to some tests. --- xds/src/main/java/io/grpc/xds/XdsConfig.java | 13 +- .../io/grpc/xds/XdsDependencyManager.java | 230 +++++++----------- .../io/grpc/xds/XdsDependencyManagerTest.java | 39 ++- 3 files changed, 130 insertions(+), 152 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsConfig.java b/xds/src/main/java/io/grpc/xds/XdsConfig.java index ec8f3dc076d..1f464aa1321 100644 --- a/xds/src/main/java/io/grpc/xds/XdsConfig.java +++ b/xds/src/main/java/io/grpc/xds/XdsConfig.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import io.grpc.StatusOr; import io.grpc.xds.XdsClusterResource.CdsUpdate; @@ -26,9 +27,9 @@ import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import java.io.Closeable; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; /** * Represents the xDS configuration tree for a specified Listener. @@ -191,10 +192,14 @@ public String toString() { // The list of leaf clusters for an aggregate cluster. static final class AggregateConfig implements ClusterChild { - private final Set leafNames; + private final List leafNames; - public AggregateConfig(Set leafNames) { - this.leafNames = checkNotNull(leafNames, "leafNames"); + public AggregateConfig(List leafNames) { + this.leafNames = ImmutableList.copyOf(checkNotNull(leafNames, "leafNames")); + } + + public List getLeafNames() { + return leafNames; } @Override diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index d786e525c0c..7bbd0064ed4 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -22,6 +22,7 @@ import static io.grpc.xds.client.XdsLogger.XdsLogLevel.DEBUG; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import io.grpc.InternalLogId; import io.grpc.NameResolver; @@ -42,12 +43,12 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; -import java.util.stream.Collectors; import javax.annotation.Nullable; /** @@ -101,7 +102,7 @@ public Closeable subscribeToCluster(String clusterName) { subscription.closed = true; return; // shutdown() called } - addClusterWatcher(clusterName, subscription, 1); + addClusterWatcher(clusterName, subscription); }); return subscription; @@ -164,7 +165,7 @@ private static void throwIfParentContextsNotEmpty(XdsWatcherBase watcher) { CdsWatcher cdsWatcher = (CdsWatcher) watcher; if (!cdsWatcher.parentContexts.isEmpty()) { String msg = String.format("CdsWatcher %s has parent contexts %s", - cdsWatcher.resourceName(), cdsWatcher.parentContexts.keySet()); + cdsWatcher.resourceName(), cdsWatcher.parentContexts); throw new IllegalStateException(msg); } } else if (watcher instanceof EdsWatcher) { @@ -309,24 +310,14 @@ StatusOr buildUpdate() { } builder.setVirtualHost(activeVirtualHost); - Map> edsWatchers = - getWatchers(ENDPOINT_RESOURCE); - Map> cdsWatchers = - getWatchers(CLUSTER_RESOURCE); - - // Only care about aggregates from LDS/RDS or subscriptions and the leaf clusters - List topLevelClusters = - cdsWatchers.values().stream() - .filter(XdsDependencyManager::isTopLevelCluster) - .map(XdsWatcherBase::resourceName) - .distinct() - .collect(Collectors.toList()); - - // Flatten multi-level aggregates into lists of leaf clusters - Set leafNames = - addTopLevelClustersToBuilder(builder, edsWatchers, cdsWatchers, topLevelClusters); - - addLeavesToBuilder(builder, edsWatchers, leafNames); + Map> clusters = new HashMap<>(); + LinkedHashSet ancestors = new LinkedHashSet<>(); + for (String cluster : getWatchers(CLUSTER_RESOURCE).keySet()) { + addConfigForCluster(clusters, cluster, ancestors); + } + for (Map.Entry> me : clusters.entrySet()) { + builder.addCluster(me.getKey(), me.getValue()); + } return StatusOr.fromValue(builder.build()); } @@ -344,111 +335,81 @@ private Map> getWatchers( return tTypeWatchers.watchers; } - private void addLeavesToBuilder( - XdsConfig.XdsConfigBuilder builder, - Map> edsWatchers, - Set leafNames) { - for (String clusterName : leafNames) { - CdsWatcher cdsWatcher = getCluster(clusterName); - StatusOr cdsUpdateOr = cdsWatcher.getData(); + private void addConfigForCluster( + Map> clusters, + String clusterName, + @SuppressWarnings("NonApiType") // Need order-preserving set for errors + LinkedHashSet ancestors) { + if (clusters.containsKey(clusterName)) { + return; + } + if (ancestors.contains(clusterName)) { + clusters.put(clusterName, StatusOr.fromStatus( + Status.INTERNAL.withDescription( + "Aggregate cluster cycle detected: " + ancestors))); + return; + } + if (ancestors.size() > MAX_CLUSTER_RECURSION_DEPTH) { + clusters.put(clusterName, StatusOr.fromStatus( + Status.INTERNAL.withDescription("Recursion limit reached: " + ancestors))); + return; + } - if (!cdsUpdateOr.hasValue()) { - builder.addCluster(clusterName, StatusOr.fromStatus(cdsUpdateOr.getStatus())); - continue; - } + CdsWatcher cdsWatcher = (CdsWatcher) getWatchers(CLUSTER_RESOURCE).get(clusterName); + StatusOr cdsWatcherDataOr = cdsWatcher.getData(); + if (!cdsWatcherDataOr.hasValue()) { + clusters.put(clusterName, StatusOr.fromStatus(cdsWatcherDataOr.getStatus())); + return; + } - XdsClusterResource.CdsUpdate cdsUpdate = cdsUpdateOr.getValue(); - if (cdsUpdate.clusterType() == ClusterType.EDS) { + XdsClusterResource.CdsUpdate cdsUpdate = cdsWatcherDataOr.getValue(); + XdsConfig.XdsClusterConfig.ClusterChild child; + switch (cdsUpdate.clusterType()) { + case AGGREGATE: + // Re-inserting a present element into a LinkedHashSet does not reorder the entries, so it + // preserves the priority across all aggregate clusters + LinkedHashSet leafNames = new LinkedHashSet(); + ancestors.add(clusterName); + for (String childCluster : cdsUpdate.prioritizedClusterNames()) { + addConfigForCluster(clusters, childCluster, ancestors); + StatusOr config = clusters.get(childCluster); + if (!config.hasValue()) { + clusters.put(clusterName, StatusOr.fromStatus(Status.INTERNAL.withDescription( + "Unable to get leaves for " + clusterName + ": " + + config.getStatus().getDescription()))); + return; + } + XdsConfig.XdsClusterConfig.ClusterChild children = config.getValue().getChildren(); + if (children instanceof AggregateConfig) { + leafNames.addAll(((AggregateConfig) children).getLeafNames()); + } else { + leafNames.add(childCluster); + } + } + ancestors.remove(clusterName); + + child = new AggregateConfig(ImmutableList.copyOf(leafNames)); + break; + case EDS: XdsWatcherBase edsWatcher = - edsWatchers.get(cdsWatcher.getEdsServiceName()); - EndpointConfig child; + getWatchers(ENDPOINT_RESOURCE).get(cdsWatcher.getEdsServiceName()); if (edsWatcher != null) { child = new EndpointConfig(edsWatcher.getData()); } else { child = new EndpointConfig(StatusOr.fromStatus(Status.INTERNAL.withDescription( "EDS resource not found for cluster " + clusterName))); } - builder.addCluster(clusterName, StatusOr.fromValue( - new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, child))); - } else if (cdsUpdate.clusterType() == ClusterType.LOGICAL_DNS) { - builder.addCluster(clusterName, StatusOr.fromStatus( + break; + case LOGICAL_DNS: + // TODO get the resolved endpoint configuration + child = new EndpointConfig(StatusOr.fromStatus( Status.INTERNAL.withDescription("Logical DNS in dependency manager unsupported"))); - } - } - } - - // Adds the top-level clusters to the builder and returns the leaf cluster names - private Set addTopLevelClustersToBuilder( - XdsConfig.XdsConfigBuilder builder, - Map> edsWatchers, - Map> cdsWatchers, - List topLevelClusters) { - - Set leafClusterNames = new HashSet<>(); - for (String clusterName : topLevelClusters) { - CdsWatcher cdsWatcher = (CdsWatcher) cdsWatchers.get(clusterName); - StatusOr cdsWatcherDataOr = cdsWatcher.getData(); - if (!cdsWatcher.hasDataValue()) { - builder.addCluster(clusterName, StatusOr.fromStatus(cdsWatcherDataOr.getStatus())); - continue; - } - - XdsClusterResource.CdsUpdate cdsUpdate = cdsWatcherDataOr.getValue(); - XdsConfig.XdsClusterConfig.ClusterChild child; - switch (cdsUpdate.clusterType()) { - case AGGREGATE: - Set leafNames = new HashSet<>(); - addLeafNames(leafNames, cdsUpdate); - child = new AggregateConfig(leafNames); - leafClusterNames.addAll(leafNames); - break; - case EDS: - XdsWatcherBase edsWatcher = - edsWatchers.get(cdsWatcher.getEdsServiceName()); - if (edsWatcher != null) { - child = new EndpointConfig(edsWatcher.getData()); - } else { - child = new EndpointConfig(StatusOr.fromStatus(Status.INTERNAL.withDescription( - "EDS resource not found for cluster " + clusterName))); - } - break; - case LOGICAL_DNS: - // TODO get the resolved endpoint configuration - child = new EndpointConfig(StatusOr.fromStatus( - Status.INTERNAL.withDescription("Logical DNS in dependency manager unsupported"))); - break; - default: - throw new IllegalStateException("Unexpected value: " + cdsUpdate.clusterType()); - } - builder.addCluster(clusterName, StatusOr.fromValue( - new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, child))); - } - - return leafClusterNames; - } - - private void addLeafNames(Set leafNames, XdsClusterResource.CdsUpdate cdsUpdate) { - for (String cluster : cdsUpdate.prioritizedClusterNames()) { - if (leafNames.contains(cluster)) { - continue; - } - StatusOr data = getCluster(cluster).getData(); - if (data == null || !data.hasValue() || data.getValue() == null) { - leafNames.add(cluster); - continue; - } - if (data.getValue().clusterType() == ClusterType.AGGREGATE) { - addLeafNames(leafNames, data.getValue()); - } else { - leafNames.add(cluster); - } + break; + default: + throw new IllegalStateException("Unexpected value: " + cdsUpdate.clusterType()); } - } - - private static boolean isTopLevelCluster( - XdsWatcherBase cdsWatcher) { - return ((CdsWatcher)cdsWatcher).parentContexts.values().stream() - .anyMatch(depth -> depth == 1); + clusters.put(clusterName, StatusOr.fromValue( + new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, child))); } @Override @@ -467,14 +428,14 @@ private void addEdsWatcher(String edsServiceName, CdsWatcher parentContext) { addWatcher(new EdsWatcher(edsServiceName, parentContext)); } - private void addClusterWatcher(String clusterName, Object parentContext, int depth) { + private void addClusterWatcher(String clusterName, Object parentContext) { CdsWatcher watcher = (CdsWatcher) getWatchers(CLUSTER_RESOURCE).get(clusterName); if (watcher != null) { - watcher.parentContexts.put(parentContext, depth); + watcher.parentContexts.add(parentContext); return; } - addWatcher(new CdsWatcher(clusterName, parentContext, depth)); + addWatcher(new CdsWatcher(clusterName, parentContext)); } private void updateRoutes(List virtualHosts, Object newParentContext, @@ -494,9 +455,9 @@ private void updateRoutes(List virtualHosts, Object newParentContex deletedClusters.forEach(watcher -> cancelClusterWatcherTree(getCluster(watcher), newParentContext)); - addedClusters.forEach((cluster) -> addClusterWatcher(cluster, newParentContext, 1)); + addedClusters.forEach((cluster) -> addClusterWatcher(cluster, newParentContext)); } else { - newClusters.forEach((cluster) -> addClusterWatcher(cluster, newParentContext, 1)); + newClusters.forEach((cluster) -> addClusterWatcher(cluster, newParentContext)); } } @@ -805,11 +766,11 @@ public StatusOr getRdsUpdate() { } private class CdsWatcher extends XdsWatcherBase { - Map parentContexts = new HashMap<>(); + Set parentContexts = new HashSet<>(); - CdsWatcher(String resourceName, Object parentContext, int depth) { + CdsWatcher(String resourceName, Object parentContext) { super(CLUSTER_RESOURCE, checkNotNull(resourceName, "resourceName")); - this.parentContexts.put(checkNotNull(parentContext, "parentContext"), depth); + this.parentContexts.add(checkNotNull(parentContext, "parentContext")); } @Override @@ -829,14 +790,6 @@ public void onChanged(XdsClusterResource.CdsUpdate update) { break; case AGGREGATE: Object parentContext = this; - int depth = parentContexts.values().stream().max(Integer::compare).orElse(0) + 1; - if (depth > MAX_CLUSTER_RECURSION_DEPTH) { - logger.log(XdsLogger.XdsLogLevel.WARNING, - "Cluster recursion depth limit exceeded for cluster {0}", resourceName()); - Status error = Status.UNAVAILABLE.withDescription( - "aggregate cluster graph exceeds max depth at " + resourceName() + nodeInfo()); - setDataAsStatus(error); - } if (hasDataValue()) { Set oldNames = getData().getValue().clusterType() == ClusterType.AGGREGATE ? new HashSet<>(getData().getValue().prioritizedClusterNames()) @@ -847,21 +800,18 @@ public void onChanged(XdsClusterResource.CdsUpdate update) { deletedClusters.forEach((cluster) -> cancelClusterWatcherTree(getCluster(cluster), parentContext)); - if (depth <= MAX_CLUSTER_RECURSION_DEPTH) { - setData(update); - Set addedClusters = Sets.difference(newNames, oldNames); - addedClusters.forEach((cluster) -> addClusterWatcher(cluster, parentContext, depth)); - } - - } else if (depth <= MAX_CLUSTER_RECURSION_DEPTH) { + setData(update); + Set addedClusters = Sets.difference(newNames, oldNames); + addedClusters.forEach((cluster) -> addClusterWatcher(cluster, parentContext)); + } else { setData(update); update.prioritizedClusterNames() - .forEach(name -> addClusterWatcher(name, parentContext, depth)); + .forEach(name -> addClusterWatcher(name, parentContext)); } break; default: Status error = Status.UNAVAILABLE.withDescription( - "aggregate cluster graph exceeds max depth at " + resourceName() + nodeInfo()); + "unknown cluster type in " + resourceName() + " " + update.clusterType()); setDataAsStatus(error); } maybePublishConfig(); diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index 14f554412a8..da960e1e133 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat; import static io.grpc.StatusMatcher.statusHasCode; -import static io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType.AGGREGATE; import static io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType.EDS; import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS; import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_EDS; @@ -239,9 +238,10 @@ public void verify_simple_aggregate() { testWatcher.lastConfig.getClusters(); assertThat(lastConfigClusters).hasSize(childNames.size() + 1); StatusOr rootC = lastConfigClusters.get(rootName); - CdsUpdate rootUpdate = rootC.getValue().getClusterResource(); - assertThat(rootUpdate.clusterType()).isEqualTo(AGGREGATE); - assertThat(rootUpdate.prioritizedClusterNames()).isEqualTo(childNames); + assertThat(rootC.getValue().getChildren()).isInstanceOf(XdsClusterConfig.AggregateConfig.class); + XdsClusterConfig.AggregateConfig aggConfig = + (XdsClusterConfig.AggregateConfig) rootC.getValue().getChildren(); + assertThat(aggConfig.getLeafNames()).isEqualTo(childNames); for (String childName : childNames) { assertThat(lastConfigClusters).containsKey(childName); @@ -552,7 +552,7 @@ public void testMultipleParentsInCdsTree() throws IOException { controlPlaneService.setXdsConfig( ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, newRouteConfig)); inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); - assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet().size()).isEqualTo(4); + assertThat(xdsUpdateCaptor.getValue().getValue().getClusters()).hasSize(8); // Now that it is released, we should only have A11 rootSub.close(); @@ -561,6 +561,29 @@ public void testMultipleParentsInCdsTree() throws IOException { .containsExactly("clusterA11"); } + @Test + public void testCdsCycle() throws Exception { + RouteConfiguration routeConfig = + XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "clusterA"); + Map clusterMap = new HashMap<>(); + Map edsMap = new HashMap<>(); + clusterMap.put("clusterA", XdsTestUtils.buildAggCluster("clusterA", Arrays.asList("clusterB"))); + clusterMap.put("clusterB", XdsTestUtils.buildAggCluster("clusterB", Arrays.asList("clusterA"))); + XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterC"); + controlPlaneService.setXdsConfig( + ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig)); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); + + InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); + XdsConfig config = xdsUpdateCaptor.getValue().getValue(); + assertThat(config.getClusters().get("clusterA").hasValue()).isFalse(); + assertThat(config.getClusters().get("clusterA").getStatus().getDescription()).contains("cycle"); + } + @Test public void testMultipleCdsReferToSameEds() { // Create the maps and Update the config to have 2 clusters that refer to the same EDS resource @@ -646,7 +669,7 @@ public void testChangeRdsName_FromLds_complexTree() { inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); XdsConfig config = xdsUpdateCaptor.getValue().getValue(); assertThat(config.getVirtualHost().name()).isEqualTo(newRdsName); - assertThat(config.getClusters().size()).isEqualTo(4); + assertThat(config.getClusters()).hasSize(8); } @Test @@ -697,8 +720,8 @@ public void testChangeAggCluster() { controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); // Verify that the config is updated as expected - ClusterNameMatcher nameMatcher - = new ClusterNameMatcher(Arrays.asList("root", "clusterA21", "clusterA22")); + ClusterNameMatcher nameMatcher = new ClusterNameMatcher(Arrays.asList( + "root", "clusterA", "clusterA2", "clusterA21", "clusterA22")); inOrder.verify(xdsConfigWatcher).onUpdate(argThat(nameMatcher)); } From c20642874957778a9ca622a364f70250e65863f0 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 5 Jun 2025 19:07:59 -0700 Subject: [PATCH 281/591] binder: Rationalize @ThreadSafe-ty of BinderTransport. (#12130) - Use @BinderThread to document restrictions on methods and certain fields. - Make TransactionHandler non-public since only Android should call it. - Replace an unnecessary AtomicLong with a plain old long. --- .../grpc/binder/internal/BinderTransport.java | 28 ++++++++++--------- .../binder/internal/LeakSafeOneWayBinder.java | 2 ++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index f61c455edd5..c8900031432 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -29,6 +29,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.TransactionTooLargeException; +import androidx.annotation.BinderThread; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Ticker; import com.google.common.base.Verify; @@ -105,8 +106,7 @@ * https://github.com/grpc/proposal/blob/master/L73-java-binderchannel/wireformat.md */ @ThreadSafe -public abstract class BinderTransport - implements LeakSafeOneWayBinder.TransactionHandler, IBinder.DeathRecipient { +public abstract class BinderTransport implements IBinder.DeathRecipient { private static final Logger logger = Logger.getLogger(BinderTransport.class.getName()); @@ -210,9 +210,11 @@ protected enum TransportState { private final FlowController flowController; /** The number of incoming bytes we've received. */ - private final AtomicLong numIncomingBytes; + // Only read/written on @BinderThread. + private long numIncomingBytes; /** The number of incoming bytes we've told our peer we've received. */ + // Only read/written on @BinderThread. private long acknowledgedIncomingBytes; private BinderTransport( @@ -225,10 +227,9 @@ private BinderTransport( this.attributes = attributes; this.logId = logId; scheduledExecutorService = executorServicePool.getObject(); - incomingBinder = new LeakSafeOneWayBinder(this); + incomingBinder = new LeakSafeOneWayBinder(this::handleTransaction); ongoingCalls = new ConcurrentHashMap<>(); flowController = new FlowController(TRANSACTION_BYTES_WINDOW); - numIncomingBytes = new AtomicLong(); } // Override in child class. @@ -423,8 +424,9 @@ final void sendOutOfBandClose(int callId, Status status) { } } - @Override - public final boolean handleTransaction(int code, Parcel parcel) { + @BinderThread + @VisibleForTesting + final boolean handleTransaction(int code, Parcel parcel) { try { return handleTransactionInternal(code, parcel); } catch (RuntimeException e) { @@ -440,6 +442,7 @@ public final boolean handleTransaction(int code, Parcel parcel) { } } + @BinderThread private boolean handleTransactionInternal(int code, Parcel parcel) { if (code < FIRST_CALL_ID) { synchronized (this) { @@ -483,11 +486,12 @@ private boolean handleTransactionInternal(int code, Parcel parcel) { if (inbound != null) { inbound.handleTransaction(parcel); } - long nib = numIncomingBytes.addAndGet(size); - if ((nib - acknowledgedIncomingBytes) > TRANSACTION_BYTES_WINDOW_FORCE_ACK) { + numIncomingBytes += size; + if ((numIncomingBytes - acknowledgedIncomingBytes) > TRANSACTION_BYTES_WINDOW_FORCE_ACK) { synchronized (this) { - sendAcknowledgeBytes(checkNotNull(outgoingBinder)); + sendAcknowledgeBytes(checkNotNull(outgoingBinder), numIncomingBytes); } + acknowledgedIncomingBytes = numIncomingBytes; } return true; } @@ -519,10 +523,8 @@ private final void handlePing(Parcel requestParcel) { protected void handlePingResponse(Parcel parcel) {} @GuardedBy("this") - private void sendAcknowledgeBytes(OneWayBinderProxy iBinder) { + private void sendAcknowledgeBytes(OneWayBinderProxy iBinder, long n) { // Send a transaction to acknowledge reception of incoming data. - long n = numIncomingBytes.get(); - acknowledgedIncomingBytes = n; try (ParcelHolder parcel = ParcelHolder.obtain()) { parcel.get().writeLong(n); iBinder.transact(ACKNOWLEDGE_BYTES, parcel); diff --git a/binder/src/main/java/io/grpc/binder/internal/LeakSafeOneWayBinder.java b/binder/src/main/java/io/grpc/binder/internal/LeakSafeOneWayBinder.java index a12a5cb13cc..e7837b520f8 100644 --- a/binder/src/main/java/io/grpc/binder/internal/LeakSafeOneWayBinder.java +++ b/binder/src/main/java/io/grpc/binder/internal/LeakSafeOneWayBinder.java @@ -19,6 +19,7 @@ import android.os.Binder; import android.os.IBinder; import android.os.Parcel; +import androidx.annotation.BinderThread; import io.grpc.Internal; import java.util.logging.Level; import java.util.logging.Logger; @@ -58,6 +59,7 @@ public interface TransactionHandler { * @return the value to return from {@link Binder#onTransact}. NB: "oneway" semantics mean this * result will not delivered to the caller of {@link IBinder#transact} */ + @BinderThread boolean handleTransaction(int code, Parcel data); } From dc192f5c5e3b087fc78eb17ffdc053e203e5e3e4 Mon Sep 17 00:00:00 2001 From: eshitachandwani <59800922+eshitachandwani@users.noreply.github.com> Date: Fri, 6 Jun 2025 19:24:27 +0530 Subject: [PATCH 282/591] Create SPIFFE tests config (#12133) --- buildscripts/kokoro/psm-spiffe.cfg | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 buildscripts/kokoro/psm-spiffe.cfg diff --git a/buildscripts/kokoro/psm-spiffe.cfg b/buildscripts/kokoro/psm-spiffe.cfg new file mode 100644 index 00000000000..b04d715fca1 --- /dev/null +++ b/buildscripts/kokoro/psm-spiffe.cfg @@ -0,0 +1,17 @@ +# Config file for internal CI + +# Location of the continuous shell script in repository. +build_file: "grpc-java/buildscripts/kokoro/psm-interop-test-java.sh" +timeout_mins: 240 + +action { + define_artifacts { + regex: "artifacts/**/*sponge_log.xml" + regex: "artifacts/**/*.log" + strip_prefix: "artifacts" + } +} +env_vars { + key: "PSM_TEST_SUITE" + value: "spiffe" +} From 1fd29bc804d0820a8f72c061bd671811adf85546 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 29 May 2025 17:08:33 -0700 Subject: [PATCH 283/591] xds: Use tracing GC in XdsDepManager Reference counting doesn't release cycles, so swap to a tracing garbage collector. This greatly simplifies the code as well, as diffing is no longer necessary. (If vanilla reference counting was used, diffing wouldn't have been necessary either as you just increment all the new objects and decrement the old ones. But that doesn't work when use a set instead of an integer.) --- .../io/grpc/xds/XdsDependencyManager.java | 364 +++++++----------- .../io/grpc/xds/XdsDependencyManagerTest.java | 47 ++- .../java/io/grpc/xds/XdsNameResolverTest.java | 65 ++-- 3 files changed, 210 insertions(+), 266 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index 7bbd0064ed4..78d4dbb198a 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -19,12 +19,9 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.xds.client.XdsClient.ResourceUpdate; -import static io.grpc.xds.client.XdsLogger.XdsLogLevel.DEBUG; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Sets; -import io.grpc.InternalLogId; import io.grpc.NameResolver; import io.grpc.Status; import io.grpc.StatusOr; @@ -36,7 +33,6 @@ import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsClient.ResourceWatcher; -import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsResourceType; import java.io.Closeable; import java.io.IOException; @@ -61,22 +57,21 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi public static final XdsClusterResource CLUSTER_RESOURCE = XdsClusterResource.getInstance(); public static final XdsEndpointResource ENDPOINT_RESOURCE = XdsEndpointResource.getInstance(); private static final int MAX_CLUSTER_RECURSION_DEPTH = 16; // Matches C++ + private final String listenerName; private final XdsClient xdsClient; private final XdsConfigWatcher xdsConfigWatcher; private final SynchronizationContext syncContext; private final String dataPlaneAuthority; - private final InternalLogId logId; - private final XdsLogger logger; private StatusOr lastUpdate = null; private final Map, TypeWatchers> resourceWatchers = new HashMap<>(); + private final Set subscriptions = new HashSet<>(); XdsDependencyManager(XdsClient xdsClient, XdsConfigWatcher xdsConfigWatcher, SynchronizationContext syncContext, String dataPlaneAuthority, String listenerName, NameResolver.Args nameResolverArgs, ScheduledExecutorService scheduler) { - logId = InternalLogId.allocate("xds-dependency-manager", listenerName); - logger = XdsLogger.withLogId(logId); + this.listenerName = checkNotNull(listenerName, "listenerName"); this.xdsClient = checkNotNull(xdsClient, "xdsClient"); this.xdsConfigWatcher = checkNotNull(xdsConfigWatcher, "xdsConfigWatcher"); this.syncContext = checkNotNull(syncContext, "syncContext"); @@ -102,7 +97,8 @@ public Closeable subscribeToCluster(String clusterName) { subscription.closed = true; return; // shutdown() called } - addClusterWatcher(clusterName, subscription); + subscriptions.add(subscription); + addClusterWatcher(clusterName); }); return subscription; @@ -117,73 +113,13 @@ private void addWatcher(XdsWatcherBase watcher) { xdsClient.watchXdsResource(type, resourceName, watcher, syncContext); } - private void cancelCdsWatcher(CdsWatcher watcher, Object parentContext) { - if (watcher == null) { - return; - } - watcher.parentContexts.remove(parentContext); - if (watcher.parentContexts.isEmpty()) { - cancelWatcher(watcher); - } - } - - private void cancelEdsWatcher(EdsWatcher watcher, CdsWatcher parentContext) { - if (watcher == null) { - return; - } - watcher.parentContexts.remove(parentContext); - if (watcher.parentContexts.isEmpty()) { - cancelWatcher(watcher); - } - } - - private void cancelWatcher(XdsWatcherBase watcher) { - syncContext.throwIfNotInThisSynchronizationContext(); - - if (watcher == null) { - return; - } - - if (watcher instanceof CdsWatcher || watcher instanceof EdsWatcher) { - throwIfParentContextsNotEmpty(watcher); - } - - watcher.cancelled = true; - XdsResourceType type = watcher.type; - String resourceName = watcher.resourceName; - - if (getWatchers(type).remove(resourceName) == null) { - logger.log(DEBUG, "Trying to cancel watcher {0}, but it isn't watched", watcher); - return; - } - - xdsClient.cancelXdsResourceWatch(type, resourceName, watcher); - } - - private static void throwIfParentContextsNotEmpty(XdsWatcherBase watcher) { - if (watcher instanceof CdsWatcher) { - CdsWatcher cdsWatcher = (CdsWatcher) watcher; - if (!cdsWatcher.parentContexts.isEmpty()) { - String msg = String.format("CdsWatcher %s has parent contexts %s", - cdsWatcher.resourceName(), cdsWatcher.parentContexts); - throw new IllegalStateException(msg); - } - } else if (watcher instanceof EdsWatcher) { - EdsWatcher edsWatcher = (EdsWatcher) watcher; - if (!edsWatcher.parentContexts.isEmpty()) { - String msg = String.format("CdsWatcher %s has parent contexts %s", - edsWatcher.resourceName(), edsWatcher.parentContexts); - throw new IllegalStateException(msg); - } - } - } - public void shutdown() { syncContext.execute(() -> { for (TypeWatchers watchers : resourceWatchers.values()) { shutdownWatchersForType(watchers); } resourceWatchers.clear(); + subscriptions.clear(); }); } @@ -197,54 +133,18 @@ private void shutdownWatchersForType(TypeWatchers private void releaseSubscription(ClusterSubscription subscription) { checkNotNull(subscription, "subscription"); - String clusterName = subscription.getClusterName(); syncContext.execute(() -> { if (subscription.closed) { return; } subscription.closed = true; - XdsWatcherBase cdsWatcher - = getWatchers(CLUSTER_RESOURCE).get(clusterName); - if (cdsWatcher == null) { + if (!subscriptions.remove(subscription)) { return; // shutdown() called } - cancelClusterWatcherTree((CdsWatcher) cdsWatcher, subscription); maybePublishConfig(); }); } - private void cancelClusterWatcherTree(CdsWatcher root, Object parentContext) { - checkNotNull(root, "root"); - - cancelCdsWatcher(root, parentContext); - - if (!root.hasDataValue() || !root.parentContexts.isEmpty()) { - return; - } - - XdsClusterResource.CdsUpdate cdsUpdate = root.getData().getValue(); - switch (cdsUpdate.clusterType()) { - case EDS: - String edsServiceName = root.getEdsServiceName(); - EdsWatcher edsWatcher = (EdsWatcher) getWatchers(ENDPOINT_RESOURCE).get(edsServiceName); - cancelEdsWatcher(edsWatcher, root); - break; - case AGGREGATE: - for (String cluster : cdsUpdate.prioritizedClusterNames()) { - CdsWatcher clusterWatcher = (CdsWatcher) getWatchers(CLUSTER_RESOURCE).get(cluster); - if (clusterWatcher != null) { - cancelClusterWatcherTree(clusterWatcher, root); - } - } - break; - case LOGICAL_DNS: - // no eds needed, so everything happens in cancelCdsWatcher() - break; - default: - throw new AssertionError("Unknown cluster type: " + cdsUpdate.clusterType()); - } - } - /** * Check if all resources have results, and if so, generate a new XdsConfig and send it to all * the watchers. @@ -274,27 +174,40 @@ private void maybePublishConfig() { @VisibleForTesting StatusOr buildUpdate() { + // Create a config and discard any watchers not accessed + WatcherTracer tracer = new WatcherTracer(resourceWatchers); + StatusOr config = buildUpdate( + tracer, listenerName, dataPlaneAuthority, subscriptions); + tracer.closeUnusedWatchers(); + return config; + } + + private static StatusOr buildUpdate( + WatcherTracer tracer, + String listenerName, + String dataPlaneAuthority, + Set subscriptions) { XdsConfig.XdsConfigBuilder builder = new XdsConfig.XdsConfigBuilder(); // Iterate watchers and build the XdsConfig - // Will only be 1 listener and 1 route resource - RdsUpdateSupplier routeSource = null; - for (XdsWatcherBase ldsWatcher : - getWatchers(XdsListenerResource.getInstance()).values()) { - if (!ldsWatcher.getData().hasValue()) { - return StatusOr.fromStatus(ldsWatcher.getData().getStatus()); - } - XdsListenerResource.LdsUpdate ldsUpdate = ldsWatcher.getData().getValue(); - builder.setListener(ldsUpdate); - routeSource = ((LdsWatcher) ldsWatcher).getRouteSource(); + XdsWatcherBase ldsWatcher + = tracer.getWatcher(XdsListenerResource.getInstance(), listenerName); + if (ldsWatcher == null) { + return StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( + "Bug: No listener watcher found for " + listenerName)); } + if (!ldsWatcher.getData().hasValue()) { + return StatusOr.fromStatus(ldsWatcher.getData().getStatus()); + } + XdsListenerResource.LdsUpdate ldsUpdate = ldsWatcher.getData().getValue(); + builder.setListener(ldsUpdate); + RdsUpdateSupplier routeSource = ((LdsWatcher) ldsWatcher).getRouteSource(tracer); if (routeSource == null) { return StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( "Bug: No route source found for listener " + dataPlaneAuthority)); } - StatusOr statusOrRdsUpdate = routeSource.getRdsUpdate(); if (!statusOrRdsUpdate.hasValue()) { return StatusOr.fromStatus(statusOrRdsUpdate.getStatus()); @@ -312,8 +225,11 @@ StatusOr buildUpdate() { Map> clusters = new HashMap<>(); LinkedHashSet ancestors = new LinkedHashSet<>(); - for (String cluster : getWatchers(CLUSTER_RESOURCE).keySet()) { - addConfigForCluster(clusters, cluster, ancestors); + for (String cluster : getClusterNamesFromVirtualHost(activeVirtualHost)) { + addConfigForCluster(clusters, cluster, ancestors, tracer); + } + for (ClusterSubscription subscription : subscriptions) { + addConfigForCluster(clusters, subscription.getClusterName(), ancestors, tracer); } for (Map.Entry> me : clusters.entrySet()) { builder.addCluster(me.getKey(), me.getValue()); @@ -335,11 +251,12 @@ private Map> getWatchers( return tTypeWatchers.watchers; } - private void addConfigForCluster( + private static void addConfigForCluster( Map> clusters, String clusterName, @SuppressWarnings("NonApiType") // Need order-preserving set for errors - LinkedHashSet ancestors) { + LinkedHashSet ancestors, + WatcherTracer tracer) { if (clusters.containsKey(clusterName)) { return; } @@ -355,7 +272,7 @@ private void addConfigForCluster( return; } - CdsWatcher cdsWatcher = (CdsWatcher) getWatchers(CLUSTER_RESOURCE).get(clusterName); + CdsWatcher cdsWatcher = (CdsWatcher) tracer.getWatcher(CLUSTER_RESOURCE, clusterName); StatusOr cdsWatcherDataOr = cdsWatcher.getData(); if (!cdsWatcherDataOr.hasValue()) { clusters.put(clusterName, StatusOr.fromStatus(cdsWatcherDataOr.getStatus())); @@ -371,7 +288,7 @@ private void addConfigForCluster( LinkedHashSet leafNames = new LinkedHashSet(); ancestors.add(clusterName); for (String childCluster : cdsUpdate.prioritizedClusterNames()) { - addConfigForCluster(clusters, childCluster, ancestors); + addConfigForCluster(clusters, childCluster, ancestors, tracer); StatusOr config = clusters.get(childCluster); if (!config.hasValue()) { clusters.put(clusterName, StatusOr.fromStatus(Status.INTERNAL.withDescription( @@ -392,7 +309,7 @@ private void addConfigForCluster( break; case EDS: XdsWatcherBase edsWatcher = - getWatchers(ENDPOINT_RESOURCE).get(cdsWatcher.getEdsServiceName()); + tracer.getWatcher(ENDPOINT_RESOURCE, cdsWatcher.getEdsServiceName()); if (edsWatcher != null) { child = new EndpointConfig(edsWatcher.getData()); } else { @@ -412,53 +329,35 @@ private void addConfigForCluster( new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, child))); } - @Override - public String toString() { - return logId.toString(); + private void addRdsWatcher(String resourceName) { + if (getWatchers(XdsRouteConfigureResource.getInstance()).containsKey(resourceName)) { + return; + } + + addWatcher(new RdsWatcher(resourceName)); } - private void addEdsWatcher(String edsServiceName, CdsWatcher parentContext) { - EdsWatcher watcher - = (EdsWatcher) getWatchers(XdsEndpointResource.getInstance()).get(edsServiceName); - if (watcher != null) { - watcher.addParentContext(parentContext); // Is a set, so don't need to check for existence + private void addEdsWatcher(String edsServiceName) { + if (getWatchers(XdsEndpointResource.getInstance()).containsKey(edsServiceName)) { return; } - addWatcher(new EdsWatcher(edsServiceName, parentContext)); + addWatcher(new EdsWatcher(edsServiceName)); } - private void addClusterWatcher(String clusterName, Object parentContext) { - CdsWatcher watcher = (CdsWatcher) getWatchers(CLUSTER_RESOURCE).get(clusterName); - if (watcher != null) { - watcher.parentContexts.add(parentContext); + private void addClusterWatcher(String clusterName) { + if (getWatchers(CLUSTER_RESOURCE).containsKey(clusterName)) { return; } - addWatcher(new CdsWatcher(clusterName, parentContext)); + addWatcher(new CdsWatcher(clusterName)); } - private void updateRoutes(List virtualHosts, Object newParentContext, - List oldVirtualHosts, boolean sameParentContext) { - VirtualHost oldVirtualHost = - RoutingUtils.findVirtualHostForHostName(oldVirtualHosts, dataPlaneAuthority); + private void updateRoutes(List virtualHosts) { VirtualHost virtualHost = RoutingUtils.findVirtualHostForHostName(virtualHosts, dataPlaneAuthority); - Set newClusters = getClusterNamesFromVirtualHost(virtualHost); - Set oldClusters = getClusterNamesFromVirtualHost(oldVirtualHost); - - if (sameParentContext) { - // Calculate diffs. - Set addedClusters = Sets.difference(newClusters, oldClusters); - Set deletedClusters = Sets.difference(oldClusters, newClusters); - - deletedClusters.forEach(watcher -> - cancelClusterWatcherTree(getCluster(watcher), newParentContext)); - addedClusters.forEach((cluster) -> addClusterWatcher(cluster, newParentContext)); - } else { - newClusters.forEach((cluster) -> addClusterWatcher(cluster, newParentContext)); - } + newClusters.forEach((cluster) -> addClusterWatcher(cluster)); } private String nodeInfo() { @@ -489,10 +388,6 @@ private static Set getClusterNamesFromVirtualHost(VirtualHost virtualHos return clusters; } - private CdsWatcher getCluster(String clusterName) { - return (CdsWatcher) getWatchers(CLUSTER_RESOURCE).get(clusterName); - } - private static class TypeWatchers { // Key is resource name final Map> watchers = new HashMap<>(); @@ -529,6 +424,64 @@ public void close() throws IOException { } } + /** State for tracing garbage collector. */ + private static final class WatcherTracer { + private final Map, TypeWatchers> resourceWatchers; + private final Map, TypeWatchers> usedWatchers; + + public WatcherTracer(Map, TypeWatchers> resourceWatchers) { + this.resourceWatchers = resourceWatchers; + + this.usedWatchers = new HashMap<>(); + for (XdsResourceType type : resourceWatchers.keySet()) { + usedWatchers.put(type, newTypeWatchers(type)); + } + } + + private static TypeWatchers newTypeWatchers( + XdsResourceType type) { + return new TypeWatchers(type); + } + + public XdsWatcherBase getWatcher( + XdsResourceType resourceType, String name) { + TypeWatchers typeWatchers = resourceWatchers.get(resourceType); + if (typeWatchers == null) { + return null; + } + assert typeWatchers.resourceType == resourceType; + @SuppressWarnings("unchecked") + TypeWatchers tTypeWatchers = (TypeWatchers) typeWatchers; + XdsWatcherBase watcher = tTypeWatchers.watchers.get(name); + if (watcher == null) { + return null; + } + @SuppressWarnings("unchecked") + TypeWatchers usedTypeWatchers = (TypeWatchers) usedWatchers.get(resourceType); + usedTypeWatchers.watchers.put(name, watcher); + return watcher; + } + + /** Shut down unused watchers. */ + public void closeUnusedWatchers() { + boolean changed = false; // Help out the GC by preferring old objects + for (XdsResourceType type : resourceWatchers.keySet()) { + TypeWatchers orig = resourceWatchers.get(type); + TypeWatchers used = usedWatchers.get(type); + for (String name : orig.watchers.keySet()) { + if (used.watchers.containsKey(name)) { + continue; + } + orig.watchers.get(name).close(); + changed = true; + } + } + if (changed) { + resourceWatchers.putAll(usedWatchers); + } + } + } + private abstract class XdsWatcherBase implements ResourceWatcher { private final XdsResourceType type; @@ -571,6 +524,10 @@ public void onResourceDoesNotExist(String resourceName) { maybePublishConfig(); } + public void close() { + xdsClient.cancelXdsResourceWatch(type, resourceName, this); + } + boolean missingResult() { return data == null; } @@ -633,24 +590,14 @@ public void onChanged(XdsListenerResource.LdsUpdate update) { virtualHosts = httpConnectionManager.virtualHosts(); rdsName = httpConnectionManager.rdsName(); } - StatusOr activeRdsUpdate = getRouteSource().getRdsUpdate(); - List activeVirtualHosts = activeRdsUpdate.hasValue() - ? activeRdsUpdate.getValue().virtualHosts - : Collections.emptyList(); - - boolean changedRdsName = !Objects.equals(rdsName, this.rdsName); - if (changedRdsName) { - cleanUpRdsWatcher(); - } if (virtualHosts != null) { // No RDS watcher since we are getting RDS updates via LDS - updateRoutes(virtualHosts, this, activeVirtualHosts, this.rdsName == null); + updateRoutes(virtualHosts); this.rdsName = null; - } else if (changedRdsName) { + } else { this.rdsName = rdsName; - addWatcher(new RdsWatcher(rdsName)); - logger.log(XdsLogger.XdsLogLevel.INFO, "Start watching RDS resource {0}", rdsName); + addRdsWatcher(rdsName); } setData(update); @@ -666,36 +613,18 @@ public void onResourceDoesNotExist(String resourceName) { checkArgument(resourceName().equals(resourceName), "Resource name does not match"); setDataAsStatus(Status.UNAVAILABLE.withDescription( toContextString() + " does not exist" + nodeInfo())); - cleanUpRdsWatcher(); rdsName = null; maybePublishConfig(); } - private void cleanUpRdsWatcher() { - RdsWatcher oldRdsWatcher = getRdsWatcher(); - if (oldRdsWatcher != null) { - cancelWatcher(oldRdsWatcher); - logger.log(XdsLogger.XdsLogLevel.DEBUG, "Stop watching RDS resource {0}", rdsName); - - // Cleanup clusters (as appropriate) that had the old rds watcher as a parent - if (!oldRdsWatcher.hasDataValue()) { - return; - } - for (XdsWatcherBase watcher : - getWatchers(CLUSTER_RESOURCE).values()) { - cancelCdsWatcher((CdsWatcher) watcher, oldRdsWatcher); - } - } - } - - private RdsWatcher getRdsWatcher() { + private RdsWatcher getRdsWatcher(WatcherTracer tracer) { if (rdsName == null) { return null; } - return (RdsWatcher) getWatchers(XdsRouteConfigureResource.getInstance()).get(rdsName); + return (RdsWatcher) tracer.getWatcher(XdsRouteConfigureResource.getInstance(), rdsName); } - public RdsUpdateSupplier getRouteSource() { + public RdsUpdateSupplier getRouteSource(WatcherTracer tracer) { if (!hasDataValue()) { return this; } @@ -707,7 +636,7 @@ public RdsUpdateSupplier getRouteSource() { if (virtualHosts != null) { return this; } - RdsWatcher rdsWatcher = getRdsWatcher(); + RdsWatcher rdsWatcher = getRdsWatcher(tracer); assert rdsWatcher != null; return rdsWatcher; } @@ -748,11 +677,8 @@ public void onChanged(RdsUpdate update) { if (cancelled) { return; } - List oldVirtualHosts = hasDataValue() - ? getData().getValue().virtualHosts - : Collections.emptyList(); setData(update); - updateRoutes(update.virtualHosts, this, oldVirtualHosts, true); + updateRoutes(update.virtualHosts); maybePublishConfig(); } @@ -766,11 +692,8 @@ public StatusOr getRdsUpdate() { } private class CdsWatcher extends XdsWatcherBase { - Set parentContexts = new HashSet<>(); - - CdsWatcher(String resourceName, Object parentContext) { + CdsWatcher(String resourceName) { super(CLUSTER_RESOURCE, checkNotNull(resourceName, "resourceName")); - this.parentContexts.add(checkNotNull(parentContext, "parentContext")); } @Override @@ -782,32 +705,16 @@ public void onChanged(XdsClusterResource.CdsUpdate update) { switch (update.clusterType()) { case EDS: setData(update); - addEdsWatcher(getEdsServiceName(), this); + addEdsWatcher(getEdsServiceName()); break; case LOGICAL_DNS: setData(update); // no eds needed break; case AGGREGATE: - Object parentContext = this; - if (hasDataValue()) { - Set oldNames = getData().getValue().clusterType() == ClusterType.AGGREGATE - ? new HashSet<>(getData().getValue().prioritizedClusterNames()) - : Collections.emptySet(); - Set newNames = new HashSet<>(update.prioritizedClusterNames()); - - Set deletedClusters = Sets.difference(oldNames, newNames); - deletedClusters.forEach((cluster) - -> cancelClusterWatcherTree(getCluster(cluster), parentContext)); - - setData(update); - Set addedClusters = Sets.difference(newNames, oldNames); - addedClusters.forEach((cluster) -> addClusterWatcher(cluster, parentContext)); - } else { - setData(update); - update.prioritizedClusterNames() - .forEach(name -> addClusterWatcher(name, parentContext)); - } + setData(update); + update.prioritizedClusterNames() + .forEach(name -> addClusterWatcher(name)); break; default: Status error = Status.UNAVAILABLE.withDescription( @@ -829,11 +736,8 @@ public String getEdsServiceName() { } private class EdsWatcher extends XdsWatcherBase { - private final Set parentContexts = new HashSet<>(); - - private EdsWatcher(String resourceName, CdsWatcher parentContext) { + private EdsWatcher(String resourceName) { super(ENDPOINT_RESOURCE, checkNotNull(resourceName, "resourceName")); - parentContexts.add(checkNotNull(parentContext, "parentContext")); } @Override @@ -844,9 +748,5 @@ public void onChanged(XdsEndpointResource.EdsUpdate update) { setData(checkNotNull(update, "update")); maybePublishConfig(); } - - void addParentContext(CdsWatcher parentContext) { - parentContexts.add(checkNotNull(parentContext, "parentContext")); - } } } diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index da960e1e133..aea1ad66d72 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -66,7 +66,9 @@ import io.grpc.xds.XdsEndpointResource.EdsUpdate; import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.client.XdsClient; +import io.grpc.xds.client.XdsClient.ResourceMetadata; import io.grpc.xds.client.XdsClientMetricReporter; +import io.grpc.xds.client.XdsResourceType; import io.grpc.xds.client.XdsTransportFactory; import java.io.Closeable; import java.io.IOException; @@ -562,7 +564,39 @@ public void testMultipleParentsInCdsTree() throws IOException { } @Test - public void testCdsCycle() throws Exception { + public void testCdsDeleteUnsubscribesChild() throws Exception { + RouteConfiguration routeConfig = + XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "clusterA"); + Map clusterMap = new HashMap<>(); + Map edsMap = new HashMap<>(); + XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterA"); + controlPlaneService.setXdsConfig( + ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig)); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); + + InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); + xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + serverName, serverName, nameResolverArgs, scheduler); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); + XdsConfig config = xdsUpdateCaptor.getValue().getValue(); + assertThat(config.getClusters().get("clusterA").hasValue()).isTrue(); + Map, Map> watches = + xdsClient.getSubscribedResourcesMetadataSnapshot().get(); + assertThat(watches.get(XdsEndpointResource.getInstance()).keySet()) + .containsExactly("eds_clusterA"); + + // Delete cluster + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of()); + inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); + config = xdsUpdateCaptor.getValue().getValue(); + assertThat(config.getClusters().get("clusterA").hasValue()).isFalse(); + watches = xdsClient.getSubscribedResourcesMetadataSnapshot().get(); + assertThat(watches).doesNotContainKey(XdsEndpointResource.getInstance()); + } + + @Test + public void testCdsCycleReclaimed() throws Exception { RouteConfiguration routeConfig = XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "clusterA"); Map clusterMap = new HashMap<>(); @@ -575,6 +609,7 @@ public void testCdsCycle() throws Exception { controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); + // The cycle is loaded and detected InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, serverName, serverName, nameResolverArgs, scheduler); @@ -582,6 +617,16 @@ public void testCdsCycle() throws Exception { XdsConfig config = xdsUpdateCaptor.getValue().getValue(); assertThat(config.getClusters().get("clusterA").hasValue()).isFalse(); assertThat(config.getClusters().get("clusterA").getStatus().getDescription()).contains("cycle"); + + // Orphan the cycle and it is discarded + routeConfig = + XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "clusterC"); + controlPlaneService.setXdsConfig( + ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig)); + inOrder.verify(xdsConfigWatcher).onUpdate(any()); + Map, Map> watches = + xdsClient.getSubscribedResourcesMetadataSnapshot().get(); + assertThat(watches.get(XdsClusterResource.getInstance()).keySet()).containsExactly("clusterC"); } @Test diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index ab966ce0025..f5c40fa2117 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -241,7 +241,7 @@ public void tearDown() { resolver.shutdown(); if (xdsClient != null) { assertThat(xdsClient.ldsWatcher).isNull(); - assertThat(xdsClient.rdsWatcher).isNull(); + assertThat(xdsClient.rdsWatchers).isEmpty(); } } @@ -421,7 +421,7 @@ public void resolving_ldsResourceUpdateRdsName() { resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdateForRdsName(RDS_RESOURCE_NAME); - assertThat(xdsClient.rdsResource).isEqualTo(RDS_RESOURCE_NAME); + assertThat(xdsClient.rdsWatchers.keySet()).containsExactly(RDS_RESOURCE_NAME); VirtualHost virtualHost = VirtualHost.create("virtualhost", Collections.singletonList(AUTHORITY), Collections.singletonList(route1), @@ -438,13 +438,14 @@ public void resolving_ldsResourceUpdateRdsName() { ArgumentCaptor.forClass(ResolutionResult.class); String alternativeRdsResource = "route-configuration-alter.googleapis.com"; xdsClient.deliverLdsUpdateForRdsName(alternativeRdsResource); - assertThat(xdsClient.rdsResource).isEqualTo(alternativeRdsResource); + assertThat(xdsClient.rdsWatchers.keySet()).contains(alternativeRdsResource); virtualHost = VirtualHost.create("virtualhost-alter", Collections.singletonList(AUTHORITY), Collections.singletonList(route2), ImmutableMap.of()); xdsClient.deliverRdsUpdate(alternativeRdsResource, Collections.singletonList(virtualHost)); createAndDeliverClusterUpdates(xdsClient, cluster2); + assertThat(xdsClient.rdsWatchers.keySet()).containsExactly(alternativeRdsResource); // Two new service config updates triggered: // - with load balancing config being able to select cluster1 and cluster2 // - with load balancing config being able to select cluster2 only @@ -477,7 +478,7 @@ public void resolving_ldsResourceRevokedAndAddedBack() { resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdateForRdsName(RDS_RESOURCE_NAME); - assertThat(xdsClient.rdsResource).isEqualTo(RDS_RESOURCE_NAME); + assertThat(xdsClient.rdsWatchers.keySet()).containsExactly(RDS_RESOURCE_NAME); VirtualHost virtualHost = VirtualHost.create("virtualhost", Collections.singletonList(AUTHORITY), Collections.singletonList(route), @@ -491,14 +492,14 @@ public void resolving_ldsResourceRevokedAndAddedBack() { reset(mockListener); xdsClient.deliverLdsResourceNotFound(); // revoke LDS resource - assertThat(xdsClient.rdsResource).isNull(); // stop subscribing to stale RDS resource + assertThat(xdsClient.rdsWatchers.keySet()).isEmpty(); // stop subscribing to stale RDS resource assertEmptyResolutionResult(expectedLdsResourceName); reset(mockListener); xdsClient.deliverLdsUpdateForRdsName(RDS_RESOURCE_NAME); // No name resolution result until new RDS resource update is received. Do not use stale config verifyNoInteractions(mockListener); - assertThat(xdsClient.rdsResource).isEqualTo(RDS_RESOURCE_NAME); + assertThat(xdsClient.rdsWatchers.keySet()).containsExactly(RDS_RESOURCE_NAME); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); createAndDeliverClusterUpdates(xdsClient, cluster1); verify(mockListener).onResult2(resolutionResultCaptor.capture()); @@ -518,7 +519,7 @@ public void resolving_rdsResourceRevokedAndAddedBack() { resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdateForRdsName(RDS_RESOURCE_NAME); - assertThat(xdsClient.rdsResource).isEqualTo(RDS_RESOURCE_NAME); + assertThat(xdsClient.rdsWatchers.keySet()).containsExactly(RDS_RESOURCE_NAME); VirtualHost virtualHost = VirtualHost.create("virtualhost", Collections.singletonList(AUTHORITY), Collections.singletonList(route), @@ -537,6 +538,7 @@ public void resolving_rdsResourceRevokedAndAddedBack() { // Simulate management server adds back the previously used RDS resource. reset(mockListener); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); + createAndDeliverClusterUpdates(xdsClient, cluster1); verify(mockListener).onResult2(resolutionResultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster1), @@ -2430,9 +2432,8 @@ public List getTargets() { private class FakeXdsClient extends XdsClient { // Should never be subscribing to more than one LDS and RDS resource at any point of time. private String ldsResource; // should always be AUTHORITY - private String rdsResource; private ResourceWatcher ldsWatcher; - private ResourceWatcher rdsWatcher; + private final Map>> rdsWatchers = new HashMap<>(); private final Map>> cdsWatchers = new HashMap<>(); private final Map>> edsWatchers = new HashMap<>(); @@ -2457,10 +2458,8 @@ public void watchXdsResource(XdsResourceType resou ldsWatcher = (ResourceWatcher) watcher; break; case "RDS": - assertThat(rdsResource).isNull(); - assertThat(rdsWatcher).isNull(); - rdsResource = resourceName; - rdsWatcher = (ResourceWatcher) watcher; + rdsWatchers.computeIfAbsent(resourceName, k -> new ArrayList<>()) + .add((ResourceWatcher) watcher); break; case "CDS": cdsWatchers.computeIfAbsent(resourceName, k -> new ArrayList<>()) @@ -2488,10 +2487,12 @@ public void cancelXdsResourceWatch(XdsResourceType ldsWatcher = null; break; case "RDS": - assertThat(rdsResource).isNotNull(); - assertThat(rdsWatcher).isNotNull(); - rdsResource = null; - rdsWatcher = null; + assertThat(rdsWatchers).containsKey(resourceName); + assertThat(rdsWatchers.get(resourceName)).contains(watcher); + rdsWatchers.get(resourceName).remove((ResourceWatcher) watcher); + if (rdsWatchers.get(resourceName).isEmpty()) { + rdsWatchers.remove(resourceName); + } break; case "CDS": assertThat(cdsWatchers).containsKey(resourceName); @@ -2659,9 +2660,6 @@ private List getClusterNames(List routes) { void deliverRdsUpdateWithFaultInjection( String resourceName, @Nullable FaultConfig virtualHostFaultConfig, @Nullable FaultConfig routFaultConfig, @Nullable FaultConfig weightedClusterFaultConfig) { - if (!resourceName.equals(rdsResource)) { - return; - } ImmutableMap overrideConfig = weightedClusterFaultConfig == null ? ImmutableMap.of() : ImmutableMap.of( @@ -2690,18 +2688,19 @@ void deliverRdsUpdateWithFaultInjection( Collections.singletonList(expectedLdsResourceName), Collections.singletonList(route), overrideConfig); - syncContext.execute(() -> { - rdsWatcher.onChanged(new RdsUpdate(Collections.singletonList(virtualHost))); - createAndDeliverClusterUpdates(this, cluster1); - }); + deliverRdsUpdate(resourceName, virtualHost); + createAndDeliverClusterUpdates(this, cluster1); } void deliverRdsUpdate(String resourceName, List virtualHosts) { - if (!resourceName.equals(rdsResource)) { + if (!rdsWatchers.containsKey(resourceName)) { return; } syncContext.execute(() -> { - rdsWatcher.onChanged(new RdsUpdate(virtualHosts)); + RdsUpdate update = new RdsUpdate(virtualHosts); + List> resourceWatchers = + ImmutableList.copyOf(rdsWatchers.get(resourceName)); + resourceWatchers.forEach(w -> w.onChanged(update)); }); } @@ -2710,11 +2709,13 @@ void deliverRdsUpdate(String resourceName, VirtualHost virtualHost) { } void deliverRdsResourceNotFound(String resourceName) { - if (!resourceName.equals(rdsResource)) { + if (!rdsWatchers.containsKey(resourceName)) { return; } syncContext.execute(() -> { - rdsWatcher.onResourceDoesNotExist(rdsResource); + List> resourceWatchers = + ImmutableList.copyOf(rdsWatchers.get(resourceName)); + resourceWatchers.forEach(w -> w.onResourceDoesNotExist(resourceName)); }); } @@ -2747,12 +2748,10 @@ void deliverError(final Status error) { ldsWatcher.onError(error); }); } - if (rdsWatcher != null) { - syncContext.execute(() -> { - rdsWatcher.onError(error); - }); - } syncContext.execute(() -> { + rdsWatchers.values().stream() + .flatMap(List::stream) + .forEach(w -> w.onError(error)); cdsWatchers.values().stream() .flatMap(List::stream) .forEach(w -> w.onError(error)); From 4ee662fbcf5cb9b1d3da6b046536890db69e4bc5 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sat, 7 Jun 2025 07:23:09 -0700 Subject: [PATCH 284/591] xds: cancelled=true on watch close in XdsDepManager 1fd29bc80 replaced cancelWatcher() with watcher.close(). But setting cancelled was missing. Because the config update checks for shutdown, the cancelled flag no longer avoids exceptions. But it seems best to continue avoiding any processing after close to avoid surprises. --- xds/src/main/java/io/grpc/xds/XdsDependencyManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index 78d4dbb198a..c0054c16332 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -525,6 +525,7 @@ public void onResourceDoesNotExist(String resourceName) { } public void close() { + cancelled = true; xdsClient.cancelXdsResourceWatch(type, resourceName, this); } From 6afacf589e873e12c53ec7ecb43dfc65ec5ade0e Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sat, 31 May 2025 07:27:15 -0700 Subject: [PATCH 285/591] xds: Don't cache rdsName in XdsDepManager We can easily compute the rdsName and avoiding the state means we don't need to override onResourceDoesNotExist() to keep the cache in-sync with the config. --- .../io/grpc/xds/XdsDependencyManager.java | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index c0054c16332..45ae7074d16 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -567,7 +567,6 @@ private interface RdsUpdateSupplier { private class LdsWatcher extends XdsWatcherBase implements RdsUpdateSupplier { - String rdsName; private LdsWatcher(String resourceName) { super(XdsListenerResource.getInstance(), resourceName); @@ -582,22 +581,18 @@ public void onChanged(XdsListenerResource.LdsUpdate update) { HttpConnectionManager httpConnectionManager = update.httpConnectionManager(); List virtualHosts; - String rdsName; if (httpConnectionManager == null) { // TCP listener. Unsupported config virtualHosts = Collections.emptyList(); // Not null, to not delegate to RDS - rdsName = null; } else { virtualHosts = httpConnectionManager.virtualHosts(); - rdsName = httpConnectionManager.rdsName(); } - if (virtualHosts != null) { - // No RDS watcher since we are getting RDS updates via LDS updateRoutes(virtualHosts); - this.rdsName = null; - } else { - this.rdsName = rdsName; + } + + String rdsName = getRdsName(update); + if (rdsName != null) { addRdsWatcher(rdsName); } @@ -605,20 +600,17 @@ public void onChanged(XdsListenerResource.LdsUpdate update) { maybePublishConfig(); } - @Override - public void onResourceDoesNotExist(String resourceName) { - if (cancelled) { - return; + private String getRdsName(XdsListenerResource.LdsUpdate update) { + HttpConnectionManager httpConnectionManager = update.httpConnectionManager(); + if (httpConnectionManager == null) { + // TCP listener. Unsupported config + return null; } - - checkArgument(resourceName().equals(resourceName), "Resource name does not match"); - setDataAsStatus(Status.UNAVAILABLE.withDescription( - toContextString() + " does not exist" + nodeInfo())); - rdsName = null; - maybePublishConfig(); + return httpConnectionManager.rdsName(); } - private RdsWatcher getRdsWatcher(WatcherTracer tracer) { + private RdsWatcher getRdsWatcher(XdsListenerResource.LdsUpdate update, WatcherTracer tracer) { + String rdsName = getRdsName(update); if (rdsName == null) { return null; } @@ -637,7 +629,7 @@ public RdsUpdateSupplier getRouteSource(WatcherTracer tracer) { if (virtualHosts != null) { return this; } - RdsWatcher rdsWatcher = getRdsWatcher(tracer); + RdsWatcher rdsWatcher = getRdsWatcher(getData().getValue(), tracer); assert rdsWatcher != null; return rdsWatcher; } From a16d6559194d1598197d210a8aa9593e336128b1 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 10 Jun 2025 10:31:03 +0530 Subject: [PATCH 286/591] compiler: generate blocking v2 unary calls that throw StatusException (#12126) --- .../LoadBalancerStatsServiceGrpc.java | 8 +++--- .../integration/MetricsServiceGrpc.java | 4 +-- .../integration/ReconnectServiceGrpc.java | 8 +++--- .../testing/integration/TestServiceGrpc.java | 16 +++++------ .../integration/UnimplementedServiceGrpc.java | 4 +-- .../XdsUpdateClientConfigureServiceGrpc.java | 4 +-- .../XdsUpdateHealthServiceGrpc.java | 8 +++--- .../LoadBalancerStatsServiceGrpc.java | 8 +++--- .../integration/MetricsServiceGrpc.java | 4 +-- .../integration/ReconnectServiceGrpc.java | 8 +++--- .../testing/integration/TestServiceGrpc.java | 16 +++++------ .../integration/UnimplementedServiceGrpc.java | 4 +-- .../XdsUpdateClientConfigureServiceGrpc.java | 4 +-- .../XdsUpdateHealthServiceGrpc.java | 8 +++--- .../proto/BenchmarkServiceGrpc.java | 4 +-- .../proto/ReportQpsScenarioServiceGrpc.java | 4 +-- .../benchmarks/proto/WorkerServiceGrpc.java | 8 +++--- .../src/java_plugin/cpp/java_generator.cpp | 6 ++-- .../golden/TestDeprecatedService.java.txt | 4 +-- compiler/src/test/golden/TestService.java.txt | 12 ++++---- .../golden/TestDeprecatedService.java.txt | 4 +-- .../src/testLite/golden/TestService.java.txt | 12 ++++---- .../LoadBalancerStatsServiceGrpc.java | 8 +++--- .../integration/MetricsServiceGrpc.java | 4 +-- .../integration/ReconnectServiceGrpc.java | 8 +++--- .../testing/integration/TestServiceGrpc.java | 16 +++++------ .../integration/UnimplementedServiceGrpc.java | 4 +-- .../XdsUpdateClientConfigureServiceGrpc.java | 4 +-- .../XdsUpdateHealthServiceGrpc.java | 8 +++--- .../io/istio/test/EchoTestServiceGrpc.java | 8 +++--- .../lookup/v1/RouteLookupServiceGrpc.java | 4 +-- .../io/grpc/channelz/v1/ChannelzGrpc.java | 28 +++++++++---------- .../grpc/io/grpc/health/v1/HealthGrpc.java | 4 +-- .../testing/AnotherDynamicServiceGrpc.java | 4 +-- .../AnotherReflectableServiceGrpc.java | 4 +-- .../testing/DynamicServiceGrpc.java | 4 +-- .../testing/ReflectableServiceGrpc.java | 4 +-- .../main/java/io/grpc/stub/ClientCalls.java | 17 +++++++++++ .../testing/protobuf/SimpleServiceGrpc.java | 4 +-- .../v3/ClientStatusDiscoveryServiceGrpc.java | 4 +-- 40 files changed, 157 insertions(+), 138 deletions(-) diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java index 42934e94c5b..33b914bb4b3 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java @@ -242,8 +242,8 @@ protected LoadBalancerStatsServiceBlockingV2Stub build( * Gets the backend distribution for RPCs sent by a test client. * */ - public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetClientStatsMethod(), getCallOptions(), request); } @@ -252,8 +252,8 @@ public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientS * Gets the accumulated stats for RPCs sent by a test client. * */ - public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetClientAccumulatedStatsMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java index 6c2166468f6..c99abcff7cb 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java @@ -242,8 +242,8 @@ protected MetricsServiceBlockingV2Stub build( * Returns the value of one gauge * */ - public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetGaugeMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java index 07ce250bc4b..fffcaad2df2 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java @@ -227,15 +227,15 @@ protected ReconnectServiceBlockingV2Stub build( /** */ - public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getStartMethod(), getCallOptions(), request); } /** */ - public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getStopMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java index 4593215b601..1d7805e3a3f 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/TestServiceGrpc.java @@ -573,8 +573,8 @@ protected TestServiceBlockingV2Stub build( * One empty request followed by one empty response. * */ - public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getEmptyCallMethod(), getCallOptions(), request); } @@ -583,8 +583,8 @@ public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.i * One request followed by one response. * */ - public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnaryCallMethod(), getCallOptions(), request); } @@ -595,8 +595,8 @@ public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.tes * satisfy subsequent requests. * */ - public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getCacheableUnaryCallMethod(), getCallOptions(), request); } @@ -661,8 +661,8 @@ public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io * to test the behavior when clients call unimplemented methods. * */ - public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java index d9ef0e6ddd9..bec9b5a723a 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java @@ -196,8 +196,8 @@ protected UnimplementedServiceBlockingV2Stub build( * A call that no server should implement * */ - public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java index 8ae0c2f93a4..3453b6c01be 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java @@ -191,8 +191,8 @@ protected XdsUpdateClientConfigureServiceBlockingV2Stub build( * Update the tes client's configuration. * */ - public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getConfigureMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java index 5b950c73c12..fb5f2cdebc7 100644 --- a/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java +++ b/android-interop-testing/src/generated/debug/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java @@ -227,15 +227,15 @@ protected XdsUpdateHealthServiceBlockingV2Stub build( /** */ - public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getSetServingMethod(), getCallOptions(), request); } /** */ - public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getSetNotServingMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java index 42934e94c5b..33b914bb4b3 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java @@ -242,8 +242,8 @@ protected LoadBalancerStatsServiceBlockingV2Stub build( * Gets the backend distribution for RPCs sent by a test client. * */ - public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetClientStatsMethod(), getCallOptions(), request); } @@ -252,8 +252,8 @@ public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientS * Gets the accumulated stats for RPCs sent by a test client. * */ - public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetClientAccumulatedStatsMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java index 6c2166468f6..c99abcff7cb 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java @@ -242,8 +242,8 @@ protected MetricsServiceBlockingV2Stub build( * Returns the value of one gauge * */ - public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetGaugeMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java index 07ce250bc4b..fffcaad2df2 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java @@ -227,15 +227,15 @@ protected ReconnectServiceBlockingV2Stub build( /** */ - public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getStartMethod(), getCallOptions(), request); } /** */ - public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getStopMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java index 4593215b601..1d7805e3a3f 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/TestServiceGrpc.java @@ -573,8 +573,8 @@ protected TestServiceBlockingV2Stub build( * One empty request followed by one empty response. * */ - public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getEmptyCallMethod(), getCallOptions(), request); } @@ -583,8 +583,8 @@ public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.i * One request followed by one response. * */ - public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnaryCallMethod(), getCallOptions(), request); } @@ -595,8 +595,8 @@ public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.tes * satisfy subsequent requests. * */ - public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getCacheableUnaryCallMethod(), getCallOptions(), request); } @@ -661,8 +661,8 @@ public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io * to test the behavior when clients call unimplemented methods. * */ - public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java index d9ef0e6ddd9..bec9b5a723a 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java @@ -196,8 +196,8 @@ protected UnimplementedServiceBlockingV2Stub build( * A call that no server should implement * */ - public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java index 8ae0c2f93a4..3453b6c01be 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java @@ -191,8 +191,8 @@ protected XdsUpdateClientConfigureServiceBlockingV2Stub build( * Update the tes client's configuration. * */ - public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getConfigureMethod(), getCallOptions(), request); } } diff --git a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java index 5b950c73c12..fb5f2cdebc7 100644 --- a/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java +++ b/android-interop-testing/src/generated/release/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java @@ -227,15 +227,15 @@ protected XdsUpdateHealthServiceBlockingV2Stub build( /** */ - public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getSetServingMethod(), getCallOptions(), request); } /** */ - public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getSetNotServingMethod(), getCallOptions(), request); } } diff --git a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java index 15b9a67918b..68e911afc4a 100644 --- a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java +++ b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/BenchmarkServiceGrpc.java @@ -398,8 +398,8 @@ protected BenchmarkServiceBlockingV2Stub build( * The server returns the client payload as-is. * */ - public io.grpc.benchmarks.proto.Messages.SimpleResponse unaryCall(io.grpc.benchmarks.proto.Messages.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.benchmarks.proto.Messages.SimpleResponse unaryCall(io.grpc.benchmarks.proto.Messages.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnaryCallMethod(), getCallOptions(), request); } diff --git a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java index 8fe5f926d99..c5064875bb6 100644 --- a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java +++ b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/ReportQpsScenarioServiceGrpc.java @@ -177,8 +177,8 @@ protected ReportQpsScenarioServiceBlockingV2Stub build( * Report results of a QPS test benchmark scenario. * */ - public io.grpc.benchmarks.proto.Control.Void reportScenario(io.grpc.benchmarks.proto.Control.ScenarioResult request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.benchmarks.proto.Control.Void reportScenario(io.grpc.benchmarks.proto.Control.ScenarioResult request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getReportScenarioMethod(), getCallOptions(), request); } } diff --git a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java index bf9649e8377..721b4f9ab19 100644 --- a/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java +++ b/benchmarks/src/generated/main/grpc/io/grpc/benchmarks/proto/WorkerServiceGrpc.java @@ -387,8 +387,8 @@ protected WorkerServiceBlockingV2Stub build( * Just return the core count - unary call * */ - public io.grpc.benchmarks.proto.Control.CoreResponse coreCount(io.grpc.benchmarks.proto.Control.CoreRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.benchmarks.proto.Control.CoreResponse coreCount(io.grpc.benchmarks.proto.Control.CoreRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getCoreCountMethod(), getCallOptions(), request); } @@ -397,8 +397,8 @@ public io.grpc.benchmarks.proto.Control.CoreResponse coreCount(io.grpc.benchmark * Quit this worker * */ - public io.grpc.benchmarks.proto.Control.Void quitWorker(io.grpc.benchmarks.proto.Control.Void request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.benchmarks.proto.Control.Void quitWorker(io.grpc.benchmarks.proto.Control.Void request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getQuitWorkerMethod(), getCallOptions(), request); } } diff --git a/compiler/src/java_plugin/cpp/java_generator.cpp b/compiler/src/java_plugin/cpp/java_generator.cpp index 8421a4a9207..4cee7999402 100644 --- a/compiler/src/java_plugin/cpp/java_generator.cpp +++ b/compiler/src/java_plugin/cpp/java_generator.cpp @@ -745,9 +745,10 @@ static void PrintStub( " $lower_method_name$($input_type$ request)"); } else { // Simple RPC + (*vars)["throws_decl"] = " throws io.grpc.StatusException"; p->Print( *vars, - "$output_type$ $lower_method_name$($input_type$ request)"); + "$output_type$ $lower_method_name$($input_type$ request)$throws_decl$"); } break; case ASYNC_CALL: @@ -827,7 +828,8 @@ static void PrintStub( if (server_streaming) { (*vars)["calls_method"] = "io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall"; } else { - (*vars)["calls_method"] = "io.grpc.stub.ClientCalls.blockingUnaryCall"; + (*vars)["calls_method"] = "io.grpc.stub.ClientCalls.blockingV2UnaryCall"; + (*vars)["throws_decl"] = " throws io.grpc.StatusException"; } p->Print( diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index 712be32132e..548a9e3d806 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -203,8 +203,8 @@ public final class TestDeprecatedServiceGrpc { * */ @java.lang.Deprecated - public io.grpc.testing.compiler.Test.SimpleResponse deprecatedMethod(io.grpc.testing.compiler.Test.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.compiler.Test.SimpleResponse deprecatedMethod(io.grpc.testing.compiler.Test.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getDeprecatedMethodMethod(), getCallOptions(), request); } } diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index e3c759639ec..93551f6d582 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -580,8 +580,8 @@ public final class TestServiceGrpc { * The server returns the client payload as-is. * */ - public io.grpc.testing.compiler.Test.SimpleResponse unaryCall(io.grpc.testing.compiler.Test.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.compiler.Test.SimpleResponse unaryCall(io.grpc.testing.compiler.Test.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnaryCallMethod(), getCallOptions(), request); } @@ -658,8 +658,8 @@ public final class TestServiceGrpc { * A unary call that is Safe. * */ - public io.grpc.testing.compiler.Test.SimpleResponse safeCall(io.grpc.testing.compiler.Test.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.compiler.Test.SimpleResponse safeCall(io.grpc.testing.compiler.Test.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getSafeCallMethod(), getCallOptions(), request); } @@ -668,8 +668,8 @@ public final class TestServiceGrpc { * A unary call that is Idempotent. * */ - public io.grpc.testing.compiler.Test.SimpleResponse idempotentCall(io.grpc.testing.compiler.Test.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.compiler.Test.SimpleResponse idempotentCall(io.grpc.testing.compiler.Test.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getIdempotentCallMethod(), getCallOptions(), request); } } diff --git a/compiler/src/testLite/golden/TestDeprecatedService.java.txt b/compiler/src/testLite/golden/TestDeprecatedService.java.txt index 22a57c71523..89ea2e698bf 100644 --- a/compiler/src/testLite/golden/TestDeprecatedService.java.txt +++ b/compiler/src/testLite/golden/TestDeprecatedService.java.txt @@ -199,8 +199,8 @@ public final class TestDeprecatedServiceGrpc { * */ @java.lang.Deprecated - public io.grpc.testing.compiler.Test.SimpleResponse deprecatedMethod(io.grpc.testing.compiler.Test.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.compiler.Test.SimpleResponse deprecatedMethod(io.grpc.testing.compiler.Test.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getDeprecatedMethodMethod(), getCallOptions(), request); } } diff --git a/compiler/src/testLite/golden/TestService.java.txt b/compiler/src/testLite/golden/TestService.java.txt index d13d9959a4a..4e9dfb8d682 100644 --- a/compiler/src/testLite/golden/TestService.java.txt +++ b/compiler/src/testLite/golden/TestService.java.txt @@ -569,8 +569,8 @@ public final class TestServiceGrpc { * The server returns the client payload as-is. * */ - public io.grpc.testing.compiler.Test.SimpleResponse unaryCall(io.grpc.testing.compiler.Test.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.compiler.Test.SimpleResponse unaryCall(io.grpc.testing.compiler.Test.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnaryCallMethod(), getCallOptions(), request); } @@ -647,8 +647,8 @@ public final class TestServiceGrpc { * A unary call that is Safe. * */ - public io.grpc.testing.compiler.Test.SimpleResponse safeCall(io.grpc.testing.compiler.Test.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.compiler.Test.SimpleResponse safeCall(io.grpc.testing.compiler.Test.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getSafeCallMethod(), getCallOptions(), request); } @@ -657,8 +657,8 @@ public final class TestServiceGrpc { * A unary call that is Idempotent. * */ - public io.grpc.testing.compiler.Test.SimpleResponse idempotentCall(io.grpc.testing.compiler.Test.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.compiler.Test.SimpleResponse idempotentCall(io.grpc.testing.compiler.Test.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getIdempotentCallMethod(), getCallOptions(), request); } } diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java index 766d63a51dc..22c64d12f33 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/LoadBalancerStatsServiceGrpc.java @@ -244,8 +244,8 @@ protected LoadBalancerStatsServiceBlockingV2Stub build( * Gets the backend distribution for RPCs sent by a test client. * */ - public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetClientStatsMethod(), getCallOptions(), request); } @@ -254,8 +254,8 @@ public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientS * Gets the accumulated stats for RPCs sent by a test client. * */ - public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetClientAccumulatedStatsMethod(), getCallOptions(), request); } } diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java index 8693f1086bb..980dee010f1 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/MetricsServiceGrpc.java @@ -244,8 +244,8 @@ protected MetricsServiceBlockingV2Stub build( * Returns the value of one gauge * */ - public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetGaugeMethod(), getCallOptions(), request); } } diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java index 164c92295cc..05d46ce8e95 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/ReconnectServiceGrpc.java @@ -229,15 +229,15 @@ protected ReconnectServiceBlockingV2Stub build( /** */ - public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty start(io.grpc.testing.integration.Messages.ReconnectParams request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getStartMethod(), getCallOptions(), request); } /** */ - public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.ReconnectInfo stop(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getStopMethod(), getCallOptions(), request); } } diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java index b8c220925e1..a881c85c150 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/TestServiceGrpc.java @@ -581,8 +581,8 @@ protected TestServiceBlockingV2Stub build( * One empty request followed by one empty response. * */ - public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getEmptyCallMethod(), getCallOptions(), request); } @@ -591,8 +591,8 @@ public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.i * One request followed by one response. * */ - public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnaryCallMethod(), getCallOptions(), request); } @@ -603,8 +603,8 @@ public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.tes * satisfy subsequent requests. * */ - public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getCacheableUnaryCallMethod(), getCallOptions(), request); } @@ -669,8 +669,8 @@ public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io * to test the behavior when clients call unimplemented methods. * */ - public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); } } diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java index fe6c7c7d1c6..fdd8d5650ed 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/UnimplementedServiceGrpc.java @@ -197,8 +197,8 @@ protected UnimplementedServiceBlockingV2Stub build( * A call that no server should implement * */ - public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); } } diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java index 9a628ea1b1a..6c019efefea 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateClientConfigureServiceGrpc.java @@ -192,8 +192,8 @@ protected XdsUpdateClientConfigureServiceBlockingV2Stub build( * Update the tes client's configuration. * */ - public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getConfigureMethod(), getCallOptions(), request); } } diff --git a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java index 5582c60d9cc..5531033ae5c 100644 --- a/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java +++ b/interop-testing/src/generated/main/grpc/io/grpc/testing/integration/XdsUpdateHealthServiceGrpc.java @@ -229,15 +229,15 @@ protected XdsUpdateHealthServiceBlockingV2Stub build( /** */ - public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty setServing(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getSetServingMethod(), getCallOptions(), request); } /** */ - public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.integration.EmptyProtos.Empty setNotServing(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getSetNotServingMethod(), getCallOptions(), request); } } diff --git a/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java b/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java index c6947a075d5..61d20d2f7bb 100644 --- a/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java +++ b/istio-interop-testing/src/generated/main/grpc/io/istio/test/EchoTestServiceGrpc.java @@ -214,15 +214,15 @@ protected EchoTestServiceBlockingV2Stub build( /** */ - public io.istio.test.Echo.EchoResponse echo(io.istio.test.Echo.EchoRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.istio.test.Echo.EchoResponse echo(io.istio.test.Echo.EchoRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getEchoMethod(), getCallOptions(), request); } /** */ - public io.istio.test.Echo.ForwardEchoResponse forwardEcho(io.istio.test.Echo.ForwardEchoRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.istio.test.Echo.ForwardEchoResponse forwardEcho(io.istio.test.Echo.ForwardEchoRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getForwardEchoMethod(), getCallOptions(), request); } } diff --git a/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java b/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java index 39e9d0f4144..be060e576a4 100644 --- a/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java +++ b/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java @@ -177,8 +177,8 @@ protected RouteLookupServiceBlockingV2Stub build( * Lookup returns a target for a single key. * */ - public io.grpc.lookup.v1.RouteLookupResponse routeLookup(io.grpc.lookup.v1.RouteLookupRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.lookup.v1.RouteLookupResponse routeLookup(io.grpc.lookup.v1.RouteLookupRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getRouteLookupMethod(), getCallOptions(), request); } } diff --git a/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java b/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java index f839f11cfe5..c4ac4076d22 100644 --- a/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java +++ b/services/src/generated/main/grpc/io/grpc/channelz/v1/ChannelzGrpc.java @@ -512,8 +512,8 @@ protected ChannelzBlockingV2Stub build( * created). This does not include subchannels nor non-top level channels. * */ - public io.grpc.channelz.v1.GetTopChannelsResponse getTopChannels(io.grpc.channelz.v1.GetTopChannelsRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.channelz.v1.GetTopChannelsResponse getTopChannels(io.grpc.channelz.v1.GetTopChannelsRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetTopChannelsMethod(), getCallOptions(), request); } @@ -522,8 +522,8 @@ public io.grpc.channelz.v1.GetTopChannelsResponse getTopChannels(io.grpc.channel * Gets all servers that exist in the process. * */ - public io.grpc.channelz.v1.GetServersResponse getServers(io.grpc.channelz.v1.GetServersRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.channelz.v1.GetServersResponse getServers(io.grpc.channelz.v1.GetServersRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetServersMethod(), getCallOptions(), request); } @@ -532,8 +532,8 @@ public io.grpc.channelz.v1.GetServersResponse getServers(io.grpc.channelz.v1.Get * Returns a single Server, or else a NOT_FOUND code. * */ - public io.grpc.channelz.v1.GetServerResponse getServer(io.grpc.channelz.v1.GetServerRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.channelz.v1.GetServerResponse getServer(io.grpc.channelz.v1.GetServerRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetServerMethod(), getCallOptions(), request); } @@ -542,8 +542,8 @@ public io.grpc.channelz.v1.GetServerResponse getServer(io.grpc.channelz.v1.GetSe * Gets all server sockets that exist in the process. * */ - public io.grpc.channelz.v1.GetServerSocketsResponse getServerSockets(io.grpc.channelz.v1.GetServerSocketsRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.channelz.v1.GetServerSocketsResponse getServerSockets(io.grpc.channelz.v1.GetServerSocketsRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetServerSocketsMethod(), getCallOptions(), request); } @@ -552,8 +552,8 @@ public io.grpc.channelz.v1.GetServerSocketsResponse getServerSockets(io.grpc.cha * Returns a single Channel, or else a NOT_FOUND code. * */ - public io.grpc.channelz.v1.GetChannelResponse getChannel(io.grpc.channelz.v1.GetChannelRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.channelz.v1.GetChannelResponse getChannel(io.grpc.channelz.v1.GetChannelRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetChannelMethod(), getCallOptions(), request); } @@ -562,8 +562,8 @@ public io.grpc.channelz.v1.GetChannelResponse getChannel(io.grpc.channelz.v1.Get * Returns a single Subchannel, or else a NOT_FOUND code. * */ - public io.grpc.channelz.v1.GetSubchannelResponse getSubchannel(io.grpc.channelz.v1.GetSubchannelRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.channelz.v1.GetSubchannelResponse getSubchannel(io.grpc.channelz.v1.GetSubchannelRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetSubchannelMethod(), getCallOptions(), request); } @@ -572,8 +572,8 @@ public io.grpc.channelz.v1.GetSubchannelResponse getSubchannel(io.grpc.channelz. * Returns a single Socket or else a NOT_FOUND code. * */ - public io.grpc.channelz.v1.GetSocketResponse getSocket(io.grpc.channelz.v1.GetSocketRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.channelz.v1.GetSocketResponse getSocket(io.grpc.channelz.v1.GetSocketRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getGetSocketMethod(), getCallOptions(), request); } } diff --git a/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java b/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java index feb5932b0d9..b8e94ef7d20 100644 --- a/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java +++ b/services/src/generated/main/grpc/io/grpc/health/v1/HealthGrpc.java @@ -256,8 +256,8 @@ protected HealthBlockingV2Stub build( * NOT_FOUND. * */ - public io.grpc.health.v1.HealthCheckResponse check(io.grpc.health.v1.HealthCheckRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.health.v1.HealthCheckResponse check(io.grpc.health.v1.HealthCheckRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getCheckMethod(), getCallOptions(), request); } diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java index 5b7aaa1e4ec..978af2d887e 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherDynamicServiceGrpc.java @@ -192,8 +192,8 @@ protected AnotherDynamicServiceBlockingV2Stub build( * A method * */ - public io.grpc.reflection.testing.DynamicReply method(io.grpc.reflection.testing.DynamicRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.reflection.testing.DynamicReply method(io.grpc.reflection.testing.DynamicRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getMethodMethod(), getCallOptions(), request); } } diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java index f8b8d58e621..e688c3d5cca 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/AnotherReflectableServiceGrpc.java @@ -168,8 +168,8 @@ protected AnotherReflectableServiceBlockingV2Stub build( /** */ - public io.grpc.reflection.testing.Reply method(io.grpc.reflection.testing.Request request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.reflection.testing.Reply method(io.grpc.reflection.testing.Request request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getMethodMethod(), getCallOptions(), request); } } diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java index 81e60438518..efef61be151 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/DynamicServiceGrpc.java @@ -192,8 +192,8 @@ protected DynamicServiceBlockingV2Stub build( * A method * */ - public io.grpc.reflection.testing.DynamicReply method(io.grpc.reflection.testing.DynamicRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.reflection.testing.DynamicReply method(io.grpc.reflection.testing.DynamicRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getMethodMethod(), getCallOptions(), request); } } diff --git a/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java b/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java index 5f620038b2c..b5d130d6952 100644 --- a/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java +++ b/services/src/generated/test/grpc/io/grpc/reflection/testing/ReflectableServiceGrpc.java @@ -168,8 +168,8 @@ protected ReflectableServiceBlockingV2Stub build( /** */ - public io.grpc.reflection.testing.Reply method(io.grpc.reflection.testing.Request request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.reflection.testing.Reply method(io.grpc.reflection.testing.Request request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getMethodMethod(), getCallOptions(), request); } } diff --git a/stub/src/main/java/io/grpc/stub/ClientCalls.java b/stub/src/main/java/io/grpc/stub/ClientCalls.java index f307c806489..e5a94f4d864 100644 --- a/stub/src/main/java/io/grpc/stub/ClientCalls.java +++ b/stub/src/main/java/io/grpc/stub/ClientCalls.java @@ -182,6 +182,23 @@ public static RespT blockingUnaryCall( } } + /** + * Executes a unary call and blocks on the response, + * throws a checked {@link StatusException}. + * + * @return the single response message. + * @throws StatusException on error + */ + public static RespT blockingV2UnaryCall( + Channel channel, MethodDescriptor method, CallOptions callOptions, ReqT req) + throws StatusException { + try { + return blockingUnaryCall(channel, method, callOptions, req); + } catch (StatusRuntimeException e) { + throw e.getStatus().asException(e.getTrailers()); + } + } + /** * Executes a server-streaming call returning a blocking {@link Iterator} over the * response stream. The {@code call} should not be already started. After calling this method, diff --git a/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java b/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java index cccc4eb8f8a..e242fd0f513 100644 --- a/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java +++ b/testing-proto/src/generated/main/grpc/io/grpc/testing/protobuf/SimpleServiceGrpc.java @@ -348,8 +348,8 @@ protected SimpleServiceBlockingV2Stub build( * Simple unary RPC. * */ - public io.grpc.testing.protobuf.SimpleResponse unaryRpc(io.grpc.testing.protobuf.SimpleRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.grpc.testing.protobuf.SimpleResponse unaryRpc(io.grpc.testing.protobuf.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getUnaryRpcMethod(), getCallOptions(), request); } diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java index 775fa0c1e3e..cb166503566 100644 --- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java +++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/status/v3/ClientStatusDiscoveryServiceGrpc.java @@ -248,8 +248,8 @@ protected ClientStatusDiscoveryServiceBlockingV2Stub build( /** */ - public io.envoyproxy.envoy.service.status.v3.ClientStatusResponse fetchClientStatus(io.envoyproxy.envoy.service.status.v3.ClientStatusRequest request) { - return io.grpc.stub.ClientCalls.blockingUnaryCall( + public io.envoyproxy.envoy.service.status.v3.ClientStatusResponse fetchClientStatus(io.envoyproxy.envoy.service.status.v3.ClientStatusRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( getChannel(), getFetchClientStatusMethod(), getCallOptions(), request); } } From 297ab05efeb0565c195518a819dfa851d1c0d62b Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 11 Jun 2025 18:56:13 +0000 Subject: [PATCH 287/591] xds: Convert CdsLb to XdsDepManager I noticed we deviated from gRFC A37 in some ways. It turned out those were added to the gRFC later in https://github.com/grpc/proposal/pull/344: - NACKing empty aggregate clusters - Failing aggregate cluster when children could not be loaded - Recusion limit of 16. We had this behavior already, but it was ascribed to matching C++ There's disagreement on whether we should actually fail the aggregate cluster for bad children, so I'm preserving the pre-existing behavior for now. The code is now doing a depth-first leaf traversal, not breadth-first. This was odd to see, but the code was also pretty old, so the reasoning seems lost to history. Since we haven't seen more than a single level of aggregate clusters in practice, this wouldn't have been noticed by users. XdsDependencyManager.start() was created to guarantee that the callback could not be called before returning from the constructor. Otherwise XDS_CLUSTER_SUBSCRIPT_REGISTRY could potentially be null. --- .../java/io/grpc/xds/CdsLoadBalancer2.java | 463 ++------ .../io/grpc/xds/CdsLoadBalancerProvider.java | 26 +- .../xds/RingHashLoadBalancerProvider.java | 2 +- .../java/io/grpc/xds/XdsClusterResource.java | 8 +- xds/src/main/java/io/grpc/xds/XdsConfig.java | 8 +- .../io/grpc/xds/XdsDependencyManager.java | 47 +- .../java/io/grpc/xds/XdsNameResolver.java | 9 +- .../io/grpc/xds/CdsLoadBalancer2Test.java | 1035 +++++++---------- .../grpc/xds/GrpcXdsClientImplTestBase.java | 17 + .../xds/RingHashLoadBalancerProviderTest.java | 8 +- .../io/grpc/xds/XdsDependencyManagerTest.java | 112 +- .../test/java/io/grpc/xds/XdsTestUtils.java | 32 + 12 files changed, 731 insertions(+), 1036 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java index bb44071a484..c50f844d388 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java @@ -21,36 +21,30 @@ import static io.grpc.xds.XdsLbPolicies.CLUSTER_RESOLVER_POLICY_NAME; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.InternalLogId; import io.grpc.LoadBalancer; import io.grpc.LoadBalancerRegistry; import io.grpc.NameResolver; import io.grpc.Status; -import io.grpc.SynchronizationContext; -import io.grpc.internal.ObjectPool; +import io.grpc.StatusOr; import io.grpc.util.GracefulSwitchLoadBalancer; import io.grpc.xds.CdsLoadBalancerProvider.CdsConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; import io.grpc.xds.XdsClusterResource.CdsUpdate; import io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType; -import io.grpc.xds.client.XdsClient; -import io.grpc.xds.client.XdsClient.ResourceWatcher; +import io.grpc.xds.XdsConfig.Subscription; +import io.grpc.xds.XdsConfig.XdsClusterConfig; +import io.grpc.xds.XdsConfig.XdsClusterConfig.AggregateConfig; +import io.grpc.xds.XdsConfig.XdsClusterConfig.EndpointConfig; import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger.XdsLogLevel; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import javax.annotation.Nullable; /** * Load balancer for cds_experimental LB policy. One instance per top-level cluster. @@ -60,13 +54,11 @@ final class CdsLoadBalancer2 extends LoadBalancer { private final XdsLogger logger; private final Helper helper; - private final SynchronizationContext syncContext; private final LoadBalancerRegistry lbRegistry; // Following fields are effectively final. - private ObjectPool xdsClientPool; - private XdsClient xdsClient; - private CdsLbState cdsLbState; - private ResolvedAddresses resolvedAddresses; + private String clusterName; + private Subscription clusterSubscription; + private LoadBalancer childLb; CdsLoadBalancer2(Helper helper) { this(helper, LoadBalancerRegistry.getDefaultRegistry()); @@ -75,7 +67,6 @@ final class CdsLoadBalancer2 extends LoadBalancer { @VisibleForTesting CdsLoadBalancer2(Helper helper, LoadBalancerRegistry lbRegistry) { this.helper = checkNotNull(helper, "helper"); - this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); this.lbRegistry = checkNotNull(lbRegistry, "lbRegistry"); logger = XdsLogger.withLogId(InternalLogId.allocate("cds-lb", helper.getAuthority())); logger.log(XdsLogLevel.INFO, "Created"); @@ -83,25 +74,115 @@ final class CdsLoadBalancer2 extends LoadBalancer { @Override public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - if (this.resolvedAddresses != null) { + logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses); + if (this.clusterName == null) { + CdsConfig config = (CdsConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); + logger.log(XdsLogLevel.INFO, "Config: {0}", config); + if (config.isDynamic) { + clusterSubscription = resolvedAddresses.getAttributes() + .get(XdsAttributes.XDS_CLUSTER_SUBSCRIPT_REGISTRY) + .subscribeToCluster(config.name); + } + this.clusterName = config.name; + } + XdsConfig xdsConfig = resolvedAddresses.getAttributes().get(XdsAttributes.XDS_CONFIG); + StatusOr clusterConfigOr = xdsConfig.getClusters().get(clusterName); + if (clusterConfigOr == null) { + if (clusterSubscription == null) { + // Should be impossible, because XdsDependencyManager wouldn't have generated this + return fail(Status.INTERNAL.withDescription( + errorPrefix() + "Unable to find non-dynamic root cluster")); + } + // The dynamic cluster must not have loaded yet return Status.OK; } - logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses); - this.resolvedAddresses = resolvedAddresses; - xdsClientPool = resolvedAddresses.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL); - xdsClient = xdsClientPool.getObject(); - CdsConfig config = (CdsConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); - logger.log(XdsLogLevel.INFO, "Config: {0}", config); - cdsLbState = new CdsLbState(config.name); - cdsLbState.start(); - return Status.OK; + if (!clusterConfigOr.hasValue()) { + return fail(clusterConfigOr.getStatus()); + } + XdsClusterConfig clusterConfig = clusterConfigOr.getValue(); + List leafNames; + if (clusterConfig.getChildren() instanceof AggregateConfig) { + leafNames = ((AggregateConfig) clusterConfig.getChildren()).getLeafNames(); + } else if (clusterConfig.getChildren() instanceof EndpointConfig) { + leafNames = ImmutableList.of(clusterName); + } else { + return fail(Status.INTERNAL.withDescription( + errorPrefix() + "Unexpected cluster children type: " + + clusterConfig.getChildren().getClass())); + } + if (leafNames.isEmpty()) { + // Should be impossible, because XdsClusterResource validated this + return fail(Status.UNAVAILABLE.withDescription( + errorPrefix() + "Zero leaf clusters for root cluster " + clusterName)); + } + + Status noneFoundError = Status.INTERNAL + .withDescription(errorPrefix() + "No leaves and no error; this is a bug"); + List instances = new ArrayList<>(); + for (String leafName : leafNames) { + StatusOr leafConfigOr = xdsConfig.getClusters().get(leafName); + if (!leafConfigOr.hasValue()) { + noneFoundError = leafConfigOr.getStatus(); + continue; + } + if (!(leafConfigOr.getValue().getChildren() instanceof EndpointConfig)) { + noneFoundError = Status.INTERNAL.withDescription( + errorPrefix() + "Unexpected child " + leafName + " cluster children type: " + + leafConfigOr.getValue().getChildren().getClass()); + continue; + } + CdsUpdate result = leafConfigOr.getValue().getClusterResource(); + DiscoveryMechanism instance; + if (result.clusterType() == ClusterType.EDS) { + instance = DiscoveryMechanism.forEds( + leafName, + result.edsServiceName(), + result.lrsServerInfo(), + result.maxConcurrentRequests(), + result.upstreamTlsContext(), + result.filterMetadata(), + result.outlierDetection()); + } else { + instance = DiscoveryMechanism.forLogicalDns( + leafName, + result.dnsHostName(), + result.lrsServerInfo(), + result.maxConcurrentRequests(), + result.upstreamTlsContext(), + result.filterMetadata()); + } + instances.add(instance); + } + if (instances.isEmpty()) { + return fail(noneFoundError); + } + + // The LB policy config is provided in service_config.proto/JSON format. + NameResolver.ConfigOrError configOrError = + GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig( + Arrays.asList(clusterConfig.getClusterResource().lbPolicyConfig()), lbRegistry); + if (configOrError.getError() != null) { + // Should be impossible, because XdsClusterResource validated this + return fail(Status.INTERNAL.withDescription( + errorPrefix() + "Unable to parse the LB config: " + configOrError.getError())); + } + + ClusterResolverConfig config = new ClusterResolverConfig( + Collections.unmodifiableList(instances), + configOrError.getConfig(), + clusterConfig.getClusterResource().isHttp11ProxyAvailable()); + if (childLb == null) { + childLb = lbRegistry.getProvider(CLUSTER_RESOLVER_POLICY_NAME).newLoadBalancer(helper); + } + return childLb.acceptResolvedAddresses( + resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(config).build()); } @Override public void handleNameResolutionError(Status error) { logger.log(XdsLogLevel.WARNING, "Received name resolution error: {0}", error); - if (cdsLbState != null && cdsLbState.childLb != null) { - cdsLbState.childLb.handleNameResolutionError(error); + if (childLb != null) { + childLb.handleNameResolutionError(error); } else { helper.updateBalancingState( TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error))); @@ -111,314 +192,28 @@ public void handleNameResolutionError(Status error) { @Override public void shutdown() { logger.log(XdsLogLevel.INFO, "Shutdown"); - if (cdsLbState != null) { - cdsLbState.shutdown(); + if (childLb != null) { + childLb.shutdown(); + childLb = null; } - if (xdsClientPool != null) { - xdsClientPool.returnObject(xdsClient); + if (clusterSubscription != null) { + clusterSubscription.close(); + clusterSubscription = null; } } - /** - * The state of a CDS working session of {@link CdsLoadBalancer2}. Created and started when - * receiving the CDS LB policy config with the top-level cluster name. - */ - private final class CdsLbState { - - private final ClusterState root; - private final Map clusterStates = new ConcurrentHashMap<>(); - private LoadBalancer childLb; - - private CdsLbState(String rootCluster) { - root = new ClusterState(rootCluster); - } - - private void start() { - root.start(); - } - - private void shutdown() { - root.shutdown(); - if (childLb != null) { - childLb.shutdown(); - } + @CheckReturnValue // don't forget to return up the stack after the fail call + private Status fail(Status error) { + if (childLb != null) { + childLb.shutdown(); + childLb = null; } + helper.updateBalancingState( + TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error))); + return Status.OK; // XdsNameResolver isn't a polling NR, so this value doesn't matter + } - private void handleClusterDiscovered() { - List instances = new ArrayList<>(); - - // Used for loop detection to break the infinite recursion that loops would cause - Map> parentClusters = new HashMap<>(); - Status loopStatus = null; - - // Level-order traversal. - // Collect configurations for all non-aggregate (leaf) clusters. - Queue queue = new ArrayDeque<>(); - queue.add(root); - while (!queue.isEmpty()) { - int size = queue.size(); - for (int i = 0; i < size; i++) { - ClusterState clusterState = queue.remove(); - if (!clusterState.discovered) { - return; // do not proceed until all clusters discovered - } - if (clusterState.result == null) { // resource revoked or not exists - continue; - } - if (clusterState.isLeaf) { - if (instances.stream().map(inst -> inst.cluster).noneMatch(clusterState.name::equals)) { - DiscoveryMechanism instance; - if (clusterState.result.clusterType() == ClusterType.EDS) { - instance = DiscoveryMechanism.forEds( - clusterState.name, clusterState.result.edsServiceName(), - clusterState.result.lrsServerInfo(), - clusterState.result.maxConcurrentRequests(), - clusterState.result.upstreamTlsContext(), - clusterState.result.filterMetadata(), - clusterState.result.outlierDetection()); - } else { // logical DNS - instance = DiscoveryMechanism.forLogicalDns( - clusterState.name, clusterState.result.dnsHostName(), - clusterState.result.lrsServerInfo(), - clusterState.result.maxConcurrentRequests(), - clusterState.result.upstreamTlsContext(), - clusterState.result.filterMetadata()); - } - instances.add(instance); - } - } else { - if (clusterState.childClusterStates == null) { - continue; - } - // Do loop detection and break recursion if detected - List namesCausingLoops = identifyLoops(clusterState, parentClusters); - if (namesCausingLoops.isEmpty()) { - queue.addAll(clusterState.childClusterStates.values()); - } else { - // Do cleanup - if (childLb != null) { - childLb.shutdown(); - childLb = null; - } - if (loopStatus != null) { - logger.log(XdsLogLevel.WARNING, - "Multiple loops in CDS config. Old msg: " + loopStatus.getDescription()); - } - loopStatus = Status.UNAVAILABLE.withDescription(String.format( - "CDS error: circular aggregate clusters directly under %s for " - + "root cluster %s, named %s, xDS node ID: %s", - clusterState.name, root.name, namesCausingLoops, - xdsClient.getBootstrapInfo().node().getId())); - } - } - } - } - - if (loopStatus != null) { - helper.updateBalancingState( - TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(loopStatus))); - return; - } - - if (instances.isEmpty()) { // none of non-aggregate clusters exists - if (childLb != null) { - childLb.shutdown(); - childLb = null; - } - Status unavailable = Status.UNAVAILABLE.withDescription(String.format( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster %s" - + " xDS node ID: %s", root.name, xdsClient.getBootstrapInfo().node().getId())); - helper.updateBalancingState( - TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(unavailable))); - return; - } - - // The LB policy config is provided in service_config.proto/JSON format. - NameResolver.ConfigOrError configOrError = - GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig( - Arrays.asList(root.result.lbPolicyConfig()), lbRegistry); - if (configOrError.getError() != null) { - throw configOrError.getError().augmentDescription("Unable to parse the LB config") - .asRuntimeException(); - } - - ClusterResolverConfig config = new ClusterResolverConfig( - Collections.unmodifiableList(instances), - configOrError.getConfig(), - root.result.isHttp11ProxyAvailable()); - if (childLb == null) { - childLb = lbRegistry.getProvider(CLUSTER_RESOLVER_POLICY_NAME).newLoadBalancer(helper); - } - childLb.handleResolvedAddresses( - resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(config).build()); - } - - /** - * Returns children that would cause loops and builds up the parentClusters map. - **/ - - private List identifyLoops(ClusterState clusterState, - Map> parentClusters) { - Set ancestors = new HashSet<>(); - ancestors.add(clusterState.name); - addAncestors(ancestors, clusterState, parentClusters); - - List namesCausingLoops = new ArrayList<>(); - for (ClusterState state : clusterState.childClusterStates.values()) { - if (ancestors.contains(state.name)) { - namesCausingLoops.add(state.name); - } - } - - // Update parent map with entries from remaining children to clusterState - clusterState.childClusterStates.values().stream() - .filter(child -> !namesCausingLoops.contains(child.name)) - .forEach( - child -> parentClusters.computeIfAbsent(child, k -> new ArrayList<>()) - .add(clusterState)); - - return namesCausingLoops; - } - - /** Recursively add all parents to the ancestors list. **/ - private void addAncestors(Set ancestors, ClusterState clusterState, - Map> parentClusters) { - List directParents = parentClusters.get(clusterState); - if (directParents != null) { - directParents.stream().map(c -> c.name).forEach(ancestors::add); - directParents.forEach(p -> addAncestors(ancestors, p, parentClusters)); - } - } - - private void handleClusterDiscoveryError(Status error) { - String description = error.getDescription() == null ? "" : error.getDescription() + " "; - Status errorWithNodeId = error.withDescription( - description + "xDS node ID: " + xdsClient.getBootstrapInfo().node().getId()); - if (childLb != null) { - childLb.handleNameResolutionError(errorWithNodeId); - } else { - helper.updateBalancingState( - TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(errorWithNodeId))); - } - } - - private final class ClusterState implements ResourceWatcher { - private final String name; - @Nullable - private Map childClusterStates; - @Nullable - private CdsUpdate result; - // Following fields are effectively final. - private boolean isLeaf; - private boolean discovered; - private boolean shutdown; - - private ClusterState(String name) { - this.name = name; - } - - private void start() { - shutdown = false; - xdsClient.watchXdsResource(XdsClusterResource.getInstance(), name, this, syncContext); - } - - void shutdown() { - shutdown = true; - xdsClient.cancelXdsResourceWatch(XdsClusterResource.getInstance(), name, this); - if (childClusterStates != null) { - // recursively shut down all descendants - childClusterStates.values().stream() - .filter(state -> !state.shutdown) - .forEach(ClusterState::shutdown); - } - } - - @Override - public void onError(Status error) { - Status status = Status.UNAVAILABLE - .withDescription( - String.format("Unable to load CDS %s. xDS server returned: %s: %s", - name, error.getCode(), error.getDescription())) - .withCause(error.getCause()); - if (shutdown) { - return; - } - // All watchers should receive the same error, so we only propagate it once. - if (ClusterState.this == root) { - handleClusterDiscoveryError(status); - } - } - - @Override - public void onResourceDoesNotExist(String resourceName) { - if (shutdown) { - return; - } - discovered = true; - result = null; - if (childClusterStates != null) { - for (ClusterState state : childClusterStates.values()) { - state.shutdown(); - } - childClusterStates = null; - } - handleClusterDiscovered(); - } - - @Override - public void onChanged(final CdsUpdate update) { - if (shutdown) { - return; - } - logger.log(XdsLogLevel.DEBUG, "Received cluster update {0}", update); - discovered = true; - result = update; - if (update.clusterType() == ClusterType.AGGREGATE) { - isLeaf = false; - logger.log(XdsLogLevel.INFO, "Aggregate cluster {0}, underlying clusters: {1}", - update.clusterName(), update.prioritizedClusterNames()); - Map newChildStates = new LinkedHashMap<>(); - for (String cluster : update.prioritizedClusterNames()) { - if (newChildStates.containsKey(cluster)) { - logger.log(XdsLogLevel.WARNING, - String.format("duplicate cluster name %s in aggregate %s is being ignored", - cluster, update.clusterName())); - continue; - } - if (childClusterStates == null || !childClusterStates.containsKey(cluster)) { - ClusterState childState; - if (clusterStates.containsKey(cluster)) { - childState = clusterStates.get(cluster); - if (childState.shutdown) { - childState.start(); - } - } else { - childState = new ClusterState(cluster); - clusterStates.put(cluster, childState); - childState.start(); - } - newChildStates.put(cluster, childState); - } else { - newChildStates.put(cluster, childClusterStates.remove(cluster)); - } - } - if (childClusterStates != null) { // stop subscribing to revoked child clusters - for (ClusterState watcher : childClusterStates.values()) { - watcher.shutdown(); - } - } - childClusterStates = newChildStates; - } else if (update.clusterType() == ClusterType.EDS) { - isLeaf = true; - logger.log(XdsLogLevel.INFO, "EDS cluster {0}, edsServiceName: {1}", - update.clusterName(), update.edsServiceName()); - } else { // logical DNS - isLeaf = true; - logger.log(XdsLogLevel.INFO, "Logical DNS cluster {0}", update.clusterName()); - } - handleClusterDiscovered(); - } - - } + private String errorPrefix() { + return "CdsLb for " + clusterName + ": "; } } diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java index 01bd2ab27f6..9b242822f6c 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java @@ -36,8 +36,6 @@ @Internal public class CdsLoadBalancerProvider extends LoadBalancerProvider { - private static final String CLUSTER_KEY = "cluster"; - @Override public boolean isAvailable() { return true; @@ -70,9 +68,12 @@ public ConfigOrError parseLoadBalancingPolicyConfig( */ static ConfigOrError parseLoadBalancingConfigPolicy(Map rawLoadBalancingPolicyConfig) { try { - String cluster = - JsonUtil.getString(rawLoadBalancingPolicyConfig, CLUSTER_KEY); - return ConfigOrError.fromConfig(new CdsConfig(cluster)); + String cluster = JsonUtil.getString(rawLoadBalancingPolicyConfig, "cluster"); + Boolean isDynamic = JsonUtil.getBoolean(rawLoadBalancingPolicyConfig, "is_dynamic"); + if (isDynamic == null) { + isDynamic = Boolean.FALSE; + } + return ConfigOrError.fromConfig(new CdsConfig(cluster, isDynamic)); } catch (RuntimeException e) { return ConfigOrError.fromError( Status.UNAVAILABLE.withCause(e).withDescription( @@ -89,15 +90,28 @@ static final class CdsConfig { * Name of cluster to query CDS for. */ final String name; + /** + * Whether this cluster was dynamically chosen, so the XdsDependencyManager may be unaware of + * it without an explicit cluster subscription. + */ + final boolean isDynamic; CdsConfig(String name) { + this(name, false); + } + + CdsConfig(String name, boolean isDynamic) { checkArgument(name != null && !name.isEmpty(), "name is null or empty"); this.name = name; + this.isDynamic = isDynamic; } @Override public String toString() { - return MoreObjects.toStringHelper(this).add("name", name).toString(); + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("isDynamic", isDynamic) + .toString(); } } } diff --git a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancerProvider.java index 035ff76c585..bb4f8de5a5f 100644 --- a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancerProvider.java @@ -104,7 +104,7 @@ private ConfigOrError parseLoadBalancingPolicyConfigInternal( } if (minRingSize <= 0 || maxRingSize <= 0 || minRingSize > maxRingSize) { return ConfigOrError.fromError(Status.UNAVAILABLE.withDescription( - "Invalid 'mingRingSize'/'maxRingSize'")); + "Invalid 'minRingSize'/'maxRingSize'")); } return ConfigOrError.fromConfig( new RingHashConfig(minRingSize, maxRingSize, requestHashHeader)); diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index 3f2b2d8fd7e..a5220515b6c 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -172,7 +172,9 @@ static CdsUpdate processCluster(Cluster cluster, lbConfig.getPolicyName()).parseLoadBalancingPolicyConfig( lbConfig.getRawConfigValue()); if (configOrError.getError() != null) { - throw new ResourceInvalidException(structOrError.getErrorDetail()); + throw new ResourceInvalidException( + "Failed to parse lb config for cluster '" + cluster.getName() + "': " + + configOrError.getError()); } updateBuilder.lbPolicyConfig(lbPolicyConfig); @@ -209,6 +211,10 @@ private static StructOrError parseAggregateCluster(Cluster cl } catch (InvalidProtocolBufferException e) { return StructOrError.fromError("Cluster " + clusterName + ": malformed ClusterConfig: " + e); } + if (clusterConfig.getClustersList().isEmpty()) { + return StructOrError.fromError("Cluster " + clusterName + + ": aggregate ClusterConfig.clusters must not be empty"); + } return StructOrError.fromStruct(CdsUpdate.forAggregate( clusterName, clusterConfig.getClustersList())); } diff --git a/xds/src/main/java/io/grpc/xds/XdsConfig.java b/xds/src/main/java/io/grpc/xds/XdsConfig.java index 1f464aa1321..d184f08de55 100644 --- a/xds/src/main/java/io/grpc/xds/XdsConfig.java +++ b/xds/src/main/java/io/grpc/xds/XdsConfig.java @@ -254,6 +254,12 @@ XdsConfig build() { } public interface XdsClusterSubscriptionRegistry { - Closeable subscribeToCluster(String clusterName); + Subscription subscribeToCluster(String clusterName); + } + + public interface Subscription extends Closeable { + /** Release resources without throwing exceptions. */ + @Override + void close(); } } diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index 45ae7074d16..0428852648d 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import static io.grpc.xds.client.XdsClient.ResourceUpdate; import com.google.common.annotations.VisibleForTesting; @@ -34,8 +35,6 @@ import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsClient.ResourceWatcher; import io.grpc.xds.client.XdsResourceType; -import java.io.Closeable; -import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -56,39 +55,43 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegistry { public static final XdsClusterResource CLUSTER_RESOURCE = XdsClusterResource.getInstance(); public static final XdsEndpointResource ENDPOINT_RESOURCE = XdsEndpointResource.getInstance(); - private static final int MAX_CLUSTER_RECURSION_DEPTH = 16; // Matches C++ + private static final int MAX_CLUSTER_RECURSION_DEPTH = 16; // Specified by gRFC A37 private final String listenerName; private final XdsClient xdsClient; - private final XdsConfigWatcher xdsConfigWatcher; private final SynchronizationContext syncContext; private final String dataPlaneAuthority; + private XdsConfigWatcher xdsConfigWatcher; private StatusOr lastUpdate = null; private final Map, TypeWatchers> resourceWatchers = new HashMap<>(); private final Set subscriptions = new HashSet<>(); - XdsDependencyManager(XdsClient xdsClient, XdsConfigWatcher xdsConfigWatcher, + XdsDependencyManager(XdsClient xdsClient, SynchronizationContext syncContext, String dataPlaneAuthority, String listenerName, NameResolver.Args nameResolverArgs, ScheduledExecutorService scheduler) { this.listenerName = checkNotNull(listenerName, "listenerName"); this.xdsClient = checkNotNull(xdsClient, "xdsClient"); - this.xdsConfigWatcher = checkNotNull(xdsConfigWatcher, "xdsConfigWatcher"); this.syncContext = checkNotNull(syncContext, "syncContext"); this.dataPlaneAuthority = checkNotNull(dataPlaneAuthority, "dataPlaneAuthority"); checkNotNull(nameResolverArgs, "nameResolverArgs"); checkNotNull(scheduler, "scheduler"); - - // start the ball rolling - syncContext.execute(() -> addWatcher(new LdsWatcher(listenerName))); } public static String toContextStr(String typeName, String resourceName) { return typeName + " resource " + resourceName; } + public void start(XdsConfigWatcher xdsConfigWatcher) { + checkState(this.xdsConfigWatcher == null, "dep manager may not be restarted"); + this.xdsConfigWatcher = checkNotNull(xdsConfigWatcher, "xdsConfigWatcher"); + // start the ball rolling + syncContext.execute(() -> addWatcher(new LdsWatcher(listenerName))); + } + @Override - public Closeable subscribeToCluster(String clusterName) { + public XdsConfig.Subscription subscribeToCluster(String clusterName) { + checkState(this.xdsConfigWatcher != null, "dep manager must first be started"); checkNotNull(clusterName, "clusterName"); ClusterSubscription subscription = new ClusterSubscription(clusterName); @@ -291,10 +294,17 @@ private static void addConfigForCluster( addConfigForCluster(clusters, childCluster, ancestors, tracer); StatusOr config = clusters.get(childCluster); if (!config.hasValue()) { - clusters.put(clusterName, StatusOr.fromStatus(Status.INTERNAL.withDescription( - "Unable to get leaves for " + clusterName + ": " - + config.getStatus().getDescription()))); - return; + // gRFC A37 says: If any of a CDS policy's watchers reports that the resource does not + // exist the policy should report that it is in TRANSIENT_FAILURE. If any of the + // watchers reports a transient ADS stream error, the policy should report that it is in + // TRANSIENT_FAILURE if it has never passed a config to its child. + // + // But there's currently disagreement about whether that is actually what we want, and + // that was not originally implemented in gRPC Java. So we're keeping Java's old + // behavior for now and only failing the "leaves" (which is a bit arbitrary for a + // cycle). + leafNames.add(childCluster); + continue; } XdsConfig.XdsClusterConfig.ClusterChild children = config.getValue().getChildren(); if (children instanceof AggregateConfig) { @@ -325,6 +335,11 @@ private static void addConfigForCluster( default: throw new IllegalStateException("Unexpected value: " + cdsUpdate.clusterType()); } + if (clusters.containsKey(clusterName)) { + // If a cycle is detected, we'll have detected it while recursing, so now there will be a key + // present. We don't want to overwrite it with a non-error value. + return; + } clusters.put(clusterName, StatusOr.fromValue( new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, child))); } @@ -406,7 +421,7 @@ public interface XdsConfigWatcher { void onUpdate(StatusOr config); } - private final class ClusterSubscription implements Closeable { + private final class ClusterSubscription implements XdsConfig.Subscription { private final String clusterName; boolean closed; // Accessed from syncContext @@ -419,7 +434,7 @@ String getClusterName() { } @Override - public void close() throws IOException { + public void close() { releaseSubscription(this); } } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index a14abf95f41..37a8e19ef3f 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -230,7 +230,8 @@ public void start(Listener2 listener) { ldsResourceName = XdsClient.canonifyResourceName(ldsResourceName); callCounterProvider = SharedCallCounterMap.getInstance(); - resolveState = new ResolveState(ldsResourceName); // auto starts + resolveState = new ResolveState(ldsResourceName); + resolveState.start(); } private static String expandPercentS(String template, String replacement) { @@ -653,10 +654,14 @@ class ResolveState implements XdsDependencyManager.XdsConfigWatcher { private ResolveState(String ldsResourceName) { authority = overrideAuthority != null ? overrideAuthority : encodedServiceAuthority; xdsDependencyManager = - new XdsDependencyManager(xdsClient, this, syncContext, authority, ldsResourceName, + new XdsDependencyManager(xdsClient, syncContext, authority, ldsResourceName, nameResolverArgs, scheduler); } + void start() { + xdsDependencyManager.start(this); + } + private void shutdown() { if (stopped) { return; diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java index 479bde76ce5..f9a09f704a7 100644 --- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java +++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java @@ -18,28 +18,50 @@ import static com.google.common.truth.Truth.assertThat; import static io.grpc.xds.XdsLbPolicies.CLUSTER_RESOLVER_POLICY_NAME; -import static org.junit.Assert.fail; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_EDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_LDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_RDS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import com.google.common.collect.ImmutableList; +import com.github.xds.type.v3.TypedStruct; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.protobuf.Any; +import com.google.protobuf.Struct; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.Value; +import io.envoyproxy.envoy.config.cluster.v3.CircuitBreakers; +import io.envoyproxy.envoy.config.cluster.v3.Cluster; +import io.envoyproxy.envoy.config.cluster.v3.LoadBalancingPolicy; +import io.envoyproxy.envoy.config.cluster.v3.LoadBalancingPolicy.Policy; +import io.envoyproxy.envoy.config.cluster.v3.OutlierDetection; +import io.envoyproxy.envoy.config.core.v3.Address; +import io.envoyproxy.envoy.config.core.v3.AggregatedConfigSource; +import io.envoyproxy.envoy.config.core.v3.ConfigSource; +import io.envoyproxy.envoy.config.core.v3.RoutingPriority; +import io.envoyproxy.envoy.config.core.v3.SelfConfigSource; +import io.envoyproxy.envoy.config.core.v3.SocketAddress; +import io.envoyproxy.envoy.config.core.v3.TransportSocket; +import io.envoyproxy.envoy.config.core.v3.TypedExtensionConfig; +import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; +import io.envoyproxy.envoy.config.endpoint.v3.Endpoint; +import io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint; +import io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints; +import io.envoyproxy.envoy.extensions.clusters.aggregate.v3.ClusterConfig; +import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext; import io.grpc.Attributes; +import io.grpc.ChannelLogger; import io.grpc.ConnectivityState; -import io.grpc.EquivalentAddressGroup; -import io.grpc.InsecureChannelCredentials; import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancer.PickResult; import io.grpc.LoadBalancer.PickSubchannelArgs; import io.grpc.LoadBalancer.ResolvedAddresses; -import io.grpc.LoadBalancer.Subchannel; import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.LoadBalancerProvider; import io.grpc.LoadBalancerRegistry; @@ -47,31 +69,25 @@ import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.SynchronizationContext; -import io.grpc.internal.ObjectPool; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.internal.FakeClock; +import io.grpc.testing.GrpcCleanupRule; import io.grpc.util.GracefulSwitchLoadBalancerAccessor; import io.grpc.xds.CdsLoadBalancerProvider.CdsConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; -import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; +import io.grpc.xds.EnvoyServerProtoData.FailurePercentageEjection; import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection; -import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; -import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; -import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; -import io.grpc.xds.XdsClusterResource.CdsUpdate; -import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.Bootstrapper.ServerInfo; -import io.grpc.xds.client.EnvoyProtoData; import io.grpc.xds.client.XdsClient; -import io.grpc.xds.client.XdsResourceType; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.Executor; -import javax.annotation.Nullable; +import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -90,601 +106,446 @@ @RunWith(JUnit4.class) public class CdsLoadBalancer2Test { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + @Rule + public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule(); + private static final String SERVER_NAME = "example.com"; private static final String CLUSTER = "cluster-foo.googleapis.com"; private static final String EDS_SERVICE_NAME = "backend-service-1.googleapis.com"; - private static final String DNS_HOST_NAME = "backend-service-dns.googleapis.com:443"; - private static final ServerInfo LRS_SERVER_INFO = - ServerInfo.create("lrs.googleapis.com", InsecureChannelCredentials.create()); - private static final String SERVER_URI = "trafficdirector.googleapis.com"; - private static final String NODE_ID = - "projects/42/networks/default/nodes/5c85b298-6f5b-4722-b74a-f7d1f0ccf5ad"; - private static final EnvoyProtoData.Node BOOTSTRAP_NODE = - EnvoyProtoData.Node.newBuilder().setId(NODE_ID).build(); - private static final BootstrapInfo BOOTSTRAP_INFO = BootstrapInfo.builder() - .servers(ImmutableList.of( - ServerInfo.create(SERVER_URI, InsecureChannelCredentials.create()))) - .node(BOOTSTRAP_NODE) + private static final String NODE_ID = "node-id"; + private final io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext upstreamTlsContext = + CommonTlsContextTestsUtil.buildUpstreamTlsContext("cert-instance-name", true); + private static final Cluster EDS_CLUSTER = Cluster.newBuilder() + .setName(CLUSTER) + .setType(Cluster.DiscoveryType.EDS) + .setEdsClusterConfig(Cluster.EdsClusterConfig.newBuilder() + .setServiceName(EDS_SERVICE_NAME) + .setEdsConfig(ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.newBuilder()))) .build(); - private final UpstreamTlsContext upstreamTlsContext = - CommonTlsContextTestsUtil.buildUpstreamTlsContext("google_cloud_private_spiffe", true); - private final OutlierDetection outlierDetection = OutlierDetection.create( - null, null, null, null, SuccessRateEjection.create(null, null, null, null), null); - - - private static final SynchronizationContext syncContext = new SynchronizationContext( - new Thread.UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread t, Throwable e) { - throw new RuntimeException(e); - //throw new AssertionError(e); - } - }); + + private final FakeClock fakeClock = new FakeClock(); private final LoadBalancerRegistry lbRegistry = new LoadBalancerRegistry(); private final List childBalancers = new ArrayList<>(); - private final FakeXdsClient xdsClient = new FakeXdsClient(); - private final ObjectPool xdsClientPool = new ObjectPool() { - @Override - public XdsClient getObject() { - xdsClientRefs++; - return xdsClient; - } - - @Override - public XdsClient returnObject(Object object) { - xdsClientRefs--; - return null; - } - }; + private final XdsTestControlPlaneService controlPlaneService = new XdsTestControlPlaneService(); + private final XdsClient xdsClient = XdsTestUtils.createXdsClient( + Arrays.asList("control-plane.example.com"), + serverInfo -> new GrpcXdsTransportFactory.GrpcXdsTransport( + InProcessChannelBuilder + .forName(serverInfo.target()) + .directExecutor() + .build()), + fakeClock); + private final ServerInfo lrsServerInfo = xdsClient.getBootstrapInfo().servers().get(0); + private XdsDependencyManager xdsDepManager; @Mock private Helper helper; @Captor private ArgumentCaptor pickerCaptor; - private int xdsClientRefs; - private CdsLoadBalancer2 loadBalancer; + private CdsLoadBalancer2 loadBalancer; + private XdsConfig lastXdsConfig; @Before - public void setUp() { - when(helper.getSynchronizationContext()).thenReturn(syncContext); + public void setUp() throws Exception { lbRegistry.register(new FakeLoadBalancerProvider(CLUSTER_RESOLVER_POLICY_NAME)); lbRegistry.register(new FakeLoadBalancerProvider("round_robin")); lbRegistry.register( new FakeLoadBalancerProvider("ring_hash_experimental", new RingHashLoadBalancerProvider())); lbRegistry.register(new FakeLoadBalancerProvider("least_request_experimental", new LeastRequestLoadBalancerProvider())); + lbRegistry.register(new FakeLoadBalancerProvider("wrr_locality_experimental", + new WrrLocalityLoadBalancerProvider())); loadBalancer = new CdsLoadBalancer2(helper, lbRegistry); - loadBalancer.acceptResolvedAddresses( - ResolvedAddresses.newBuilder() - .setAddresses(Collections.emptyList()) - .setAttributes( - // Other attributes not used by cluster_resolver LB are omitted. - Attributes.newBuilder() - .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) - .build()) - .setLoadBalancingPolicyConfig(new CdsConfig(CLUSTER)) - .build()); - assertThat(Iterables.getOnlyElement(xdsClient.watchers.keySet())).isEqualTo(CLUSTER); + + cleanupRule.register(InProcessServerBuilder + .forName("control-plane.example.com") + .addService(controlPlaneService) + .directExecutor() + .build() + .start()); + + SynchronizationContext syncContext = new SynchronizationContext((t, e) -> { + throw new AssertionError(e); + }); + + NameResolver.Args nameResolverArgs = NameResolver.Args.newBuilder() + .setDefaultPort(8080) + .setProxyDetector((address) -> null) + .setSynchronizationContext(syncContext) + .setServiceConfigParser(mock(NameResolver.ServiceConfigParser.class)) + .setChannelLogger(mock(ChannelLogger.class)) + .setScheduledExecutorService(fakeClock.getScheduledExecutorService()) + .build(); + + xdsDepManager = new XdsDependencyManager( + xdsClient, + syncContext, + SERVER_NAME, + SERVER_NAME, + nameResolverArgs, + fakeClock.getScheduledExecutorService()); + + controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, ImmutableMap.of( + SERVER_NAME, ControlPlaneRule.buildClientListener(SERVER_NAME, "my-route"))); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_RDS, ImmutableMap.of( + "my-route", XdsTestUtils.buildRouteConfiguration(SERVER_NAME, "my-route", CLUSTER))); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, ControlPlaneRule.buildClusterLoadAssignment( + "127.0.0.1", "", 1234, EDS_SERVICE_NAME))); } @After public void tearDown() { - loadBalancer.shutdown(); - assertThat(xdsClient.watchers).isEmpty(); - assertThat(xdsClientRefs).isEqualTo(0); + if (loadBalancer != null) { + shutdownLoadBalancer(); + } assertThat(childBalancers).isEmpty(); + + if (xdsDepManager != null) { + xdsDepManager.shutdown(); + } + xdsClient.shutdown(); + } + + private void shutdownLoadBalancer() { + LoadBalancer lb = this.loadBalancer; + this.loadBalancer = null; // Must avoid calling acceptResolvedAddresses after shutdown + lb.shutdown(); } @Test public void discoverTopLevelEdsCluster() { - CdsUpdate update = - CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, - outlierDetection, false) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); + Cluster cluster = Cluster.newBuilder() + .setName(CLUSTER) + .setType(Cluster.DiscoveryType.EDS) + .setEdsClusterConfig(Cluster.EdsClusterConfig.newBuilder() + .setServiceName(EDS_SERVICE_NAME) + .setEdsConfig(ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.newBuilder()))) + .setLbPolicy(Cluster.LbPolicy.ROUND_ROBIN) + .setLrsServer(ConfigSource.newBuilder() + .setSelf(SelfConfigSource.getDefaultInstance())) + .setCircuitBreakers(CircuitBreakers.newBuilder() + .addThresholds(CircuitBreakers.Thresholds.newBuilder() + .setPriority(RoutingPriority.DEFAULT) + .setMaxRequests(UInt32Value.newBuilder().setValue(100)))) + .setTransportSocket(TransportSocket.newBuilder() + .setName("envoy.transport_sockets.tls") + .setTypedConfig(Any.pack(UpstreamTlsContext.newBuilder() + .setCommonTlsContext(upstreamTlsContext.getCommonTlsContext()) + .build()))) + .setOutlierDetection(OutlierDetection.getDefaultInstance()) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of(CLUSTER, cluster)); + startXdsDepManager(); + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.name).isEqualTo(CLUSTER_RESOLVER_POLICY_NAME); ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).hasSize(1); - DiscoveryMechanism instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); - assertDiscoveryMechanism(instance, CLUSTER, DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, - null, LRS_SERVER_INFO, 100L, upstreamTlsContext, outlierDetection); + assertThat(childLbConfig.discoveryMechanisms).isEqualTo( + Arrays.asList( + DiscoveryMechanism.forEds( + CLUSTER, EDS_SERVICE_NAME, lrsServerInfo, 100L, upstreamTlsContext, + Collections.emptyMap(), io.grpc.xds.EnvoyServerProtoData.OutlierDetection.create( + null, null, null, null, SuccessRateEjection.create(null, null, null, null), + FailurePercentageEjection.create(null, null, null, null))))); assertThat( GracefulSwitchLoadBalancerAccessor.getChildProvider(childLbConfig.lbConfig).getPolicyName()) - .isEqualTo("round_robin"); + .isEqualTo("wrr_locality_experimental"); } @Test public void discoverTopLevelLogicalDnsCluster() { - CdsUpdate update = - CdsUpdate.forLogicalDns(CLUSTER, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, - false) - .leastRequestLbPolicy(3).build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); + Cluster cluster = Cluster.newBuilder() + .setName(CLUSTER) + .setType(Cluster.DiscoveryType.LOGICAL_DNS) + .setLoadAssignment(ClusterLoadAssignment.newBuilder() + .addEndpoints(LocalityLbEndpoints.newBuilder() + .addLbEndpoints(LbEndpoint.newBuilder() + .setEndpoint(Endpoint.newBuilder() + .setAddress(Address.newBuilder() + .setSocketAddress(SocketAddress.newBuilder() + .setAddress("dns.example.com") + .setPortValue(1111))))))) + .setEdsClusterConfig(Cluster.EdsClusterConfig.newBuilder() + .setServiceName(EDS_SERVICE_NAME) + .setEdsConfig(ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.newBuilder()))) + .setLbPolicy(Cluster.LbPolicy.LEAST_REQUEST) + .setLrsServer(ConfigSource.newBuilder() + .setSelf(SelfConfigSource.getDefaultInstance())) + .setCircuitBreakers(CircuitBreakers.newBuilder() + .addThresholds(CircuitBreakers.Thresholds.newBuilder() + .setPriority(RoutingPriority.DEFAULT) + .setMaxRequests(UInt32Value.newBuilder().setValue(100)))) + .setTransportSocket(TransportSocket.newBuilder() + .setName("envoy.transport_sockets.tls") + .setTypedConfig(Any.pack(UpstreamTlsContext.newBuilder() + .setCommonTlsContext(upstreamTlsContext.getCommonTlsContext()) + .build()))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of(CLUSTER, cluster)); + startXdsDepManager(); + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.name).isEqualTo(CLUSTER_RESOLVER_POLICY_NAME); ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).hasSize(1); - DiscoveryMechanism instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); - assertDiscoveryMechanism(instance, CLUSTER, DiscoveryMechanism.Type.LOGICAL_DNS, null, - DNS_HOST_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, null); + assertThat(childLbConfig.discoveryMechanisms).isEqualTo( + Arrays.asList( + DiscoveryMechanism.forLogicalDns( + CLUSTER, "dns.example.com:1111", lrsServerInfo, 100L, upstreamTlsContext, + Collections.emptyMap()))); assertThat( GracefulSwitchLoadBalancerAccessor.getChildProvider(childLbConfig.lbConfig).getPolicyName()) - .isEqualTo("least_request_experimental"); - LeastRequestConfig lrConfig = (LeastRequestConfig) - GracefulSwitchLoadBalancerAccessor.getChildConfig(childLbConfig.lbConfig); - assertThat(lrConfig.choiceCount).isEqualTo(3); + .isEqualTo("wrr_locality_experimental"); } @Test public void nonAggregateCluster_resourceNotExist_returnErrorPicker() { - xdsClient.deliverResourceNotExist(CLUSTER); + startXdsDepManager(); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER - + " xDS node ID: " + NODE_ID); - assertPicker(pickerCaptor.getValue(), unavailable, null); + "CDS resource " + CLUSTER + " does not exist nodeID: " + NODE_ID); + assertPickerStatus(pickerCaptor.getValue(), unavailable); assertThat(childBalancers).isEmpty(); } @Test public void nonAggregateCluster_resourceUpdate() { - CdsUpdate update = - CdsUpdate.forEds(CLUSTER, null, null, 100L, upstreamTlsContext, outlierDetection, false) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); + Cluster cluster = EDS_CLUSTER.toBuilder() + .setCircuitBreakers(CircuitBreakers.newBuilder() + .addThresholds(CircuitBreakers.Thresholds.newBuilder() + .setPriority(RoutingPriority.DEFAULT) + .setMaxRequests(UInt32Value.newBuilder().setValue(100)))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of(CLUSTER, cluster)); + startXdsDepManager(); + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - DiscoveryMechanism instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); - assertDiscoveryMechanism(instance, CLUSTER, DiscoveryMechanism.Type.EDS, null, null, null, - 100L, upstreamTlsContext, outlierDetection); - - update = CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, null, - outlierDetection, false).roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); + assertThat(childLbConfig.discoveryMechanisms).isEqualTo( + Arrays.asList( + DiscoveryMechanism.forEds( + CLUSTER, EDS_SERVICE_NAME, null, 100L, null, Collections.emptyMap(), null))); + + cluster = EDS_CLUSTER.toBuilder() + .setCircuitBreakers(CircuitBreakers.newBuilder() + .addThresholds(CircuitBreakers.Thresholds.newBuilder() + .setPriority(RoutingPriority.DEFAULT) + .setMaxRequests(UInt32Value.newBuilder().setValue(200)))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of(CLUSTER, cluster)); + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); + assertThat(childBalancers).hasSize(1); + childBalancer = Iterables.getOnlyElement(childBalancers); childLbConfig = (ClusterResolverConfig) childBalancer.config; - instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); - assertDiscoveryMechanism(instance, CLUSTER, DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, - null, LRS_SERVER_INFO, 200L, null, outlierDetection); + assertThat(childLbConfig.discoveryMechanisms).isEqualTo( + Arrays.asList( + DiscoveryMechanism.forEds( + CLUSTER, EDS_SERVICE_NAME, null, 200L, null, Collections.emptyMap(), null))); } @Test public void nonAggregateCluster_resourceRevoked() { - CdsUpdate update = - CdsUpdate.forLogicalDns(CLUSTER, DNS_HOST_NAME, null, 100L, upstreamTlsContext, - false) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of(CLUSTER, EDS_CLUSTER)); + startXdsDepManager(); + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - DiscoveryMechanism instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); - assertDiscoveryMechanism(instance, CLUSTER, DiscoveryMechanism.Type.LOGICAL_DNS, null, - DNS_HOST_NAME, null, 100L, upstreamTlsContext, null); + assertThat(childLbConfig.discoveryMechanisms).isEqualTo( + Arrays.asList( + DiscoveryMechanism.forEds( + CLUSTER, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null))); + + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of()); - xdsClient.deliverResourceNotExist(CLUSTER); assertThat(childBalancer.shutdown).isTrue(); Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER - + " xDS node ID: " + NODE_ID); + "CDS resource " + CLUSTER + " does not exist nodeID: " + NODE_ID); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - assertPicker(pickerCaptor.getValue(), unavailable, null); + assertPickerStatus(pickerCaptor.getValue(), unavailable); assertThat(childBalancer.shutdown).isTrue(); assertThat(childBalancers).isEmpty(); } + @Test + public void dynamicCluster() { + String clusterName = "cluster2"; + Cluster cluster = EDS_CLUSTER.toBuilder() + .setName(clusterName) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + clusterName, cluster, + CLUSTER, Cluster.newBuilder().setName(CLUSTER).build())); + startXdsDepManager(new CdsConfig(clusterName, /*dynamic=*/ true)); + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); + assertThat(childBalancers).hasSize(1); + FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); + ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; + assertThat(childLbConfig.discoveryMechanisms).isEqualTo( + Arrays.asList( + DiscoveryMechanism.forEds( + clusterName, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null))); + + assertThat(this.lastXdsConfig.getClusters()).containsKey(clusterName); + shutdownLoadBalancer(); + assertThat(this.lastXdsConfig.getClusters()).doesNotContainKey(clusterName); + } + @Test public void discoverAggregateCluster() { String cluster1 = "cluster-01.googleapis.com"; String cluster2 = "cluster-02.googleapis.com"; - // CLUSTER (aggr.) -> [cluster1 (aggr.), cluster2 (logical DNS)] - CdsUpdate update = - CdsUpdate.forAggregate(CLUSTER, Arrays.asList(cluster1, cluster2)) - .ringHashLbPolicy(100L, 1000L).build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1, cluster2); - assertThat(childBalancers).isEmpty(); String cluster3 = "cluster-03.googleapis.com"; String cluster4 = "cluster-04.googleapis.com"; - // cluster1 (aggr.) -> [cluster3 (EDS), cluster4 (EDS)] - CdsUpdate update1 = - CdsUpdate.forAggregate(cluster1, Arrays.asList(cluster3, cluster4)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster1, update1); - assertThat(xdsClient.watchers.keySet()).containsExactly( - CLUSTER, cluster1, cluster2, cluster3, cluster4); - assertThat(childBalancers).isEmpty(); - CdsUpdate update3 = CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, - upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster3, update3); - assertThat(childBalancers).isEmpty(); - CdsUpdate update2 = - CdsUpdate.forLogicalDns(cluster2, DNS_HOST_NAME, null, 100L, null, false) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster2, update2); - assertThat(childBalancers).isEmpty(); - CdsUpdate update4 = - CdsUpdate.forEds(cluster4, null, LRS_SERVER_INFO, 300L, null, outlierDetection, false) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster4, update4); - assertThat(childBalancers).hasSize(1); // all non-aggregate clusters discovered + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + // CLUSTER (aggr.) -> [cluster1 (aggr.), cluster2 (logical DNS), cluster3 (EDS)] + CLUSTER, Cluster.newBuilder() + .setName(CLUSTER) + .setClusterType(Cluster.CustomClusterType.newBuilder() + .setName("envoy.clusters.aggregate") + .setTypedConfig(Any.pack(ClusterConfig.newBuilder() + .addClusters(cluster1) + .addClusters(cluster2) + .addClusters(cluster3) + .build()))) + .setLbPolicy(Cluster.LbPolicy.RING_HASH) + .build(), + // cluster1 (aggr.) -> [cluster3 (EDS), cluster4 (EDS)] + cluster1, Cluster.newBuilder() + .setName(cluster1) + .setClusterType(Cluster.CustomClusterType.newBuilder() + .setName("envoy.clusters.aggregate") + .setTypedConfig(Any.pack(ClusterConfig.newBuilder() + .addClusters(cluster3) + .addClusters(cluster4) + .build()))) + .build(), + cluster2, Cluster.newBuilder() + .setName(cluster2) + .setType(Cluster.DiscoveryType.LOGICAL_DNS) + .setLoadAssignment(ClusterLoadAssignment.newBuilder() + .addEndpoints(LocalityLbEndpoints.newBuilder() + .addLbEndpoints(LbEndpoint.newBuilder() + .setEndpoint(Endpoint.newBuilder() + .setAddress(Address.newBuilder() + .setSocketAddress(SocketAddress.newBuilder() + .setAddress("dns.example.com") + .setPortValue(1111))))))) + .build(), + cluster3, EDS_CLUSTER.toBuilder() + .setName(cluster3) + .setCircuitBreakers(CircuitBreakers.newBuilder() + .addThresholds(CircuitBreakers.Thresholds.newBuilder() + .setPriority(RoutingPriority.DEFAULT) + .setMaxRequests(UInt32Value.newBuilder().setValue(100)))) + .build(), + cluster4, EDS_CLUSTER.toBuilder().setName(cluster4).build())); + startXdsDepManager(); + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); + assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.name).isEqualTo(CLUSTER_RESOLVER_POLICY_NAME); ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).hasSize(3); - // Clusters on higher level has higher priority: [cluster2, cluster3, cluster4] - assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(0), cluster2, - DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, null, 100L, null, null); - assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(1), cluster3, - DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, null, LRS_SERVER_INFO, 200L, - upstreamTlsContext, outlierDetection); - assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(2), cluster4, - DiscoveryMechanism.Type.EDS, null, null, LRS_SERVER_INFO, 300L, null, outlierDetection); + // Clusters are resolved recursively, later duplicates removed: [cluster3, cluster4, cluster2] + assertThat(childLbConfig.discoveryMechanisms).isEqualTo( + Arrays.asList( + DiscoveryMechanism.forEds( + cluster3, EDS_SERVICE_NAME, null, 100L, null, Collections.emptyMap(), null), + DiscoveryMechanism.forEds( + cluster4, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null), + DiscoveryMechanism.forLogicalDns( + cluster2, "dns.example.com:1111", null, null, null, Collections.emptyMap()))); assertThat( GracefulSwitchLoadBalancerAccessor.getChildProvider(childLbConfig.lbConfig).getPolicyName()) .isEqualTo("ring_hash_experimental"); // dominated by top-level cluster's config - RingHashConfig ringHashConfig = (RingHashConfig) - GracefulSwitchLoadBalancerAccessor.getChildConfig(childLbConfig.lbConfig); - assertThat(ringHashConfig.minRingSize).isEqualTo(100L); - assertThat(ringHashConfig.maxRingSize).isEqualTo(1000L); - } - - @Test - public void aggregateCluster_noNonAggregateClusterExits_returnErrorPicker() { - String cluster1 = "cluster-01.googleapis.com"; - // CLUSTER (aggr.) -> [cluster1 (EDS)] - CdsUpdate update = - CdsUpdate.forAggregate(CLUSTER, Collections.singletonList(cluster1)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1); - xdsClient.deliverResourceNotExist(cluster1); - verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER - + " xDS node ID: " + NODE_ID); - assertPicker(pickerCaptor.getValue(), unavailable, null); - assertThat(childBalancers).isEmpty(); } @Test - public void aggregateCluster_descendantClustersRevoked() { - String cluster1 = "cluster-01.googleapis.com"; - String cluster2 = "cluster-02.googleapis.com"; - // CLUSTER (aggr.) -> [cluster1 (EDS), cluster2 (logical DNS)] - CdsUpdate update = - CdsUpdate.forAggregate(CLUSTER, Arrays.asList(cluster1, cluster2)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1, cluster2); - CdsUpdate update1 = CdsUpdate.forEds(cluster1, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, - upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster1, update1); - CdsUpdate update2 = - CdsUpdate.forLogicalDns(cluster2, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null, - false) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster2, update2); - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).hasSize(2); - assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(0), cluster1, - DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, null, LRS_SERVER_INFO, 200L, - upstreamTlsContext, outlierDetection); - assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(1), cluster2, - DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null, - null); - - // Revoke cluster1, should still be able to proceed with cluster2. - xdsClient.deliverResourceNotExist(cluster1); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1, cluster2); - childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).hasSize(1); - assertDiscoveryMechanism(Iterables.getOnlyElement(childLbConfig.discoveryMechanisms), cluster2, - DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null, - null); - verify(helper, never()).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), any(SubchannelPicker.class)); - - // All revoked. - xdsClient.deliverResourceNotExist(cluster2); - verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER - + " xDS node ID: " + NODE_ID); - assertPicker(pickerCaptor.getValue(), unavailable, null); - assertThat(childBalancer.shutdown).isTrue(); - assertThat(childBalancers).isEmpty(); - } - - @Test - public void aggregateCluster_rootClusterRevoked() { - String cluster1 = "cluster-01.googleapis.com"; - String cluster2 = "cluster-02.googleapis.com"; - // CLUSTER (aggr.) -> [cluster1 (EDS), cluster2 (logical DNS)] - CdsUpdate update = - CdsUpdate.forAggregate(CLUSTER, Arrays.asList(cluster1, cluster2)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1, cluster2); - CdsUpdate update1 = CdsUpdate.forEds(cluster1, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, - upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster1, update1); - CdsUpdate update2 = - CdsUpdate.forLogicalDns(cluster2, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null, - false) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster2, update2); - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).hasSize(2); - assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(0), cluster1, - DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, null, LRS_SERVER_INFO, 200L, - upstreamTlsContext, outlierDetection); - assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(1), cluster2, - DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null, - null); - - xdsClient.deliverResourceNotExist(CLUSTER); - assertThat(xdsClient.watchers.keySet()) - .containsExactly(CLUSTER); // subscription to all descendant clusters cancelled - verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER - + " xDS node ID: " + NODE_ID); - assertPicker(pickerCaptor.getValue(), unavailable, null); - assertThat(childBalancer.shutdown).isTrue(); - assertThat(childBalancers).isEmpty(); - } - - @Test - public void aggregateCluster_intermediateClusterChanges() { - String cluster1 = "cluster-01.googleapis.com"; - // CLUSTER (aggr.) -> [cluster1] - CdsUpdate update = - CdsUpdate.forAggregate(CLUSTER, Collections.singletonList(cluster1)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1); - - // CLUSTER (aggr.) -> [cluster2 (aggr.)] - String cluster2 = "cluster-02.googleapis.com"; - update = - CdsUpdate.forAggregate(CLUSTER, Collections.singletonList(cluster2)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster2); - - // cluster2 (aggr.) -> [cluster3 (EDS)] - String cluster3 = "cluster-03.googleapis.com"; - CdsUpdate update2 = - CdsUpdate.forAggregate(cluster2, Collections.singletonList(cluster3)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster2, update2); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster2, cluster3); - CdsUpdate update3 = CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, - upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster3, update3); - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).hasSize(1); - DiscoveryMechanism instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); - assertDiscoveryMechanism(instance, cluster3, DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, - null, LRS_SERVER_INFO, 100L, upstreamTlsContext, outlierDetection); - - // cluster2 revoked - xdsClient.deliverResourceNotExist(cluster2); - assertThat(xdsClient.watchers.keySet()) - .containsExactly(CLUSTER, cluster2); // cancelled subscription to cluster3 - verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: found 0 leaf (logical DNS or EDS) clusters for root cluster " + CLUSTER - + " xDS node ID: " + NODE_ID); - assertPicker(pickerCaptor.getValue(), unavailable, null); - assertThat(childBalancer.shutdown).isTrue(); + public void aggregateCluster_noChildren() { + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + // CLUSTER (aggr.) -> [] + CLUSTER, Cluster.newBuilder() + .setName(CLUSTER) + .setClusterType(Cluster.CustomClusterType.newBuilder() + .setName("envoy.clusters.aggregate") + .setTypedConfig(Any.pack(ClusterConfig.newBuilder() + .build()))) + .build())); + startXdsDepManager(); + + verify(helper) + .updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); + PickResult result = pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)); + Status actualStatus = result.getStatus(); + assertThat(actualStatus.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(actualStatus.getDescription()) + .contains("aggregate ClusterConfig.clusters must not be empty"); assertThat(childBalancers).isEmpty(); } @Test - public void aggregateCluster_withLoops() { - String cluster1 = "cluster-01.googleapis.com"; - // CLUSTER (aggr.) -> [cluster1] - CdsUpdate update = - CdsUpdate.forAggregate(CLUSTER, Collections.singletonList(cluster1)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1); - - // CLUSTER (aggr.) -> [cluster2 (aggr.)] - String cluster2 = "cluster-02.googleapis.com"; - update = - CdsUpdate.forAggregate(cluster1, Collections.singletonList(cluster2)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster1, update); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1, cluster2); - - // cluster2 (aggr.) -> [cluster3 (EDS), cluster1 (parent), cluster2 (self), cluster3 (dup)] - String cluster3 = "cluster-03.googleapis.com"; - CdsUpdate update2 = - CdsUpdate.forAggregate(cluster2, Arrays.asList(cluster3, cluster1, cluster2, cluster3)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster2, update2); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1, cluster2, cluster3); - - reset(helper); - CdsUpdate update3 = CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, - upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster3, update3); - verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: circular aggregate clusters directly under cluster-02.googleapis.com for root" - + " cluster cluster-foo.googleapis.com, named [cluster-01.googleapis.com," - + " cluster-02.googleapis.com], xDS node ID: " + NODE_ID); - assertPicker(pickerCaptor.getValue(), unavailable, null); - } - - @Test - public void aggregateCluster_withLoops_afterEds() { - String cluster1 = "cluster-01.googleapis.com"; - // CLUSTER (aggr.) -> [cluster1] - CdsUpdate update = - CdsUpdate.forAggregate(CLUSTER, Collections.singletonList(cluster1)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1); - - // CLUSTER (aggr.) -> [cluster2 (aggr.)] - String cluster2 = "cluster-02.googleapis.com"; - update = - CdsUpdate.forAggregate(cluster1, Collections.singletonList(cluster2)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster1, update); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1, cluster2); - - String cluster3 = "cluster-03.googleapis.com"; - CdsUpdate update2 = - CdsUpdate.forAggregate(cluster2, Arrays.asList(cluster3)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster2, update2); - CdsUpdate update3 = CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, - upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster3, update3); - - // cluster2 (aggr.) -> [cluster3 (EDS)] - CdsUpdate update2a = - CdsUpdate.forAggregate(cluster2, Arrays.asList(cluster3, cluster1, cluster2, cluster3)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster2, update2a); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1, cluster2, cluster3); - verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS error: circular aggregate clusters directly under cluster-02.googleapis.com for root" - + " cluster cluster-foo.googleapis.com, named [cluster-01.googleapis.com," - + " cluster-02.googleapis.com], xDS node ID: " + NODE_ID); - assertPicker(pickerCaptor.getValue(), unavailable, null); - } - - @Test - public void aggregateCluster_duplicateChildren() { + public void aggregateCluster_noNonAggregateClusterExits_returnErrorPicker() { String cluster1 = "cluster-01.googleapis.com"; - String cluster2 = "cluster-02.googleapis.com"; - String cluster3 = "cluster-03.googleapis.com"; - String cluster4 = "cluster-04.googleapis.com"; - - // CLUSTER (aggr.) -> [cluster1] - CdsUpdate update = - CdsUpdate.forAggregate(CLUSTER, Collections.singletonList(cluster1)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1); - - // cluster1 (aggr) -> [cluster3 (EDS), cluster2 (aggr), cluster4 (aggr)] - CdsUpdate update1 = - CdsUpdate.forAggregate(cluster1, Arrays.asList(cluster3, cluster2, cluster4, cluster3)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster1, update1); - assertThat(xdsClient.watchers.keySet()).containsExactly( - cluster3, cluster4, cluster2, cluster1, CLUSTER); - xdsClient.watchers.values().forEach(list -> assertThat(list.size()).isEqualTo(1)); - - // cluster2 (agg) -> [cluster3 (EDS), cluster4 {agg}] with dups - CdsUpdate update2 = - CdsUpdate.forAggregate(cluster2, Arrays.asList(cluster3, cluster4, cluster3)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster2, update2); - - // Define EDS cluster - CdsUpdate update3 = CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, - upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster3, update3); - - // cluster4 (agg) -> [cluster3 (EDS)] with dups (3 copies) - CdsUpdate update4 = - CdsUpdate.forAggregate(cluster4, Arrays.asList(cluster3, cluster3, cluster3)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster4, update4); - xdsClient.watchers.values().forEach(list -> assertThat(list.size()).isEqualTo(1)); - - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).hasSize(1); - DiscoveryMechanism instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); - assertDiscoveryMechanism(instance, cluster3, DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, - null, LRS_SERVER_INFO, 100L, upstreamTlsContext, outlierDetection); - } + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + // CLUSTER (aggr.) -> [cluster1 (missing)] + CLUSTER, Cluster.newBuilder() + .setName(CLUSTER) + .setClusterType(Cluster.CustomClusterType.newBuilder() + .setName("envoy.clusters.aggregate") + .setTypedConfig(Any.pack(ClusterConfig.newBuilder() + .addClusters(cluster1) + .build()))) + .setLbPolicy(Cluster.LbPolicy.RING_HASH) + .build())); + startXdsDepManager(); - @Test - public void aggregateCluster_discoveryErrorBeforeChildLbCreated_returnErrorPicker() { - String cluster1 = "cluster-01.googleapis.com"; - // CLUSTER (aggr.) -> [cluster1] - CdsUpdate update = - CdsUpdate.forAggregate(CLUSTER, Collections.singletonList(cluster1)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); - assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1); - Status error = Status.RESOURCE_EXHAUSTED.withDescription("OOM"); - xdsClient.deliverError(error); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - Status expectedError = Status.UNAVAILABLE.withDescription( - "Unable to load CDS cluster-foo.googleapis.com. xDS server returned: " - + "RESOURCE_EXHAUSTED: OOM xDS node ID: " + NODE_ID); - assertPicker(pickerCaptor.getValue(), expectedError, null); + Status status = Status.UNAVAILABLE.withDescription( + "CDS resource " + cluster1 + " does not exist nodeID: " + NODE_ID); + assertPickerStatus(pickerCaptor.getValue(), status); assertThat(childBalancers).isEmpty(); } @Test - public void aggregateCluster_discoveryErrorAfterChildLbCreated_propagateToChildLb() { - String cluster1 = "cluster-01.googleapis.com"; - // CLUSTER (aggr.) -> [cluster1 (logical DNS)] - CdsUpdate update = - CdsUpdate.forAggregate(CLUSTER, Collections.singletonList(cluster1)) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); - CdsUpdate update1 = - CdsUpdate.forLogicalDns(cluster1, DNS_HOST_NAME, LRS_SERVER_INFO, 200L, null, - false) - .roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(cluster1, update1); - FakeLoadBalancer childLb = Iterables.getOnlyElement(childBalancers); - ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childLb.config; - assertThat(childLbConfig.discoveryMechanisms).hasSize(1); - - Status error = Status.RESOURCE_EXHAUSTED.withDescription("OOM"); - xdsClient.deliverError(error); - assertThat(childLb.upstreamError.getCode()).isEqualTo(Status.Code.UNAVAILABLE); - assertThat(childLb.upstreamError.getDescription()).contains("RESOURCE_EXHAUSTED: OOM"); - assertThat(childLb.shutdown).isFalse(); // child LB may choose to keep working - } - - @Test - public void handleNameResolutionErrorFromUpstream_beforeChildLbCreated_returnErrorPicker() { - Status upstreamError = Status.UNAVAILABLE.withDescription( - "unreachable xDS node ID: " + NODE_ID); - loadBalancer.handleNameResolutionError(upstreamError); + public void handleNameResolutionErrorFromUpstream_beforeChildLbCreated_failingPicker() { + Status status = Status.UNAVAILABLE.withDescription("unreachable"); + loadBalancer.handleNameResolutionError(status); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - assertPicker(pickerCaptor.getValue(), upstreamError, null); + assertPickerStatus(pickerCaptor.getValue(), status); } @Test public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_fallThrough() { - CdsUpdate update = CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, - upstreamTlsContext, outlierDetection, false).roundRobinLbPolicy().build(); - xdsClient.deliverCdsUpdate(CLUSTER, update); + Cluster cluster = Cluster.newBuilder() + .setName(CLUSTER) + .setType(Cluster.DiscoveryType.EDS) + .setEdsClusterConfig(Cluster.EdsClusterConfig.newBuilder() + .setServiceName(EDS_SERVICE_NAME) + .setEdsConfig(ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.newBuilder()))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of(CLUSTER, cluster)); + startXdsDepManager(); + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.shutdown).isFalse(); + loadBalancer.handleNameResolutionError(Status.UNAVAILABLE.withDescription("unreachable")); assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(childBalancer.upstreamError.getDescription()).isEqualTo("unreachable"); @@ -694,56 +555,91 @@ public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_fallThroug @Test public void unknownLbProvider() { - try { - xdsClient.deliverCdsUpdate(CLUSTER, - CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, - outlierDetection, false) - .lbPolicyConfig(ImmutableMap.of("unknownLb", ImmutableMap.of("foo", "bar"))).build()); - } catch (Exception e) { - assertThat(e).hasMessageThat().contains("unknownLb"); - return; - } - fail("Expected the unknown LB to cause an exception"); + Cluster cluster = Cluster.newBuilder() + .setName(CLUSTER) + .setType(Cluster.DiscoveryType.EDS) + .setEdsClusterConfig(Cluster.EdsClusterConfig.newBuilder() + .setServiceName(EDS_SERVICE_NAME) + .setEdsConfig(ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.newBuilder()))) + .setLoadBalancingPolicy(LoadBalancingPolicy.newBuilder() + .addPolicies(Policy.newBuilder() + .setTypedExtensionConfig(TypedExtensionConfig.newBuilder() + .setTypedConfig(Any.pack(TypedStruct.newBuilder() + .setTypeUrl("type.googleapis.com/unknownLb") + .setValue(Struct.getDefaultInstance()) + .build()))))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of(CLUSTER, cluster)); + startXdsDepManager(); + verify(helper).updateBalancingState( + eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); + PickResult result = pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)); + Status actualStatus = result.getStatus(); + assertThat(actualStatus.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(actualStatus.getDescription()).contains("Invalid LoadBalancingPolicy"); } @Test public void invalidLbConfig() { - try { - xdsClient.deliverCdsUpdate(CLUSTER, - CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, - outlierDetection, false).lbPolicyConfig( - ImmutableMap.of("ring_hash_experimental", ImmutableMap.of("minRingSize", "-1"))) + Cluster cluster = Cluster.newBuilder() + .setName(CLUSTER) + .setType(Cluster.DiscoveryType.EDS) + .setEdsClusterConfig(Cluster.EdsClusterConfig.newBuilder() + .setServiceName(EDS_SERVICE_NAME) + .setEdsConfig(ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.newBuilder()))) + .setLoadBalancingPolicy(LoadBalancingPolicy.newBuilder() + .addPolicies(Policy.newBuilder() + .setTypedExtensionConfig(TypedExtensionConfig.newBuilder() + .setTypedConfig(Any.pack(TypedStruct.newBuilder() + .setTypeUrl("type.googleapis.com/ring_hash_experimental") + .setValue(Struct.newBuilder() + .putFields("minRingSize", Value.newBuilder().setNumberValue(-1).build())) + .build()))))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of(CLUSTER, cluster)); + startXdsDepManager(); + verify(helper).updateBalancingState( + eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); + PickResult result = pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)); + Status actualStatus = result.getStatus(); + assertThat(actualStatus.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(actualStatus.getDescription()).contains("Invalid 'minRingSize'"); + } + + private void startXdsDepManager() { + startXdsDepManager(new CdsConfig(CLUSTER)); + } + + private void startXdsDepManager(final CdsConfig cdsConfig) { + xdsDepManager.start( + xdsConfig -> { + if (!xdsConfig.hasValue()) { + throw new AssertionError("" + xdsConfig.getStatus()); + } + this.lastXdsConfig = xdsConfig.getValue(); + if (loadBalancer == null) { + return; + } + loadBalancer.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(Collections.emptyList()) + .setAttributes(Attributes.newBuilder() + .set(XdsAttributes.XDS_CONFIG, xdsConfig.getValue()) + .set(XdsAttributes.XDS_CLUSTER_SUBSCRIPT_REGISTRY, xdsDepManager) + .build()) + .setLoadBalancingPolicyConfig(cdsConfig) .build()); - } catch (Exception e) { - assertThat(e).hasMessageThat().contains("Unable to parse"); - return; - } - fail("Expected the invalid config to cause an exception"); + }); + // trigger does not exist timer, so broken config is more obvious + fakeClock.forwardTime(10, TimeUnit.MINUTES); } - private static void assertPicker(SubchannelPicker picker, Status expectedStatus, - @Nullable Subchannel expectedSubchannel) { + private static void assertPickerStatus(SubchannelPicker picker, Status expectedStatus) { PickResult result = picker.pickSubchannel(mock(PickSubchannelArgs.class)); Status actualStatus = result.getStatus(); assertThat(actualStatus.getCode()).isEqualTo(expectedStatus.getCode()); assertThat(actualStatus.getDescription()).isEqualTo(expectedStatus.getDescription()); - if (actualStatus.isOk()) { - assertThat(result.getSubchannel()).isSameInstanceAs(expectedSubchannel); - } - } - - private static void assertDiscoveryMechanism(DiscoveryMechanism instance, String name, - DiscoveryMechanism.Type type, @Nullable String edsServiceName, @Nullable String dnsHostName, - @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, - @Nullable UpstreamTlsContext tlsContext, @Nullable OutlierDetection outlierDetection) { - assertThat(instance.cluster).isEqualTo(name); - assertThat(instance.type).isEqualTo(type); - assertThat(instance.edsServiceName).isEqualTo(edsServiceName); - assertThat(instance.dnsHostName).isEqualTo(dnsHostName); - assertThat(instance.lrsServerInfo).isEqualTo(lrsServerInfo); - assertThat(instance.maxConcurrentRequests).isEqualTo(maxConcurrentRequests); - assertThat(instance.tlsContext).isEqualTo(tlsContext); - assertThat(instance.outlierDetection).isEqualTo(outlierDetection); } private final class FakeLoadBalancerProvider extends LoadBalancerProvider { @@ -802,8 +698,9 @@ private final class FakeLoadBalancer extends LoadBalancer { } @Override - public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { + public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { config = resolvedAddresses.getLoadBalancingPolicyConfig(); + return Status.OK; } @Override @@ -817,58 +714,4 @@ public void shutdown() { childBalancers.remove(this); } } - - private static final class FakeXdsClient extends XdsClient { - // watchers needs to support any non-cyclic shaped graphs - private final Map>> watchers = new HashMap<>(); - - @Override - @SuppressWarnings("unchecked") - public void watchXdsResource(XdsResourceType type, - String resourceName, - ResourceWatcher watcher, Executor syncContext) { - assertThat(type.typeName()).isEqualTo("CDS"); - watchers.computeIfAbsent(resourceName, k -> new ArrayList<>()) - .add((ResourceWatcher)watcher); - } - - @Override - public void cancelXdsResourceWatch(XdsResourceType type, - String resourceName, - ResourceWatcher watcher) { - assertThat(type.typeName()).isEqualTo("CDS"); - assertThat(watchers).containsKey(resourceName); - List> watcherList = watchers.get(resourceName); - assertThat(watcherList.remove(watcher)).isTrue(); - if (watcherList.isEmpty()) { - watchers.remove(resourceName); - } - } - - @Override - public BootstrapInfo getBootstrapInfo() { - return BOOTSTRAP_INFO; - } - - private void deliverCdsUpdate(String clusterName, CdsUpdate update) { - if (watchers.containsKey(clusterName)) { - List> resourceWatchers = - ImmutableList.copyOf(watchers.get(clusterName)); - resourceWatchers.forEach(w -> w.onChanged(update)); - } - } - - private void deliverResourceNotExist(String clusterName) { - if (watchers.containsKey(clusterName)) { - ImmutableList.copyOf(watchers.get(clusterName)) - .forEach(w -> w.onResourceDoesNotExist(clusterName)); - } - } - - private void deliverError(Status error) { - watchers.values().stream() - .flatMap(List::stream) - .forEach(w -> w.onError(error)); - } - } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 006440be6c1..32361684a6d 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -2211,6 +2211,23 @@ public void cdsResponseWithAggregateCluster() { verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); } + @Test + public void cdsResponseWithEmptyAggregateCluster() { + DiscoveryRpcCall call = startResourceWatcher(XdsClusterResource.getInstance(), CDS_RESOURCE, + cdsResourceWatcher); + List candidates = Arrays.asList(); + Any clusterAggregate = + Any.pack(mf.buildAggregateCluster(CDS_RESOURCE, "round_robin", null, null, candidates)); + call.sendResponse(CDS, clusterAggregate, VERSION_1, "0000"); + + // Client sent an ACK CDS request. + String errorMsg = "CDS response Cluster 'cluster.googleapis.com' validation error: " + + "Cluster cluster.googleapis.com: aggregate ClusterConfig.clusters must not be empty"; + call.verifyRequestNack(CDS, CDS_RESOURCE, "", "0000", NODE, ImmutableList.of(errorMsg)); + verify(cdsResourceWatcher).onError(errorCaptor.capture()); + verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + } + @Test public void cdsResponseWithCircuitBreakers() { DiscoveryRpcCall call = startResourceWatcher(XdsClusterResource.getInstance(), CDS_RESOURCE, diff --git a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerProviderTest.java b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerProviderTest.java index 3036db5b09f..66c9c5c537e 100644 --- a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerProviderTest.java @@ -106,7 +106,7 @@ public void parseLoadBalancingConfig_invalid_negativeSize() throws IOException { assertThat(configOrError.getError()).isNotNull(); assertThat(configOrError.getError().getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(configOrError.getError().getDescription()) - .isEqualTo("Invalid 'mingRingSize'/'maxRingSize'"); + .isEqualTo("Invalid 'minRingSize'/'maxRingSize'"); } @Test @@ -117,7 +117,7 @@ public void parseLoadBalancingConfig_invalid_minGreaterThanMax() throws IOExcept assertThat(configOrError.getError()).isNotNull(); assertThat(configOrError.getError().getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(configOrError.getError().getDescription()) - .isEqualTo("Invalid 'mingRingSize'/'maxRingSize'"); + .isEqualTo("Invalid 'minRingSize'/'maxRingSize'"); } @Test @@ -214,7 +214,7 @@ public void parseLoadBalancingConfig_zeroMinRingSize() throws IOException { assertThat(configOrError.getError()).isNotNull(); assertThat(configOrError.getError().getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(configOrError.getError().getDescription()) - .isEqualTo("Invalid 'mingRingSize'/'maxRingSize'"); + .isEqualTo("Invalid 'minRingSize'/'maxRingSize'"); } @Test @@ -225,7 +225,7 @@ public void parseLoadBalancingConfig_minRingSizeGreaterThanMaxRingSize() throws assertThat(configOrError.getError()).isNotNull(); assertThat(configOrError.getError().getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(configOrError.getError().getDescription()) - .isEqualTo("Invalid 'mingRingSize'/'maxRingSize'"); + .isEqualTo("Invalid 'minRingSize'/'maxRingSize'"); } @Test diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index aea1ad66d72..8ac0f8c9d8c 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -28,7 +28,6 @@ import static io.grpc.xds.XdsTestUtils.ENDPOINT_PORT; import static io.grpc.xds.XdsTestUtils.RDS_NAME; import static io.grpc.xds.XdsTestUtils.getEdsNameForCluster; -import static io.grpc.xds.client.CommonBootstrapperTestUtils.SERVER_URI; import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; @@ -48,28 +47,22 @@ import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; import io.grpc.BindableService; import io.grpc.ChannelLogger; -import io.grpc.ManagedChannel; import io.grpc.NameResolver; -import io.grpc.Server; import io.grpc.Status; import io.grpc.StatusOr; import io.grpc.StatusOrMatcher; import io.grpc.SynchronizationContext; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; -import io.grpc.internal.ExponentialBackoffPolicy; import io.grpc.internal.FakeClock; import io.grpc.internal.GrpcUtil; import io.grpc.testing.GrpcCleanupRule; import io.grpc.xds.XdsClusterResource.CdsUpdate; import io.grpc.xds.XdsConfig.XdsClusterConfig; import io.grpc.xds.XdsEndpointResource.EdsUpdate; -import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsClient.ResourceMetadata; -import io.grpc.xds.client.XdsClientMetricReporter; import io.grpc.xds.client.XdsResourceType; -import io.grpc.xds.client.XdsTransportFactory; import java.io.Closeable; import java.io.IOException; import java.util.ArrayDeque; @@ -96,7 +89,6 @@ import org.mockito.ArgumentMatchers; import org.mockito.Captor; import org.mockito.InOrder; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -108,21 +100,20 @@ public class XdsDependencyManagerTest { public static final String CLUSTER_TYPE_NAME = XdsClusterResource.getInstance().typeName(); public static final String ENDPOINT_TYPE_NAME = XdsEndpointResource.getInstance().typeName(); - @Mock - private XdsClientMetricReporter xdsClientMetricReporter; - private final SynchronizationContext syncContext = new SynchronizationContext((t, e) -> { throw new AssertionError(e); }); + private final FakeClock fakeClock = new FakeClock(); + + private XdsClient xdsClient = XdsTestUtils.createXdsClient( + Collections.singletonList("control-plane"), + serverInfo -> new GrpcXdsTransportFactory.GrpcXdsTransport( + InProcessChannelBuilder.forName(serverInfo.target()).directExecutor().build()), + fakeClock); - private ManagedChannel channel; - private XdsClient xdsClient; - private XdsDependencyManager xdsDependencyManager; private TestWatcher xdsConfigWatcher; - private Server xdsServer; - private final FakeClock fakeClock = new FakeClock(); private final String serverName = "the-service-name"; private final Queue loadReportCalls = new ArrayDeque<>(); private final AtomicBoolean adsEnded = new AtomicBoolean(true); @@ -150,10 +141,12 @@ public class XdsDependencyManagerTest { .build(); private final ScheduledExecutorService scheduler = fakeClock.getScheduledExecutorService(); + private XdsDependencyManager xdsDependencyManager = new XdsDependencyManager( + xdsClient, syncContext, serverName, serverName, nameResolverArgs, scheduler); @Before public void setUp() throws Exception { - xdsServer = cleanupRule.register(InProcessServerBuilder + cleanupRule.register(InProcessServerBuilder .forName("control-plane") .addService(controlPlaneService) .addService(lrsService) @@ -163,15 +156,6 @@ public void setUp() throws Exception { XdsTestUtils.setAdsConfig(controlPlaneService, serverName); - channel = cleanupRule.register( - InProcessChannelBuilder.forName("control-plane").directExecutor().build()); - XdsTransportFactory xdsTransportFactory = - ignore -> new GrpcXdsTransportFactory.GrpcXdsTransport(channel); - - xdsClient = CommonBootstrapperTestUtils.createXdsClient( - Collections.singletonList(SERVER_URI), xdsTransportFactory, fakeClock, - new ExponentialBackoffPolicy.Provider(), MessagePrinter.INSTANCE, xdsClientMetricReporter); - testWatcher = new TestWatcher(); xdsConfigWatcher = mock(TestWatcher.class, delegatesTo(testWatcher)); defaultXdsConfig = XdsTestUtils.getDefaultXdsConfig(serverName); @@ -183,9 +167,6 @@ public void tearDown() throws InterruptedException { xdsDependencyManager.shutdown(); } xdsClient.shutdown(); - channel.shutdown(); // channel not owned by XdsClient - - xdsServer.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); assertThat(adsEnded.get()).isTrue(); assertThat(lrsEnded.get()).isTrue(); @@ -194,8 +175,7 @@ public void tearDown() throws InterruptedException { @Test public void verify_basic_config() { - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig)); testWatcher.verifyStats(1, 0); @@ -203,8 +183,7 @@ public void verify_basic_config() { @Test public void verify_config_update() { - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig)); @@ -221,8 +200,7 @@ public void verify_config_update() { @Test public void verify_simple_aggregate() { InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig)); List childNames = Arrays.asList("clusterC", "clusterB", "clusterA"); @@ -281,8 +259,7 @@ public void testComplexRegisteredAggregate() throws IOException { List childNames2 = Arrays.asList("clusterA", "clusterX"); XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName2, childNames2); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); inOrder.verify(xdsConfigWatcher).onUpdate(any()); Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1); @@ -313,8 +290,7 @@ public void testComplexRegisteredAggregate() throws IOException { @Test public void testDelayedSubscription() { InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig)); String rootName1 = "root_c"; @@ -360,8 +336,7 @@ public void testMissingCdsAndEds() { edsMap.put("garbageEds", clusterLoadAssignment); controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); fakeClock.forwardTime(16, TimeUnit.SECONDS); verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); @@ -393,8 +368,9 @@ public void testMissingCdsAndEds() { @Test public void testMissingLds() { String ldsName = "badLdsName"; - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + xdsDependencyManager = new XdsDependencyManager(xdsClient, syncContext, serverName, ldsName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); fakeClock.forwardTime(16, TimeUnit.SECONDS); verify(xdsConfigWatcher).onUpdate( @@ -409,8 +385,7 @@ public void testTcpListenerErrors() { Listener serverListener = ControlPlaneRule.buildServerListener().toBuilder().setName(serverName).build(); controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, ImmutableMap.of(serverName, serverListener)); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); fakeClock.forwardTime(16, TimeUnit.SECONDS); verify(xdsConfigWatcher).onUpdate( @@ -427,8 +402,7 @@ public void testMissingRds() { controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, ImmutableMap.of(serverName, clientListener)); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); fakeClock.forwardTime(16, TimeUnit.SECONDS); verify(xdsConfigWatcher).onUpdate( @@ -444,8 +418,7 @@ public void testUpdateToMissingVirtualHost() { "wrong-virtual-host", XdsTestUtils.RDS_NAME, XdsTestUtils.CLUSTER_NAME); controlPlaneService.setXdsConfig( ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig)); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); // Update with a config that has a virtual host that doesn't match the server name verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); @@ -460,8 +433,9 @@ public void testCorruptLds() { String ldsResourceName = "xdstp://unknown.example.com/envoy.config.listener.v3.Listener/listener1"; - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, + xdsDependencyManager = new XdsDependencyManager(xdsClient, syncContext, serverName, ldsResourceName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); verify(xdsConfigWatcher).onUpdate( argThat(StatusOrMatcher.hasStatus( @@ -474,8 +448,7 @@ public void testCorruptLds() { @Test public void testChangeRdsName_fromLds() { InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig)); String newRdsName = "newRdsName1"; @@ -530,8 +503,7 @@ public void testMultipleParentsInCdsTree() throws IOException { // Start the actual test InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); XdsConfig initialConfig = xdsUpdateCaptor.getValue().getValue(); @@ -576,8 +548,7 @@ public void testCdsDeleteUnsubscribesChild() throws Exception { controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); XdsConfig config = xdsUpdateCaptor.getValue().getValue(); assertThat(config.getClusters().get("clusterA").hasValue()).isTrue(); @@ -611,12 +582,12 @@ public void testCdsCycleReclaimed() throws Exception { // The cycle is loaded and detected InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); XdsConfig config = xdsUpdateCaptor.getValue().getValue(); assertThat(config.getClusters().get("clusterA").hasValue()).isFalse(); assertThat(config.getClusters().get("clusterA").getStatus().getDescription()).contains("cycle"); + assertThat(config.getClusters().get("clusterB").hasValue()).isTrue(); // Orphan the cycle and it is discarded routeConfig = @@ -657,8 +628,7 @@ public void testMultipleCdsReferToSameEds() { controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); // Start the actual test - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); XdsConfig initialConfig = xdsUpdateCaptor.getValue().getValue(); assertThat(initialConfig.getClusters().keySet()) @@ -675,8 +645,7 @@ public void testMultipleCdsReferToSameEds() { @Test public void testChangeRdsName_FromLds_complexTree() { - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); // Create the same tree as in testMultipleParentsInCdsTree Cluster rootCluster = @@ -721,8 +690,7 @@ public void testChangeRdsName_FromLds_complexTree() { public void testChangeAggCluster() { InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); inOrder.verify(xdsConfigWatcher).onUpdate(any()); // Setup initial config A -> A1 -> (A11, A12) @@ -775,8 +743,7 @@ public void testCdsError() throws IOException { controlPlaneService.setXdsConfig( ADS_TYPE_URL_CDS, ImmutableMap.of(XdsTestUtils.CLUSTER_NAME, Cluster.newBuilder().setName(XdsTestUtils.CLUSTER_NAME).build())); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); Status status = xdsUpdateCaptor.getValue().getValue() @@ -789,8 +756,7 @@ public void ldsUpdateAfterShutdown() { XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS", ENDPOINT_HOSTNAME, ENDPOINT_PORT); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); verify(xdsConfigWatcher).onUpdate(any()); @@ -822,8 +788,7 @@ public void rdsUpdateAfterShutdown() { XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS", ENDPOINT_HOSTNAME, ENDPOINT_PORT); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); verify(xdsConfigWatcher).onUpdate(any()); @@ -855,8 +820,7 @@ public void cdsUpdateAfterShutdown() { XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS", ENDPOINT_HOSTNAME, ENDPOINT_PORT); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); verify(xdsConfigWatcher).onUpdate(any()); @@ -888,8 +852,7 @@ public void edsUpdateAfterShutdown() { XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS", ENDPOINT_HOSTNAME, ENDPOINT_PORT); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); verify(xdsConfigWatcher).onUpdate(any()); @@ -922,8 +885,7 @@ public void subscribeToClusterAfterShutdown() throws Exception { ENDPOINT_HOSTNAME, ENDPOINT_PORT); InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); - xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext, - serverName, serverName, nameResolverArgs, scheduler); + xdsDependencyManager.start(xdsConfigWatcher); inOrder.verify(xdsConfigWatcher).onUpdate(any()); xdsDependencyManager.shutdown(); diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java index ec1ccd7c6d0..e85058f0f3f 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -52,14 +52,20 @@ import io.grpc.Context; import io.grpc.Context.CancellationListener; import io.grpc.StatusOr; +import io.grpc.internal.ExponentialBackoffPolicy; +import io.grpc.internal.FakeClock; import io.grpc.internal.JsonParser; import io.grpc.stub.StreamObserver; import io.grpc.xds.Endpoints.LbEndpoint; import io.grpc.xds.Endpoints.LocalityLbEndpoints; import io.grpc.xds.XdsConfig.XdsClusterConfig.EndpointConfig; import io.grpc.xds.client.Bootstrapper; +import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.client.Locality; +import io.grpc.xds.client.XdsClient; +import io.grpc.xds.client.XdsClientMetricReporter; import io.grpc.xds.client.XdsResourceType; +import io.grpc.xds.client.XdsTransportFactory; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -364,6 +370,32 @@ static Listener buildInlineClientListener(String rdsName, String clusterName, St .setApiListener(clientListenerBuilder.build()).build(); } + public static XdsClient createXdsClient( + List serverUris, + XdsTransportFactory xdsTransportFactory, + FakeClock fakeClock) { + return createXdsClient( + CommonBootstrapperTestUtils.buildBootStrap(serverUris), + xdsTransportFactory, + fakeClock, + new XdsClientMetricReporter() {}); + } + + /** Calls {@link CommonBootstrapperTestUtils#createXdsClient} with gRPC-specific values. */ + public static XdsClient createXdsClient( + Bootstrapper.BootstrapInfo bootstrapInfo, + XdsTransportFactory xdsTransportFactory, + FakeClock fakeClock, + XdsClientMetricReporter xdsClientMetricReporter) { + return CommonBootstrapperTestUtils.createXdsClient( + bootstrapInfo, + xdsTransportFactory, + fakeClock, + new ExponentialBackoffPolicy.Provider(), + MessagePrinter.INSTANCE, + xdsClientMetricReporter); + } + /** * Matches a {@link LoadStatsRequest} containing a collection of {@link ClusterStats} with * the same list of clusterName:clusterServiceName pair. From 30f6a4db77227a75220d82111e85497d339a3ba4 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Wed, 11 Jun 2025 12:19:19 -0700 Subject: [PATCH 288/591] google-java-format a line that was too long (#12147) --- .../main/java/io/grpc/binder/internal/BinderTransport.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index c8900031432..4b35137aa54 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -300,7 +300,10 @@ protected boolean setOutgoingBinder(OneWayBinderProxy binder) { @Override public synchronized void binderDied() { - shutdownInternal(Status.UNAVAILABLE.withDescription("Peer process crashed, exited or was killed (binderDied)"), true); + shutdownInternal( + Status.UNAVAILABLE.withDescription( + "Peer process crashed, exited or was killed (binderDied)"), + true); } @GuardedBy("this") From 13fe0080448e1fcb7e1dae925bce12c5c1c9071b Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 12 Jun 2025 04:01:19 +0000 Subject: [PATCH 289/591] rls: Refactor estimatedSizeBytes updates (#12145) Just use a regular method instead of reusing the EvictionListener API. Fix a few comments as well. Both of these changes were based on review comments to pre-existing code in #11203. Contributes to #11243 --- .../java/io/grpc/rls/LinkedHashLruCache.java | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java b/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java index b39b463c762..9a961759693 100644 --- a/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java +++ b/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java @@ -43,7 +43,8 @@ abstract class LinkedHashLruCache implements LruCache { private final LinkedHashMap delegate; private final Ticker ticker; - private final EvictionListener evictionListener; + @Nullable + private final EvictionListener evictionListener; private long estimatedSizeBytes; private long estimatedMaxSizeBytes; @@ -53,7 +54,7 @@ abstract class LinkedHashLruCache implements LruCache { final Ticker ticker) { checkState(estimatedMaxSizeBytes > 0, "max estimated cache size should be positive"); this.estimatedMaxSizeBytes = estimatedMaxSizeBytes; - this.evictionListener = new SizeHandlingEvictionListener(evictionListener); + this.evictionListener = evictionListener; this.ticker = checkNotNull(ticker, "ticker"); delegate = new LinkedHashMap( // rough estimate or minimum hashmap default @@ -135,7 +136,7 @@ public final V cache(K key, V value) { estimatedSizeBytes += size; existing = delegate.put(key, new SizedValue(size, value)); if (existing != null) { - evictionListener.onEviction(key, existing, EvictionType.REPLACED); + fireOnEviction(key, existing, EvictionType.REPLACED); } return existing == null ? null : existing.value; } @@ -174,7 +175,7 @@ private V invalidate(K key, EvictionType cause) { checkNotNull(cause, "cause"); SizedValue existing = delegate.remove(key); if (existing != null) { - evictionListener.onEviction(key, existing, cause); + fireOnEviction(key, existing, cause); } return existing == null ? null : existing.value; } @@ -185,7 +186,7 @@ public final void invalidateAll() { while (iterator.hasNext()) { Map.Entry entry = iterator.next(); if (entry.getValue() != null) { - evictionListener.onEviction(entry.getKey(), entry.getValue(), EvictionType.EXPLICIT); + fireOnEviction(entry.getKey(), entry.getValue(), EvictionType.EXPLICIT); } iterator.remove(); } @@ -215,14 +216,13 @@ public final List values() { protected final boolean fitToLimit() { boolean removedAnyUnexpired = false; if (estimatedSizeBytes <= estimatedMaxSizeBytes) { - // new size is larger no need to do cleanup return false; } // cleanup expired entries long now = ticker.read(); cleanupExpiredEntries(now); - // cleanup eldest entry until new size limit + // cleanup eldest entry until the size of all entries fits within the limit Iterator> lruIter = delegate.entrySet().iterator(); while (lruIter.hasNext() && estimatedMaxSizeBytes < this.estimatedSizeBytes) { Map.Entry entry = lruIter.next(); @@ -230,8 +230,8 @@ protected final boolean fitToLimit() { break; // Violates some constraint like minimum age so stop our cleanup } lruIter.remove(); - // eviction listener will update the estimatedSizeBytes - evictionListener.onEviction(entry.getKey(), entry.getValue(), EvictionType.SIZE); + // fireOnEviction will update the estimatedSizeBytes + fireOnEviction(entry.getKey(), entry.getValue(), EvictionType.SIZE); removedAnyUnexpired = true; } return removedAnyUnexpired; @@ -270,7 +270,7 @@ private boolean cleanupExpiredEntries(int maxExpiredEntries, long now) { Map.Entry entry = lruIter.next(); if (isExpired(entry.getKey(), entry.getValue().value, now)) { lruIter.remove(); - evictionListener.onEviction(entry.getKey(), entry.getValue(), EvictionType.EXPIRED); + fireOnEviction(entry.getKey(), entry.getValue(), EvictionType.EXPIRED); removedAny = true; maxExpiredEntries--; } @@ -283,21 +283,10 @@ public final void close() { invalidateAll(); } - /** A {@link EvictionListener} keeps track of size. */ - private final class SizeHandlingEvictionListener implements EvictionListener { - - private final EvictionListener delegate; - - SizeHandlingEvictionListener(@Nullable EvictionListener delegate) { - this.delegate = delegate; - } - - @Override - public void onEviction(K key, SizedValue value, EvictionType cause) { - estimatedSizeBytes -= value.size; - if (delegate != null) { - delegate.onEviction(key, value.value, cause); - } + private void fireOnEviction(K key, SizedValue value, EvictionType cause) { + estimatedSizeBytes -= value.size; + if (evictionListener != null) { + evictionListener.onEviction(key, value.value, cause); } } From 6cc2ff1cedc8a64c4494e62f2ea5f25bb129c7fe Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 10 Jun 2025 14:32:26 -0700 Subject: [PATCH 290/591] util: In OutlierDetectionLb, don't box longs if they can't be null Changed the builder pattern to pass the builder to the constructor, since I was changing almost all the arguments of the constructor anyway. --- .../util/OutlierDetectionLoadBalancer.java | 146 ++++++++---------- 1 file changed, 61 insertions(+), 85 deletions(-) diff --git a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java index 59b2d38ecd9..bd9faef3f99 100644 --- a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java @@ -142,7 +142,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { // If outlier detection is actually configured, start a timer that will periodically try to // detect outliers. if (config.outlierDetectionEnabled()) { - Long initialDelayNanos; + long initialDelayNanos; if (detectionTimerStartNanos == null) { // On the first go we use the configured interval. @@ -723,7 +723,7 @@ void swapCounters() { * that don't have ejected subchannels and uneject ones that have spent the maximum ejection * time allowed. */ - void maybeUnejectOutliers(Long detectionTimerStartNanos) { + void maybeUnejectOutliers(long detectionTimerStartNanos) { for (EndpointTracker tracker : trackerMap.values()) { if (!tracker.subchannelsEjected()) { tracker.decrementEjectionTimeMultiplier(); @@ -951,64 +951,54 @@ private static boolean hasSingleAddress(List addressGrou */ public static final class OutlierDetectionLoadBalancerConfig { - public final Long intervalNanos; - public final Long baseEjectionTimeNanos; - public final Long maxEjectionTimeNanos; - public final Integer maxEjectionPercent; + public final long intervalNanos; + public final long baseEjectionTimeNanos; + public final long maxEjectionTimeNanos; + public final int maxEjectionPercent; public final SuccessRateEjection successRateEjection; public final FailurePercentageEjection failurePercentageEjection; public final Object childConfig; - private OutlierDetectionLoadBalancerConfig(Long intervalNanos, - Long baseEjectionTimeNanos, - Long maxEjectionTimeNanos, - Integer maxEjectionPercent, - SuccessRateEjection successRateEjection, - FailurePercentageEjection failurePercentageEjection, - Object childConfig) { - this.intervalNanos = intervalNanos; - this.baseEjectionTimeNanos = baseEjectionTimeNanos; - this.maxEjectionTimeNanos = maxEjectionTimeNanos; - this.maxEjectionPercent = maxEjectionPercent; - this.successRateEjection = successRateEjection; - this.failurePercentageEjection = failurePercentageEjection; - this.childConfig = childConfig; + private OutlierDetectionLoadBalancerConfig(Builder builder) { + this.intervalNanos = builder.intervalNanos; + this.baseEjectionTimeNanos = builder.baseEjectionTimeNanos; + this.maxEjectionTimeNanos = builder.maxEjectionTimeNanos; + this.maxEjectionPercent = builder.maxEjectionPercent; + this.successRateEjection = builder.successRateEjection; + this.failurePercentageEjection = builder.failurePercentageEjection; + this.childConfig = builder.childConfig; } /** Builds a new {@link OutlierDetectionLoadBalancerConfig}. */ public static class Builder { - Long intervalNanos = 10_000_000_000L; // 10s - Long baseEjectionTimeNanos = 30_000_000_000L; // 30s - Long maxEjectionTimeNanos = 300_000_000_000L; // 300s - Integer maxEjectionPercent = 10; + long intervalNanos = 10_000_000_000L; // 10s + long baseEjectionTimeNanos = 30_000_000_000L; // 30s + long maxEjectionTimeNanos = 300_000_000_000L; // 300s + int maxEjectionPercent = 10; SuccessRateEjection successRateEjection; FailurePercentageEjection failurePercentageEjection; Object childConfig; /** The interval between outlier detection sweeps. */ - public Builder setIntervalNanos(Long intervalNanos) { - checkArgument(intervalNanos != null); + public Builder setIntervalNanos(long intervalNanos) { this.intervalNanos = intervalNanos; return this; } /** The base time an address is ejected for. */ - public Builder setBaseEjectionTimeNanos(Long baseEjectionTimeNanos) { - checkArgument(baseEjectionTimeNanos != null); + public Builder setBaseEjectionTimeNanos(long baseEjectionTimeNanos) { this.baseEjectionTimeNanos = baseEjectionTimeNanos; return this; } /** The longest time an address can be ejected. */ - public Builder setMaxEjectionTimeNanos(Long maxEjectionTimeNanos) { - checkArgument(maxEjectionTimeNanos != null); + public Builder setMaxEjectionTimeNanos(long maxEjectionTimeNanos) { this.maxEjectionTimeNanos = maxEjectionTimeNanos; return this; } /** The algorithm agnostic maximum percentage of addresses that can be ejected. */ - public Builder setMaxEjectionPercent(Integer maxEjectionPercent) { - checkArgument(maxEjectionPercent != null); + public Builder setMaxEjectionPercent(int maxEjectionPercent) { this.maxEjectionPercent = maxEjectionPercent; return this; } @@ -1040,64 +1030,57 @@ public Builder setChildConfig(Object childConfig) { /** Builds a new instance of {@link OutlierDetectionLoadBalancerConfig}. */ public OutlierDetectionLoadBalancerConfig build() { checkState(childConfig != null); - return new OutlierDetectionLoadBalancerConfig(intervalNanos, baseEjectionTimeNanos, - maxEjectionTimeNanos, maxEjectionPercent, successRateEjection, - failurePercentageEjection, childConfig); + return new OutlierDetectionLoadBalancerConfig(this); } } /** The configuration for success rate ejection. */ public static class SuccessRateEjection { - public final Integer stdevFactor; - public final Integer enforcementPercentage; - public final Integer minimumHosts; - public final Integer requestVolume; - - SuccessRateEjection(Integer stdevFactor, Integer enforcementPercentage, Integer minimumHosts, - Integer requestVolume) { - this.stdevFactor = stdevFactor; - this.enforcementPercentage = enforcementPercentage; - this.minimumHosts = minimumHosts; - this.requestVolume = requestVolume; + public final int stdevFactor; + public final int enforcementPercentage; + public final int minimumHosts; + public final int requestVolume; + + SuccessRateEjection(Builder builder) { + this.stdevFactor = builder.stdevFactor; + this.enforcementPercentage = builder.enforcementPercentage; + this.minimumHosts = builder.minimumHosts; + this.requestVolume = builder.requestVolume; } /** Builds new instances of {@link SuccessRateEjection}. */ public static final class Builder { - Integer stdevFactor = 1900; - Integer enforcementPercentage = 100; - Integer minimumHosts = 5; - Integer requestVolume = 100; + int stdevFactor = 1900; + int enforcementPercentage = 100; + int minimumHosts = 5; + int requestVolume = 100; /** The product of this and the standard deviation of success rates determine the ejection * threshold. */ - public Builder setStdevFactor(Integer stdevFactor) { - checkArgument(stdevFactor != null); + public Builder setStdevFactor(int stdevFactor) { this.stdevFactor = stdevFactor; return this; } /** Only eject this percentage of outliers. */ - public Builder setEnforcementPercentage(Integer enforcementPercentage) { - checkArgument(enforcementPercentage != null); + public Builder setEnforcementPercentage(int enforcementPercentage) { checkArgument(enforcementPercentage >= 0 && enforcementPercentage <= 100); this.enforcementPercentage = enforcementPercentage; return this; } /** The minimum amount of hosts needed for success rate ejection. */ - public Builder setMinimumHosts(Integer minimumHosts) { - checkArgument(minimumHosts != null); + public Builder setMinimumHosts(int minimumHosts) { checkArgument(minimumHosts >= 0); this.minimumHosts = minimumHosts; return this; } /** The minimum address request volume to be considered for success rate ejection. */ - public Builder setRequestVolume(Integer requestVolume) { - checkArgument(requestVolume != null); + public Builder setRequestVolume(int requestVolume) { checkArgument(requestVolume >= 0); this.requestVolume = requestVolume; return this; @@ -1105,53 +1088,48 @@ public Builder setRequestVolume(Integer requestVolume) { /** Builds a new instance of {@link SuccessRateEjection}. */ public SuccessRateEjection build() { - return new SuccessRateEjection(stdevFactor, enforcementPercentage, minimumHosts, - requestVolume); + return new SuccessRateEjection(this); } } } /** The configuration for failure percentage ejection. */ public static class FailurePercentageEjection { - public final Integer threshold; - public final Integer enforcementPercentage; - public final Integer minimumHosts; - public final Integer requestVolume; - - FailurePercentageEjection(Integer threshold, Integer enforcementPercentage, - Integer minimumHosts, Integer requestVolume) { - this.threshold = threshold; - this.enforcementPercentage = enforcementPercentage; - this.minimumHosts = minimumHosts; - this.requestVolume = requestVolume; + public final int threshold; + public final int enforcementPercentage; + public final int minimumHosts; + public final int requestVolume; + + FailurePercentageEjection(Builder builder) { + this.threshold = builder.threshold; + this.enforcementPercentage = builder.enforcementPercentage; + this.minimumHosts = builder.minimumHosts; + this.requestVolume = builder.requestVolume; } /** For building new {@link FailurePercentageEjection} instances. */ public static class Builder { - Integer threshold = 85; - Integer enforcementPercentage = 100; - Integer minimumHosts = 5; - Integer requestVolume = 50; + int threshold = 85; + int enforcementPercentage = 100; + int minimumHosts = 5; + int requestVolume = 50; /** The failure percentage that will result in an address being considered an outlier. */ - public Builder setThreshold(Integer threshold) { - checkArgument(threshold != null); + public Builder setThreshold(int threshold) { checkArgument(threshold >= 0 && threshold <= 100); this.threshold = threshold; return this; } /** Only eject this percentage of outliers. */ - public Builder setEnforcementPercentage(Integer enforcementPercentage) { - checkArgument(enforcementPercentage != null); + public Builder setEnforcementPercentage(int enforcementPercentage) { checkArgument(enforcementPercentage >= 0 && enforcementPercentage <= 100); this.enforcementPercentage = enforcementPercentage; return this; } /** The minimum amount of host for failure percentage ejection to be enabled. */ - public Builder setMinimumHosts(Integer minimumHosts) { - checkArgument(minimumHosts != null); + public Builder setMinimumHosts(int minimumHosts) { checkArgument(minimumHosts >= 0); this.minimumHosts = minimumHosts; return this; @@ -1161,8 +1139,7 @@ public Builder setMinimumHosts(Integer minimumHosts) { * The request volume required for an address to be considered for failure percentage * ejection. */ - public Builder setRequestVolume(Integer requestVolume) { - checkArgument(requestVolume != null); + public Builder setRequestVolume(int requestVolume) { checkArgument(requestVolume >= 0); this.requestVolume = requestVolume; return this; @@ -1170,8 +1147,7 @@ public Builder setRequestVolume(Integer requestVolume) { /** Builds a new instance of {@link FailurePercentageEjection}. */ public FailurePercentageEjection build() { - return new FailurePercentageEjection(threshold, enforcementPercentage, minimumHosts, - requestVolume); + return new FailurePercentageEjection(this); } } } From 6f69363d90a9ae7471c40044a711be007029c59b Mon Sep 17 00:00:00 2001 From: David Sanderson <32687193+dws@users.noreply.github.com> Date: Thu, 12 Jun 2025 11:43:23 -0400 Subject: [PATCH 291/591] bazel: Migrate java_grpc_library to use DefaultInfo (#12148) We here address the following obstacles in grpc-java to using Bazel's --incompatible_disable_target_default_provider_fields flag: ``` ERROR: /private/var/tmp/_bazel_dws/7fd3cd5077fbf76d9e2ae421c39ef7ed/external/googleapis+/google/devtools/build/v1/BUILD.bazel:81:18: in _java_grpc_library rule @@googleapis+//google/devtools/build/v1:build_java_grpc: Traceback (most recent call last): File "/private/var/tmp/_bazel_dws/7fd3cd5077fbf76d9e2ae421c39ef7ed/external/grpc-java+/java_grpc_library.bzl", line 94, column 30, in _java_rpc_library_impl args.add(toolchain.plugin.files_to_run.executable, format = "--plugin=protoc-gen-rpc-plugin=%s") Error: Accessing the default provider in this manner is deprecated and will be removed soon. It may be temporarily re-enabled by setting --incompatible_disable_target_default_provider_fields=false. See https://github.com/bazelbuild/bazel/issues/20183 for details. ERROR: /private/var/tmp/_bazel_dws/7fd3cd5077fbf76d9e2ae421c39ef7ed/external/googleapis+/google/devtools/build/v1/BUILD.bazel:81:18: Analysis of target '@@googleapis+//google/devtools/build/v1:build_java_grpc' failed ERROR: Analysis of target '//src:bazel' failed; build aborted: Analysis failed ``` --- java_grpc_library.bzl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/java_grpc_library.bzl b/java_grpc_library.bzl index 630ced383f7..e7d133a2769 100644 --- a/java_grpc_library.bzl +++ b/java_grpc_library.bzl @@ -91,15 +91,15 @@ def _java_rpc_library_impl(ctx): srcjar = ctx.actions.declare_file("%s-proto-gensrc.jar" % ctx.label.name) args = ctx.actions.args() - args.add(toolchain.plugin.files_to_run.executable, format = "--plugin=protoc-gen-rpc-plugin=%s") + args.add(toolchain.plugin[DefaultInfo].files_to_run.executable, format = "--plugin=protoc-gen-rpc-plugin=%s") args.add("--rpc-plugin_out={0}:{1}".format(toolchain.plugin_arg, srcjar.path)) args.add_joined("--descriptor_set_in", descriptor_set_in, join_with = ctx.configuration.host_path_separator) args.add_all(srcs, map_each = _path_ignoring_repository) ctx.actions.run( - inputs = depset(srcs, transitive = [descriptor_set_in, toolchain.plugin.files]), + inputs = depset(srcs, transitive = [descriptor_set_in, toolchain.plugin[DefaultInfo].files]), outputs = [srcjar], - executable = toolchain.protoc.files_to_run, + executable = toolchain.protoc[DefaultInfo].files_to_run, arguments = [args], use_default_shell_env = True, toolchain = None, From 26bd0eee4705e4ddac0e351fded5a8bd4e49f863 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Fri, 13 Jun 2025 12:30:13 +0530 Subject: [PATCH 292/591] core: Use lazy message formatting in checkState (#12144) --- core/src/main/java/io/grpc/internal/InternalSubchannel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/io/grpc/internal/InternalSubchannel.java b/core/src/main/java/io/grpc/internal/InternalSubchannel.java index 27a80f7c191..a27e46eaf60 100644 --- a/core/src/main/java/io/grpc/internal/InternalSubchannel.java +++ b/core/src/main/java/io/grpc/internal/InternalSubchannel.java @@ -346,7 +346,7 @@ private void gotoState(final ConnectivityStateInfo newState) { if (state.getState() != newState.getState()) { Preconditions.checkState(state.getState() != SHUTDOWN, - "Cannot transition out of SHUTDOWN to " + newState); + "Cannot transition out of SHUTDOWN to %s", newState.getState()); if (reconnectDisabled && newState.getState() == TRANSIENT_FAILURE) { state = ConnectivityStateInfo.forNonError(IDLE); } else { From 240f731e00f659b6dd271bfb9941e4ba587e154f Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sat, 31 May 2025 07:44:55 -0700 Subject: [PATCH 293/591] xds: Avoid changing cache when watching children in XdsDepManager The watchers can be completely regular, so the base class can do the cache management while the subclasses are only concerned with subscribing to children. --- .../io/grpc/xds/XdsDependencyManager.java | 74 ++++++------------- 1 file changed, 22 insertions(+), 52 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index 0428852648d..c7333b93c11 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -333,7 +333,8 @@ private static void addConfigForCluster( Status.INTERNAL.withDescription("Logical DNS in dependency manager unsupported"))); break; default: - throw new IllegalStateException("Unexpected value: " + cdsUpdate.clusterType()); + child = new EndpointConfig(StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( + "Unknown type in cluster " + clusterName + " " + cdsUpdate.clusterType()))); } if (clusters.containsKey(clusterName)) { // If a cycle is detected, we'll have detected it while recursing, so now there will be a key @@ -520,7 +521,7 @@ public void onError(Status error) { } // Don't update configuration on error, if we've already received configuration if (!hasDataValue()) { - setDataAsStatus(Status.UNAVAILABLE.withDescription( + this.data = StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( String.format("Error retrieving %s: %s: %s", toContextString(), error.getCode(), error.getDescription()))); maybePublishConfig(); @@ -534,11 +535,25 @@ public void onResourceDoesNotExist(String resourceName) { } checkArgument(this.resourceName.equals(resourceName), "Resource name does not match"); - setDataAsStatus(Status.UNAVAILABLE.withDescription( + this.data = StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( toContextString() + " does not exist" + nodeInfo())); maybePublishConfig(); } + @Override + public void onChanged(T update) { + checkNotNull(update, "update"); + if (cancelled) { + return; + } + + this.data = StatusOr.fromValue(update); + subscribeToChildren(update); + maybePublishConfig(); + } + + protected abstract void subscribeToChildren(T update); + public void close() { cancelled = true; xdsClient.cancelXdsResourceWatch(type, resourceName, this); @@ -557,20 +572,6 @@ boolean hasDataValue() { return data != null && data.hasValue(); } - String resourceName() { - return resourceName; - } - - protected void setData(T data) { - checkNotNull(data, "data"); - this.data = StatusOr.fromValue(data); - } - - protected void setDataAsStatus(Status status) { - checkNotNull(status, "status"); - this.data = StatusOr.fromStatus(status); - } - public String toContextString() { return toContextStr(type.typeName(), resourceName); } @@ -588,12 +589,7 @@ private LdsWatcher(String resourceName) { } @Override - public void onChanged(XdsListenerResource.LdsUpdate update) { - checkNotNull(update, "update"); - if (cancelled) { - return; - } - + public void subscribeToChildren(XdsListenerResource.LdsUpdate update) { HttpConnectionManager httpConnectionManager = update.httpConnectionManager(); List virtualHosts; if (httpConnectionManager == null) { @@ -610,9 +606,6 @@ public void onChanged(XdsListenerResource.LdsUpdate update) { if (rdsName != null) { addRdsWatcher(rdsName); } - - setData(update); - maybePublishConfig(); } private String getRdsName(XdsListenerResource.LdsUpdate update) { @@ -680,14 +673,8 @@ public RdsWatcher(String resourceName) { } @Override - public void onChanged(RdsUpdate update) { - checkNotNull(update, "update"); - if (cancelled) { - return; - } - setData(update); + public void subscribeToChildren(RdsUpdate update) { updateRoutes(update.virtualHosts); - maybePublishConfig(); } @Override @@ -705,31 +692,20 @@ private class CdsWatcher extends XdsWatcherBase { } @Override - public void onChanged(XdsClusterResource.CdsUpdate update) { - checkNotNull(update, "update"); - if (cancelled) { - return; - } + public void subscribeToChildren(XdsClusterResource.CdsUpdate update) { switch (update.clusterType()) { case EDS: - setData(update); addEdsWatcher(getEdsServiceName()); break; case LOGICAL_DNS: - setData(update); // no eds needed break; case AGGREGATE: - setData(update); update.prioritizedClusterNames() .forEach(name -> addClusterWatcher(name)); break; default: - Status error = Status.UNAVAILABLE.withDescription( - "unknown cluster type in " + resourceName() + " " + update.clusterType()); - setDataAsStatus(error); } - maybePublishConfig(); } public String getEdsServiceName() { @@ -749,12 +725,6 @@ private EdsWatcher(String resourceName) { } @Override - public void onChanged(XdsEndpointResource.EdsUpdate update) { - if (cancelled) { - return; - } - setData(checkNotNull(update, "update")); - maybePublishConfig(); - } + public void subscribeToChildren(XdsEndpointResource.EdsUpdate update) {} } } From d88ef97a87a6c930f65ab85bca00a9c7c95acf76 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 12 Jun 2025 11:47:05 -0700 Subject: [PATCH 294/591] core: Remove RetryingNR.RESOLUTION_RESULT_LISTENER_KEY It was introduced in fcb5c54e4 because at the time we didn't change the API to communicate the status. When onResult2() was introduced in 90d0fabb1 this hack stopped being necessary. --- .../io/grpc/internal/ManagedChannelImpl.java | 14 +----- .../grpc/internal/RetryingNameResolver.java | 32 +------------- .../grpc/internal/ManagedChannelImplTest.java | 4 -- .../internal/RetryingNameResolverTest.java | 43 ++----------------- 4 files changed, 6 insertions(+), 87 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 976587a6db9..9b756c3165d 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -94,7 +94,6 @@ import io.grpc.internal.ManagedChannelServiceConfig.ServiceConfigConvertedSelector; import io.grpc.internal.RetriableStream.ChannelBufferMeter; import io.grpc.internal.RetriableStream.Throttle; -import io.grpc.internal.RetryingNameResolver.ResolutionResultListener; import java.net.URI; import java.util.ArrayList; import java.util.Collection; @@ -1653,18 +1652,7 @@ final class NameResolverListener extends NameResolver.Listener2 { @Override public void onResult(final ResolutionResult resolutionResult) { - final class NamesResolved implements Runnable { - - @Override - public void run() { - Status status = onResult2(resolutionResult); - ResolutionResultListener resolutionResultListener = resolutionResult.getAttributes() - .get(RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY); - resolutionResultListener.resolutionAttempted(status); - } - } - - syncContext.execute(new NamesResolved()); + syncContext.execute(() -> onResult2(resolutionResult)); } @SuppressWarnings("ReferenceEquality") diff --git a/core/src/main/java/io/grpc/internal/RetryingNameResolver.java b/core/src/main/java/io/grpc/internal/RetryingNameResolver.java index 6dcfcd3534a..55fedea932e 100644 --- a/core/src/main/java/io/grpc/internal/RetryingNameResolver.java +++ b/core/src/main/java/io/grpc/internal/RetryingNameResolver.java @@ -17,7 +17,6 @@ package io.grpc.internal; import com.google.common.annotations.VisibleForTesting; -import io.grpc.Attributes; import io.grpc.NameResolver; import io.grpc.Status; import io.grpc.SynchronizationContext; @@ -34,9 +33,6 @@ final class RetryingNameResolver extends ForwardingNameResolver { private final RetryScheduler retryScheduler; private final SynchronizationContext syncContext; - static final Attributes.Key RESOLUTION_RESULT_LISTENER_KEY - = Attributes.Key.create( - "io.grpc.internal.RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY"); /** * Creates a new {@link RetryingNameResolver}. @@ -88,18 +84,7 @@ private class RetryingListener extends Listener2 { @Override public void onResult(ResolutionResult resolutionResult) { - // If the resolution result listener is already an attribute it indicates that a name resolver - // has already been wrapped with this class. This indicates a misconfiguration. - if (resolutionResult.getAttributes().get(RESOLUTION_RESULT_LISTENER_KEY) != null) { - throw new IllegalStateException( - "RetryingNameResolver can only be used once to wrap a NameResolver"); - } - - // To have retry behavior for name resolvers that haven't migrated to onResult2. - delegateListener.onResult(resolutionResult.toBuilder().setAttributes( - resolutionResult.getAttributes().toBuilder() - .set(RESOLUTION_RESULT_LISTENER_KEY, new ResolutionResultListener()).build()) - .build()); + syncContext.execute(() -> onResult2(resolutionResult)); } @Override @@ -119,19 +104,4 @@ public void onError(Status error) { syncContext.execute(() -> retryScheduler.schedule(new DelayedNameResolverRefresh())); } } - - /** - * Simple callback class to store in {@link ResolutionResult} attributes so that - * ManagedChannel can indicate if the resolved addresses were accepted. Temporary until - * the Listener2.onResult() API can be changed to return a boolean for this purpose. - */ - class ResolutionResultListener { - public void resolutionAttempted(Status successStatus) { - if (successStatus.isOk()) { - retryScheduler.reset(); - } else { - retryScheduler.schedule(new DelayedNameResolverRefresh()); - } - } - } } diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index ac845947f1c..6dfba7404c6 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -3844,8 +3844,6 @@ public double nextDouble() { verify(mockLoadBalancer).acceptResolvedAddresses(resolvedAddressCaptor.capture()); ResolvedAddresses resolvedAddresses = resolvedAddressCaptor.getValue(); assertThat(resolvedAddresses.getAddresses()).isEqualTo(nameResolverFactory.servers); - assertThat(resolvedAddresses.getAttributes() - .get(RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY)).isNotNull(); // simulating request connection and then transport ready after resolved address Subchannel subchannel = @@ -3951,8 +3949,6 @@ public void hedgingScheduledThenChannelShutdown_hedgeShouldStillHappen_newCallSh verify(mockLoadBalancer).acceptResolvedAddresses(resolvedAddressCaptor.capture()); ResolvedAddresses resolvedAddresses = resolvedAddressCaptor.getValue(); assertThat(resolvedAddresses.getAddresses()).isEqualTo(nameResolverFactory.servers); - assertThat(resolvedAddresses.getAttributes() - .get(RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY)).isNotNull(); // simulating request connection and then transport ready after resolved address Subchannel subchannel = diff --git a/core/src/test/java/io/grpc/internal/RetryingNameResolverTest.java b/core/src/test/java/io/grpc/internal/RetryingNameResolverTest.java index 6347416f0ca..1da93f05fe2 100644 --- a/core/src/test/java/io/grpc/internal/RetryingNameResolverTest.java +++ b/core/src/test/java/io/grpc/internal/RetryingNameResolverTest.java @@ -17,7 +17,6 @@ package io.grpc.internal; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -28,7 +27,6 @@ import io.grpc.NameResolver.ResolutionResult; import io.grpc.Status; import io.grpc.SynchronizationContext; -import io.grpc.internal.RetryingNameResolver.ResolutionResultListener; import java.lang.Thread.UncaughtExceptionHandler; import org.junit.Before; import org.junit.Rule; @@ -58,8 +56,6 @@ public class RetryingNameResolverTest { private RetryScheduler mockRetryScheduler; @Captor private ArgumentCaptor listenerCaptor; - @Captor - private ArgumentCaptor onResultCaptor; private final SynchronizationContext syncContext = new SynchronizationContext( mock(UncaughtExceptionHandler.class)); @@ -77,21 +73,14 @@ public void startAndShutdown() { retryingNameResolver.shutdown(); } - // Make sure the ResolutionResultListener callback is added to the ResolutionResult attributes, - // and the retry scheduler is reset since the name resolution was successful. @Test public void onResult_success() { + when(mockListener.onResult2(isA(ResolutionResult.class))).thenReturn(Status.OK); retryingNameResolver.start(mockListener); verify(mockNameResolver).start(listenerCaptor.capture()); listenerCaptor.getValue().onResult(ResolutionResult.newBuilder().build()); - verify(mockListener).onResult(onResultCaptor.capture()); - ResolutionResultListener resolutionResultListener = onResultCaptor.getValue() - .getAttributes() - .get(RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY); - assertThat(resolutionResultListener).isNotNull(); - resolutionResultListener.resolutionAttempted(Status.OK); verify(mockRetryScheduler).reset(); } @@ -107,21 +96,15 @@ public void onResult2_sucesss() { verify(mockRetryScheduler).reset(); } - // Make sure the ResolutionResultListener callback is added to the ResolutionResult attributes, - // and that a retry gets scheduled when the resolution results are rejected. + // Make sure that a retry gets scheduled when the resolution results are rejected. @Test public void onResult_failure() { + when(mockListener.onResult2(isA(ResolutionResult.class))).thenReturn(Status.UNAVAILABLE); retryingNameResolver.start(mockListener); verify(mockNameResolver).start(listenerCaptor.capture()); listenerCaptor.getValue().onResult(ResolutionResult.newBuilder().build()); - verify(mockListener).onResult(onResultCaptor.capture()); - ResolutionResultListener resolutionResultListener = onResultCaptor.getValue() - .getAttributes() - .get(RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY); - assertThat(resolutionResultListener).isNotNull(); - resolutionResultListener.resolutionAttempted(Status.UNAVAILABLE); verify(mockRetryScheduler).schedule(isA(Runnable.class)); } @@ -138,24 +121,6 @@ public void onResult2_failure() { verify(mockRetryScheduler).schedule(isA(Runnable.class)); } - // Wrapping a NameResolver more than once is a misconfiguration. - @Test - public void onResult_failure_doubleWrapped() { - NameResolver doubleWrappedResolver = new RetryingNameResolver(retryingNameResolver, - mockRetryScheduler, syncContext); - - doubleWrappedResolver.start(mockListener); - verify(mockNameResolver).start(listenerCaptor.capture()); - - try { - listenerCaptor.getValue().onResult(ResolutionResult.newBuilder().build()); - } catch (IllegalStateException e) { - assertThat(e).hasMessageThat().contains("can only be used once"); - return; - } - fail("An exception should have been thrown for a double wrapped NAmeResolver"); - } - // A retry should get scheduled when name resolution fails. @Test public void onError() { @@ -165,4 +130,4 @@ public void onError() { verify(mockListener).onError(Status.DEADLINE_EXCEEDED); verify(mockRetryScheduler).schedule(isA(Runnable.class)); } -} \ No newline at end of file +} From 8974a306af0bb5334cbe2bf4278cd898e5110668 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 13 Jun 2025 17:52:00 +0000 Subject: [PATCH 295/591] util: Mark OutlierDetectionLb classes final None of these classes were intended to be extended. Even non-public classes need final to prevent mocks from doing horrible things. --- .../util/OutlierDetectionLoadBalancer.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java index bd9faef3f99..07376af2110 100644 --- a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java @@ -189,7 +189,7 @@ public void shutdown() { * This timer will be invoked periodically, according to configuration, and it will look for any * outlier subchannels. */ - class DetectionTimer implements Runnable { + final class DetectionTimer implements Runnable { OutlierDetectionLoadBalancerConfig config; ChannelLogger logger; @@ -217,7 +217,7 @@ public void run() { * This child helper wraps the provided helper so that it can hand out wrapped {@link * OutlierDetectionSubchannel}s and manage the address info map. */ - class ChildHelper extends ForwardingLoadBalancerHelper { + final class ChildHelper extends ForwardingLoadBalancerHelper { private Helper delegate; @@ -259,7 +259,7 @@ public void updateBalancingState(ConnectivityState newState, SubchannelPicker ne } } - class OutlierDetectionSubchannel extends ForwardingSubchannel { + final class OutlierDetectionSubchannel extends ForwardingSubchannel { private final Subchannel delegate; private EndpointTracker endpointTracker; @@ -398,7 +398,7 @@ protected Subchannel delegate() { /** * Wraps the actual listener so that state changes from the actual one can be intercepted. */ - class OutlierDetectionSubchannelStateListener implements SubchannelStateListener { + final class OutlierDetectionSubchannelStateListener implements SubchannelStateListener { private final SubchannelStateListener delegate; @@ -428,7 +428,7 @@ public String toString() { * This picker delegates the actual picking logic to a wrapped delegate, but associates a {@link * ClientStreamTracer} with each pick to track the results of each subchannel stream. */ - class OutlierDetectionPicker extends SubchannelPicker { + final class OutlierDetectionPicker extends SubchannelPicker { private final SubchannelPicker delegate; @@ -454,7 +454,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { * Builds instances of a {@link ClientStreamTracer} that increments the call count in the * tracker for each closed stream. */ - class ResultCountingClientStreamTracerFactory extends ClientStreamTracer.Factory { + final class ResultCountingClientStreamTracerFactory extends ClientStreamTracer.Factory { private final EndpointTracker tracker; @@ -498,7 +498,7 @@ public void streamClosed(Status status) { /** * Tracks additional information about the endpoint needed for outlier detection. */ - static class EndpointTracker { + static final class EndpointTracker { private OutlierDetectionLoadBalancerConfig config; // Marked as volatile to assure that when the inactive counter is swapped in as the new active @@ -642,7 +642,7 @@ public boolean maxEjectionTimeElapsed(long currentTimeNanos) { } /** Tracks both successful and failed call counts. */ - private static class CallCounter { + private static final class CallCounter { AtomicLong successCount = new AtomicLong(); AtomicLong failureCount = new AtomicLong(); @@ -663,7 +663,7 @@ public String toString() { /** * Maintains a mapping from endpoint (a set of addresses) to their trackers. */ - static class EndpointTrackerMap extends ForwardingMap, EndpointTracker> { + static final class EndpointTrackerMap extends ForwardingMap, EndpointTracker> { private final Map, EndpointTracker> trackerMap; EndpointTrackerMap() { @@ -784,7 +784,7 @@ static List forConfig(OutlierDetectionLoadBalancerConf * required rate is not fixed, but is based on the mean and standard deviation of the success * rates of all of the addresses. */ - static class SuccessRateOutlierEjectionAlgorithm implements OutlierEjectionAlgorithm { + static final class SuccessRateOutlierEjectionAlgorithm implements OutlierEjectionAlgorithm { private final OutlierDetectionLoadBalancerConfig config; @@ -869,7 +869,7 @@ static double standardDeviation(Collection values, double mean) { } } - static class FailurePercentageOutlierEjectionAlgorithm implements OutlierEjectionAlgorithm { + static final class FailurePercentageOutlierEjectionAlgorithm implements OutlierEjectionAlgorithm { private final OutlierDetectionLoadBalancerConfig config; @@ -970,7 +970,7 @@ private OutlierDetectionLoadBalancerConfig(Builder builder) { } /** Builds a new {@link OutlierDetectionLoadBalancerConfig}. */ - public static class Builder { + public static final class Builder { long intervalNanos = 10_000_000_000L; // 10s long baseEjectionTimeNanos = 30_000_000_000L; // 30s long maxEjectionTimeNanos = 300_000_000_000L; // 300s @@ -1035,7 +1035,7 @@ public OutlierDetectionLoadBalancerConfig build() { } /** The configuration for success rate ejection. */ - public static class SuccessRateEjection { + public static final class SuccessRateEjection { public final int stdevFactor; public final int enforcementPercentage; @@ -1094,7 +1094,7 @@ public SuccessRateEjection build() { } /** The configuration for failure percentage ejection. */ - public static class FailurePercentageEjection { + public static final class FailurePercentageEjection { public final int threshold; public final int enforcementPercentage; public final int minimumHosts; From d5b4fb51c2e5ed0fa4dc3fdc979e9bc99c82b86a Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 12 Jun 2025 09:47:22 -0700 Subject: [PATCH 296/591] xds: Support tracking non-xds resources in XdsDepManager This will be used for logical dns clusters as part of gRFC A74. Swapping to EnumMap wasn't really necessary, but was easy given the new type system. I can't say I'm particularly happy with the name of the new TrackedWatcher type, but XdsConfigWatcher prevented using "Watcher" because it won't implement the new interface, and ResourceWatcher already exists in XdsClient. So we have TrackedWatcher, WatcherTracer, TypeWatchers, and TrackedWatcherType. --- .../io/grpc/xds/XdsDependencyManager.java | 164 ++++++++++-------- 1 file changed, 94 insertions(+), 70 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index c7333b93c11..82cfd9ccbdf 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -36,6 +36,7 @@ import io.grpc.xds.client.XdsClient.ResourceWatcher; import io.grpc.xds.client.XdsResourceType; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; @@ -53,8 +54,19 @@ * applies to a single data plane authority. */ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegistry { - public static final XdsClusterResource CLUSTER_RESOURCE = XdsClusterResource.getInstance(); - public static final XdsEndpointResource ENDPOINT_RESOURCE = XdsEndpointResource.getInstance(); + private enum TrackedWatcherTypeEnum { + LDS, RDS, CDS, EDS + } + + private static final TrackedWatcherType LDS_TYPE = + new TrackedWatcherType<>(TrackedWatcherTypeEnum.LDS); + private static final TrackedWatcherType RDS_TYPE = + new TrackedWatcherType<>(TrackedWatcherTypeEnum.RDS); + private static final TrackedWatcherType CDS_TYPE = + new TrackedWatcherType<>(TrackedWatcherTypeEnum.CDS); + private static final TrackedWatcherType EDS_TYPE = + new TrackedWatcherType<>(TrackedWatcherTypeEnum.EDS); + private static final int MAX_CLUSTER_RECURSION_DEPTH = 16; // Specified by gRFC A37 private final String listenerName; private final XdsClient xdsClient; @@ -63,7 +75,8 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi private XdsConfigWatcher xdsConfigWatcher; private StatusOr lastUpdate = null; - private final Map, TypeWatchers> resourceWatchers = new HashMap<>(); + private final Map> resourceWatchers = + new EnumMap<>(TrackedWatcherTypeEnum.class); private final Set subscriptions = new HashSet<>(); XdsDependencyManager(XdsClient xdsClient, @@ -86,7 +99,7 @@ public void start(XdsConfigWatcher xdsConfigWatcher) { checkState(this.xdsConfigWatcher == null, "dep manager may not be restarted"); this.xdsConfigWatcher = checkNotNull(xdsConfigWatcher, "xdsConfigWatcher"); // start the ball rolling - syncContext.execute(() -> addWatcher(new LdsWatcher(listenerName))); + syncContext.execute(() -> addWatcher(LDS_TYPE, new LdsWatcher(listenerName))); } @Override @@ -96,7 +109,7 @@ public XdsConfig.Subscription subscribeToCluster(String clusterName) { ClusterSubscription subscription = new ClusterSubscription(clusterName); syncContext.execute(() -> { - if (getWatchers(XdsListenerResource.getInstance()).isEmpty()) { + if (getWatchers(LDS_TYPE).isEmpty()) { subscription.closed = true; return; // shutdown() called } @@ -107,33 +120,28 @@ public XdsConfig.Subscription subscribeToCluster(String clusterName) { return subscription; } - private void addWatcher(XdsWatcherBase watcher) { + private void addWatcher( + TrackedWatcherType watcherType, XdsWatcherBase watcher) { syncContext.throwIfNotInThisSynchronizationContext(); XdsResourceType type = watcher.type; String resourceName = watcher.resourceName; - getWatchers(type).put(resourceName, watcher); + getWatchers(watcherType).put(resourceName, watcher); xdsClient.watchXdsResource(type, resourceName, watcher, syncContext); } public void shutdown() { syncContext.execute(() -> { for (TypeWatchers watchers : resourceWatchers.values()) { - shutdownWatchersForType(watchers); + for (TrackedWatcher watcher : watchers.watchers.values()) { + watcher.close(); + } } resourceWatchers.clear(); subscriptions.clear(); }); } - private void shutdownWatchersForType(TypeWatchers watchers) { - for (Map.Entry> watcherEntry : watchers.watchers.entrySet()) { - xdsClient.cancelXdsResourceWatch(watchers.resourceType, watcherEntry.getKey(), - watcherEntry.getValue()); - watcherEntry.getValue().cancelled = true; - } - } - private void releaseSubscription(ClusterSubscription subscription) { checkNotNull(subscription, "subscription"); syncContext.execute(() -> { @@ -154,12 +162,12 @@ private void releaseSubscription(ClusterSubscription subscription) { */ private void maybePublishConfig() { syncContext.throwIfNotInThisSynchronizationContext(); - if (getWatchers(XdsListenerResource.getInstance()).isEmpty()) { + if (getWatchers(LDS_TYPE).isEmpty()) { return; // shutdown() called } boolean waitingOnResource = resourceWatchers.values().stream() .flatMap(typeWatchers -> typeWatchers.watchers.values().stream()) - .anyMatch(XdsWatcherBase::missingResult); + .anyMatch(TrackedWatcher::missingResult); if (waitingOnResource) { return; } @@ -194,8 +202,8 @@ private static StatusOr buildUpdate( // Iterate watchers and build the XdsConfig - XdsWatcherBase ldsWatcher - = tracer.getWatcher(XdsListenerResource.getInstance(), listenerName); + TrackedWatcher ldsWatcher + = tracer.getWatcher(LDS_TYPE, listenerName); if (ldsWatcher == null) { return StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( "Bug: No listener watcher found for " + listenerName)); @@ -241,14 +249,13 @@ private static StatusOr buildUpdate( return StatusOr.fromValue(builder.build()); } - private Map> getWatchers( - XdsResourceType resourceType) { - TypeWatchers typeWatchers = resourceWatchers.get(resourceType); + private Map> getWatchers(TrackedWatcherType watcherType) { + TypeWatchers typeWatchers = resourceWatchers.get(watcherType.typeEnum); if (typeWatchers == null) { - typeWatchers = new TypeWatchers(resourceType); - resourceWatchers.put(resourceType, typeWatchers); + typeWatchers = new TypeWatchers(watcherType); + resourceWatchers.put(watcherType.typeEnum, typeWatchers); } - assert typeWatchers.resourceType == resourceType; + assert typeWatchers.watcherType == watcherType; @SuppressWarnings("unchecked") TypeWatchers tTypeWatchers = (TypeWatchers) typeWatchers; return tTypeWatchers.watchers; @@ -275,7 +282,7 @@ private static void addConfigForCluster( return; } - CdsWatcher cdsWatcher = (CdsWatcher) tracer.getWatcher(CLUSTER_RESOURCE, clusterName); + CdsWatcher cdsWatcher = (CdsWatcher) tracer.getWatcher(CDS_TYPE, clusterName); StatusOr cdsWatcherDataOr = cdsWatcher.getData(); if (!cdsWatcherDataOr.hasValue()) { clusters.put(clusterName, StatusOr.fromStatus(cdsWatcherDataOr.getStatus())); @@ -318,8 +325,8 @@ private static void addConfigForCluster( child = new AggregateConfig(ImmutableList.copyOf(leafNames)); break; case EDS: - XdsWatcherBase edsWatcher = - tracer.getWatcher(ENDPOINT_RESOURCE, cdsWatcher.getEdsServiceName()); + TrackedWatcher edsWatcher = + tracer.getWatcher(EDS_TYPE, cdsWatcher.getEdsServiceName()); if (edsWatcher != null) { child = new EndpointConfig(edsWatcher.getData()); } else { @@ -346,27 +353,27 @@ private static void addConfigForCluster( } private void addRdsWatcher(String resourceName) { - if (getWatchers(XdsRouteConfigureResource.getInstance()).containsKey(resourceName)) { + if (getWatchers(RDS_TYPE).containsKey(resourceName)) { return; } - addWatcher(new RdsWatcher(resourceName)); + addWatcher(RDS_TYPE, new RdsWatcher(resourceName)); } private void addEdsWatcher(String edsServiceName) { - if (getWatchers(XdsEndpointResource.getInstance()).containsKey(edsServiceName)) { + if (getWatchers(EDS_TYPE).containsKey(edsServiceName)) { return; } - addWatcher(new EdsWatcher(edsServiceName)); + addWatcher(EDS_TYPE, new EdsWatcher(edsServiceName)); } private void addClusterWatcher(String clusterName) { - if (getWatchers(CLUSTER_RESOURCE).containsKey(clusterName)) { + if (getWatchers(CDS_TYPE).containsKey(clusterName)) { return; } - addWatcher(new CdsWatcher(clusterName)); + addWatcher(CDS_TYPE, new CdsWatcher(clusterName)); } private void updateRoutes(List virtualHosts) { @@ -404,13 +411,13 @@ private static Set getClusterNamesFromVirtualHost(VirtualHost virtualHos return clusters; } - private static class TypeWatchers { + private static class TypeWatchers { // Key is resource name - final Map> watchers = new HashMap<>(); - final XdsResourceType resourceType; + final Map> watchers = new HashMap<>(); + final TrackedWatcherType watcherType; - TypeWatchers(XdsResourceType resourceType) { - this.resourceType = resourceType; + TypeWatchers(TrackedWatcherType watcherType) { + this.watcherType = checkNotNull(watcherType, "watcherType"); } } @@ -442,38 +449,36 @@ public void close() { /** State for tracing garbage collector. */ private static final class WatcherTracer { - private final Map, TypeWatchers> resourceWatchers; - private final Map, TypeWatchers> usedWatchers; + private final Map> resourceWatchers; + private final Map> usedWatchers; - public WatcherTracer(Map, TypeWatchers> resourceWatchers) { + public WatcherTracer(Map> resourceWatchers) { this.resourceWatchers = resourceWatchers; - this.usedWatchers = new HashMap<>(); - for (XdsResourceType type : resourceWatchers.keySet()) { - usedWatchers.put(type, newTypeWatchers(type)); + this.usedWatchers = new EnumMap<>(TrackedWatcherTypeEnum.class); + for (Map.Entry> me : resourceWatchers.entrySet()) { + usedWatchers.put(me.getKey(), newTypeWatchers(me.getValue().watcherType)); } } - private static TypeWatchers newTypeWatchers( - XdsResourceType type) { + private static TypeWatchers newTypeWatchers(TrackedWatcherType type) { return new TypeWatchers(type); } - public XdsWatcherBase getWatcher( - XdsResourceType resourceType, String name) { - TypeWatchers typeWatchers = resourceWatchers.get(resourceType); + public TrackedWatcher getWatcher(TrackedWatcherType watcherType, String name) { + TypeWatchers typeWatchers = resourceWatchers.get(watcherType.typeEnum); if (typeWatchers == null) { return null; } - assert typeWatchers.resourceType == resourceType; + assert typeWatchers.watcherType == watcherType; @SuppressWarnings("unchecked") TypeWatchers tTypeWatchers = (TypeWatchers) typeWatchers; - XdsWatcherBase watcher = tTypeWatchers.watchers.get(name); + TrackedWatcher watcher = tTypeWatchers.watchers.get(name); if (watcher == null) { return null; } @SuppressWarnings("unchecked") - TypeWatchers usedTypeWatchers = (TypeWatchers) usedWatchers.get(resourceType); + TypeWatchers usedTypeWatchers = (TypeWatchers) usedWatchers.get(watcherType.typeEnum); usedTypeWatchers.watchers.put(name, watcher); return watcher; } @@ -481,9 +486,9 @@ public XdsWatcherBase getWatcher( /** Shut down unused watchers. */ public void closeUnusedWatchers() { boolean changed = false; // Help out the GC by preferring old objects - for (XdsResourceType type : resourceWatchers.keySet()) { - TypeWatchers orig = resourceWatchers.get(type); - TypeWatchers used = usedWatchers.get(type); + for (TrackedWatcherTypeEnum key : resourceWatchers.keySet()) { + TypeWatchers orig = resourceWatchers.get(key); + TypeWatchers used = usedWatchers.get(key); for (String name : orig.watchers.keySet()) { if (used.watchers.containsKey(name)) { continue; @@ -498,8 +503,33 @@ public void closeUnusedWatchers() { } } + @SuppressWarnings("UnusedTypeParameter") + private static final class TrackedWatcherType { + public final TrackedWatcherTypeEnum typeEnum; + + public TrackedWatcherType(TrackedWatcherTypeEnum typeEnum) { + this.typeEnum = checkNotNull(typeEnum, "typeEnum"); + } + } + + private interface TrackedWatcher { + @Nullable + StatusOr getData(); + + default boolean missingResult() { + return getData() == null; + } + + default boolean hasDataValue() { + StatusOr data = getData(); + return data != null && data.hasValue(); + } + + void close(); + } + private abstract class XdsWatcherBase - implements ResourceWatcher { + implements ResourceWatcher, TrackedWatcher { private final XdsResourceType type; private final String resourceName; boolean cancelled; @@ -554,24 +584,18 @@ public void onChanged(T update) { protected abstract void subscribeToChildren(T update); + @Override public void close() { cancelled = true; xdsClient.cancelXdsResourceWatch(type, resourceName, this); } - boolean missingResult() { - return data == null; - } - + @Override @Nullable - StatusOr getData() { + public StatusOr getData() { return data; } - boolean hasDataValue() { - return data != null && data.hasValue(); - } - public String toContextString() { return toContextStr(type.typeName(), resourceName); } @@ -622,7 +646,7 @@ private RdsWatcher getRdsWatcher(XdsListenerResource.LdsUpdate update, WatcherTr if (rdsName == null) { return null; } - return (RdsWatcher) tracer.getWatcher(XdsRouteConfigureResource.getInstance(), rdsName); + return (RdsWatcher) tracer.getWatcher(RDS_TYPE, rdsName); } public RdsUpdateSupplier getRouteSource(WatcherTracer tracer) { @@ -688,7 +712,7 @@ public StatusOr getRdsUpdate() { private class CdsWatcher extends XdsWatcherBase { CdsWatcher(String resourceName) { - super(CLUSTER_RESOURCE, checkNotNull(resourceName, "resourceName")); + super(XdsClusterResource.getInstance(), checkNotNull(resourceName, "resourceName")); } @Override @@ -721,7 +745,7 @@ public String getEdsServiceName() { private class EdsWatcher extends XdsWatcherBase { private EdsWatcher(String resourceName) { - super(ENDPOINT_RESOURCE, checkNotNull(resourceName, "resourceName")); + super(XdsEndpointResource.getInstance(), checkNotNull(resourceName, "resourceName")); } @Override From 1c430989902856e609ddbff203be1dc4c412ff43 Mon Sep 17 00:00:00 2001 From: vimanikag Date: Mon, 16 Jun 2025 14:47:36 +0000 Subject: [PATCH 297/591] util: OutlierDetection should use Ticker, not TimeProvider (#12110) TimeProvider provides wall time. That can move forward and backward as time is adjusted. OutlierDetection is measuring durations, so it should use a monotonic clock. Fixes #11622 --- .../io/grpc/util/OutlierDetectionLoadBalancer.java | 14 +++++++------- .../util/OutlierDetectionLoadBalancerProvider.java | 4 ++-- .../util/OutlierDetectionLoadBalancerTest.java | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java index 07376af2110..d72a85012f2 100644 --- a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java @@ -22,6 +22,7 @@ import static java.util.concurrent.TimeUnit.NANOSECONDS; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Ticker; import com.google.common.collect.ForwardingMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -39,7 +40,6 @@ import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; -import io.grpc.internal.TimeProvider; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Collection; @@ -82,7 +82,7 @@ public final class OutlierDetectionLoadBalancer extends LoadBalancer { private final SynchronizationContext syncContext; private final Helper childHelper; private final GracefulSwitchLoadBalancer switchLb; - private TimeProvider timeProvider; + private Ticker ticker; private final ScheduledExecutorService timeService; private ScheduledHandle detectionTimerHandle; private Long detectionTimerStartNanos; @@ -95,14 +95,14 @@ public final class OutlierDetectionLoadBalancer extends LoadBalancer { /** * Creates a new instance of {@link OutlierDetectionLoadBalancer}. */ - public OutlierDetectionLoadBalancer(Helper helper, TimeProvider timeProvider) { + public OutlierDetectionLoadBalancer(Helper helper, Ticker ticker) { logger = helper.getChannelLogger(); childHelper = new ChildHelper(checkNotNull(helper, "helper")); switchLb = new GracefulSwitchLoadBalancer(childHelper); endpointTrackerMap = new EndpointTrackerMap(); this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); - this.timeProvider = timeProvider; + this.ticker = ticker; logger.log(ChannelLogLevel.DEBUG, "OutlierDetection lb created."); } @@ -151,7 +151,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { // If a timer has started earlier we cancel it and use the difference between the start // time and now as the interval. initialDelayNanos = Math.max(0L, - config.intervalNanos - (timeProvider.currentTimeNanos() - detectionTimerStartNanos)); + config.intervalNanos - (ticker.read() - detectionTimerStartNanos)); } // If a timer has been previously created we need to cancel it and reset all the call counters @@ -201,7 +201,7 @@ final class DetectionTimer implements Runnable { @Override public void run() { - detectionTimerStartNanos = timeProvider.currentTimeNanos(); + detectionTimerStartNanos = ticker.read(); endpointTrackerMap.swapCounters(); @@ -638,7 +638,7 @@ public boolean maxEjectionTimeElapsed(long currentTimeNanos) { config.baseEjectionTimeNanos * ejectionTimeMultiplier, maxEjectionDurationSecs); - return currentTimeNanos > maxEjectionTimeNanos; + return currentTimeNanos - maxEjectionTimeNanos > 0; } /** Tracks both successful and failed call counts. */ diff --git a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java index b35e1144581..c76d68a03de 100644 --- a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java +++ b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java @@ -16,6 +16,7 @@ package io.grpc.util; +import com.google.common.base.Ticker; import io.grpc.Internal; import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; @@ -23,7 +24,6 @@ import io.grpc.NameResolver.ConfigOrError; import io.grpc.Status; import io.grpc.internal.JsonUtil; -import io.grpc.internal.TimeProvider; import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig; import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig.FailurePercentageEjection; import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig.SuccessRateEjection; @@ -34,7 +34,7 @@ public final class OutlierDetectionLoadBalancerProvider extends LoadBalancerProv @Override public LoadBalancer newLoadBalancer(Helper helper) { - return new OutlierDetectionLoadBalancer(helper, TimeProvider.SYSTEM_TIME_PROVIDER); + return new OutlierDetectionLoadBalancer(helper, Ticker.systemTicker()); } @Override diff --git a/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java b/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java index d81740e116a..c4eb4c7bae5 100644 --- a/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java @@ -227,7 +227,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { when(mockStreamTracerFactory.newClientStreamTracer(any(), any())).thenReturn(mockStreamTracer); - loadBalancer = new OutlierDetectionLoadBalancer(mockHelper, fakeClock.getTimeProvider()); + loadBalancer = new OutlierDetectionLoadBalancer(mockHelper, fakeClock.getTicker()); } @Test From 2604ce8a551244849f5d22dcefe99a7b2b474117 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 13 Jun 2025 13:26:51 -0700 Subject: [PATCH 298/591] xds: XdsNR should be subscribing to clusters with XdsDepManager This is missing behavior defined in gRFC A74: > As per gRFC A31, the ConfigSelector gives each RPC a ref to the > cluster that was selected for it to ensure that the cluster is not > removed from the xds_cluster_manager LB policy config before the RPC > is done with its LB picks. These cluster refs will also hold a > subscription for the cluster from the XdsDependencyManager, so that > the XdsDependencyManager will not stop watching the cluster resource > until the cluster is removed from the xds_cluster_manager LB policy > config. Without the logic, RPCs can race and see the error: > INTERNAL: CdsLb for cluster0: Unable to find non-dynamic root cluster Fixes #12152. This fixes the regression introduced in 297ab05e --- .../java/io/grpc/xds/XdsNameResolver.java | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 37a8e19ef3f..25918facf7e 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -548,7 +548,7 @@ public void run() { if (clusterRefs.get(cluster).refCount.get() != 0) { throw new AssertionError(); } - clusterRefs.remove(cluster); + clusterRefs.remove(cluster).close(); if (resolveState.lastConfigOrStatus.hasValue()) { updateResolutionResult(resolveState.lastConfigOrStatus.getValue()); } else { @@ -793,9 +793,13 @@ private void updateRoutes( clusterRefs.get(cluster).refCount.incrementAndGet(); } else { if (clusterNameMap.containsKey(cluster)) { + assert cluster.startsWith("cluster:"); + XdsConfig.Subscription subscription = + xdsDependencyManager.subscribeToCluster(cluster.substring("cluster:".length())); clusterRefs.put( cluster, - ClusterRefState.forCluster(new AtomicInteger(1), clusterNameMap.get(cluster))); + ClusterRefState.forCluster( + new AtomicInteger(1), clusterNameMap.get(cluster), subscription)); } if (rlsPluginConfigMap.containsKey(cluster)) { clusterRefs.put( @@ -826,7 +830,7 @@ private void updateRoutes( for (String cluster : deletedClusters) { int count = clusterRefs.get(cluster).refCount.decrementAndGet(); if (count == 0) { - clusterRefs.remove(cluster); + clusterRefs.remove(cluster).close(); shouldUpdateResult = true; } } @@ -879,7 +883,7 @@ private void cleanUpRoutes(Status error) { for (String cluster : existingClusters) { int count = clusterRefs.get(cluster).refCount.decrementAndGet(); if (count == 0) { - clusterRefs.remove(cluster); + clusterRefs.remove(cluster).close(); } } existingClusters = null; @@ -965,15 +969,18 @@ private static class ClusterRefState { final String traditionalCluster; @Nullable final RlsPluginConfig rlsPluginConfig; + @Nullable + final XdsConfig.Subscription subscription; private ClusterRefState( AtomicInteger refCount, @Nullable String traditionalCluster, - @Nullable RlsPluginConfig rlsPluginConfig) { + @Nullable RlsPluginConfig rlsPluginConfig, @Nullable XdsConfig.Subscription subscription) { this.refCount = refCount; checkArgument(traditionalCluster == null ^ rlsPluginConfig == null, "There must be exactly one non-null value in traditionalCluster and pluginConfig"); this.traditionalCluster = traditionalCluster; this.rlsPluginConfig = rlsPluginConfig; + this.subscription = subscription; } private Map toLbPolicy() { @@ -993,12 +1000,21 @@ private ClusterRefState( } } - static ClusterRefState forCluster(AtomicInteger refCount, String name) { - return new ClusterRefState(refCount, name, null); + private void close() { + if (subscription != null) { + subscription.close(); + } + } + + static ClusterRefState forCluster( + AtomicInteger refCount, String name, XdsConfig.Subscription subscription) { + return new ClusterRefState(refCount, name, null, checkNotNull(subscription, "subscription")); } - static ClusterRefState forRlsPlugin(AtomicInteger refCount, RlsPluginConfig rlsPluginConfig) { - return new ClusterRefState(refCount, null, rlsPluginConfig); + static ClusterRefState forRlsPlugin( + AtomicInteger refCount, + RlsPluginConfig rlsPluginConfig) { + return new ClusterRefState(refCount, null, rlsPluginConfig, null); } } } From f07eb47cac4713feef6711078b827c6082971533 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 17 Jun 2025 14:14:32 +0000 Subject: [PATCH 299/591] util: Deliver addresses in a random order in MultiChildLb This should often not matter much, but in b/412468630 it was cleary visible that child creation order can skew load for the first batch of RPCs. This doesn't solve all the cases, as further-away backends will still be less likely chosen initially and it is ignorant of the LB policy. But this doesn't impact correctness, is easy, and is one fewer cases to worry about. --- .../io/grpc/util/MultiChildLoadBalancer.java | 27 ++++++++++++-- .../grpc/util/MultiChildLoadBalancerTest.java | 36 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java index ed338343c11..2a93ef964f7 100644 --- a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java @@ -24,7 +24,9 @@ import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Iterables; import com.google.common.collect.Maps; +import com.google.common.primitives.UnsignedInts; import io.grpc.Attributes; import io.grpc.ConnectivityState; import io.grpc.EquivalentAddressGroup; @@ -40,6 +42,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -52,6 +55,7 @@ public abstract class MultiChildLoadBalancer extends LoadBalancer { private static final Logger logger = Logger.getLogger(MultiChildLoadBalancer.class.getName()); + private static final int OFFSET_SEED = new Random().nextInt(); // Modify by replacing the list to release memory when no longer used. private List childLbStates = new ArrayList<>(0); private final Helper helper; @@ -168,9 +172,15 @@ private Status updateChildrenWithResolvedAddresses( childLbState = createChildLbState(entry.getKey()); } newChildLbStates.add(childLbState); - if (entry.getValue() != null) { + } + // Use a random start position for child updates to weakly "shuffle" connection creation order. + // The network will often add noise to the creation order, but this avoids giving earlier + // children a consistent head start. + for (ChildLbState childLbState : offsetIterable(newChildLbStates, OFFSET_SEED)) { + ResolvedAddresses addresses = newChildAddresses.get(childLbState.getKey()); + if (addresses != null) { // update child LB - Status newStatus = childLbState.lb.acceptResolvedAddresses(entry.getValue()); + Status newStatus = childLbState.lb.acceptResolvedAddresses(addresses); if (!newStatus.isOk()) { status = newStatus; } @@ -188,6 +198,19 @@ private Status updateChildrenWithResolvedAddresses( return status; } + @VisibleForTesting + static Iterable offsetIterable(Collection c, int seed) { + int pos; + if (c.isEmpty()) { + pos = 0; + } else { + pos = UnsignedInts.remainder(seed, c.size()); + } + return Iterables.concat( + Iterables.skip(c, pos), + Iterables.limit(c, pos)); + } + @Nullable protected static ConnectivityState aggregateState( @Nullable ConnectivityState overallState, ConnectivityState childState) { diff --git a/util/src/test/java/io/grpc/util/MultiChildLoadBalancerTest.java b/util/src/test/java/io/grpc/util/MultiChildLoadBalancerTest.java index c128364dfd3..14dc8518756 100644 --- a/util/src/test/java/io/grpc/util/MultiChildLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/MultiChildLoadBalancerTest.java @@ -264,6 +264,42 @@ public void testEndpoint_equals() { .testEquals(); } + @Test + public void offsetIterable_positive() { + assertThat(MultiChildLoadBalancer.offsetIterable(Arrays.asList(1, 2, 3, 4), 9)) + .containsExactly(2, 3, 4, 1) + .inOrder(); + assertThat(MultiChildLoadBalancer.offsetIterable(Arrays.asList(1, 2, 3, 4, 5), 9)) + .containsExactly(5, 1, 2, 3, 4) + .inOrder(); + assertThat(MultiChildLoadBalancer.offsetIterable(Arrays.asList(1, 2, 3), 3)) + .containsExactly(1, 2, 3) + .inOrder(); + assertThat(MultiChildLoadBalancer.offsetIterable(Arrays.asList(1, 2, 3), 0)) + .containsExactly(1, 2, 3) + .inOrder(); + assertThat(MultiChildLoadBalancer.offsetIterable(Arrays.asList(1), 123)) + .containsExactly(1) + .inOrder(); + } + + @Test + public void offsetIterable_negative() { + assertThat(MultiChildLoadBalancer.offsetIterable(Arrays.asList(1, 2, 3, 4), -1)) + .containsExactly(4, 1, 2, 3) + .inOrder(); + } + + @Test + public void offsetIterable_empty() { + assertThat(MultiChildLoadBalancer.offsetIterable(Arrays.asList(), 1)) + .isEmpty(); + assertThat(MultiChildLoadBalancer.offsetIterable(Arrays.asList(), 0)) + .isEmpty(); + assertThat(MultiChildLoadBalancer.offsetIterable(Arrays.asList(), -1)) + .isEmpty(); + } + private String addressesOnlyString(EquivalentAddressGroup eag) { if (eag == null) { return null; From d2d8ed8efaa6e10b393188f0b3bd4a0edcb37763 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 12 Jun 2025 15:45:26 -0700 Subject: [PATCH 300/591] xds: Add logical dns cluster support to XdsDepManager ClusterResolverLb gets the NameResolverRegistry from LoadBalancer.Helper, so a new API was added in NameResover.Args to propagate the same object to the name resolver tree. RetryingNameResolver was exposed to xds. This is expected to be temporary, as the retrying is being removed from ManagedChannelImpl and moved into the resolvers. At that point, DnsNameResolverProvider would wrap DnsNameResolver with a similar API to RetryingNameResolver and xds would no longer be responsible. --- api/src/main/java/io/grpc/NameResolver.java | 27 +++ .../io/grpc/internal/ManagedChannelImpl.java | 9 +- .../grpc/internal/RetryingNameResolver.java | 13 +- .../io/grpc/internal/DnsNameResolverTest.java | 9 +- .../io/grpc/xds/XdsDependencyManager.java | 205 +++++++++++++++++- .../java/io/grpc/xds/XdsNameResolver.java | 2 +- .../io/grpc/xds/CdsLoadBalancer2Test.java | 5 +- .../io/grpc/xds/XdsDependencyManagerTest.java | 90 +++++++- 8 files changed, 324 insertions(+), 36 deletions(-) diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java index 21064d7f115..9483f5f3b0a 100644 --- a/api/src/main/java/io/grpc/NameResolver.java +++ b/api/src/main/java/io/grpc/NameResolver.java @@ -303,6 +303,7 @@ public static final class Args { @Nullable private final Executor executor; @Nullable private final String overrideAuthority; @Nullable private final MetricRecorder metricRecorder; + @Nullable private final NameResolverRegistry nameResolverRegistry; @Nullable private final IdentityHashMap, Object> customArgs; private Args(Builder builder) { @@ -316,6 +317,7 @@ private Args(Builder builder) { this.executor = builder.executor; this.overrideAuthority = builder.overrideAuthority; this.metricRecorder = builder.metricRecorder; + this.nameResolverRegistry = builder.nameResolverRegistry; this.customArgs = cloneCustomArgs(builder.customArgs); } @@ -447,6 +449,18 @@ public MetricRecorder getMetricRecorder() { return metricRecorder; } + /** + * Returns the {@link NameResolverRegistry} that the Channel uses to look for {@link + * NameResolver}s. + * + * @since 1.74.0 + */ + public NameResolverRegistry getNameResolverRegistry() { + if (nameResolverRegistry == null) { + throw new IllegalStateException("NameResolverRegistry is not set in Builder"); + } + return nameResolverRegistry; + } @Override public String toString() { @@ -461,6 +475,7 @@ public String toString() { .add("executor", executor) .add("overrideAuthority", overrideAuthority) .add("metricRecorder", metricRecorder) + .add("nameResolverRegistry", nameResolverRegistry) .toString(); } @@ -480,6 +495,7 @@ public Builder toBuilder() { builder.setOffloadExecutor(executor); builder.setOverrideAuthority(overrideAuthority); builder.setMetricRecorder(metricRecorder); + builder.setNameResolverRegistry(nameResolverRegistry); builder.customArgs = cloneCustomArgs(customArgs); return builder; } @@ -508,6 +524,7 @@ public static final class Builder { private Executor executor; private String overrideAuthority; private MetricRecorder metricRecorder; + private NameResolverRegistry nameResolverRegistry; private IdentityHashMap, Object> customArgs; Builder() { @@ -614,6 +631,16 @@ public Builder setMetricRecorder(MetricRecorder metricRecorder) { return this; } + /** + * See {@link Args#getNameResolverRegistry}. This is an optional field. + * + * @since 1.74.0 + */ + public Builder setNameResolverRegistry(NameResolverRegistry registry) { + this.nameResolverRegistry = registry; + return this; + } + /** * Builds an {@link Args}. * diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 9b756c3165d..e8f106c7775 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -597,7 +597,8 @@ ClientStream newSubstream( .setChannelLogger(channelLogger) .setOffloadExecutor(this.offloadExecutorHolder) .setOverrideAuthority(this.authorityOverride) - .setMetricRecorder(this.metricRecorder); + .setMetricRecorder(this.metricRecorder) + .setNameResolverRegistry(builder.nameResolverRegistry); builder.copyAllNameResolverCustomArgsTo(nameResolverArgsBuilder); this.nameResolverArgs = nameResolverArgsBuilder.build(); this.nameResolver = getNameResolver( @@ -685,11 +686,7 @@ static NameResolver getNameResolver( // We wrap the name resolver in a RetryingNameResolver to give it the ability to retry failures. // TODO: After a transition period, all NameResolver implementations that need retry should use // RetryingNameResolver directly and this step can be removed. - NameResolver usedNameResolver = new RetryingNameResolver(resolver, - new BackoffPolicyRetryScheduler(new ExponentialBackoffPolicy.Provider(), - nameResolverArgs.getScheduledExecutorService(), - nameResolverArgs.getSynchronizationContext()), - nameResolverArgs.getSynchronizationContext()); + NameResolver usedNameResolver = RetryingNameResolver.wrap(resolver, nameResolverArgs); if (overrideAuthority == null) { return usedNameResolver; diff --git a/core/src/main/java/io/grpc/internal/RetryingNameResolver.java b/core/src/main/java/io/grpc/internal/RetryingNameResolver.java index 55fedea932e..90827fa8acb 100644 --- a/core/src/main/java/io/grpc/internal/RetryingNameResolver.java +++ b/core/src/main/java/io/grpc/internal/RetryingNameResolver.java @@ -27,13 +27,22 @@ * *

    The {@link NameResolver} used with this */ -final class RetryingNameResolver extends ForwardingNameResolver { +public final class RetryingNameResolver extends ForwardingNameResolver { + public static NameResolver wrap(NameResolver retriedNameResolver, Args args) { + // For migration, this might become conditional + return new RetryingNameResolver( + retriedNameResolver, + new BackoffPolicyRetryScheduler( + new ExponentialBackoffPolicy.Provider(), + args.getScheduledExecutorService(), + args.getSynchronizationContext()), + args.getSynchronizationContext()); + } private final NameResolver retriedNameResolver; private final RetryScheduler retryScheduler; private final SynchronizationContext syncContext; - /** * Creates a new {@link RetryingNameResolver}. * diff --git a/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java b/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java index 130c01d1c04..f5be078f83a 100644 --- a/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java +++ b/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java @@ -207,14 +207,7 @@ private RetryingNameResolver newResolver( // In practice the DNS name resolver provider always wraps the resolver in a // RetryingNameResolver which adds retry capabilities to it. We use the same setup here. - return new RetryingNameResolver( - dnsResolver, - new BackoffPolicyRetryScheduler( - new ExponentialBackoffPolicy.Provider(), - fakeExecutor.getScheduledExecutorService(), - syncContext - ), - syncContext); + return (RetryingNameResolver) RetryingNameResolver.wrap(dnsResolver, args); } @Before diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index 82cfd9ccbdf..cc419e63a70 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -23,18 +23,27 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.grpc.EquivalentAddressGroup; import io.grpc.NameResolver; +import io.grpc.NameResolverProvider; import io.grpc.Status; import io.grpc.StatusOr; import io.grpc.SynchronizationContext; +import io.grpc.internal.RetryingNameResolver; +import io.grpc.xds.Endpoints.LocalityLbEndpoints; import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight; import io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType; import io.grpc.xds.XdsConfig.XdsClusterConfig.AggregateConfig; import io.grpc.xds.XdsConfig.XdsClusterConfig.EndpointConfig; import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; +import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsClient.ResourceWatcher; import io.grpc.xds.client.XdsResourceType; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; @@ -44,7 +53,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.concurrent.ScheduledExecutorService; import javax.annotation.Nullable; /** @@ -55,7 +63,7 @@ */ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegistry { private enum TrackedWatcherTypeEnum { - LDS, RDS, CDS, EDS + LDS, RDS, CDS, EDS, DNS } private static final TrackedWatcherType LDS_TYPE = @@ -66,12 +74,19 @@ private enum TrackedWatcherTypeEnum { new TrackedWatcherType<>(TrackedWatcherTypeEnum.CDS); private static final TrackedWatcherType EDS_TYPE = new TrackedWatcherType<>(TrackedWatcherTypeEnum.EDS); + private static final TrackedWatcherType> DNS_TYPE = + new TrackedWatcherType<>(TrackedWatcherTypeEnum.DNS); + + // DNS-resolved endpoints do not have the definition of the locality it belongs to, just hardcode + // to an empty locality. + private static final Locality LOGICAL_DNS_CLUSTER_LOCALITY = Locality.create("", "", ""); private static final int MAX_CLUSTER_RECURSION_DEPTH = 16; // Specified by gRFC A37 private final String listenerName; private final XdsClient xdsClient; private final SynchronizationContext syncContext; private final String dataPlaneAuthority; + private final NameResolver.Args nameResolverArgs; private XdsConfigWatcher xdsConfigWatcher; private StatusOr lastUpdate = null; @@ -79,16 +94,17 @@ private enum TrackedWatcherTypeEnum { new EnumMap<>(TrackedWatcherTypeEnum.class); private final Set subscriptions = new HashSet<>(); - XdsDependencyManager(XdsClient xdsClient, - SynchronizationContext syncContext, String dataPlaneAuthority, - String listenerName, NameResolver.Args nameResolverArgs, - ScheduledExecutorService scheduler) { + XdsDependencyManager( + XdsClient xdsClient, + SynchronizationContext syncContext, + String dataPlaneAuthority, + String listenerName, + NameResolver.Args nameResolverArgs) { this.listenerName = checkNotNull(listenerName, "listenerName"); this.xdsClient = checkNotNull(xdsClient, "xdsClient"); this.syncContext = checkNotNull(syncContext, "syncContext"); this.dataPlaneAuthority = checkNotNull(dataPlaneAuthority, "dataPlaneAuthority"); - checkNotNull(nameResolverArgs, "nameResolverArgs"); - checkNotNull(scheduler, "scheduler"); + this.nameResolverArgs = checkNotNull(nameResolverArgs, "nameResolverArgs"); } public static String toContextStr(String typeName, String resourceName) { @@ -120,6 +136,18 @@ public XdsConfig.Subscription subscribeToCluster(String clusterName) { return subscription; } + /** + * For all logical dns clusters refresh their results. + */ + public void requestReresolution() { + syncContext.execute(() -> { + for (TrackedWatcher> watcher : getWatchers(DNS_TYPE).values()) { + DnsWatcher dnsWatcher = (DnsWatcher) watcher; + dnsWatcher.refresh(); + } + }); + } + private void addWatcher( TrackedWatcherType watcherType, XdsWatcherBase watcher) { syncContext.throwIfNotInThisSynchronizationContext(); @@ -335,9 +363,9 @@ private static void addConfigForCluster( } break; case LOGICAL_DNS: - // TODO get the resolved endpoint configuration - child = new EndpointConfig(StatusOr.fromStatus( - Status.INTERNAL.withDescription("Logical DNS in dependency manager unsupported"))); + TrackedWatcher> dnsWatcher = + tracer.getWatcher(DNS_TYPE, cdsUpdate.dnsHostName()); + child = new EndpointConfig(dnsToEdsUpdate(dnsWatcher.getData(), cdsUpdate.dnsHostName())); break; default: child = new EndpointConfig(StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( @@ -352,6 +380,24 @@ private static void addConfigForCluster( new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, child))); } + private static StatusOr dnsToEdsUpdate( + StatusOr> dnsData, String dnsHostName) { + if (!dnsData.hasValue()) { + return StatusOr.fromStatus(dnsData.getStatus()); + } + + List endpoints = new ArrayList<>(); + for (EquivalentAddressGroup eag : dnsData.getValue()) { + endpoints.add(Endpoints.LbEndpoint.create(eag, 1, true, dnsHostName, ImmutableMap.of())); + } + LocalityLbEndpoints lbEndpoints = + LocalityLbEndpoints.create(endpoints, 1, 0, ImmutableMap.of()); + return StatusOr.fromValue(new XdsEndpointResource.EdsUpdate( + "fakeEds_logicalDns", + Collections.singletonMap(LOGICAL_DNS_CLUSTER_LOCALITY, lbEndpoints), + new ArrayList<>())); + } + private void addRdsWatcher(String resourceName) { if (getWatchers(RDS_TYPE).containsKey(resourceName)) { return; @@ -376,6 +422,17 @@ private void addClusterWatcher(String clusterName) { addWatcher(CDS_TYPE, new CdsWatcher(clusterName)); } + private void addDnsWatcher(String dnsHostName) { + syncContext.throwIfNotInThisSynchronizationContext(); + if (getWatchers(DNS_TYPE).containsKey(dnsHostName)) { + return; + } + + DnsWatcher watcher = new DnsWatcher(dnsHostName, nameResolverArgs); + getWatchers(DNS_TYPE).put(dnsHostName, watcher); + watcher.start(); + } + private void updateRoutes(List virtualHosts) { VirtualHost virtualHost = RoutingUtils.findVirtualHostForHostName(virtualHosts, dataPlaneAuthority); @@ -411,6 +468,33 @@ private static Set getClusterNamesFromVirtualHost(VirtualHost virtualHos return clusters; } + private static NameResolver createNameResolver( + String dnsHostName, + NameResolver.Args nameResolverArgs) { + URI uri; + try { + uri = new URI("dns", "", "/" + dnsHostName, null); + } catch (URISyntaxException e) { + return new FailingNameResolver( + Status.INTERNAL.withDescription("Bug, invalid URI creation: " + dnsHostName) + .withCause(e)); + } + + NameResolverProvider provider = + nameResolverArgs.getNameResolverRegistry().getProviderForScheme("dns"); + if (provider == null) { + return new FailingNameResolver( + Status.INTERNAL.withDescription("Could not find dns name resolver")); + } + + NameResolver bareResolver = provider.newNameResolver(uri, nameResolverArgs); + if (bareResolver == null) { + return new FailingNameResolver( + Status.INTERNAL.withDescription("DNS name resolver provider returned null: " + uri)); + } + return RetryingNameResolver.wrap(bareResolver, nameResolverArgs); + } + private static class TypeWatchers { // Key is resource name final Map> watchers = new HashMap<>(); @@ -722,7 +806,7 @@ public void subscribeToChildren(XdsClusterResource.CdsUpdate update) { addEdsWatcher(getEdsServiceName()); break; case LOGICAL_DNS: - // no eds needed + addDnsWatcher(update.dnsHostName()); break; case AGGREGATE: update.prioritizedClusterNames() @@ -751,4 +835,101 @@ private EdsWatcher(String resourceName) { @Override public void subscribeToChildren(XdsEndpointResource.EdsUpdate update) {} } + + private final class DnsWatcher implements TrackedWatcher> { + private final NameResolver resolver; + @Nullable + private StatusOr> data; + private boolean cancelled; + + public DnsWatcher(String dnsHostName, NameResolver.Args nameResolverArgs) { + this.resolver = createNameResolver(dnsHostName, nameResolverArgs); + } + + public void start() { + resolver.start(new NameResolverListener()); + } + + public void refresh() { + if (cancelled) { + return; + } + resolver.refresh(); + } + + @Override + @Nullable + public StatusOr> getData() { + return data; + } + + @Override + public void close() { + if (cancelled) { + return; + } + cancelled = true; + resolver.shutdown(); + } + + private class NameResolverListener extends NameResolver.Listener2 { + @Override + public void onResult(final NameResolver.ResolutionResult resolutionResult) { + syncContext.execute(() -> onResult2(resolutionResult)); + } + + @Override + public Status onResult2(final NameResolver.ResolutionResult resolutionResult) { + if (cancelled) { + return Status.OK; + } + data = resolutionResult.getAddressesOrError(); + maybePublishConfig(); + return resolutionResult.getAddressesOrError().getStatus(); + } + + @Override + public void onError(final Status error) { + syncContext.execute(new Runnable() { + @Override + public void run() { + if (cancelled) { + return; + } + // DnsNameResolver cannot distinguish between address-not-found and transient errors. + // Assume it is a transient error. + // TODO: Once the resolution note API is available, don't throw away the error if + // hasDataValue(); pass it as the note instead + if (!hasDataValue()) { + data = StatusOr.fromStatus(error); + maybePublishConfig(); + } + } + }); + } + } + } + + private static final class FailingNameResolver extends NameResolver { + private final Status status; + + public FailingNameResolver(Status status) { + checkNotNull(status, "status"); + checkArgument(!status.isOk(), "Status must not be OK"); + this.status = status; + } + + @Override + public void start(Listener2 listener) { + listener.onError(status); + } + + @Override + public String getServiceAuthority() { + return "bug-if-you-see-this-authority"; + } + + @Override + public void shutdown() {} + } } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 25918facf7e..c71e4dc255d 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -655,7 +655,7 @@ private ResolveState(String ldsResourceName) { authority = overrideAuthority != null ? overrideAuthority : encodedServiceAuthority; xdsDependencyManager = new XdsDependencyManager(xdsClient, syncContext, authority, ldsResourceName, - nameResolverArgs, scheduler); + nameResolverArgs); } void start() { diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java index f9a09f704a7..258e2909203 100644 --- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java +++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java @@ -66,6 +66,7 @@ import io.grpc.LoadBalancerProvider; import io.grpc.LoadBalancerRegistry; import io.grpc.NameResolver; +import io.grpc.NameResolverRegistry; import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.SynchronizationContext; @@ -176,6 +177,7 @@ public void setUp() throws Exception { .setServiceConfigParser(mock(NameResolver.ServiceConfigParser.class)) .setChannelLogger(mock(ChannelLogger.class)) .setScheduledExecutorService(fakeClock.getScheduledExecutorService()) + .setNameResolverRegistry(new NameResolverRegistry()) .build(); xdsDepManager = new XdsDependencyManager( @@ -183,8 +185,7 @@ public void setUp() throws Exception { syncContext, SERVER_NAME, SERVER_NAME, - nameResolverArgs, - fakeClock.getScheduledExecutorService()); + nameResolverArgs); controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, ImmutableMap.of( SERVER_NAME, ControlPlaneRule.buildClientListener(SERVER_NAME, "my-route"))); diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index 8ac0f8c9d8c..5a897de3a72 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -42,12 +42,19 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.Message; import io.envoyproxy.envoy.config.cluster.v3.Cluster; +import io.envoyproxy.envoy.config.core.v3.Address; +import io.envoyproxy.envoy.config.core.v3.SocketAddress; import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; +import io.envoyproxy.envoy.config.endpoint.v3.Endpoint; +import io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint; +import io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints; import io.envoyproxy.envoy.config.listener.v3.Listener; import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; import io.grpc.BindableService; import io.grpc.ChannelLogger; +import io.grpc.EquivalentAddressGroup; import io.grpc.NameResolver; +import io.grpc.NameResolverRegistry; import io.grpc.Status; import io.grpc.StatusOr; import io.grpc.StatusOrMatcher; @@ -56,10 +63,12 @@ import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.internal.FakeClock; import io.grpc.internal.GrpcUtil; +import io.grpc.internal.testing.FakeNameResolverProvider; import io.grpc.testing.GrpcCleanupRule; import io.grpc.xds.XdsClusterResource.CdsUpdate; import io.grpc.xds.XdsConfig.XdsClusterConfig; import io.grpc.xds.XdsEndpointResource.EdsUpdate; +import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsClient.ResourceMetadata; import io.grpc.xds.client.XdsResourceType; @@ -74,7 +83,6 @@ import java.util.Map; import java.util.Queue; import java.util.Set; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; @@ -121,6 +129,7 @@ public class XdsDependencyManagerTest { private final XdsTestControlPlaneService controlPlaneService = new XdsTestControlPlaneService(); private final BindableService lrsService = XdsTestUtils.createLrsService(lrsEnded, loadReportCalls); + private final NameResolverRegistry nameResolverRegistry = new NameResolverRegistry(); @Rule public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule(); @@ -138,11 +147,11 @@ public class XdsDependencyManagerTest { .setServiceConfigParser(mock(NameResolver.ServiceConfigParser.class)) .setChannelLogger(mock(ChannelLogger.class)) .setScheduledExecutorService(fakeClock.getScheduledExecutorService()) + .setNameResolverRegistry(nameResolverRegistry) .build(); - private final ScheduledExecutorService scheduler = fakeClock.getScheduledExecutorService(); private XdsDependencyManager xdsDependencyManager = new XdsDependencyManager( - xdsClient, syncContext, serverName, serverName, nameResolverArgs, scheduler); + xdsClient, syncContext, serverName, serverName, nameResolverArgs); @Before public void setUp() throws Exception { @@ -369,7 +378,7 @@ public void testMissingCdsAndEds() { public void testMissingLds() { String ldsName = "badLdsName"; xdsDependencyManager = new XdsDependencyManager(xdsClient, syncContext, - serverName, ldsName, nameResolverArgs, scheduler); + serverName, ldsName, nameResolverArgs); xdsDependencyManager.start(xdsConfigWatcher); fakeClock.forwardTime(16, TimeUnit.SECONDS); @@ -434,7 +443,7 @@ public void testCorruptLds() { "xdstp://unknown.example.com/envoy.config.listener.v3.Listener/listener1"; xdsDependencyManager = new XdsDependencyManager(xdsClient, syncContext, - serverName, ldsResourceName, nameResolverArgs, scheduler); + serverName, ldsResourceName, nameResolverArgs); xdsDependencyManager.start(xdsConfigWatcher); verify(xdsConfigWatcher).onUpdate( @@ -738,6 +747,75 @@ public void testChangeAggCluster() { inOrder.verify(xdsConfigWatcher).onUpdate(argThat(nameMatcher)); } + @Test + public void testLogicalDns_success() { + FakeSocketAddress fakeAddress = new FakeSocketAddress(); + nameResolverRegistry.register(new FakeNameResolverProvider( + "dns:///dns.example.com:1111", fakeAddress)); + Cluster cluster = Cluster.newBuilder() + .setName(CLUSTER_NAME) + .setType(Cluster.DiscoveryType.LOGICAL_DNS) + .setLoadAssignment(ClusterLoadAssignment.newBuilder() + .addEndpoints(LocalityLbEndpoints.newBuilder() + .addLbEndpoints(LbEndpoint.newBuilder() + .setEndpoint(Endpoint.newBuilder() + .setAddress(Address.newBuilder() + .setSocketAddress(SocketAddress.newBuilder() + .setAddress("dns.example.com") + .setPortValue(1111))))))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, + ImmutableMap.of(CLUSTER_NAME, cluster)); + xdsDependencyManager.start(xdsConfigWatcher); + + verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); + XdsConfig config = xdsUpdateCaptor.getValue().getValue(); + XdsClusterConfig.ClusterChild clusterChild = + config.getClusters().get(CLUSTER_NAME).getValue().getChildren(); + assertThat(clusterChild).isInstanceOf(XdsClusterConfig.EndpointConfig.class); + StatusOr endpointOr = ((XdsClusterConfig.EndpointConfig) clusterChild).getEndpoint(); + assertThat(endpointOr.getStatus()).isEqualTo(Status.OK); + assertThat(endpointOr.getValue()).isEqualTo(new EdsUpdate( + "fakeEds_logicalDns", + ImmutableMap.of( + Locality.create("", "", ""), + Endpoints.LocalityLbEndpoints.create( + Arrays.asList(Endpoints.LbEndpoint.create( + new EquivalentAddressGroup(fakeAddress), + 1, true, "dns.example.com:1111", ImmutableMap.of())), + 1, 0, ImmutableMap.of())), + Arrays.asList())); + } + + @Test + public void testLogicalDns_noDnsNr() { + Cluster cluster = Cluster.newBuilder() + .setName(CLUSTER_NAME) + .setType(Cluster.DiscoveryType.LOGICAL_DNS) + .setLoadAssignment(ClusterLoadAssignment.newBuilder() + .addEndpoints(LocalityLbEndpoints.newBuilder() + .addLbEndpoints(LbEndpoint.newBuilder() + .setEndpoint(Endpoint.newBuilder() + .setAddress(Address.newBuilder() + .setSocketAddress(SocketAddress.newBuilder() + .setAddress("dns.example.com") + .setPortValue(1111))))))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, + ImmutableMap.of(CLUSTER_NAME, cluster)); + xdsDependencyManager.start(xdsConfigWatcher); + + verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture()); + XdsConfig config = xdsUpdateCaptor.getValue().getValue(); + XdsClusterConfig.ClusterChild clusterChild = + config.getClusters().get(CLUSTER_NAME).getValue().getChildren(); + assertThat(clusterChild).isInstanceOf(XdsClusterConfig.EndpointConfig.class); + StatusOr endpointOr = ((XdsClusterConfig.EndpointConfig) clusterChild).getEndpoint(); + assertThat(endpointOr.getStatus().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(endpointOr.getStatus().getDescription()) + .isEqualTo("Could not find dns name resolver"); + } + @Test public void testCdsError() throws IOException { controlPlaneService.setXdsConfig( @@ -943,4 +1021,6 @@ public boolean matches(StatusOr update) { && xdsConfig.getClusters().keySet().containsAll(expectedNames); } } + + private static class FakeSocketAddress extends java.net.SocketAddress {} } From 922dc8a999531c91b0fa77386e0609745fae939f Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 17 Jun 2025 22:29:21 -0700 Subject: [PATCH 301/591] Mark a few test helper methods as @CanIgnoreReturnValue (#12162) --- .../io/grpc/binder/internal/BinderClientTransportTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java index 33c127f97a7..4a4657e7814 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java @@ -27,6 +27,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.concurrent.GuardedBy; import com.google.protobuf.Empty; import io.grpc.CallOptions; @@ -154,17 +155,20 @@ private class BinderClientTransportBuilder { .setScheduledExecutorPool(executorServicePool) .setOffloadExecutorPool(offloadServicePool); + @CanIgnoreReturnValue public BinderClientTransportBuilder setSecurityPolicy(SecurityPolicy securityPolicy) { factoryBuilder.setSecurityPolicy(securityPolicy); return this; } + @CanIgnoreReturnValue public BinderClientTransportBuilder setBinderDecorator( OneWayBinderProxy.Decorator binderDecorator) { factoryBuilder.setBinderDecorator(binderDecorator); return this; } + @CanIgnoreReturnValue public BinderClientTransportBuilder setReadyTimeoutMillis(int timeoutMillis) { factoryBuilder.setReadyTimeoutMillis(timeoutMillis); return this; From e6e7bcadafccf908e7bb6f847d86759cfa74fbdb Mon Sep 17 00:00:00 2001 From: Kun Zhang Date: Wed, 18 Jun 2025 14:47:24 -0700 Subject: [PATCH 302/591] binder: stops emulating for 21/22 Lollipop in tests See cl/769936336 internally --- .../test/java/io/grpc/binder/SecurityPoliciesTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java b/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java index 84c76a84bf2..71180ed43c5 100644 --- a/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java +++ b/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java @@ -357,7 +357,7 @@ public void testIsDeviceOwner_failsWhenNoPackagesForUid() throws Exception { } @Test - @Config(sdk = 21) + @Config(sdk = Config.OLDEST_SDK) public void testIsProfileOwner_succeedsForProfileOwner() throws Exception { PackageInfo info = newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); @@ -371,7 +371,7 @@ public void testIsProfileOwner_succeedsForProfileOwner() throws Exception { } @Test - @Config(sdk = 21) + @Config(sdk = Config.OLDEST_SDK) public void testIsProfileOwner_failsForNotProfileOwner() throws Exception { PackageInfo info = newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); @@ -385,7 +385,7 @@ public void testIsProfileOwner_failsForNotProfileOwner() throws Exception { } @Test - @Config(sdk = 21) + @Config(sdk = Config.OLDEST_SDK) public void testIsProfileOwner_failsWhenNoPackagesForUid() throws Exception { policy = SecurityPolicies.isProfileOwner(appContext); @@ -425,7 +425,7 @@ public void testIsProfileOwnerOnOrgOwned_failsForProfileOwnerOnNonOrgOwned() thr } @Test - @Config(sdk = 21) + @Config(sdk = Config.OLDEST_SDK) public void testIsProfileOwnerOnOrgOwned_failsForNotProfileOwner() throws Exception { PackageInfo info = newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); @@ -439,7 +439,7 @@ public void testIsProfileOwnerOnOrgOwned_failsForNotProfileOwner() throws Except } @Test - @Config(sdk = 21) + @Config(sdk = Config.OLDEST_SDK) public void testIsProfileOwnerOnOrgOwned_failsWhenNoPackagesForUid() throws Exception { policy = SecurityPolicies.isProfileOwnerOnOrganizationOwnedDevice(appContext); From 9a6bdc70af8ef015e24a2b963b57c9d9e6f9bf00 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Mon, 23 Jun 2025 12:00:25 -0700 Subject: [PATCH 303/591] download maven using archive/permalink url (#12169) --- buildscripts/grpc-java-artifacts/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildscripts/grpc-java-artifacts/Dockerfile b/buildscripts/grpc-java-artifacts/Dockerfile index 0cc5634b9df..54c595cd960 100644 --- a/buildscripts/grpc-java-artifacts/Dockerfile +++ b/buildscripts/grpc-java-artifacts/Dockerfile @@ -31,7 +31,7 @@ RUN curl -Ls https://github.com/Kitware/CMake/releases/download/v3.26.3/cmake-3. tar xz -C /var/local # Install Maven -RUN curl -Ls https://dlcdn.apache.org/maven/maven-3/3.8.8/binaries/apache-maven-3.8.8-bin.tar.gz | \ +RUN curl -Ls https://archive.apache.org/dist/maven/maven-3/3.8.8/binaries/apache-maven-3.8.8-bin.tar.gz | \ tar xz -C /var/local ENV PATH /var/local/cmake-3.26.3-linux-x86_64/bin:/var/local/apache-maven-3.8.8/bin:$PATH From 30d40a6179c514ab88e8892d9aec4ced0763f60b Mon Sep 17 00:00:00 2001 From: John Cormie Date: Mon, 23 Jun 2025 12:51:40 -0700 Subject: [PATCH 304/591] binder: Cancel checkAuthorization() request if still pending upon termination (#12167) --- .../internal/BinderClientTransportTest.java | 63 +++++++------- .../grpc/binder/internal/BinderTransport.java | 9 +- .../internal/SettableAsyncSecurityPolicy.java | 83 +++++++++++++++++++ 3 files changed, 120 insertions(+), 35 deletions(-) create mode 100644 binder/src/testFixtures/java/io/grpc/binder/internal/SettableAsyncSecurityPolicy.java diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java index 4a4657e7814..2af7210e9a7 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java @@ -17,6 +17,7 @@ package io.grpc.binder.internal; import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.SECONDS; import android.content.Context; import android.os.DeadObjectException; @@ -24,8 +25,6 @@ import android.os.RemoteException; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.concurrent.GuardedBy; @@ -39,13 +38,13 @@ import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.binder.AndroidComponentAddress; -import io.grpc.binder.AsyncSecurityPolicy; import io.grpc.binder.BinderServerBuilder; import io.grpc.binder.HostServices; import io.grpc.binder.SecurityPolicy; import io.grpc.binder.internal.OneWayBinderProxies.BlackHoleOneWayBinderProxy; import io.grpc.binder.internal.OneWayBinderProxies.BlockingBinderDecorator; import io.grpc.binder.internal.OneWayBinderProxies.ThrowingOneWayBinderProxy; +import io.grpc.binder.internal.SettableAsyncSecurityPolicy.AuthRequest; import io.grpc.internal.ClientStream; import io.grpc.internal.ClientStreamListener; import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; @@ -64,7 +63,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import org.junit.After; import org.junit.Before; @@ -193,7 +191,7 @@ public void tearDown() throws Exception { private static void shutdownAndTerminate(ExecutorService executorService) throws InterruptedException { executorService.shutdownNow(); - if (!executorService.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + if (!executorService.awaitTermination(TIMEOUT_SECONDS, SECONDS)) { throw new AssertionError("executor failed to terminate promptly"); } } @@ -375,17 +373,23 @@ public void testBlackHoleEndpointConnectTimeout() throws Exception { @Test public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception { + SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy(); transport = new BinderClientTransportBuilder() - .setSecurityPolicy(blockingSecurityPolicy) + .setSecurityPolicy(securityPolicy) .setReadyTimeoutMillis(1_234) .build(); transport.start(transportListener).run(); + // Take the next authRequest but don't respond to it, in order to trigger the ready timeout. + AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS); + Status transportStatus = transportListener.awaitShutdown(); assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED); assertThat(transportStatus.getDescription()).contains("1234"); transportListener.awaitTermination(); - blockingSecurityPolicy.provideNextCheckAuthorizationResult(Status.OK); + + // If the transport gave up waiting on auth, it should cancel its request. + assertThat(authRequest.isCancelled()).isTrue(); } @Test @@ -393,8 +397,8 @@ public void testAsyncSecurityPolicyFailure() throws Exception { SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy(); transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build(); RuntimeException exception = new NullPointerException(); - securityPolicy.setAuthorizationException(exception); transport.start(transportListener).run(); + securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception); Status transportStatus = transportListener.awaitShutdown(); assertThat(transportStatus.getCode()).isEqualTo(Code.INTERNAL); assertThat(transportStatus.getCause()).isEqualTo(exception); @@ -405,13 +409,27 @@ public void testAsyncSecurityPolicyFailure() throws Exception { public void testAsyncSecurityPolicySuccess() throws Exception { SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy(); transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build(); - securityPolicy.setAuthorizationResult(Status.PERMISSION_DENIED); transport.start(transportListener).run(); + securityPolicy + .takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS) + .setResult(Status.PERMISSION_DENIED); Status transportStatus = transportListener.awaitShutdown(); assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED); transportListener.awaitTermination(); } + @Test + public void testAsyncSecurityPolicyCancelledUponExternalTermination() throws Exception { + SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy(); + transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build(); + transport.start(transportListener).run(); + AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS); + transport.shutdownNow(Status.UNAVAILABLE); // 'authRequest' remains unanswered! + transportListener.awaitShutdown(); + transportListener.awaitTermination(); + assertThat(authRequest.isCancelled()).isTrue(); + } + private static void startAndAwaitReady( BinderTransport.BinderClientTransport transport, TestTransportListener transportListener) throws Exception { @@ -433,7 +451,7 @@ public void transportShutdown(Status shutdownStatus) { } public Status awaitShutdown() throws Exception { - return shutdownStatus.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return shutdownStatus.get(TIMEOUT_SECONDS, SECONDS); } @Override @@ -444,7 +462,7 @@ public void transportTerminated() { } public void awaitTermination() throws Exception { - isTerminated.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + isTerminated.get(TIMEOUT_SECONDS, SECONDS); } @Override @@ -455,7 +473,7 @@ public void transportReady() { } public void awaitReady() throws Exception { - isReady.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + isReady.get(TIMEOUT_SECONDS, SECONDS); } @Override @@ -571,25 +589,4 @@ public Status checkAuthorization(int uid) { } } } - - /** An AsyncSecurityPolicy that lets a test specify the outcome of checkAuthorizationAsync(). */ - static class SettableAsyncSecurityPolicy extends AsyncSecurityPolicy { - private SettableFuture result = SettableFuture.create(); - - public void clearAuthorizationResult() { - result = SettableFuture.create(); - } - - public boolean setAuthorizationResult(Status status) { - return result.set(status); - } - - public boolean setAuthorizationException(Throwable t) { - return result.setException(t); - } - - public ListenableFuture checkAuthorizationAsync(int uid) { - return Futures.nonCancellationPropagating(result); - } - } } diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 4b35137aa54..92a58b91cf0 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -582,6 +582,8 @@ public static final class BinderClientTransport extends BinderTransport @GuardedBy("this") private ScheduledFuture readyTimeoutFuture; // != null iff timeout scheduled. + @GuardedBy("this") + @Nullable private ListenableFuture authResultFuture; // null before we check auth. /** * Constructs a new transport instance. @@ -756,6 +758,9 @@ void notifyTerminated() { readyTimeoutFuture.cancel(false); readyTimeoutFuture = null; } + if (authResultFuture != null) { + authResultFuture.cancel(false); // No effect if already complete. + } serviceBinding.unbind(); clientTransportListener.transportTerminated(); } @@ -775,13 +780,13 @@ protected void handleSetupTransport(Parcel parcel) { shutdownInternal( Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); } else { - ListenableFuture authFuture = + authResultFuture = (securityPolicy instanceof AsyncSecurityPolicy) ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) : Futures.submit( () -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); Futures.addCallback( - authFuture, + authResultFuture, new FutureCallback() { @Override public void onSuccess(Status result) { diff --git a/binder/src/testFixtures/java/io/grpc/binder/internal/SettableAsyncSecurityPolicy.java b/binder/src/testFixtures/java/io/grpc/binder/internal/SettableAsyncSecurityPolicy.java new file mode 100644 index 00000000000..2cb22c2fdbf --- /dev/null +++ b/binder/src/testFixtures/java/io/grpc/binder/internal/SettableAsyncSecurityPolicy.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.binder.internal; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import io.grpc.Status; +import io.grpc.binder.AsyncSecurityPolicy; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * An {@link AsyncSecurityPolicy} that lets unit tests verify the exact order of authorization + * requests and respond to them one at a time. + */ +public class SettableAsyncSecurityPolicy extends AsyncSecurityPolicy { + private final LinkedBlockingDeque pendingRequests = new LinkedBlockingDeque<>(); + + @Override + public ListenableFuture checkAuthorizationAsync(int uid) { + AuthRequest request = new AuthRequest(uid); + pendingRequests.add(request); + return request.resultFuture; + } + + /** + * Waits for the next "check authorization" request to be made and returns it, throwing in case no + * request arrives in time. + */ + public AuthRequest takeNextAuthRequest(long timeout, TimeUnit unit) + throws InterruptedException, TimeoutException { + AuthRequest nextAuthRequest = pendingRequests.poll(timeout, unit); + if (nextAuthRequest == null) { + throw new TimeoutException(); + } + return nextAuthRequest; + } + + /** Represents a single call to {@link AsyncSecurityPolicy#checkAuthorizationAsync(int)}. */ + public static class AuthRequest { + + /** The argument passed to {@link AsyncSecurityPolicy#checkAuthorizationAsync(int)}. */ + public final int uid; + + private final SettableFuture resultFuture = SettableFuture.create(); + + private AuthRequest(int uid) { + this.uid = uid; + } + + /** Provides this SecurityPolicy's response to this authorization request. */ + public void setResult(Status result) { + checkState(resultFuture.set(result)); + } + + /** Simulates an exceptional response to this authorization request. */ + public void setResult(Throwable t) { + checkState(resultFuture.setException(t)); + } + + /** Tests if the future returned for this authorization request was cancelled by the caller. */ + public boolean isCancelled() { + return resultFuture.isCancelled(); + } + } +} \ No newline at end of file From f99b2aaef884da0fbe4ade56df98c8a037e974a3 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 24 Jun 2025 10:12:35 +0530 Subject: [PATCH 305/591] release: Migrate artifacts publishing from legacy OSSRH to Central Portal (#12156) --- README.md | 2 +- RELEASING.md | 2 +- build.gradle | 8 ++++---- buildscripts/sonatype-upload.sh | 17 ++++++++++++++++- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 012eab498e6..a925d1535f5 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ For [Bazel](https://bazel.build), you can either https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.73.0 Development snapshots are available in [Sonatypes's snapshot -repository](https://oss.sonatype.org/content/repositories/snapshots/). +repository](https://central.sonatype.com/repository/maven-snapshots/). Generated Code -------------- diff --git a/RELEASING.md b/RELEASING.md index 120b0ea6ac1..c57829b8c25 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -160,7 +160,7 @@ Tagging the Release repository can then be `released`, which will begin the process of pushing the new artifacts to Maven Central (the staging repository will be destroyed in the process). You can see the complete process for releasing to Maven - Central on the [OSSRH site](https://central.sonatype.org/pages/releasing-the-deployment.html). + Central on the [OSSRH site](https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/#deploying). 10. We have containers for each release to detect compatibility regressions with old releases. Generate one for the new release by following the [GCR image diff --git a/build.gradle b/build.gradle index 089c78639b4..afea703923f 100644 --- a/build.gradle +++ b/build.gradle @@ -389,11 +389,11 @@ subprojects { url = new File(rootProject.repositoryDir).toURI() } else { String stagingUrl + String baseUrl = "https://ossrh-staging-api.central.sonatype.com/service/local" if (rootProject.hasProperty('repositoryId')) { - stagingUrl = 'https://oss.sonatype.org/service/local/staging/deployByRepositoryId/' + - rootProject.repositoryId + stagingUrl = "${baseUrl}/staging/deployByRepositoryId/" + rootProject.repositoryId } else { - stagingUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' + stagingUrl = "${baseUrl}/staging/deploy/maven2/" } credentials { if (rootProject.hasProperty('ossrhUsername') && rootProject.hasProperty('ossrhPassword')) { @@ -402,7 +402,7 @@ subprojects { } } def releaseUrl = stagingUrl - def snapshotUrl = 'https://oss.sonatype.org/content/repositories/snapshots/' + def snapshotUrl = 'https://central.sonatype.com/repository/maven-snapshots/' url = version.endsWith('SNAPSHOT') ? snapshotUrl : releaseUrl } } diff --git a/buildscripts/sonatype-upload.sh b/buildscripts/sonatype-upload.sh index 16637149126..4baa4e46ca0 100755 --- a/buildscripts/sonatype-upload.sh +++ b/buildscripts/sonatype-upload.sh @@ -59,7 +59,7 @@ if [ -z "$USERNAME" -o -z "$PASSWORD" ]; then exit 1 fi -STAGING_URL="https://oss.sonatype.org/service/local/staging" +STAGING_URL="https://ossrh-staging-api.central.sonatype.com/service/local/staging" # We go through the effort of using deloyByRepositoryId/ because it is # _substantially_ faster to upload files than deploy/maven2/. When using @@ -108,3 +108,18 @@ XML=" " curl --fail-with-body -X POST -d "$XML" -u "$USERPASS" -H "Content-Type: application/xml" \ "$STAGING_URL/profiles/$PROFILE_ID/finish" + +# TODO (okshiva): After 2-3 releases make it automatic. +# After closing the repository on the staging API, we must manually trigger +# its upload to the main Central Publisher Portal. We set publishing_type=automatic +# to have it release automatically upon passing validation. +# echo "Triggering release of repository ${REPOID} to the Central Portal" + +# MANUAL_API_URL="https://ossrh-staging-api.central.sonatype.com/service/local/manual" + +#curl --fail-with-body -X POST \ +# -H "Authorization: Bearer ${USERPASS}" \ +# -H "Content-Type: application/json" \ +# "${MANUAL_API_URL}/upload/repository/${REPOID}?publishing_type=automatic" + +# echo "Release triggered. Monitor progress at https://central.sonatype.com/publishing/deployments" From d374b26b6891255556ab4476f4ce22871219ae76 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 23 Jun 2025 13:19:55 -0700 Subject: [PATCH 306/591] xds: Disable LOGICAL_DNS in XdsDepMan until used ClusterResolverLb is still doing DNS itself, so disable it in XdsDepMan until that migration has finished. EDS is fine in XdsDepman, because XdsClient will share the result with ClusterResolverLb. --- .../java/io/grpc/xds/XdsDependencyManager.java | 18 ++++++++++++++---- .../io/grpc/xds/XdsDependencyManagerTest.java | 7 +++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index cc419e63a70..5ed3821b7e4 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -82,6 +82,9 @@ private enum TrackedWatcherTypeEnum { private static final Locality LOGICAL_DNS_CLUSTER_LOCALITY = Locality.create("", "", ""); private static final int MAX_CLUSTER_RECURSION_DEPTH = 16; // Specified by gRFC A37 + + static boolean enableLogicalDns = false; + private final String listenerName; private final XdsClient xdsClient; private final SynchronizationContext syncContext; @@ -363,9 +366,14 @@ private static void addConfigForCluster( } break; case LOGICAL_DNS: - TrackedWatcher> dnsWatcher = - tracer.getWatcher(DNS_TYPE, cdsUpdate.dnsHostName()); - child = new EndpointConfig(dnsToEdsUpdate(dnsWatcher.getData(), cdsUpdate.dnsHostName())); + if (enableLogicalDns) { + TrackedWatcher> dnsWatcher = + tracer.getWatcher(DNS_TYPE, cdsUpdate.dnsHostName()); + child = new EndpointConfig(dnsToEdsUpdate(dnsWatcher.getData(), cdsUpdate.dnsHostName())); + } else { + child = new EndpointConfig(StatusOr.fromStatus( + Status.INTERNAL.withDescription("Logical DNS in dependency manager unsupported"))); + } break; default: child = new EndpointConfig(StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( @@ -806,7 +814,9 @@ public void subscribeToChildren(XdsClusterResource.CdsUpdate update) { addEdsWatcher(getEdsServiceName()); break; case LOGICAL_DNS: - addDnsWatcher(update.dnsHostName()); + if (enableLogicalDns) { + addDnsWatcher(update.dnsHostName()); + } break; case AGGREGATE: update.prioritizedClusterNames() diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index 5a897de3a72..fca7b5220e6 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -152,6 +152,7 @@ public class XdsDependencyManagerTest { private XdsDependencyManager xdsDependencyManager = new XdsDependencyManager( xdsClient, syncContext, serverName, serverName, nameResolverArgs); + private boolean savedEnableLogicalDns; @Before public void setUp() throws Exception { @@ -168,6 +169,8 @@ public void setUp() throws Exception { testWatcher = new TestWatcher(); xdsConfigWatcher = mock(TestWatcher.class, delegatesTo(testWatcher)); defaultXdsConfig = XdsTestUtils.getDefaultXdsConfig(serverName); + + savedEnableLogicalDns = XdsDependencyManager.enableLogicalDns; } @After @@ -180,6 +183,8 @@ public void tearDown() throws InterruptedException { assertThat(adsEnded.get()).isTrue(); assertThat(lrsEnded.get()).isTrue(); assertThat(fakeClock.getPendingTasks()).isEmpty(); + + XdsDependencyManager.enableLogicalDns = savedEnableLogicalDns; } @Test @@ -749,6 +754,7 @@ public void testChangeAggCluster() { @Test public void testLogicalDns_success() { + XdsDependencyManager.enableLogicalDns = true; FakeSocketAddress fakeAddress = new FakeSocketAddress(); nameResolverRegistry.register(new FakeNameResolverProvider( "dns:///dns.example.com:1111", fakeAddress)); @@ -789,6 +795,7 @@ public void testLogicalDns_success() { @Test public void testLogicalDns_noDnsNr() { + XdsDependencyManager.enableLogicalDns = true; Cluster cluster = Cluster.newBuilder() .setName(CLUSTER_NAME) .setType(Cluster.DiscoveryType.LOGICAL_DNS) From ebc6d3e932cadf0c5b5cfb56e9bcdfe0ce48102d Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 24 Jun 2025 13:18:58 -0700 Subject: [PATCH 307/591] Start 1.75.0 development cycle --- MODULE.bazel | 2 +- build.gradle | 2 +- .../src/test/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/test/golden/TestService.java.txt | 2 +- core/src/main/java/io/grpc/internal/GrpcUtil.java | 2 +- examples/MODULE.bazel | 2 +- examples/android/clientcache/app/build.gradle | 10 +++++----- examples/android/helloworld/app/build.gradle | 8 ++++---- examples/android/routeguide/app/build.gradle | 8 ++++---- examples/android/strictmode/app/build.gradle | 8 ++++---- examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 4 ++-- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 4 ++-- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 4 ++-- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 4 ++-- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 4 ++-- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- 34 files changed, 55 insertions(+), 55 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 83aa6c1b026..ed027757e86 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "grpc-java", compatibility_level = 0, repo_name = "io_grpc_grpc_java", - version = "1.74.0-SNAPSHOT", # CURRENT_GRPC_VERSION + version = "1.75.0-SNAPSHOT", # CURRENT_GRPC_VERSION ) # GRPC_DEPS_START diff --git a/build.gradle b/build.gradle index afea703923f..d47687421b0 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.74.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.75.0-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index 548a9e3d806..e56beda4b68 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.74.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.75.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index 93551f6d582..f43fe748c46 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.74.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.75.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index 1434afa884c..65c083f10ef 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -219,7 +219,7 @@ public byte[] parseAsciiString(byte[] serialized) { public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); - public static final String IMPLEMENTATION_VERSION = "1.74.0-SNAPSHOT"; // CURRENT_GRPC_VERSION + public static final String IMPLEMENTATION_VERSION = "1.75.0-SNAPSHOT"; // CURRENT_GRPC_VERSION /** * The default timeout in nanos for a keepalive ping request. diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index dd0ae167171..382e3943eab 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,5 +1,5 @@ bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") -bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.74.0-SNAPSHOT") # CURRENT_GRPC_VERSION +bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.75.0-SNAPSHOT") # CURRENT_GRPC_VERSION bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") bazel_dep(name = "rules_jvm_external", version = "6.0") diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index 1530cfb01d6..bf6064658b7 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -34,7 +34,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -54,12 +54,12 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.5' - testImplementation 'io.grpc:grpc-testing:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + testImplementation 'io.grpc:grpc-testing:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index 66ef6bd9a14..e15fec725a3 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index 7c0527237e1..f961edbbcb2 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index a48ad08e4db..6bc1d5d7edb 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -33,7 +33,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -53,8 +53,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/build.gradle b/examples/build.gradle index b6ea92e6c50..253fca272c7 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index b66f3d3ab76..57e531fb564 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index ec027796185..96569ac31b7 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index 477dfe56a64..90a8b2975c6 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -6,13 +6,13 @@ jar - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT example-debug https://github.com/grpc/grpc-java UTF-8 - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index 9793567f10c..66056756523 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index b556e95d900..7525d06c375 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -6,13 +6,13 @@ jar - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT example-dualstack https://github.com/grpc/grpc-java UTF-8 - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 037758b14c6..31f9dc41150 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index b6e1d64a745..df716875791 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,13 +6,13 @@ jar - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index b6e3d1307ab..e28e9929137 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index bf4c43627da..3c3efab4c51 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 050cb9592fb..4c9e6662ac3 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index b6217aad6dd..74c97622f9e 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,13 +6,13 @@ jar - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index e2e63521ee4..3013c6c6a72 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index 1b5d4fa6ec7..98497ec41be 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,13 +7,13 @@ jar - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index abefaa35bea..ad6e65300d0 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index 837dcede0b6..1cea56171cd 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -7,13 +7,13 @@ jar - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT example-oauth https://github.com/grpc/grpc-java UTF-8 - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index d49c52fb32f..4f5e9e70a11 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index 71baca108b1..926c7b314e3 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index 5d1c01a2ce0..66f5374cf3a 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index 96f3e12ab93..0d971942bc0 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -15,7 +15,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index 44a85d9bfac..ec32a485ef9 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index c14f88e0f9b..99ba741cbf7 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,13 +6,13 @@ jar - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT example-tls https://github.com/grpc/grpc-java UTF-8 - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index 66f42841fbf..b2d945b1d5e 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.74.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/pom.xml b/examples/pom.xml index 1584416760e..57b7a9c6b79 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,13 +6,13 @@ jar - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT examples https://github.com/grpc/grpc-java UTF-8 - 1.74.0-SNAPSHOT + 1.75.0-SNAPSHOT 3.25.5 3.25.5 From af7efeb9f5db1026822a65f919fcab0bc2cd268c Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 24 Jun 2025 13:38:41 -0700 Subject: [PATCH 308/591] core: Rely on ping-pong for flow control testing The previous code did a ping-pong to make sure the transport had enough time to process, but then proceeded to sleep 5 seconds. That sleep would have been needed without the ping-pong, but with the ping-pong we are confident all events have been drained from the transport. Deleting the unnecessary sleeps saves 10 seconds, for each of the 9 instances of this test. --- .../java/io/grpc/internal/AbstractTransportTest.java | 8 ++------ .../java/io/grpc/internal/ClientStreamListenerBase.java | 7 +++++++ .../java/io/grpc/internal/ServerStreamListenerBase.java | 4 ++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java index 32c3bff74e9..ea7f1723e64 100644 --- a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java +++ b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java @@ -1449,7 +1449,7 @@ public void flowControlPushBack() throws Exception { clientStream.flush(); clientStream.halfClose(); doPingPong(serverListener); - assertFalse(serverStreamListener.awaitHalfClosed(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertFalse(serverStreamListener.isHalfClosed()); serverStream.request(1); serverReceived += verifyMessageCountAndClose(serverStreamListener.messageQueue, 1); @@ -1461,11 +1461,7 @@ public void flowControlPushBack() throws Exception { Status status = Status.OK.withDescription("... quite a lengthy discussion"); serverStream.close(status, new Metadata()); doPingPong(serverListener); - try { - clientStreamListener.awaitClose(TIMEOUT_MS, TimeUnit.MILLISECONDS); - fail("Expected TimeoutException"); - } catch (TimeoutException expectedException) { - } + assertFalse(clientStreamListener.isClosed()); clientStream.request(1); clientReceived += verifyMessageCountAndClose(clientStreamListener.messageQueue, 1); diff --git a/core/src/testFixtures/java/io/grpc/internal/ClientStreamListenerBase.java b/core/src/testFixtures/java/io/grpc/internal/ClientStreamListenerBase.java index 97186400cb2..3c35cf59225 100644 --- a/core/src/testFixtures/java/io/grpc/internal/ClientStreamListenerBase.java +++ b/core/src/testFixtures/java/io/grpc/internal/ClientStreamListenerBase.java @@ -43,6 +43,13 @@ public Status awaitClose(int timeout, TimeUnit unit) throws Exception { return status.get(timeout, unit); } + /** + * Return {@code true} if {@code #awaitClose} would return immediately with a status. + */ + public boolean isClosed() { + return status.isDone(); + } + /** * Returns response headers from the server or throws {@link * java.util.concurrent.TimeoutException} if they aren't delivered before the timeout. diff --git a/core/src/testFixtures/java/io/grpc/internal/ServerStreamListenerBase.java b/core/src/testFixtures/java/io/grpc/internal/ServerStreamListenerBase.java index b4ded80e5b2..aaa70600542 100644 --- a/core/src/testFixtures/java/io/grpc/internal/ServerStreamListenerBase.java +++ b/core/src/testFixtures/java/io/grpc/internal/ServerStreamListenerBase.java @@ -54,6 +54,10 @@ public boolean awaitHalfClosed(int timeout, TimeUnit unit) throws Exception { return halfClosedLatch.await(timeout, unit); } + public boolean isHalfClosed() { + return halfClosedLatch.getCount() == 0; + } + public Status awaitClose(int timeout, TimeUnit unit) throws Exception { return status.get(timeout, unit); } From 64322c324394fe5837fd324aae7cde36d229283b Mon Sep 17 00:00:00 2001 From: vimanikag Date: Wed, 25 Jun 2025 10:55:00 +0530 Subject: [PATCH 309/591] 11243: RLS cleanups (#12085) --- .../io/grpc/rls/LinkedHashLruCacheTest.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/rls/src/test/java/io/grpc/rls/LinkedHashLruCacheTest.java b/rls/src/test/java/io/grpc/rls/LinkedHashLruCacheTest.java index f38b28d8416..23ffe6ec026 100644 --- a/rls/src/test/java/io/grpc/rls/LinkedHashLruCacheTest.java +++ b/rls/src/test/java/io/grpc/rls/LinkedHashLruCacheTest.java @@ -25,8 +25,10 @@ import io.grpc.internal.FakeClock; import io.grpc.rls.LruCache.EvictionListener; import io.grpc.rls.LruCache.EvictionType; +import java.util.Arrays; import java.util.Objects; import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -266,4 +268,91 @@ public int hashCode() { return Objects.hash(value, expireTime); } } + + @Test + public void testFitToLimitWithReSize() { + + Entry entry1 = new Entry("Entry1", ticker.read() + 10, 4); + Entry entry2 = new Entry("Entry2", ticker.read() + 20, 1); + Entry entry3 = new Entry("Entry3", ticker.read() + 30, 2); + + cache.cache(1, entry1); + cache.cache(2, entry2); + cache.cache(3, entry3); + + assertThat(cache.estimatedSize()).isEqualTo(2); + assertThat(cache.estimatedSizeBytes()).isEqualTo(3); + assertThat(cache.estimatedMaxSizeBytes()).isEqualTo(5); + + cache.resize(2); + assertThat(cache.estimatedSize()).isEqualTo(1); + assertThat(cache.estimatedSizeBytes()).isEqualTo(2); + assertThat(cache.estimatedMaxSizeBytes()).isEqualTo(2); + + assertThat(cache.fitToLimit()).isEqualTo(false); + } + + @Test + public void testFitToLimit() { + + TestFitToLimitEviction localCache = new TestFitToLimitEviction( + MAX_SIZE, + evictionListener, + fakeClock.getTicker() + ); + + Entry entry1 = new Entry("Entry1", ticker.read() + 10, 4); + Entry entry2 = new Entry("Entry2", ticker.read() + 20, 2); + Entry entry3 = new Entry("Entry3", ticker.read() + 30, 1); + + localCache.cache(1, entry1); + localCache.cache(2, entry2); + localCache.cache(3, entry3); + + assertThat(localCache.estimatedSize()).isEqualTo(3); + assertThat(localCache.estimatedSizeBytes()).isEqualTo(7); + assertThat(localCache.estimatedMaxSizeBytes()).isEqualTo(5); + + localCache.enableEviction(); + + assertThat(localCache.fitToLimit()).isEqualTo(true); + + assertThat(localCache.values().contains(entry1)).isFalse(); + assertThat(localCache.values().containsAll(Arrays.asList(entry2, entry3))).isTrue(); + + assertThat(localCache.estimatedSize()).isEqualTo(2); + assertThat(localCache.estimatedSizeBytes()).isEqualTo(3); + assertThat(localCache.estimatedMaxSizeBytes()).isEqualTo(5); + } + + private static class TestFitToLimitEviction extends LinkedHashLruCache { + + private boolean allowEviction = false; + + TestFitToLimitEviction( + long estimatedMaxSizeBytes, + @Nullable EvictionListener evictionListener, + Ticker ticker) { + super(estimatedMaxSizeBytes, evictionListener, ticker); + } + + @Override + protected boolean isExpired(Integer key, Entry value, long nowNanos) { + return value.expireTime - nowNanos <= 0; + } + + @Override + protected int estimateSizeOf(Integer key, Entry value) { + return value.size; + } + + @Override + protected boolean shouldInvalidateEldestEntry(Integer eldestKey, Entry eldestValue, long now) { + return allowEviction && super.shouldInvalidateEldestEntry(eldestKey, eldestValue, now); + } + + public void enableEviction() { + allowEviction = true; + } + } } From 74aee11389b6d4b6561dce29001f024c5a96e28e Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 26 Jun 2025 17:43:13 -0700 Subject: [PATCH 310/591] Clarify requirements for creating a cross-user Channel. (#12181) The @SystemApi runtime visibility requirement isn't really new. It has always been implicit in the required INTERACT_ACROSS_USERS permission, which (in production) can only be held by system apps. The SDK_INT >= 30 requirement was also always present, via @RequiresApi() on BinderChannelBuilder#bindAsUser. This change just updates its replacement APIs (AndroidComponentAddress and TARGET_ANDROID_USER) to require it too. --- .../io/grpc/binder/AndroidComponentAddress.java | 17 ++++++++++++++++- .../main/java/io/grpc/binder/ApiConstants.java | 11 +++++++---- .../io/grpc/binder/BinderChannelBuilder.java | 6 +++--- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java b/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java index c4c17bb2cef..5617a141f93 100644 --- a/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java +++ b/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java @@ -250,7 +250,22 @@ public Builder setBindIntentFromComponent(ComponentName component) { return this; } - /** See {@link AndroidComponentAddress#getTargetUser()}. */ + /** + * Specifies the Android user in which the built Address' bind Intent will be evaluated. + * + *

    Connecting to a server in a different Android user is uncommon and requires the client app + * have runtime visibility of @SystemApi's and hold certain @SystemApi permissions. + * The device must also be running Android SDK version 30 or higher. + * + *

    See https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces + * for details on which apps can call the underlying @SystemApi's needed to make this type + * of connection. + * + *

    One of the "android.permission.INTERACT_ACROSS_XXX" permissions is required. The exact one + * depends on the calling user's relationship to the target user, whether client and server are + * in the same or different apps, and the version of Android in use. See {@link + * Context#bindServiceAsUser}, the essential underlying Android API, for details. + */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173") public Builder setTargetUser(@Nullable UserHandle targetUser) { this.targetUser = targetUser; diff --git a/binder/src/main/java/io/grpc/binder/ApiConstants.java b/binder/src/main/java/io/grpc/binder/ApiConstants.java index 292c580c2b8..05950a9eaec 100644 --- a/binder/src/main/java/io/grpc/binder/ApiConstants.java +++ b/binder/src/main/java/io/grpc/binder/ApiConstants.java @@ -35,12 +35,15 @@ private ApiConstants() {} /** * Specifies the Android user in which target URIs should be resolved. * - *

    {@link UserHandle} can't reasonably be encoded in a target URI string. Instead, all - * {@link io.grpc.NameResolverProvider}s producing {@link AndroidComponentAddress}es should let - * clients address servers in another Android user using this argument. + *

    {@link UserHandle} can't reasonably be encoded in a target URI string. Instead, all {@link + * io.grpc.NameResolverProvider}s producing {@link AndroidComponentAddress}es should let clients + * address servers in another Android user using this argument. * - *

    See also {@link AndroidComponentAddress#getTargetUser()}. + *

    Connecting to a server in a different Android user is uncommon and can only be done by a + * "system app" client with special permissions. See {@link + * AndroidComponentAddress.Builder#setTargetUser(UserHandle)} for details. */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173") public static final NameResolver.Args.Key TARGET_ANDROID_USER = NameResolver.Args.Key.create("target-android-user"); } diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java index 0b8f0bb4b3f..f996ee7cdbf 100644 --- a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java +++ b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java @@ -242,9 +242,9 @@ public BinderChannelBuilder securityPolicy(SecurityPolicy securityPolicy) { * specify a {@link UserHandle}. If neither the Channel nor the {@link AndroidComponentAddress} * specifies a target user, the {@link UserHandle} of the current process will be used. * - *

    Targeting a Service in a different Android user is uncommon and requires special permissions - * normally reserved for system apps. See {@link android.content.Context#bindServiceAsUser} for - * details. + *

    Connecting to a server in a different Android user is uncommon and can only be done by a + * "system app" client with special permissions. See {@link + * AndroidComponentAddress.Builder#setTargetUser(UserHandle)} for details. * * @deprecated This method's name is misleading because it implies an impersonated client identity * when it's actually specifying part of the server's location. It's also no longer necessary From 2ee4f9b488e42587adbc92ea428062004715d3c6 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 26 Jun 2025 22:28:48 -0700 Subject: [PATCH 311/591] AndroidComponentAddress constructor can be private. (#12188) --- .../src/main/java/io/grpc/binder/AndroidComponentAddress.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java b/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java index 5617a141f93..b390c1f0ccd 100644 --- a/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java +++ b/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java @@ -58,7 +58,7 @@ public final class AndroidComponentAddress extends SocketAddress { @Nullable private final UserHandle targetUser; // null means the same user that hosts this process. - protected AndroidComponentAddress(Intent bindIntent, @Nullable UserHandle targetUser) { + private AndroidComponentAddress(Intent bindIntent, @Nullable UserHandle targetUser) { checkArgument( bindIntent.getComponent() != null || bindIntent.getPackage() != null, "'bindIntent' must be explicit. Specify either a package or ComponentName."); From ca99a8c478eff08b34c6ddd3035ef37aca9754ee Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sun, 29 Jun 2025 20:05:03 -0700 Subject: [PATCH 312/591] Fix RLS regressions from XdsDepMan conversion 297ab05ef converted CDS to XdsDependencyManager. This caused three regressions: * CdsLB2 as a RLS child would always fail with "Unable to find non-dynamic root cluster" because is_dynamic=true was missing in its service config * XdsNameResolver only propagated resolution updates when the clusters changed, so a CdsUpdate change would be ignored. This caused a hang for RLS even with is_dynamic=true. For non-RLS the lack config update broke the circuit breaking psm interop test. This would have been more severe if ClusterResolverLb had been converted to XdsDependenceManager, as it would have ignored EDS updates * RLS did not propagate resolution updates, so CdsLB2 even with is_dynamic=true the CdsUpdate for the new cluster would never arrive, causing a hang b/428120265 b/427912384 --- .../java/io/grpc/rls/CachingRlsLbClient.java | 7 +++ .../io/grpc/rls/LbPolicyConfiguration.java | 28 ++++++++-- .../java/io/grpc/rls/RlsLoadBalancer.java | 4 +- .../java/io/grpc/xds/XdsNameResolver.java | 6 +- .../java/io/grpc/xds/XdsNameResolverTest.java | 56 ++++++++++++------- 5 files changed, 76 insertions(+), 25 deletions(-) diff --git a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java index 70833416d5d..7855468ee61 100644 --- a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java +++ b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java @@ -255,6 +255,13 @@ void init() { } } + Status acceptResolvedAddressFactory(ResolvedAddressFactory childLbResolvedAddressFactory) { + synchronized (lock) { + return refCountedChildPolicyWrapperFactory.acceptResolvedAddressFactory( + childLbResolvedAddressFactory); + } + } + /** * Convert the status to UNAVAILABLE and enhance the error message. * @param status status as provided by server diff --git a/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java b/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java index 4d6ceed9235..226176d25ff 100644 --- a/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java +++ b/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java @@ -31,6 +31,7 @@ import io.grpc.LoadBalancerProvider; import io.grpc.LoadBalancerRegistry; import io.grpc.NameResolver.ConfigOrError; +import io.grpc.Status; import io.grpc.internal.ObjectPool; import io.grpc.rls.ChildLoadBalancerHelper.ChildLoadBalancerHelperProvider; import io.grpc.rls.RlsProtoData.RouteLookupConfig; @@ -211,7 +212,7 @@ static final class RefCountedChildPolicyWrapperFactory { private final ChildLoadBalancerHelperProvider childLbHelperProvider; private final ChildLbStatusListener childLbStatusListener; private final ChildLoadBalancingPolicy childPolicy; - private final ResolvedAddressFactory childLbResolvedAddressFactory; + private ResolvedAddressFactory childLbResolvedAddressFactory; public RefCountedChildPolicyWrapperFactory( ChildLoadBalancingPolicy childPolicy, @@ -229,6 +230,19 @@ void init() { childLbHelperProvider.init(); } + Status acceptResolvedAddressFactory(ResolvedAddressFactory childLbResolvedAddressFactory) { + this.childLbResolvedAddressFactory = childLbResolvedAddressFactory; + Status status = Status.OK; + for (RefCountedChildPolicyWrapper wrapper : childPolicyMap.values()) { + Status newStatus = + wrapper.childPolicyWrapper.acceptResolvedAddressFactory(childLbResolvedAddressFactory); + if (!newStatus.isOk()) { + status = newStatus; + } + } + return status; + } + ChildPolicyWrapper createOrGet(String target) { // TODO(creamsoup) check if the target is valid or not RefCountedChildPolicyWrapper pooledChildPolicyWrapper = childPolicyMap.get(target); @@ -277,6 +291,7 @@ static final class ChildPolicyWrapper { private final String target; private final ChildPolicyReportingHelper helper; private final LoadBalancer lb; + private final Object childLbConfig; private volatile SubchannelPicker picker; private ConnectivityState state; @@ -295,14 +310,14 @@ public ChildPolicyWrapper( .parseLoadBalancingPolicyConfig( childPolicy.getEffectiveChildPolicy(target)); this.lb = lbProvider.newLoadBalancer(helper); + this.childLbConfig = lbConfig.getConfig(); helper.getChannelLogger().log( - ChannelLogLevel.DEBUG, "RLS child lb created. config: {0}", lbConfig.getConfig()); + ChannelLogLevel.DEBUG, "RLS child lb created. config: {0}", childLbConfig); helper.getSynchronizationContext().execute( new Runnable() { @Override public void run() { - if (!lb.acceptResolvedAddresses( - childLbResolvedAddressFactory.create(lbConfig.getConfig())).isOk()) { + if (!acceptResolvedAddressFactory(childLbResolvedAddressFactory).isOk()) { helper.refreshNameResolution(); } lb.requestConnection(); @@ -310,6 +325,11 @@ public void run() { }); } + Status acceptResolvedAddressFactory(ResolvedAddressFactory childLbResolvedAddressFactory) { + helper.getSynchronizationContext().throwIfNotInThisSynchronizationContext(); + return lb.acceptResolvedAddresses(childLbResolvedAddressFactory.create(childLbConfig)); + } + String getTarget() { return target; } diff --git a/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java b/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java index 81ef8fdb31a..6e59e867e32 100644 --- a/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java +++ b/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java @@ -79,7 +79,9 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { // not required. this.lbPolicyConfiguration = lbPolicyConfiguration; } - return Status.OK; + return routeLookupClient.acceptResolvedAddressFactory( + new ChildLbResolvedAddressFactory( + resolvedAddresses.getAddresses(), resolvedAddresses.getAttributes())); } @Override diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index c71e4dc255d..58d1ff769fe 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -823,6 +823,9 @@ private void updateRoutes( if (shouldUpdateResult && routingConfig != null) { updateResolutionResult(xdsConfig); shouldUpdateResult = false; + } else { + // Need to update at least once + shouldUpdateResult = true; } // Make newly added clusters selectable by config selector and deleted clusters no longer // selectable. @@ -993,7 +996,8 @@ private ClusterRefState( .put("routeLookupConfig", rlsPluginConfig.config()) .put( "childPolicy", - ImmutableList.of(ImmutableMap.of(XdsLbPolicies.CDS_POLICY_NAME, ImmutableMap.of()))) + ImmutableList.of(ImmutableMap.of(XdsLbPolicies.CDS_POLICY_NAME, ImmutableMap.of( + "is_dynamic", true)))) .put("childPolicyConfigTargetFieldName", "cluster") .buildOrThrow(); return ImmutableMap.of("rls_experimental", rlsConfig); diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index f5c40fa2117..7015a43f6ed 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -28,6 +28,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -449,7 +450,7 @@ public void resolving_ldsResourceUpdateRdsName() { // Two new service config updates triggered: // - with load balancing config being able to select cluster1 and cluster2 // - with load balancing config being able to select cluster2 only - verify(mockListener, times(2)).onResult2(resultCaptor.capture()); + verify(mockListener, times(3)).onResult2(resultCaptor.capture()); assertServiceConfigForLoadBalancingConfig( Arrays.asList(cluster1, cluster2), (Map) resultCaptor.getAllValues().get(0).getServiceConfig().getConfig()); @@ -1070,7 +1071,9 @@ public void resolved_resourceUpdateAfterCallStarted() { assertCallSelectClusterResult(call1, configSelector, "another-cluster", 20.0); firstCall.deliverErrorStatus(); // completes previous call - verify(mockListener, times(2)).onResult2(resolutionResultCaptor.capture()); + // Two updates: one for XdsNameResolver releasing the cluster, and another for + // XdsDependencyManager updating the XdsConfig + verify(mockListener, times(3)).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); assertServiceConfigForLoadBalancingConfig( Arrays.asList(cluster2, "another-cluster"), @@ -1100,7 +1103,7 @@ public void resolved_resourceUpdatedBeforeCallStarted() { ImmutableMap.of()))); // Two consecutive service config updates: one for removing clcuster1, // one for adding "another=cluster". - verify(mockListener, times(2)).onResult2(resolutionResultCaptor.capture()); + verify(mockListener, times(3)).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); assertServiceConfigForLoadBalancingConfig( Arrays.asList(cluster2, "another-cluster"), @@ -1155,7 +1158,7 @@ public void resolved_raceBetweenCallAndRepeatedResourceUpdate() { cluster2, Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of()))); - verifyNoMoreInteractions(mockListener); // no cluster added/deleted + verify(mockListener, times(2)).onResult2(resolutionResultCaptor.capture()); assertCallSelectClusterResult(call1, configSelector, "another-cluster", 15.0); } @@ -1187,7 +1190,13 @@ public void resolved_raceBetweenClusterReleasedAndResourceUpdateAddBackAgain() { null, false), ImmutableMap.of()))); testCall.deliverErrorStatus(); - verifyNoMoreInteractions(mockListener); + verify(mockListener, times(3)).onResult2(resolutionResultCaptor.capture()); + assertServiceConfigForLoadBalancingConfig( + Arrays.asList(cluster1, cluster2), resolutionResultCaptor.getAllValues().get(1)); + assertServiceConfigForLoadBalancingConfig( + Arrays.asList(cluster1, cluster2), resolutionResultCaptor.getAllValues().get(2)); + assertServiceConfigForLoadBalancingConfig( + Arrays.asList(cluster1, cluster2), resolutionResultCaptor.getAllValues().get(3)); } @SuppressWarnings("unchecked") @@ -1268,7 +1277,7 @@ public void resolved_simpleCallSucceeds_routeToRls() { "routeLookupConfig", ImmutableMap.of("lookupService", "rls-cbt.googleapis.com"), "childPolicy", - ImmutableList.of(ImmutableMap.of("cds_experimental", ImmutableMap.of())), + ImmutableList.of(ImmutableMap.of("cds_experimental", ImmutableMap.of("is_dynamic", true))), "childPolicyConfigTargetFieldName", "cluster"); Map expectedClusterManagerLbConfig = ImmutableMap.of( @@ -1315,7 +1324,7 @@ public void resolved_simpleCallSucceeds_routeToRls() { "routeLookupConfig", ImmutableMap.of("lookupService", "rls-cbt-2.googleapis.com"), "childPolicy", - ImmutableList.of(ImmutableMap.of("cds_experimental", ImmutableMap.of())), + ImmutableList.of(ImmutableMap.of("cds_experimental", ImmutableMap.of("is_dynamic", true))), "childPolicyConfigTargetFieldName", "cluster"); Map expectedClusterManagerLbConfig2 = ImmutableMap.of( @@ -1656,7 +1665,7 @@ private void assertEmptyResolutionResult(String resource) { } private void assertClusterResolutionResult(CallInfo call, String expectedCluster) { - verify(mockListener).onResult2(resolutionResultCaptor.capture()); + verify(mockListener, atLeast(1)).onResult2(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); assertCallSelectClusterResult(call, configSelector, expectedCluster, null); @@ -1744,6 +1753,13 @@ private InternalConfigSelector resolveToClusters() { return result.getAttributes().get(InternalConfigSelector.KEY); } + private static void assertServiceConfigForLoadBalancingConfig( + List clusters, ResolutionResult result) { + @SuppressWarnings("unchecked") + Map config = (Map) result.getServiceConfig().getConfig(); + assertServiceConfigForLoadBalancingConfig(clusters, config); + } + /** * Verifies the raw service config contains an xDS load balancing config for the given clusters. */ @@ -1847,7 +1863,9 @@ public void generateServiceConfig_forClusterManagerLoadBalancingConfig() throws + " \"lookupService\": \"rls.bigtable.google.com\"\n" + " },\n" + " \"childPolicy\": [\n" - + " {\"cds_experimental\": {}}\n" + + " {\"cds_experimental\": {\n" + + " \"is_dynamic\": true\n" + + " }}\n" + " ],\n" + " \"childPolicyConfigTargetFieldName\": \"cluster\"\n" + " }\n" @@ -2035,7 +2053,7 @@ public void resolved_faultAbortInLdsUpdate() { FaultAbort.forHeader(FaultConfig.FractionalPercent.perMillion(600_000)), null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult2(resolutionResultCaptor.capture()); + verify(mockListener, times(2)).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2051,7 +2069,7 @@ public void resolved_faultAbortInLdsUpdate() { FaultAbort.forHeader(FaultConfig.FractionalPercent.perMillion(0)), null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult2(resolutionResultCaptor.capture()); + verify(mockListener, times(3)).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2066,7 +2084,7 @@ public void resolved_faultAbortInLdsUpdate() { FaultConfig.FractionalPercent.perMillion(600_000)), null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult2(resolutionResultCaptor.capture()); + verify(mockListener, times(4)).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2084,7 +2102,7 @@ public void resolved_faultAbortInLdsUpdate() { FaultConfig.FractionalPercent.perMillion(400_000)), null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult2(resolutionResultCaptor.capture()); + verify(mockListener, times(5)).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2119,7 +2137,7 @@ public void resolved_faultDelayInLdsUpdate() { httpFilterFaultConfig = FaultConfig.create( FaultDelay.forHeader(FaultConfig.FractionalPercent.perMillion(600_000)), null, null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult2(resolutionResultCaptor.capture()); + verify(mockListener, times(2)).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2130,7 +2148,7 @@ public void resolved_faultDelayInLdsUpdate() { httpFilterFaultConfig = FaultConfig.create( FaultDelay.forHeader(FaultConfig.FractionalPercent.perMillion(0)), null, null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult2(resolutionResultCaptor.capture()); + verify(mockListener, times(3)).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2143,7 +2161,7 @@ public void resolved_faultDelayInLdsUpdate() { null, null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult2(resolutionResultCaptor.capture()); + verify(mockListener, times(4)).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2156,7 +2174,7 @@ public void resolved_faultDelayInLdsUpdate() { null, null); xdsClient.deliverLdsUpdateWithFaultInjection(cluster1, httpFilterFaultConfig, null, null, null); - verify(mockListener).onResult2(resolutionResultCaptor.capture()); + verify(mockListener, times(5)).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2281,7 +2299,7 @@ public void resolved_faultConfigOverrideInLdsUpdate() { null); xdsClient.deliverLdsUpdateWithFaultInjection( cluster1, httpFilterFaultConfig, virtualHostFaultConfig, routeFaultConfig, null); - verify(mockListener).onResult2(resolutionResultCaptor.capture()); + verify(mockListener, times(2)).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, @@ -2298,7 +2316,7 @@ public void resolved_faultConfigOverrideInLdsUpdate() { xdsClient.deliverLdsUpdateWithFaultInjection( cluster1, httpFilterFaultConfig, virtualHostFaultConfig, routeFaultConfig, weightedClusterFaultConfig); - verify(mockListener).onResult2(resolutionResultCaptor.capture()); + verify(mockListener, times(3)).onResult2(resolutionResultCaptor.capture()); result = resolutionResultCaptor.getValue(); configSelector = result.getAttributes().get(InternalConfigSelector.KEY); observer = startNewCall(TestMethodDescriptors.voidMethod(), configSelector, From 919370172d65a819bb1e7ccb47f2285b0ccfd17e Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:14:28 +0000 Subject: [PATCH 313/591] census: APIs for stats and tracing (#12050) --- .../main/java/io/grpc/census/GrpcCensus.java | 176 ++++++++++++++++++ .../io/grpc/internal/ServerImplBuilder.java | 2 +- 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 census/src/main/java/io/grpc/census/GrpcCensus.java diff --git a/census/src/main/java/io/grpc/census/GrpcCensus.java b/census/src/main/java/io/grpc/census/GrpcCensus.java new file mode 100644 index 00000000000..c564c349ae4 --- /dev/null +++ b/census/src/main/java/io/grpc/census/GrpcCensus.java @@ -0,0 +1,176 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.census; + +import com.google.common.base.Stopwatch; +import com.google.common.base.Supplier; +import io.grpc.ClientInterceptor; +import io.grpc.ExperimentalApi; +import io.grpc.ManagedChannelBuilder; +import io.grpc.ServerBuilder; +import io.grpc.ServerStreamTracer; +import io.opencensus.trace.Tracing; + +/** + * The entrypoint for OpenCensus instrumentation functionality in gRPC. + * + *

    GrpcCensus uses {@link io.opencensus.api.OpenCensus} APIs for instrumentation. + * + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12178") +public final class GrpcCensus { + + private final boolean statsEnabled; + private final boolean tracingEnabled; + + private GrpcCensus(Builder builder) { + this.statsEnabled = builder.statsEnabled; + this.tracingEnabled = builder.tracingEnabled; + } + + /** + * Creates a new builder for {@link GrpcCensus}. + */ + public static Builder newBuilder() { + return new Builder(); + } + + private static final Supplier STOPWATCH_SUPPLIER = new Supplier() { + @Override + public Stopwatch get() { + return Stopwatch.createUnstarted(); + } + }; + + /** + * Configures a {@link ServerBuilder} to enable census stats and tracing. + * + * @param serverBuilder The server builder to configure. + * @return The configured server builder. + */ + public > T configureServerBuilder(T serverBuilder) { + if (statsEnabled) { + serverBuilder.addStreamTracerFactory(newServerStatsStreamTracerFactory()); + } + if (tracingEnabled) { + serverBuilder.addStreamTracerFactory(newServerTracingStreamTracerFactory()); + } + return serverBuilder; + } + + /** + * Configures a {@link ManagedChannelBuilder} to enable census stats and tracing. + * + * @param channelBuilder The channel builder to configure. + * @return The configured channel builder. + */ + public > T configureChannelBuilder(T channelBuilder) { + if (statsEnabled) { + channelBuilder.intercept(newClientStatsInterceptor()); + } + if (tracingEnabled) { + channelBuilder.intercept(newClientTracingInterceptor()); + } + return channelBuilder; + } + + /** + * Returns a {@link ClientInterceptor} with default stats implementation. + */ + private static ClientInterceptor newClientStatsInterceptor() { + CensusStatsModule censusStats = + new CensusStatsModule( + STOPWATCH_SUPPLIER, + true, + true, + true, + false, + true); + return censusStats.getClientInterceptor(); + } + + /** + * Returns a {@link ClientInterceptor} with default tracing implementation. + */ + private static ClientInterceptor newClientTracingInterceptor() { + CensusTracingModule censusTracing = + new CensusTracingModule( + Tracing.getTracer(), + Tracing.getPropagationComponent().getBinaryFormat()); + return censusTracing.getClientInterceptor(); + } + + /** + * Returns a {@link ServerStreamTracer.Factory} with default stats implementation. + */ + private static ServerStreamTracer.Factory newServerStatsStreamTracerFactory() { + CensusStatsModule censusStats = + new CensusStatsModule( + STOPWATCH_SUPPLIER, + true, + true, + true, + false, + true); + return censusStats.getServerTracerFactory(); + } + + /** + * Returns a {@link ServerStreamTracer.Factory} with default tracing implementation. + */ + private static ServerStreamTracer.Factory newServerTracingStreamTracerFactory() { + CensusTracingModule censusTracing = + new CensusTracingModule( + Tracing.getTracer(), + Tracing.getPropagationComponent().getBinaryFormat()); + return censusTracing.getServerTracerFactory(); + } + + /** + * Builder for {@link GrpcCensus}. + */ + public static final class Builder { + private boolean statsEnabled = true; + private boolean tracingEnabled = true; + + private Builder() { + } + + /** + * Disables stats collection. + */ + public Builder disableStats() { + this.statsEnabled = false; + return this; + } + + /** + * Disables tracing. + */ + public Builder disableTracing() { + this.tracingEnabled = false; + return this; + } + + /** + * Builds a new {@link GrpcCensus}. + */ + public GrpcCensus build() { + return new GrpcCensus(this); + } + } +} diff --git a/core/src/main/java/io/grpc/internal/ServerImplBuilder.java b/core/src/main/java/io/grpc/internal/ServerImplBuilder.java index b679baf3a8b..f6566e067db 100644 --- a/core/src/main/java/io/grpc/internal/ServerImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/ServerImplBuilder.java @@ -99,7 +99,7 @@ public static ServerBuilder forPort(int port) { ServerCallExecutorSupplier executorSupplier; /** - * An interface to provide to provide transport specific information for the server. This method + * An interface to provide transport specific information for the server. This method * is meant for Transport implementors and should not be used by normal users. */ public interface ClientTransportServersBuilder { From 6dfa03c51c0f67bde1db56a7c71cbd538aef6e82 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 3 Jul 2025 06:14:04 +0000 Subject: [PATCH 314/591] core: grpc-timeout should always be positive (#12201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROTOCOL-HTTP2.md specifies "TimeoutValue → {positive integer as ASCII string of at most 8 digits}". Zero is not positive, so it should be avoided. So make sure timeouts are at least 1 nanosecond instead of 0 nanoseconds. grpc-go recently began disallowing zero timeouts in https://github.com/grpc/grpc-go/pull/8290 which caused a regression as grpc-java can generate such timeouts. Apparently no gRPC implementation had previously been checking for zero timeouts. Instead of changing the max(0) to max(1) everywhere, just move the max handling into TimeoutMarshaller, since every caller of TIMEOUT_KEY was doing the same max() handling. Before fd8fd517d (in 2016!), grpc-java actually behaved correctly, as it failed RPCs with timeouts "<= 0". The commit changed the handling to the max(0) handling we see now. b/427338711 --- .../java/io/grpc/binder/internal/Outbound.java | 4 +--- .../io/grpc/internal/AbstractClientStream.java | 4 +--- .../main/java/io/grpc/internal/GrpcUtil.java | 10 ++++++---- .../internal/AbstractClientStreamTest.java | 18 ++++++++++++++++++ .../java/io/grpc/internal/GrpcUtilTest.java | 4 ++-- .../java/io/grpc/internal/ServerImplTest.java | 13 ++++++++++++- .../io/grpc/inprocess/InProcessTransport.java | 4 +--- 7 files changed, 41 insertions(+), 16 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/Outbound.java b/binder/src/main/java/io/grpc/binder/internal/Outbound.java index f395fe1701f..7db5bf0fbe4 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Outbound.java +++ b/binder/src/main/java/io/grpc/binder/internal/Outbound.java @@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static io.grpc.internal.GrpcUtil.TIMEOUT_KEY; -import static java.lang.Math.max; import android.os.Parcel; import com.google.errorprone.annotations.concurrent.GuardedBy; @@ -397,8 +396,7 @@ protected int writeSuffix(Parcel parcel) throws IOException { @GuardedBy("this") void setDeadline(Deadline deadline) { headers.discardAll(TIMEOUT_KEY); - long effectiveTimeoutNanos = max(0, deadline.timeRemaining(TimeUnit.NANOSECONDS)); - headers.put(TIMEOUT_KEY, effectiveTimeoutNanos); + headers.put(TIMEOUT_KEY, deadline.timeRemaining(TimeUnit.NANOSECONDS)); } } diff --git a/core/src/main/java/io/grpc/internal/AbstractClientStream.java b/core/src/main/java/io/grpc/internal/AbstractClientStream.java index bb346657d53..9718f8c5171 100644 --- a/core/src/main/java/io/grpc/internal/AbstractClientStream.java +++ b/core/src/main/java/io/grpc/internal/AbstractClientStream.java @@ -21,7 +21,6 @@ import static io.grpc.internal.GrpcUtil.CONTENT_ENCODING_KEY; import static io.grpc.internal.GrpcUtil.MESSAGE_ENCODING_KEY; import static io.grpc.internal.GrpcUtil.TIMEOUT_KEY; -import static java.lang.Math.max; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -124,8 +123,7 @@ protected AbstractClientStream( @Override public void setDeadline(Deadline deadline) { headers.discardAll(TIMEOUT_KEY); - long effectiveTimeout = max(0, deadline.timeRemaining(TimeUnit.NANOSECONDS)); - headers.put(TIMEOUT_KEY, effectiveTimeout); + headers.put(TIMEOUT_KEY, deadline.timeRemaining(TimeUnit.NANOSECONDS)); } @Override diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index 65c083f10ef..658a6785474 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -651,12 +651,14 @@ public Stopwatch get() { static class TimeoutMarshaller implements Metadata.AsciiMarshaller { @Override - public String toAsciiString(Long timeoutNanos) { + public String toAsciiString(Long timeoutNanosObject) { long cutoff = 100000000; + // Timeout checking is inherently racy. RPCs with timeouts in the past ideally don't even get + // here, but if the timeout is expired assume that happened recently and adjust it to the + // smallest allowed timeout + long timeoutNanos = Math.max(1, timeoutNanosObject); TimeUnit unit = TimeUnit.NANOSECONDS; - if (timeoutNanos < 0) { - throw new IllegalArgumentException("Timeout too small"); - } else if (timeoutNanos < cutoff) { + if (timeoutNanos < cutoff) { return timeoutNanos + "n"; } else if (timeoutNanos < cutoff * 1000L) { return unit.toMicros(timeoutNanos) + "u"; diff --git a/core/src/test/java/io/grpc/internal/AbstractClientStreamTest.java b/core/src/test/java/io/grpc/internal/AbstractClientStreamTest.java index 18fafe6557d..8f14b74035c 100644 --- a/core/src/test/java/io/grpc/internal/AbstractClientStreamTest.java +++ b/core/src/test/java/io/grpc/internal/AbstractClientStreamTest.java @@ -465,6 +465,24 @@ allocator, new BaseTransportState(statsTraceCtx, transportTracer), sink, statsTr .isGreaterThan(TimeUnit.MILLISECONDS.toNanos(600)); } + @Test + public void setDeadline_thePastBecomesPositive() { + AbstractClientStream.Sink sink = mock(AbstractClientStream.Sink.class); + ClientStream stream = new BaseAbstractClientStream( + allocator, new BaseTransportState(statsTraceCtx, transportTracer), sink, statsTraceCtx, + transportTracer); + + stream.setDeadline(Deadline.after(-1, TimeUnit.NANOSECONDS)); + stream.start(mockListener); + + ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(Metadata.class); + verify(sink).writeHeaders(headersCaptor.capture(), ArgumentMatchers.any()); + + Metadata headers = headersCaptor.getValue(); + assertThat(headers.get(Metadata.Key.of("grpc-timeout", Metadata.ASCII_STRING_MARSHALLER))) + .isEqualTo("1n"); + } + @Test public void appendTimeoutInsight() { InsightBuilder insight = new InsightBuilder(); diff --git a/core/src/test/java/io/grpc/internal/GrpcUtilTest.java b/core/src/test/java/io/grpc/internal/GrpcUtilTest.java index 229c593ef80..c243790028c 100644 --- a/core/src/test/java/io/grpc/internal/GrpcUtilTest.java +++ b/core/src/test/java/io/grpc/internal/GrpcUtilTest.java @@ -98,8 +98,8 @@ public void timeoutTest() { GrpcUtil.TimeoutMarshaller marshaller = new GrpcUtil.TimeoutMarshaller(); // nanos - assertEquals("0n", marshaller.toAsciiString(0L)); - assertEquals(0L, (long) marshaller.parseAsciiString("0n")); + assertEquals("1n", marshaller.toAsciiString(1L)); + assertEquals(1L, (long) marshaller.parseAsciiString("1n")); assertEquals("99999999n", marshaller.toAsciiString(99999999L)); assertEquals(99999999L, (long) marshaller.parseAsciiString("99999999n")); diff --git a/core/src/test/java/io/grpc/internal/ServerImplTest.java b/core/src/test/java/io/grpc/internal/ServerImplTest.java index 2ddaba751e4..0f18efe078c 100644 --- a/core/src/test/java/io/grpc/internal/ServerImplTest.java +++ b/core/src/test/java/io/grpc/internal/ServerImplTest.java @@ -53,6 +53,7 @@ import io.grpc.Channel; import io.grpc.Compressor; import io.grpc.Context; +import io.grpc.Deadline; import io.grpc.Grpc; import io.grpc.HandlerRegistry; import io.grpc.IntegerMarshaller; @@ -1146,11 +1147,21 @@ public ServerCall.Listener startCall( @Test public void testContextExpiredBeforeStreamCreate_StreamCancelNotCalledBeforeSetListener() throws Exception { + builder.ticker = new Deadline.Ticker() { + private long time; + + @Override + public long nanoTime() { + time += 1000; + return time; + } + }; + AtomicBoolean contextCancelled = new AtomicBoolean(false); AtomicReference context = new AtomicReference<>(); AtomicReference> callReference = new AtomicReference<>(); - testStreamClose_setup(callReference, context, contextCancelled, 0L); + testStreamClose_setup(callReference, context, contextCancelled, 1L); // This assert that stream.setListener(jumpListener) is called before stream.cancel(), which // prevents extremely short deadlines causing NPEs. diff --git a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java index e294b4eb63f..e31696eb631 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java @@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.internal.GrpcUtil.TIMEOUT_KEY; -import static java.lang.Math.max; import com.google.common.base.MoreObjects; import com.google.common.io.ByteStreams; @@ -939,8 +938,7 @@ public void setMaxOutboundMessageSize(int maxSize) {} @Override public void setDeadline(Deadline deadline) { headers.discardAll(TIMEOUT_KEY); - long effectiveTimeout = max(0, deadline.timeRemaining(TimeUnit.NANOSECONDS)); - headers.put(TIMEOUT_KEY, effectiveTimeout); + headers.put(TIMEOUT_KEY, deadline.timeRemaining(TimeUnit.NANOSECONDS)); } @Override From 94532a6b56076c56fb9278e9195bba1190a9260d Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 10 Jul 2025 14:14:36 -0700 Subject: [PATCH 315/591] binder: Introduce server pre-authorization (#12127) grpc-binder clients authorize servers by checking the UID of the sender of the SETUP_TRANSPORT Binder transaction against some SecurityPolicy. But merely binding to an unauthorized server to learn its UID can enable "keep-alive" and "background activity launch" abuse, even if security policy ultimately decides the connection is unauthorized. Pre-authorization mitigates this kind of abuse by looking up and authorizing a candidate server Application's UID before binding to it. Pre-auth is especially important when the server's address is not fixed in advance but discovered by PackageManager lookup. --- binder/build.gradle | 1 + .../internal/BinderClientTransportTest.java | 85 ++++++++- .../java/io/grpc/binder/ApiConstants.java | 14 ++ .../io/grpc/binder/BinderChannelBuilder.java | 29 +++ .../io/grpc/binder/internal/Bindable.java | 15 ++ .../BinderClientTransportFactory.java | 9 + .../grpc/binder/internal/BinderTransport.java | 74 +++++++- .../grpc/binder/internal/ServiceBinding.java | 75 ++++++++ .../binder/RobolectricBinderSecurityTest.java | 38 +++- .../RobolectricBinderTransportTest.java | 176 ++++++++++++++++-- .../binder/internal/ServiceBindingTest.java | 53 +++++- .../BinderClientTransportBuilder.java | 61 ++++++ .../grpc/internal/AbstractTransportTest.java | 4 +- 13 files changed, 598 insertions(+), 36 deletions(-) create mode 100644 binder/src/testFixtures/java/io/grpc/binder/internal/BinderClientTransportBuilder.java diff --git a/binder/build.gradle b/binder/build.gradle index 3390e02fce7..7ac23750a2a 100644 --- a/binder/build.gradle +++ b/binder/build.gradle @@ -72,6 +72,7 @@ dependencies { androidTestImplementation testFixtures(project(':grpc-core')) testFixturesImplementation libraries.guava.testlib + testFixturesImplementation testFixtures(project(':grpc-core')) } import net.ltgt.gradle.errorprone.CheckSeverity diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java index 2af7210e9a7..fe2fd587453 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java @@ -172,6 +172,12 @@ public BinderClientTransportBuilder setReadyTimeoutMillis(int timeoutMillis) { return this; } + @CanIgnoreReturnValue + public BinderClientTransportBuilder setPreAuthorizeServer(boolean preAuthorizeServer) { + factoryBuilder.setPreAuthorizeServers(preAuthorizeServer); + return this; + } + public BinderTransport.BinderClientTransport build() { return factoryBuilder .buildClientTransportFactory() @@ -372,11 +378,12 @@ public void testBlackHoleEndpointConnectTimeout() throws Exception { } @Test - public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception { + public void testBlackHoleSecurityPolicyAuthTimeout() throws Exception { SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy(); transport = new BinderClientTransportBuilder() .setSecurityPolicy(securityPolicy) + .setPreAuthorizeServer(false) .setReadyTimeoutMillis(1_234) .build(); transport.start(transportListener).run(); @@ -387,15 +394,39 @@ public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception { assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED); assertThat(transportStatus.getDescription()).contains("1234"); transportListener.awaitTermination(); - // If the transport gave up waiting on auth, it should cancel its request. assertThat(authRequest.isCancelled()).isTrue(); } @Test - public void testAsyncSecurityPolicyFailure() throws Exception { + public void testBlackHoleSecurityPolicyPreAuthTimeout() throws Exception { SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy(); - transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build(); + transport = + new BinderClientTransportBuilder() + .setSecurityPolicy(securityPolicy) + .setPreAuthorizeServer(true) + .setReadyTimeoutMillis(1_234) + .build(); + transport.start(transportListener).run(); + // Take the next authRequest but don't respond to it, in order to trigger the ready timeout. + AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS); + + Status transportStatus = transportListener.awaitShutdown(); + assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED); + assertThat(transportStatus.getDescription()).contains("1234"); + transportListener.awaitTermination(); + // If the transport gave up waiting on auth, it should cancel its request. + assertThat(preAuthRequest.isCancelled()).isTrue(); + } + + @Test + public void testAsyncSecurityPolicyAuthFailure() throws Exception { + SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy(); + transport = + new BinderClientTransportBuilder() + .setPreAuthorizeServer(false) + .setSecurityPolicy(securityPolicy) + .build(); RuntimeException exception = new NullPointerException(); transport.start(transportListener).run(); securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception); @@ -406,15 +437,55 @@ public void testAsyncSecurityPolicyFailure() throws Exception { } @Test - public void testAsyncSecurityPolicySuccess() throws Exception { + public void testAsyncSecurityPolicyPreAuthFailure() throws Exception { SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy(); - transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build(); + transport = + new BinderClientTransportBuilder() + .setPreAuthorizeServer(true) + .setSecurityPolicy(securityPolicy) + .build(); + RuntimeException exception = new NullPointerException(); + transport.start(transportListener).run(); + securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception); + Status transportStatus = transportListener.awaitShutdown(); + assertThat(transportStatus.getCode()).isEqualTo(Code.INTERNAL); + assertThat(transportStatus.getCause()).isEqualTo(exception); + transportListener.awaitTermination(); + } + + @Test + public void testAsyncSecurityPolicyAuthSuccess() throws Exception { + SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy(); + transport = + new BinderClientTransportBuilder() + .setPreAuthorizeServer(false) + .setSecurityPolicy(securityPolicy) + .build(); + transport.start(transportListener).run(); + securityPolicy + .takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS) + .setResult(Status.PERMISSION_DENIED.withDescription("xyzzy")); + Status transportStatus = transportListener.awaitShutdown(); + assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED); + assertThat(transportStatus.getDescription()).contains("xyzzy"); + transportListener.awaitTermination(); + } + + @Test + public void testAsyncSecurityPolicyPreAuthSuccess() throws Exception { + SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy(); + transport = + new BinderClientTransportBuilder() + .setPreAuthorizeServer(true) + .setSecurityPolicy(securityPolicy) + .build(); transport.start(transportListener).run(); securityPolicy .takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS) - .setResult(Status.PERMISSION_DENIED); + .setResult(Status.PERMISSION_DENIED.withDescription("xyzzy")); Status transportStatus = transportListener.awaitShutdown(); assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED); + assertThat(transportStatus.getDescription()).contains("xyzzy"); transportListener.awaitTermination(); } diff --git a/binder/src/main/java/io/grpc/binder/ApiConstants.java b/binder/src/main/java/io/grpc/binder/ApiConstants.java index 05950a9eaec..462586311c6 100644 --- a/binder/src/main/java/io/grpc/binder/ApiConstants.java +++ b/binder/src/main/java/io/grpc/binder/ApiConstants.java @@ -18,6 +18,8 @@ import android.content.Intent; import android.os.UserHandle; +import io.grpc.Attributes; +import io.grpc.EquivalentAddressGroup; import io.grpc.ExperimentalApi; import io.grpc.NameResolver; @@ -46,4 +48,16 @@ private ApiConstants() {} @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173") public static final NameResolver.Args.Key TARGET_ANDROID_USER = NameResolver.Args.Key.create("target-android-user"); + + /** + * Lets you override a Channel's pre-auth configuration (see {@link + * BinderChannelBuilder#preAuthorizeServers(boolean)}) for a given {@link EquivalentAddressGroup}. + * + *

    A {@link NameResolver} that discovers servers from an untrusted source like PackageManager + * can use this to force server pre-auth and prevent abuse. + */ + @EquivalentAddressGroup.Attr + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191") + public static final Attributes.Key PRE_AUTH_SERVER_OVERRIDE = + Attributes.Key.create("pre-auth-server-override"); } diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java index f996ee7cdbf..233ec6eac4f 100644 --- a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java +++ b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java @@ -279,6 +279,35 @@ public BinderChannelBuilder strictLifecycleManagement() { return this; } + /** + * Checks servers against this Channel's {@link SecurityPolicy} *before* binding. + * + *

    Android users can be tricked into installing a malicious app with the same package name as a + * legitimate server. That's why we don't send calls to a server until it has been authorized by + * an appropriate {@link SecurityPolicy}. But merely binding to a malicious server can enable + * "keep-alive" and "background activity launch" abuse, even if it's ultimately unauthorized. + * Pre-authorization mitigates these threats by performing a preliminary {@link SecurityPolicy} + * check against a server app's PackageManager-registered identity without actually creating an + * instance of it. This is especially important for security when the server's direct address + * isn't known in advance but rather resolved via target URI or discovered by other means. + * + *

    Note that, unlike ordinary authorization, pre-authorization is performed against the server + * app's UID, not the UID of the process hosting the bound Service. These can be different, most + * commonly due to services that set `android:isolatedProcess=true`. + * + *

    Pre-authorization is strongly recommended but it remains optional for now because of this + * behavior change and the small performance cost. + * + *

    The default value of this property is false but it will become true in a future release. + * Clients that require a particular behavior should configure it explicitly using this method + * rather than relying on the default. + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191") + public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) { + transportFactoryBuilder.setPreAuthorizeServers(preAuthorize); + return this; + } + @Override public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) { checkState( diff --git a/binder/src/main/java/io/grpc/binder/internal/Bindable.java b/binder/src/main/java/io/grpc/binder/internal/Bindable.java index 8e1af64b63d..ae0c7284faf 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Bindable.java +++ b/binder/src/main/java/io/grpc/binder/internal/Bindable.java @@ -16,10 +16,12 @@ package io.grpc.binder.internal; +import android.content.pm.ServiceInfo; import android.os.IBinder; import androidx.annotation.AnyThread; import androidx.annotation.MainThread; import io.grpc.Status; +import io.grpc.StatusException; /** An interface for managing a {@code Binder} connection. */ interface Bindable { @@ -45,6 +47,19 @@ interface Observer { void onUnbound(Status reason); } + /** + * Fetches details about the remote Service from PackageManager without binding to it. + * + *

    Resolving an untrusted address before binding to it lets you screen out problematic servers + * before giving them a chance to run. However, note that the identity/existence of the resolved + * Service can change between the time this method returns and the time you actually bind/connect + * to it. For example, suppose the target package gets uninstalled or upgraded right after this + * method returns. In {@link Observer#onBound}, you should verify that the server you resolved is + * the same one you connected to. + */ + @AnyThread + ServiceInfo resolve() throws StatusException; + /** * Attempt to bind with the remote service. * diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java index 3852b21d5c3..ef00f70e35d 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java @@ -55,6 +55,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor final InboundParcelablePolicy inboundParcelablePolicy; final OneWayBinderProxy.Decorator binderDecorator; final long readyTimeoutMillis; + final boolean preAuthorizeServers; // TODO(jdcormie): Default to true. ScheduledExecutorService executorService; Executor offloadExecutor; @@ -75,6 +76,7 @@ private BinderClientTransportFactory(Builder builder) { inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy); binderDecorator = checkNotNull(builder.binderDecorator); readyTimeoutMillis = builder.readyTimeoutMillis; + preAuthorizeServers = builder.preAuthorizeServers; executorService = scheduledExecutorPool.getObject(); offloadExecutor = offloadExecutorPool.getObject(); @@ -128,6 +130,7 @@ public static final class Builder implements ClientTransportFactoryBuilder { InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT; OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR; long readyTimeoutMillis = 60_000; + boolean preAuthorizeServers; @Override public BinderClientTransportFactory buildClientTransportFactory() { @@ -216,5 +219,11 @@ public Builder setReadyTimeoutMillis(long readyTimeoutMillis) { this.readyTimeoutMillis = readyTimeoutMillis; return this; } + + /** Whether to check server addresses against the SecurityPolicy *before* binding to them. */ + public Builder setPreAuthorizeServers(boolean preAuthorizeServers) { + this.preAuthorizeServers = preAuthorizeServers; + return this; + } } } diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 92a58b91cf0..d87cfb74044 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -19,9 +19,11 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.util.concurrent.Futures.immediateFuture; +import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE; import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; +import android.content.pm.ServiceInfo; import android.os.Binder; import android.os.DeadObjectException; import android.os.IBinder; @@ -78,7 +80,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -574,6 +575,7 @@ public static final class BinderClientTransport extends BinderTransport private final long readyTimeoutMillis; private final PingTracker pingTracker; + private final boolean preAuthorizeServer; @Nullable private ManagedClientTransport.Listener clientTransportListener; @@ -585,6 +587,10 @@ public static final class BinderClientTransport extends BinderTransport @GuardedBy("this") @Nullable private ListenableFuture authResultFuture; // null before we check auth. + @GuardedBy("this") + @Nullable + private ListenableFuture preAuthResultFuture; // null before we pre-auth. + /** * Constructs a new transport instance. * @@ -609,6 +615,9 @@ public BinderClientTransport( this.securityPolicy = factory.securityPolicy; this.offloadExecutor = offloadExecutorPool.getObject(); this.readyTimeoutMillis = factory.readyTimeoutMillis; + Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE); + this.preAuthorizeServer = + preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; numInUseStreams = new AtomicInteger(); pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); @@ -650,7 +659,16 @@ public synchronized Runnable start(ManagedClientTransport.Listener clientTranspo synchronized (BinderClientTransport.this) { if (inState(TransportState.NOT_STARTED)) { setState(TransportState.SETUP); - serviceBinding.bind(); + try { + if (preAuthorizeServer) { + preAuthorize(serviceBinding.resolve()); + } else { + serviceBinding.bind(); + } + } catch (StatusException e) { + shutdownInternal(e.getStatus(), true); + return; + } if (readyTimeoutMillis >= 0) { readyTimeoutFuture = getScheduledExecutorService() @@ -664,6 +682,43 @@ public synchronized Runnable start(ManagedClientTransport.Listener clientTranspo }; } + @GuardedBy("this") + private void preAuthorize(ServiceInfo serviceInfo) { + // It's unlikely, but the identity/existence of this Service could change by the time we + // actually connect. It doesn't matter though, because: + // - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer + // will eventually retry using a new transport instance that will see the Service's new state. + // - If pre-auth succeeds (but would fail against the server's new state), we might give an + // unauthorized server a chance to run, but the connection will still fail by SecurityPolicy + // check later in handshake. Pre-auth remains effective at mitigating abuse because malware + // can't typically control the exact timing of its installation. + preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid); + Futures.addCallback( + preAuthResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + handlePreAuthResult(result); + } + + @Override + public void onFailure(Throwable t) { + handleAuthResult(t); + } + }, + offloadExecutor); + } + + private synchronized void handlePreAuthResult(Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else { + serviceBinding.bind(); + } + } + } + private synchronized void onReadyTimeout() { if (inState(TransportState.SETUP)) { readyTimeoutFuture = null; @@ -758,6 +813,9 @@ void notifyTerminated() { readyTimeoutFuture.cancel(false); readyTimeoutFuture = null; } + if (preAuthResultFuture != null) { + preAuthResultFuture.cancel(false); // No effect if already complete. + } if (authResultFuture != null) { authResultFuture.cancel(false); // No effect if already complete. } @@ -780,11 +838,7 @@ protected void handleSetupTransport(Parcel parcel) { shutdownInternal( Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); } else { - authResultFuture = - (securityPolicy instanceof AsyncSecurityPolicy) - ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) - : Futures.submit( - () -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); + authResultFuture = checkServerAuthorizationAsync(remoteUid); Futures.addCallback( authResultFuture, new FutureCallback() { @@ -803,6 +857,12 @@ public void onFailure(Throwable t) { } } + private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { + return (securityPolicy instanceof AsyncSecurityPolicy) + ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) + : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); + } + private synchronized void handleAuthResult(IBinder binder, Status authorization) { if (inState(TransportState.SETUP)) { if (!authorization.isOk()) { diff --git a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java index ee171140045..f0cbe9ec56b 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -23,14 +23,22 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Build; import android.os.IBinder; import android.os.UserHandle; import androidx.annotation.AnyThread; import androidx.annotation.MainThread; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.VerifyException; import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Status; +import io.grpc.StatusException; import io.grpc.binder.BinderChannelCredentials; +import java.lang.reflect.Method; +import java.util.List; import java.util.concurrent.Executor; import java.util.logging.Level; import java.util.logging.Logger; @@ -85,6 +93,8 @@ public String methodName() { private final Observer observer; private final Executor mainThreadExecutor; + private static volatile Method queryIntentServicesAsUserMethod; + @GuardedBy("this") private State state; @@ -247,6 +257,71 @@ void unbindInternal(Status reason) { } } + // Sadly the PackageManager#resolveServiceAsUser() API we need isn't part of the SDK or even a + // @SystemApi as of this writing. Modern Android prevents even system apps from calling it, by any + // means (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces). + // So instead we call queryIntentServicesAsUser(), which does more than we need but *is* a + // @SystemApi in all the SDK versions where we support cross-user Channels. + @Nullable + private static ResolveInfo resolveServiceAsUser( + PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) { + List results = + queryIntentServicesAsUser(packageManager, intent, flags, targetUserHandle); + // The first query result is "what would be returned by resolveService", per the javadoc. + return (results != null && !results.isEmpty()) ? results.get(0) : null; + } + + // The cross-user Channel feature requires the client to be a system app so we assume @SystemApi + // queryIntentServicesAsUser() is visible to us at runtime. It would be visible at build time too, + // if our host system app were written to call it directly. We only have to use reflection here + // because grpc-java is a library built outside the Android source tree where the compiler can't + // see the "non-SDK" @SystemApis that we need. + @Nullable + @SuppressWarnings("unchecked") // Safe by PackageManager#queryIntentServicesAsUser spec in AOSP. + private static List queryIntentServicesAsUser( + PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) { + try { + if (queryIntentServicesAsUserMethod == null) { + synchronized (ServiceBinding.class) { + if (queryIntentServicesAsUserMethod == null) { + queryIntentServicesAsUserMethod = + PackageManager.class.getMethod( + "queryIntentServicesAsUser", Intent.class, int.class, UserHandle.class); + } + } + } + return (List) + queryIntentServicesAsUserMethod.invoke(packageManager, intent, flags, targetUserHandle); + } catch (ReflectiveOperationException e) { + throw new VerifyException(e); + } + } + + @AnyThread + @Override + public ServiceInfo resolve() throws StatusException { + checkState(sourceContext != null); + PackageManager packageManager = sourceContext.getPackageManager(); + int flags = 0; + if (Build.VERSION.SDK_INT >= 29) { + // Filter out non-'directBootAware' s when 'targetUserHandle' is locked. Here's why: + // Callers want 'bindIntent' to #resolve() to the same thing a follow-up call to #bind() will. + // But bindService() *always* ignores services that can't presently be created for lack of + // 'directBootAware'-ness. This flag explicitly tells resolveService() to act the same way. + flags |= PackageManager.MATCH_DIRECT_BOOT_AUTO; + } + ResolveInfo resolveInfo = + targetUserHandle != null + ? resolveServiceAsUser(packageManager, bindIntent, flags, targetUserHandle) + : packageManager.resolveService(bindIntent, flags); + if (resolveInfo == null) { + throw Status.UNIMPLEMENTED // Same status code as when bindService() returns false. + .withDescription("resolveService(" + bindIntent + " / " + targetUserHandle + ") was null") + .asException(); + } + return resolveInfo.serviceInfo; + } + @MainThread private void clearReferences() { sourceContext = null; diff --git a/binder/src/test/java/io/grpc/binder/RobolectricBinderSecurityTest.java b/binder/src/test/java/io/grpc/binder/RobolectricBinderSecurityTest.java index 16f06ad81c9..ffd1d89e69c 100644 --- a/binder/src/test/java/io/grpc/binder/RobolectricBinderSecurityTest.java +++ b/binder/src/test/java/io/grpc/binder/RobolectricBinderSecurityTest.java @@ -22,7 +22,13 @@ import static org.robolectric.Shadows.shadowOf; import android.app.Application; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.ServiceInfo; import androidx.test.core.app.ApplicationProvider; +import androidx.test.core.content.pm.ApplicationInfoBuilder; +import androidx.test.core.content.pm.PackageInfoBuilder; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; @@ -46,11 +52,13 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; import org.robolectric.annotation.LooperMode; import org.robolectric.annotation.LooperMode.Mode; -@RunWith(RobolectricTestRunner.class) +@RunWith(ParameterizedRobolectricTestRunner.class) @LooperMode(Mode.INSTRUMENTATION_TEST) public final class RobolectricBinderSecurityTest { @@ -62,10 +70,33 @@ public final class RobolectricBinderSecurityTest { private ManagedChannel channel; private Server server; + @Parameter public boolean preAuthServersParam; + + @Parameters(name = "preAuthServersParam={0}") + public static ImmutableList data() { + return ImmutableList.of(true, false); + } + @Before public void setUp() { + ApplicationInfo serverAppInfo = + ApplicationInfoBuilder.newBuilder().setPackageName(context.getPackageName()).build(); + serverAppInfo.uid = android.os.Process.myUid(); + PackageInfo serverPkgInfo = + PackageInfoBuilder.newBuilder() + .setPackageName(serverAppInfo.packageName) + .setApplicationInfo(serverAppInfo) + .build(); + shadowOf(context.getPackageManager()).installPackage(serverPkgInfo); + + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.name = "SomeService"; + serviceInfo.packageName = serverAppInfo.packageName; + serviceInfo.applicationInfo = serverAppInfo; + shadowOf(context.getPackageManager()).addOrUpdateService(serviceInfo); + AndroidComponentAddress listenAddress = - AndroidComponentAddress.forRemoteComponent(context.getPackageName(), "HostService"); + AndroidComponentAddress.forRemoteComponent(serviceInfo.packageName, serviceInfo.name); MethodDescriptor methodDesc = getMethodDescriptor(); ServerCallHandler callHandler = @@ -110,6 +141,7 @@ public ListenableFuture checkAuthorizationAsync(int uid) { checkNotNull(binderReceiver.get())); channel = BinderChannelBuilder.forAddress(listenAddress, context) + .preAuthorizeServers(preAuthServersParam) .build(); } diff --git a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java index 7d336067842..7275c47d51c 100644 --- a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java @@ -16,13 +16,29 @@ package io.grpc.binder.internal; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import static org.robolectric.Shadows.shadowOf; import android.app.Application; import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.ServiceInfo; import androidx.test.core.app.ApplicationProvider; +import androidx.test.core.content.pm.ApplicationInfoBuilder; +import androidx.test.core.content.pm.PackageInfoBuilder; +import com.google.common.collect.ImmutableList; +import io.grpc.Attributes; import io.grpc.ServerStreamTracer; +import io.grpc.Status; import io.grpc.binder.AndroidComponentAddress; +import io.grpc.binder.ApiConstants; +import io.grpc.binder.AsyncSecurityPolicy; +import io.grpc.binder.internal.SettableAsyncSecurityPolicy.AuthRequest; import io.grpc.internal.AbstractTransportTest; import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; import io.grpc.internal.GrpcUtil; @@ -33,12 +49,20 @@ import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; +import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; import org.robolectric.annotation.LooperMode; import org.robolectric.annotation.LooperMode.Mode; +import org.robolectric.shadows.ShadowBinder; /** * All of the AbstractTransportTest cases applied to {@link BinderTransport} running in a @@ -52,7 +76,7 @@ * meaning test cases don't run on the main thread. This supports the AbstractTransportTest approach * where the test thread frequently blocks waiting for transport state changes to take effect. */ -@RunWith(RobolectricTestRunner.class) +@RunWith(ParameterizedRobolectricTestRunner.class) @LooperMode(Mode.INSTRUMENTATION_TEST) public final class RobolectricBinderTransportTest extends AbstractTransportTest { @@ -64,14 +88,57 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest private final ObjectPool serverExecutorPool = SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR); + @Rule public MockitoRule mocks = MockitoJUnit.rule(); + + @Mock AsyncSecurityPolicy mockClientSecurityPolicy; + + ApplicationInfo serverAppInfo; + PackageInfo serverPkgInfo; + ServiceInfo serviceInfo; + private int nextServerAddress; + @Parameter public boolean preAuthServersParam; + + @Parameters(name = "preAuthServersParam={0}") + public static ImmutableList data() { + return ImmutableList.of(true, false); + } + + @Override + public void setUp() { + serverAppInfo = + ApplicationInfoBuilder.newBuilder().setPackageName("the.server.package").build(); + serverAppInfo.uid = android.os.Process.myUid(); + serverPkgInfo = + PackageInfoBuilder.newBuilder() + .setPackageName(serverAppInfo.packageName) + .setApplicationInfo(serverAppInfo) + .build(); + shadowOf(application.getPackageManager()).installPackage(serverPkgInfo); + + serviceInfo = new ServiceInfo(); + serviceInfo.name = "SomeService"; + serviceInfo.packageName = serverAppInfo.packageName; + serviceInfo.applicationInfo = serverAppInfo; + shadowOf(application.getPackageManager()).addOrUpdateService(serviceInfo); + + super.setUp(); + } + + @Before + public void requestRealisticBindServiceBehavior() { + shadowOf(application).setBindServiceCallsOnServiceConnectedDirectly(false); + shadowOf(application).setUnbindServiceCallsOnServiceDisconnected(false); + } + @Override protected InternalServer newServer(List streamTracerFactories) { - AndroidComponentAddress listenAddr = AndroidComponentAddress.forBindIntent( - new Intent() - .setClassName(application.getPackageName(), "HostService") - .setAction("io.grpc.action.BIND." + nextServerAddress++)); + AndroidComponentAddress listenAddr = + AndroidComponentAddress.forBindIntent( + new Intent() + .setClassName(serviceInfo.packageName, serviceInfo.name) + .setAction("io.grpc.action.BIND." + nextServerAddress++)); BinderServer binderServer = new BinderServer.Builder() @@ -81,6 +148,7 @@ protected InternalServer newServer(List streamTracer .setStreamTracerFactories(streamTracerFactories) .build(); + shadowOf(application.getPackageManager()).addServiceIfNotPresent(listenAddr.getComponent()); shadowOf(application) .setComponentNameAndServiceForBindServiceForIntent( listenAddr.asBindIntent(), listenAddr.getComponent(), binderServer.getHostBinder()); @@ -97,22 +165,30 @@ protected InternalServer newServer( return newServer(streamTracerFactories); } + BinderClientTransportFactory.Builder newClientTransportFactoryBuilder() { + return new BinderClientTransportFactory.Builder() + .setPreAuthorizeServers(preAuthServersParam) + .setSourceContext(application) + .setScheduledExecutorPool(executorServicePool) + .setOffloadExecutorPool(offloadExecutorPool); + } + + BinderClientTransportBuilder newClientTransportBuilder() { + return new BinderClientTransportBuilder() + .setFactory(newClientTransportFactoryBuilder().buildClientTransportFactory()) + .setServerAddress(server.getListenSocketAddress()); + } + @Override protected ManagedClientTransport newClientTransport(InternalServer server) { - BinderClientTransportFactory.Builder builder = - new BinderClientTransportFactory.Builder() - .setSourceContext(application) - .setScheduledExecutorPool(executorServicePool) - .setOffloadExecutorPool(offloadExecutorPool); - ClientTransportOptions options = new ClientTransportOptions(); options.setEagAttributes(eagAttrs()); options.setChannelLogger(transportLogger()); - return new BinderTransport.BinderClientTransport( - builder.buildClientTransportFactory(), - (AndroidComponentAddress) server.getListenSocketAddress(), - options); + return newClientTransportBuilder() + .setServerAddress(server.getListenSocketAddress()) + .setOptions(options) + .build(); } @Override @@ -120,6 +196,74 @@ protected String testAuthority(InternalServer server) { return ((AndroidComponentAddress) server.getListenSocketAddress()).getAuthority(); } + @Test + public void clientAuthorizesServerUidsInOrder() throws Exception { + // TODO(jdcormie): In real Android, Binder#getCallingUid is thread-local but Robolectric only + // lets us fake value this *globally*. So the ShadowBinder#setCallingUid() here unrealistically + // affects the server's view of the client's uid too. For now this doesn't matter because this + // test never exercises server SecurityPolicy. + ShadowBinder.setCallingUid(11111); // UID of the server *process*. + + serverPkgInfo.applicationInfo.uid = 22222; // UID of the server *app*, which can be different. + shadowOf(application.getPackageManager()).installPackage(serverPkgInfo); + shadowOf(application.getPackageManager()).addOrUpdateService(serviceInfo); + server = newServer(ImmutableList.of()); + server.start(serverListener); + + SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy(); + client = + newClientTransportBuilder() + .setFactory( + newClientTransportFactoryBuilder() + .setSecurityPolicy(securityPolicy) + .buildClientTransportFactory()) + .build(); + runIfNotNull(client.start(mockClientTransportListener)); + + if (preAuthServersParam) { + AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS); + assertThat(preAuthRequest.uid).isEqualTo(22222); + verify(mockClientTransportListener, never()).transportReady(); + preAuthRequest.setResult(Status.OK); + } + + AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS); + assertThat(authRequest.uid).isEqualTo(11111); + verify(mockClientTransportListener, never()).transportReady(); + authRequest.setResult(Status.OK); + + verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportReady(); + } + + @Test + public void eagAttributeCanOverrideChannelPreAuthServerSetting() throws Exception { + server.start(serverListener); + SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy(); + ClientTransportOptions options = new ClientTransportOptions(); + options.setEagAttributes( + Attributes.newBuilder().set(ApiConstants.PRE_AUTH_SERVER_OVERRIDE, true).build()); + client = + newClientTransportBuilder() + .setOptions(options) + .setFactory( + newClientTransportFactoryBuilder() + .setPreAuthorizeServers(preAuthServersParam) // To be overridden. + .setSecurityPolicy(securityPolicy) + .buildClientTransportFactory()) + .build(); + runIfNotNull(client.start(mockClientTransportListener)); + + AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS); + verify(mockClientTransportListener, never()).transportReady(); + preAuthRequest.setResult(Status.OK); + + AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS); + verify(mockClientTransportListener, never()).transportReady(); + authRequest.setResult(Status.OK); + + verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportReady(); + } + @Test @Ignore("See BinderTransportTest#socketStats.") @Override diff --git a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java index b0ad35e6806..bd51c522d15 100644 --- a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java @@ -19,6 +19,7 @@ import static android.content.Context.BIND_AUTO_CREATE; import static android.os.Looper.getMainLooper; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.robolectric.Shadows.shadowOf; @@ -27,6 +28,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ServiceInfo; import android.os.IBinder; import android.os.Parcel; import android.os.UserHandle; @@ -34,6 +36,7 @@ import androidx.test.core.app.ApplicationProvider; import io.grpc.Status; import io.grpc.Status.Code; +import io.grpc.StatusException; import io.grpc.binder.BinderChannelCredentials; import io.grpc.binder.internal.Bindable.Observer; import java.util.Arrays; @@ -59,6 +62,7 @@ public final class ServiceBindingTest { private Application appContext; private ComponentName serviceComponent; + private ServiceInfo serviceInfo = new ServiceInfo(); private ShadowApplication shadowApplication; private TestObserver observer; private ServiceBinding binding; @@ -67,13 +71,17 @@ public final class ServiceBindingTest { public void setUp() { appContext = ApplicationProvider.getApplicationContext(); serviceComponent = new ComponentName("DUMMY", "SERVICE"); + serviceInfo.packageName = serviceComponent.getPackageName(); + serviceInfo.name = serviceComponent.getClassName(); observer = new TestObserver(); shadowApplication = shadowOf(appContext); shadowApplication.setComponentNameAndServiceForBindService(serviceComponent, mockBinder); + shadowOf(appContext.getPackageManager()).addOrUpdateService(serviceInfo); // Don't call onServiceDisconnected() upon unbindService(), just like the real Android doesn't. shadowApplication.setUnbindServiceCallsOnServiceDisconnected(false); + shadowApplication.setBindServiceCallsOnServiceConnectedDirectly(false); binding = newBuilder().build(); shadowOf(getMainLooper()).idle(); @@ -276,6 +284,49 @@ public void testBindWithTargetUserHandle() throws Exception { assertThat(binding.isSourceContextCleared()).isFalse(); } + @Test + public void testResolve() throws Exception { + serviceInfo.processName = "x"; // ServiceInfo has no equals() so look for one distinctive field. + shadowOf(appContext.getPackageManager()).addOrUpdateService(serviceInfo); + ServiceInfo resolvedServiceInfo = binding.resolve(); + assertThat(resolvedServiceInfo.processName).isEqualTo(serviceInfo.processName); + } + + @Test + @Config(sdk = 33) + public void testResolveWithTargetUserHandle() throws Exception { + serviceInfo.processName = "x"; // ServiceInfo has no equals() so look for one distinctive field. + // Robolectric just ignores the user arg to resolveServiceAsUser() so this is all we can do. + shadowOf(appContext.getPackageManager()).addOrUpdateService(serviceInfo); + binding = newBuilder().setTargetUserHandle(generateUserHandle(/* userId= */ 0)).build(); + ServiceInfo resolvedServiceInfo = binding.resolve(); + assertThat(resolvedServiceInfo.processName).isEqualTo(serviceInfo.processName); + } + + @Test + public void testResolveNonExistentServiceThrows() throws Exception { + ComponentName doesNotExistService = new ComponentName("does.not.exist", "NoService"); + binding = newBuilder().setTargetComponent(doesNotExistService).build(); + StatusException statusException = assertThrows(StatusException.class, binding::resolve); + assertThat(statusException.getStatus().getCode()).isEqualTo(Code.UNIMPLEMENTED); + assertThat(statusException.getStatus().getDescription()).contains("does.not.exist"); + } + + @Test + @Config(sdk = 33) + public void testResolveNonExistentServiceWithTargetUserThrows() throws Exception { + ComponentName doesNotExistService = new ComponentName("does.not.exist", "NoService"); + binding = + newBuilder() + .setTargetUserHandle(generateUserHandle(/* userId= */ 12345)) + .setTargetComponent(doesNotExistService) + .build(); + StatusException statusException = assertThrows(StatusException.class, binding::resolve); + assertThat(statusException.getStatus().getCode()).isEqualTo(Code.UNIMPLEMENTED); + assertThat(statusException.getStatus().getDescription()).contains("does.not.exist"); + assertThat(statusException.getStatus().getDescription()).contains("12345"); + } + @Test @Config(sdk = 30) public void testBindWithDeviceAdmin() throws Exception { @@ -284,7 +335,7 @@ public void testBindWithDeviceAdmin() throws Exception { allowBindDeviceAdminForUser(appContext, adminComponent, /* userId= */ 0); binding = newBuilder() - .setTargetUserHandle(UserHandle.getUserHandleForUid(/* userId= */ 0)) + .setTargetUserHandle(UserHandle.getUserHandleForUid(/* uid= */ 0)) .setTargetUserHandle(generateUserHandle(/* userId= */ 0)) .setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent)) .build(); diff --git a/binder/src/testFixtures/java/io/grpc/binder/internal/BinderClientTransportBuilder.java b/binder/src/testFixtures/java/io/grpc/binder/internal/BinderClientTransportBuilder.java new file mode 100644 index 00000000000..e98daeec3ed --- /dev/null +++ b/binder/src/testFixtures/java/io/grpc/binder/internal/BinderClientTransportBuilder.java @@ -0,0 +1,61 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.binder.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import io.grpc.ChannelLogger; +import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; +import io.grpc.internal.TestUtils.NoopChannelLogger; +import java.net.SocketAddress; + +/** + * Helps unit tests create {@link BinderTransport.BinderClientTransport} instances without having to + * mention irrelevant details (go/tott/719). + */ +public class BinderClientTransportBuilder { + private BinderClientTransportFactory factory; + private SocketAddress serverAddress; + private ChannelLogger channelLogger = new NoopChannelLogger(); + private io.grpc.internal.ClientTransportFactory.ClientTransportOptions options = + new ClientTransportOptions(); + + public BinderClientTransportBuilder setServerAddress(SocketAddress serverAddress) { + this.serverAddress = checkNotNull(serverAddress); + return this; + } + + public BinderClientTransportBuilder setChannelLogger(ChannelLogger channelLogger) { + this.channelLogger = checkNotNull(channelLogger); + return this; + } + + public BinderClientTransportBuilder setOptions(ClientTransportOptions options) { + this.options = checkNotNull(options); + return this; + } + + public BinderClientTransportBuilder setFactory(BinderClientTransportFactory factory) { + this.factory = checkNotNull(factory); + return this; + } + + public BinderTransport.BinderClientTransport build() { + return factory.newClientTransport( + checkNotNull(serverAddress), checkNotNull(options), checkNotNull(channelLogger)); + } +} diff --git a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java index ea7f1723e64..c60174b2faf 100644 --- a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java +++ b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java @@ -92,7 +92,7 @@ public abstract class AbstractTransportTest { */ public static final int TEST_FLOW_CONTROL_WINDOW = 65 * 1024; - private static final int TIMEOUT_MS = 5000; + protected static final int TIMEOUT_MS = 5000; protected static final String GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES = "GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES"; @@ -2163,7 +2163,7 @@ static boolean waitForFuture(Future future, long timeout, TimeUnit unit) return true; } - private static void runIfNotNull(Runnable runnable) { + protected static void runIfNotNull(Runnable runnable) { if (runnable != null) { runnable.run(); } From 01bd63d88fbe07f06a7c28854d6db090a554ee94 Mon Sep 17 00:00:00 2001 From: Richard Belleville Date: Fri, 11 Jul 2025 15:07:00 -0700 Subject: [PATCH 316/591] Remove inactive maintainers (#12187) --- MAINTAINERS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index f1c07ccd6f2..5048c7c5aca 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -11,8 +11,6 @@ for general contribution guidelines. - [ejona86](https://github.com/ejona86), Google LLC - [jdcormie](https://github.com/jdcormie), Google LLC - [kannanjgithub](https://github.com/kannanjgithub), Google LLC -- [larry-safran](https://github.com/larry-safran), Google LLC -- [markb74](https://github.com/markb74), Google LLC - [ran-su](https://github.com/ran-su), Google LLC - [sergiitk](https://github.com/sergiitk), Google LLC - [temawi](https://github.com/temawi), Google LLC @@ -26,7 +24,9 @@ for general contribution guidelines. - [ericgribkoff](https://github.com/ericgribkoff) - [jiangtaoli2016](https://github.com/jiangtaoli2016) - [jtattermusch](https://github.com/jtattermusch) +- [larry-safran](https://github.com/larry-safran) - [louiscryan](https://github.com/louiscryan) +- [markb74](https://github.com/markb74) - [nicolasnoble](https://github.com/nicolasnoble) - [nmittler](https://github.com/nmittler) - [sanjaypujare](https://github.com/sanjaypujare) From 9d191b31b57a0076a806073871be034f6ae4bea3 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 11 Jul 2025 16:28:47 -0700 Subject: [PATCH 317/591] xds: Check isHttp11ProxyAvailable in equals() This fixes an equals/hashCode bug introduced in 12197065fe. Discovered when investigating b/430347751 --- .../ClusterResolverLoadBalancerProvider.java | 6 +++-- .../xds/ClusterResolverLoadBalancerTest.java | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java index b5dcb271368..27a884b1f4f 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java @@ -89,7 +89,7 @@ boolean isHttp11ProxyAvailable() { @Override public int hashCode() { - return Objects.hash(discoveryMechanisms, lbConfig); + return Objects.hash(discoveryMechanisms, lbConfig, isHttp11ProxyAvailable); } @Override @@ -102,7 +102,8 @@ public boolean equals(Object o) { } ClusterResolverConfig that = (ClusterResolverConfig) o; return discoveryMechanisms.equals(that.discoveryMechanisms) - && lbConfig.equals(that.lbConfig); + && lbConfig.equals(that.lbConfig) + && isHttp11ProxyAvailable == that.isHttp11ProxyAvailable; } @Override @@ -110,6 +111,7 @@ public String toString() { return MoreObjects.toStringHelper(this) .add("discoveryMechanisms", discoveryMechanisms) .add("lbConfig", lbConfig) + .add("isHttp11ProxyAvailable", isHttp11ProxyAvailable) .toString(); } diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index d701f281c01..395662cd93c 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -32,6 +32,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.common.testing.EqualsTester; import io.grpc.Attributes; import io.grpc.ChannelLogger; import io.grpc.ConnectivityState; @@ -1199,6 +1200,27 @@ public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_fallThroug any(ConnectivityState.class), any(SubchannelPicker.class)); } + @Test + public void config_equalsTester() { + new EqualsTester() + .addEqualityGroup( + new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanism1), leastRequest, false), + new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanism1), leastRequest, false)) + .addEqualityGroup(new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanism1), roundRobin, false)) + .addEqualityGroup(new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanism1), leastRequest, true)) + .addEqualityGroup(new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection), + leastRequest, + false)) + .addEqualityGroup(new ClusterResolverConfig( + Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), leastRequest, false)) + .testEquals(); + } + private void deliverLbConfig(ClusterResolverConfig config) { loadBalancer.acceptResolvedAddresses( ResolvedAddresses.newBuilder() From a8de9f07ab831686da026ec067370a6e0c5489f1 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 11 Jul 2025 16:49:06 -0700 Subject: [PATCH 318/591] xds: Implement equals in RingHashConfig Lack of equals causes cluster_resolver to consider every update a different configuration and restart itself. b/430347751 --- .../java/io/grpc/xds/RingHashLoadBalancer.java | 17 +++++++++++++++++ .../io/grpc/xds/RingHashLoadBalancerTest.java | 14 ++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java index 4f314e5391a..96ce5b97024 100644 --- a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java @@ -51,6 +51,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -523,6 +524,22 @@ static final class RingHashConfig { this.requestHashHeader = requestHashHeader; } + @Override + public boolean equals(Object o) { + if (!(o instanceof RingHashConfig)) { + return false; + } + RingHashConfig that = (RingHashConfig) o; + return this.minRingSize == that.minRingSize + && this.maxRingSize == that.maxRingSize + && Objects.equals(this.requestHashHeader, that.requestHashHeader); + } + + @Override + public int hashCode() { + return Objects.hash(minRingSize, maxRingSize, requestHashHeader); + } + @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java index f27cb772d03..fc5f21156dd 100644 --- a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java @@ -42,6 +42,7 @@ import com.google.common.collect.Iterables; import com.google.common.primitives.UnsignedInteger; +import com.google.common.testing.EqualsTester; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.ConnectivityState; @@ -1113,6 +1114,19 @@ protected Helper delegate() { assertThat(picks).containsExactly(subchannel1); } + @Test + public void config_equalsTester() { + new EqualsTester() + .addEqualityGroup( + new RingHashConfig(1, 2, "headerA"), + new RingHashConfig(1, 2, "headerA")) + .addEqualityGroup(new RingHashConfig(1, 1, "headerA")) + .addEqualityGroup(new RingHashConfig(2, 2, "headerA")) + .addEqualityGroup(new RingHashConfig(1, 2, "headerB")) + .addEqualityGroup(new RingHashConfig(1, 2, "")) + .testEquals(); + } + private List initializeLbSubchannels(RingHashConfig config, List servers, InitializationFlags... initFlags) { From 5a8326f1c794d273663f4cf442e50424210f15bc Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 15 Jul 2025 15:33:02 +0530 Subject: [PATCH 319/591] xds: add "resource_timer_is_transient_failure" server feature (#12063) --- .../java/io/grpc/xds/client/Bootstrapper.java | 9 +- .../io/grpc/xds/client/BootstrapperImpl.java | 14 +- .../java/io/grpc/xds/client/XdsClient.java | 6 +- .../io/grpc/xds/client/XdsClientImpl.java | 20 ++- .../grpc/xds/GrpcXdsClientImplDataTest.java | 2 +- .../grpc/xds/GrpcXdsClientImplTestBase.java | 160 +++++++++++++++--- .../java/io/grpc/xds/XdsNameResolverTest.java | 4 +- .../client/CommonBootstrapperTestUtils.java | 2 +- 8 files changed, 176 insertions(+), 41 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java index 90babd1e8d0..4fa75f6b335 100644 --- a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java @@ -63,17 +63,20 @@ public abstract static class ServerInfo { public abstract boolean isTrustedXdsServer(); + public abstract boolean resourceTimerIsTransientError(); + @VisibleForTesting public static ServerInfo create(String target, @Nullable Object implSpecificConfig) { - return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, false, false); + return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, + false, false, false); } @VisibleForTesting public static ServerInfo create( String target, Object implSpecificConfig, boolean ignoreResourceDeletion, - boolean isTrustedXdsServer) { + boolean isTrustedXdsServer, boolean resourceTimerIsTransientError) { return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, - ignoreResourceDeletion, isTrustedXdsServer); + ignoreResourceDeletion, isTrustedXdsServer, resourceTimerIsTransientError); } } diff --git a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java index c00685f1781..423c1a118e8 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -43,6 +43,8 @@ public abstract class BootstrapperImpl extends Bootstrapper { public static final String GRPC_EXPERIMENTAL_XDS_FALLBACK = "GRPC_EXPERIMENTAL_XDS_FALLBACK"; + public static final String GRPC_EXPERIMENTAL_XDS_DATA_ERROR_HANDLING = + "GRPC_EXPERIMENTAL_XDS_DATA_ERROR_HANDLING"; // Client features. @VisibleForTesting @@ -54,10 +56,16 @@ public abstract class BootstrapperImpl extends Bootstrapper { // Server features. private static final String SERVER_FEATURE_IGNORE_RESOURCE_DELETION = "ignore_resource_deletion"; private static final String SERVER_FEATURE_TRUSTED_XDS_SERVER = "trusted_xds_server"; + private static final String + SERVER_FEATURE_RESOURCE_TIMER_IS_TRANSIENT_ERROR = "resource_timer_is_transient_error"; @VisibleForTesting static boolean enableXdsFallback = GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_FALLBACK, true); + @VisibleForTesting + public static boolean xdsDataErrorHandlingEnabled + = GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_DATA_ERROR_HANDLING, false); + protected final XdsLogger logger; protected FileReader reader = LocalFileReader.INSTANCE; @@ -247,6 +255,7 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo Object implSpecificConfig = getImplSpecificConfig(serverConfig, serverUri); + boolean resourceTimerIsTransientError = false; boolean ignoreResourceDeletion = false; // "For forward compatibility reasons, the client will ignore any entry in the list that it // does not understand, regardless of type." @@ -254,11 +263,14 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo if (serverFeatures != null) { logger.log(XdsLogLevel.INFO, "Server features: {0}", serverFeatures); ignoreResourceDeletion = serverFeatures.contains(SERVER_FEATURE_IGNORE_RESOURCE_DELETION); + resourceTimerIsTransientError = xdsDataErrorHandlingEnabled + && serverFeatures.contains(SERVER_FEATURE_RESOURCE_TIMER_IS_TRANSIENT_ERROR); } servers.add( ServerInfo.create(serverUri, implSpecificConfig, ignoreResourceDeletion, serverFeatures != null - && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER))); + && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER), + resourceTimerIsTransientError)); } return servers.build(); } diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClient.java b/xds/src/main/java/io/grpc/xds/client/XdsClient.java index edbb0b2d74c..e2dd4169bca 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClient.java @@ -199,6 +199,10 @@ public static ResourceMetadata newResourceMetadataDoesNotExist() { return new ResourceMetadata(ResourceMetadataStatus.DOES_NOT_EXIST, "", 0, false, null, null); } + public static ResourceMetadata newResourceMetadataTimeout() { + return new ResourceMetadata(ResourceMetadataStatus.TIMEOUT, "", 0, false, null, null); + } + public static ResourceMetadata newResourceMetadataAcked( Any rawResource, String version, long updateTimeNanos) { checkNotNull(rawResource, "rawResource"); @@ -256,7 +260,7 @@ public UpdateFailureState getErrorState() { * config_dump.proto */ public enum ResourceMetadataStatus { - UNKNOWN, REQUESTED, DOES_NOT_EXIST, ACKED, NACKED + UNKNOWN, REQUESTED, DOES_NOT_EXIST, ACKED, NACKED, TIMEOUT } /** diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index 2b25d4db977..d70cfd841ef 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.xds.client.BootstrapperImpl.xdsDataErrorHandlingEnabled; import static io.grpc.xds.client.XdsResourceType.ParsedResource; import static io.grpc.xds.client.XdsResourceType.ValidatedResourceUpdate; @@ -67,6 +68,7 @@ public final class XdsClientImpl extends XdsClient implements ResourceStore { // Longest time to wait, since the subscription to some resource, for concluding its absence. @VisibleForTesting public static final int INITIAL_RESOURCE_FETCH_TIMEOUT_SEC = 15; + public static final int EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC = 30; private final SynchronizationContext syncContext = new SynchronizationContext( new Thread.UncaughtExceptionHandler() { @@ -738,6 +740,9 @@ void restartTimer() { // When client becomes ready, it triggers a restartTimer for all relevant subscribers. return; } + ServerInfo serverInfo = activeCpc.getServerInfo(); + int timeoutSec = xdsDataErrorHandlingEnabled && serverInfo.resourceTimerIsTransientError() + ? EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC : INITIAL_RESOURCE_FETCH_TIMEOUT_SEC; class ResourceNotFound implements Runnable { @Override @@ -761,8 +766,7 @@ public String toString() { respTimer.cancel(); } respTimer = syncContext.schedule( - new ResourceNotFound(), INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS, - timeService); + new ResourceNotFound(), timeoutSec, TimeUnit.SECONDS, timeService); } void stopTimer() { @@ -840,6 +844,8 @@ void onAbsent(@Nullable ProcessingTracker processingTracker, ServerInfo serverIn // Ignore deletion of State of the World resources when this feature is on, // and the resource is reusable. boolean ignoreResourceDeletionEnabled = serverInfo.ignoreResourceDeletion(); + boolean resourceTimerIsTransientError = + xdsDataErrorHandlingEnabled && serverInfo.resourceTimerIsTransientError(); if (ignoreResourceDeletionEnabled && type.isFullStateOfTheWorld() && data != null) { if (!resourceDeletionIgnored) { logger.log(XdsLogLevel.FORCE_WARNING, @@ -854,14 +860,20 @@ void onAbsent(@Nullable ProcessingTracker processingTracker, ServerInfo serverIn if (!absent) { data = null; absent = true; - metadata = ResourceMetadata.newResourceMetadataDoesNotExist(); + metadata = resourceTimerIsTransientError ? ResourceMetadata.newResourceMetadataTimeout() : + ResourceMetadata.newResourceMetadataDoesNotExist(); for (ResourceWatcher watcher : watchers.keySet()) { if (processingTracker != null) { processingTracker.startTask(); } watchers.get(watcher).execute(() -> { try { - watcher.onResourceDoesNotExist(resource); + if (resourceTimerIsTransientError) { + watcher.onError(Status.UNAVAILABLE.withDescription( + "Timed out waiting for resource " + resource + " from xDS server")); + } else { + watcher.onResourceDoesNotExist(resource); + } } finally { if (processingTracker != null) { processingTracker.onComplete(); diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 6167f491930..3650e5d5bb9 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -3549,7 +3549,7 @@ private static Filter buildHttpConnectionManagerFilter(HttpFilter... httpFilters private XdsResourceType.Args getXdsResourceTypeArgs(boolean isTrustedServer) { return new XdsResourceType.Args( - ServerInfo.create("http://td", "", false, isTrustedServer), "1.0", null, null, null, null + ServerInfo.create("http://td", "", false, isTrustedServer, false), "1.0", null, null, null, null ); } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 32361684a6d..26b0baec05b 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -85,6 +85,7 @@ import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.Bootstrapper.CertificateProviderInfo; import io.grpc.xds.client.Bootstrapper.ServerInfo; +import io.grpc.xds.client.BootstrapperImpl; import io.grpc.xds.client.EnvoyProtoData.Node; import io.grpc.xds.client.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.client.Locality; @@ -145,7 +146,7 @@ public abstract class GrpcXdsClientImplTestBase { private static final String SERVER_URI = "trafficdirector.googleapis.com"; - private static final String SERVER_URI_CUSTOME_AUTHORITY = "trafficdirector2.googleapis.com"; + private static final String SERVER_URI_CUSTOM_AUTHORITY = "trafficdirector2.googleapis.com"; private static final String SERVER_URI_EMPTY_AUTHORITY = "trafficdirector3.googleapis.com"; private static final String LDS_RESOURCE = "listener.googleapis.com"; private static final String RDS_RESOURCE = "route-configuration.googleapis.com"; @@ -304,6 +305,30 @@ public long currentTimeNanos() { private final BindableService adsService = createAdsService(); private final BindableService lrsService = createLrsService(); + private XdsTransportFactory xdsTransportFactory = new XdsTransportFactory() { + @Override + public XdsTransport create(ServerInfo serverInfo) { + if (serverInfo.target().equals(SERVER_URI)) { + return new GrpcXdsTransport(channel); + } + if (serverInfo.target().equals(SERVER_URI_CUSTOM_AUTHORITY)) { + if (channelForCustomAuthority == null) { + channelForCustomAuthority = cleanupRule.register( + InProcessChannelBuilder.forName(serverName).directExecutor().build()); + } + return new GrpcXdsTransport(channelForCustomAuthority); + } + if (serverInfo.target().equals(SERVER_URI_EMPTY_AUTHORITY)) { + if (channelForEmptyAuthority == null) { + channelForEmptyAuthority = cleanupRule.register( + InProcessChannelBuilder.forName(serverName).directExecutor().build()); + } + return new GrpcXdsTransport(channelForEmptyAuthority); + } + throw new IllegalArgumentException("Can not create channel for " + serverInfo); + } + }; + @Before public void setUp() throws IOException { when(backoffPolicyProvider.get()).thenReturn(backoffPolicy1, backoffPolicy2); @@ -322,32 +347,9 @@ public void setUp() throws IOException { .start()); channel = cleanupRule.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); - XdsTransportFactory xdsTransportFactory = new XdsTransportFactory() { - @Override - public XdsTransport create(ServerInfo serverInfo) { - if (serverInfo.target().equals(SERVER_URI)) { - return new GrpcXdsTransport(channel); - } - if (serverInfo.target().equals(SERVER_URI_CUSTOME_AUTHORITY)) { - if (channelForCustomAuthority == null) { - channelForCustomAuthority = cleanupRule.register( - InProcessChannelBuilder.forName(serverName).directExecutor().build()); - } - return new GrpcXdsTransport(channelForCustomAuthority); - } - if (serverInfo.target().equals(SERVER_URI_EMPTY_AUTHORITY)) { - if (channelForEmptyAuthority == null) { - channelForEmptyAuthority = cleanupRule.register( - InProcessChannelBuilder.forName(serverName).directExecutor().build()); - } - return new GrpcXdsTransport(channelForEmptyAuthority); - } - throw new IllegalArgumentException("Can not create channel for " + serverInfo); - } - }; xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), - true); + true, false); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -357,7 +359,7 @@ public XdsTransport create(ServerInfo serverInfo) { AuthorityInfo.create( "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/%s", ImmutableList.of(Bootstrapper.ServerInfo.create( - SERVER_URI_CUSTOME_AUTHORITY, CHANNEL_CREDENTIALS))), + SERVER_URI_CUSTOM_AUTHORITY, CHANNEL_CREDENTIALS))), "", AuthorityInfo.create( "xdstp:///envoy.config.listener.v3.Listener/%s", @@ -3155,6 +3157,108 @@ public void flowControlAbsent() throws Exception { verify(anotherWatcher).onError(any()); } + @Test + @SuppressWarnings("unchecked") + public void resourceTimerIsTransientError_schedulesExtendedTimeout() { + BootstrapperImpl.xdsDataErrorHandlingEnabled = true; + ServerInfo serverInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, + false, true, true); + BootstrapInfo bootstrapInfo = + Bootstrapper.BootstrapInfo.builder() + .servers(Collections.singletonList(serverInfo)) + .node(NODE) + .authorities(ImmutableMap.of( + "", + AuthorityInfo.create( + "xdstp:///envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS))))) + .certProviders(ImmutableMap.of()) + .build(); + xdsClient = new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + timeProvider, + MessagePrinter.INSTANCE, + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); + ResourceWatcher watcher = mock(ResourceWatcher.class); + String resourceName = "cluster.googleapis.com"; + + xdsClient.watchXdsResource( + XdsClusterResource.getInstance(), + resourceName, + watcher, + fakeClock.getScheduledExecutorService()); + + ScheduledTask task = Iterables.getOnlyElement( + fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); + assertThat(task.getDelay(TimeUnit.SECONDS)) + .isEqualTo(XdsClientImpl.EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC); + fakeClock.runDueTasks(); + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + } + + @Test + @SuppressWarnings("unchecked") + public void resourceTimerIsTransientError_callsOnErrorUnavailable() { + BootstrapperImpl.xdsDataErrorHandlingEnabled = true; + xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), + true, true); + BootstrapInfo bootstrapInfo = + Bootstrapper.BootstrapInfo.builder() + .servers(Collections.singletonList(xdsServerInfo)) + .node(NODE) + .authorities(ImmutableMap.of( + "authority.xds.com", + AuthorityInfo.create( + "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_CUSTOM_AUTHORITY, CHANNEL_CREDENTIALS))), + "", + AuthorityInfo.create( + "xdstp:///envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS))))) + .certProviders(ImmutableMap.of("cert-instance-name", + CertificateProviderInfo.create("file-watcher", ImmutableMap.of()))) + .build(); + xdsClient = new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + timeProvider, + MessagePrinter.INSTANCE, + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); + String timeoutResource = CDS_RESOURCE + "_timeout"; + ResourceWatcher timeoutWatcher = mock(ResourceWatcher.class); + + xdsClient.watchXdsResource( + XdsClusterResource.getInstance(), + timeoutResource, + timeoutWatcher, + fakeClock.getScheduledExecutorService()); + + assertThat(resourceDiscoveryCalls).hasSize(1); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + call.verifyRequest(CDS, ImmutableList.of(timeoutResource), "", "", NODE); + fakeClock.forwardTime(XdsClientImpl.EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); + fakeClock.runDueTasks(); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Status.class); + verify(timeoutWatcher).onError(errorCaptor.capture()); + Status error = errorCaptor.getValue(); + assertThat(error.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(error.getDescription()).isEqualTo( + "Timed out waiting for resource " + timeoutResource + " from xDS server"); + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + } + private Answer blockUpdate(CyclicBarrier barrier) { return new Answer() { @Override @@ -4220,7 +4324,7 @@ private XdsClientImpl createXdsClient(String serverUri) { private BootstrapInfo buildBootStrap(String serverUri) { ServerInfo xdsServerInfo = ServerInfo.create(serverUri, CHANNEL_CREDENTIALS, - ignoreResourceDeletion(), true); + ignoreResourceDeletion(), true, false); return Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -4230,7 +4334,7 @@ private BootstrapInfo buildBootStrap(String serverUri) { AuthorityInfo.create( "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/%s", ImmutableList.of(Bootstrapper.ServerInfo.create( - SERVER_URI_CUSTOME_AUTHORITY, CHANNEL_CREDENTIALS))), + SERVER_URI_CUSTOM_AUTHORITY, CHANNEL_CREDENTIALS))), "", AuthorityInfo.create( "xdstp:///envoy.config.listener.v3.Listener/%s", diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 7015a43f6ed..e2618df18b1 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -368,13 +368,13 @@ public void resolving_targetAuthorityInAuthoritiesMap() { String serviceAuthority = "[::FFFF:129.144.52.38]:80"; bootstrapInfo = BootstrapInfo.builder() .servers(ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true, true))) + "td.googleapis.com", InsecureChannelCredentials.create(), true, true, false))) .node(Node.newBuilder().build()) .authorities( ImmutableMap.of(targetAuthority, AuthorityInfo.create( "xdstp://" + targetAuthority + "/envoy.config.listener.v3.Listener/%s?foo=1&bar=2", ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true, true))))) + "td.googleapis.com", InsecureChannelCredentials.create(), true, true, false))))) .build(); expectedLdsResourceName = "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/" + "%5B::FFFF:129.144.52.38%5D:80?bar=2&foo=1"; // query param canonified diff --git a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java index 485970741c1..754e903f8a9 100644 --- a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java @@ -203,7 +203,7 @@ public static Bootstrapper.BootstrapInfo buildBootStrap(List serverUris) List serverInfos = new ArrayList<>(); for (String uri : serverUris) { - serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, false, true)); + serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, false, true, false)); } EnvoyProtoData.Node node = EnvoyProtoData.Node.newBuilder().setId("node-id").build(); From d352540a02cc3f438caf7527acfa7bec8c03ecf8 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Wed, 16 Jul 2025 09:09:43 +0000 Subject: [PATCH 320/591] api: Add more Javadoc for NameResolver.Listener2 interface (#12220) --- api/src/main/java/io/grpc/NameResolver.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java index 9483f5f3b0a..2ac4ecae69e 100644 --- a/api/src/main/java/io/grpc/NameResolver.java +++ b/api/src/main/java/io/grpc/NameResolver.java @@ -239,6 +239,9 @@ public final void onAddresses( * {@link ResolutionResult#getAddressesOrError()} is empty, {@link #onError(Status)} will be * called. * + *

    Newer NameResolver implementations should prefer calling onResult2. This method exists to + * facilitate older {@link Listener} implementations to migrate to {@link Listener2}. + * * @param resolutionResult the resolved server addresses, attributes, and Service Config. * @since 1.21.0 */ @@ -248,6 +251,10 @@ public final void onAddresses( * Handles a name resolving error from the resolver. The listener is responsible for eventually * invoking {@link NameResolver#refresh()} to re-attempt resolution. * + *

    New NameResolver implementations should prefer calling onResult2 which will have the + * address resolution error in {@link ResolutionResult}'s addressesOrError. This method exists + * to facilitate older implementations using {@link Listener} to migrate to {@link Listener2}. + * * @param error a non-OK status * @since 1.21.0 */ @@ -255,9 +262,14 @@ public final void onAddresses( public abstract void onError(Status error); /** - * Handles updates on resolved addresses and attributes. + * Handles updates on resolved addresses and attributes. Must be called from the same + * {@link SynchronizationContext} available in {@link NameResolver.Args} that is passed + * from the channel. * - * @param resolutionResult the resolved server addresses, attributes, and Service Config. + * @param resolutionResult the resolved server addresses or error in address resolution, + * attributes, and Service Config or error + * @return status indicating whether the resolutionResult was accepted by the listener, + * typically the result from a load balancer. * @since 1.66 */ public Status onResult2(ResolutionResult resolutionResult) { From d7d70c6905666876d58c3c23cdf110cabdc11b37 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Thu, 17 Jul 2025 10:26:12 +0530 Subject: [PATCH 321/591] xds: cncf/xds proto sync to 2025-05-02 (#12225) --- repositories.bzl | 6 ++--- xds/third_party/xds/import.sh | 3 ++- .../xds/src/main/proto/xds/core/v3/cidr.proto | 25 +++++++++++++++++++ .../xds/data/orca/v3/orca_load_report.proto | 2 +- .../main/proto/xds/type/matcher/v3/cel.proto | 9 ++----- .../xds/type/matcher/v3/http_inputs.proto | 6 +---- .../proto/xds/type/matcher/v3/matcher.proto | 11 +++++--- .../xds/src/main/proto/xds/type/v3/cel.proto | 7 ++++++ 8 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 xds/third_party/xds/src/main/proto/xds/core/v3/cidr.proto diff --git a/repositories.bzl b/repositories.bzl index 12a8ec7a6f1..dd74a48da4b 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -101,10 +101,10 @@ def grpc_java_repositories(bzlmod = False): if not native.existing_rule("com_github_cncf_xds"): http_archive( name = "com_github_cncf_xds", - strip_prefix = "xds-024c85f92f20cab567a83acc50934c7f9711d124", - sha256 = "5f403aa681711500ca8e62387be3e37d971977db6e88616fc21862a406430649", + strip_prefix = "xds-2ac532fd44436293585084f8d94c6bdb17835af0", + sha256 = "790c4c83b6950bb602fec221f6a529d9f368cdc8852aae7d2592d0d04b015f37", urls = [ - "https://github.com/cncf/xds/archive/024c85f92f20cab567a83acc50934c7f9711d124.tar.gz", + "https://github.com/cncf/xds/archive/2ac532fd44436293585084f8d94c6bdb17835af0.tar.gz", ], ) if not bzlmod and not native.existing_rule("com_github_grpc_grpc"): diff --git a/xds/third_party/xds/import.sh b/xds/third_party/xds/import.sh index 9e4bf71d52f..7af5c8489d1 100755 --- a/xds/third_party/xds/import.sh +++ b/xds/third_party/xds/import.sh @@ -17,7 +17,7 @@ set -e # import VERSION from one of the google internal CLs -VERSION=024c85f92f20cab567a83acc50934c7f9711d124 +VERSION=2ac532fd44436293585084f8d94c6bdb17835af0 DOWNLOAD_URL="https://github.com/cncf/xds/archive/${VERSION}.tar.gz" DOWNLOAD_BASE_DIR="xds-${VERSION}" SOURCE_PROTO_BASE_DIR="${DOWNLOAD_BASE_DIR}" @@ -40,6 +40,7 @@ xds/annotations/v3/versioning.proto xds/core/v3/authority.proto xds/core/v3/collection_entry.proto xds/core/v3/context_params.proto +xds/core/v3/cidr.proto xds/core/v3/extension.proto xds/core/v3/resource_locator.proto xds/core/v3/resource_name.proto diff --git a/xds/third_party/xds/src/main/proto/xds/core/v3/cidr.proto b/xds/third_party/xds/src/main/proto/xds/core/v3/cidr.proto new file mode 100644 index 00000000000..c40dab2f28f --- /dev/null +++ b/xds/third_party/xds/src/main/proto/xds/core/v3/cidr.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package xds.core.v3; + +import "xds/annotations/v3/status.proto"; +import "google/protobuf/wrappers.proto"; + +import "validate/validate.proto"; + +option java_outer_classname = "CidrRangeProto"; +option java_multiple_files = true; +option java_package = "com.github.xds.core.v3"; +option go_package = "github.com/cncf/xds/go/xds/core/v3"; + +option (xds.annotations.v3.file_status).work_in_progress = true; + +// CidrRange specifies an IP Address and a prefix length to construct +// the subnet mask for a `CIDR `_ range. +message CidrRange { + // IPv4 or IPv6 address, e.g. ``192.0.0.0`` or ``2001:db8::``. + string address_prefix = 1 [(validate.rules).string = {min_len: 1}]; + + // Length of prefix, e.g. 0, 32. Defaults to 0 when unset. + google.protobuf.UInt32Value prefix_len = 2 [(validate.rules).uint32 = {lte: 128}]; +} diff --git a/xds/third_party/xds/src/main/proto/xds/data/orca/v3/orca_load_report.proto b/xds/third_party/xds/src/main/proto/xds/data/orca/v3/orca_load_report.proto index 53da75f78ac..1b0847585a4 100644 --- a/xds/third_party/xds/src/main/proto/xds/data/orca/v3/orca_load_report.proto +++ b/xds/third_party/xds/src/main/proto/xds/data/orca/v3/orca_load_report.proto @@ -10,7 +10,7 @@ option go_package = "github.com/cncf/xds/go/xds/data/orca/v3"; import "validate/validate.proto"; // See section `ORCA load report format` of the design document in -// :ref:`https://github.com/envoyproxy/envoy/issues/6614`. +// https://github.com/envoyproxy/envoy/issues/6614. message OrcaLoadReport { // CPU utilization expressed as a fraction of available CPU resources. This diff --git a/xds/third_party/xds/src/main/proto/xds/type/matcher/v3/cel.proto b/xds/third_party/xds/src/main/proto/xds/type/matcher/v3/cel.proto index b1ad1faa281..a45af9534a0 100644 --- a/xds/third_party/xds/src/main/proto/xds/type/matcher/v3/cel.proto +++ b/xds/third_party/xds/src/main/proto/xds/type/matcher/v3/cel.proto @@ -2,9 +2,7 @@ syntax = "proto3"; package xds.type.matcher.v3; -import "xds/annotations/v3/status.proto"; import "xds/type/v3/cel.proto"; - import "validate/validate.proto"; option java_package = "com.github.xds.type.matcher.v3"; @@ -12,8 +10,6 @@ option java_outer_classname = "CelProto"; option java_multiple_files = true; option go_package = "github.com/cncf/xds/go/xds/type/matcher/v3"; -option (xds.annotations.v3.file_status).work_in_progress = true; - // [#protodoc-title: Common Expression Language (CEL) matchers] // Performs a match by evaluating a `Common Expression Language @@ -24,14 +20,13 @@ option (xds.annotations.v3.file_status).work_in_progress = true; // // The match is ``true``, iff the result of the evaluation is a bool AND true. // In all other cases, the match is ``false``, including but not limited to: non-bool types, -// ``false``, ``null``,`` int(1)``, etc. +// ``false``, ``null``, ``int(1)``, etc. // In case CEL expression raises an error, the result of the evaluation is interpreted "no match". // // Refer to :ref:`Unified Matcher API ` documentation // for usage details. // -// [#comment:TODO(sergiitk): Link HttpAttributesMatchInput + usage example.] -// [#comment:TODO(sergiitk): When implemented, add the extension tag.] +// [#comment: envoy.matching.matchers.cel_matcher] message CelMatcher { // Either parsed or checked representation of the CEL program. type.v3.CelExpression expr_match = 1 [(validate.rules).message = {required: true}]; diff --git a/xds/third_party/xds/src/main/proto/xds/type/matcher/v3/http_inputs.proto b/xds/third_party/xds/src/main/proto/xds/type/matcher/v3/http_inputs.proto index 0dd80cd6f66..5709d64501b 100644 --- a/xds/third_party/xds/src/main/proto/xds/type/matcher/v3/http_inputs.proto +++ b/xds/third_party/xds/src/main/proto/xds/type/matcher/v3/http_inputs.proto @@ -2,15 +2,11 @@ syntax = "proto3"; package xds.type.matcher.v3; -import "xds/annotations/v3/status.proto"; - option java_package = "com.github.xds.type.matcher.v3"; option java_outer_classname = "HttpInputsProto"; option java_multiple_files = true; option go_package = "github.com/cncf/xds/go/xds/type/matcher/v3"; -option (xds.annotations.v3.file_status).work_in_progress = true; - // [#protodoc-title: Common HTTP Inputs] // Specifies that matching should be performed on the set of :ref:`HTTP attributes @@ -22,6 +18,6 @@ option (xds.annotations.v3.file_status).work_in_progress = true; // Refer to :ref:`Unified Matcher API ` documentation // for usage details. // -// [#comment:TODO(sergiitk): When implemented, add the extension tag.] +// [#comment: envoy.matching.inputs.cel_data_input] message HttpAttributesCelMatchInput { } diff --git a/xds/third_party/xds/src/main/proto/xds/type/matcher/v3/matcher.proto b/xds/third_party/xds/src/main/proto/xds/type/matcher/v3/matcher.proto index 4966b456bee..cc03ff6e98f 100644 --- a/xds/third_party/xds/src/main/proto/xds/type/matcher/v3/matcher.proto +++ b/xds/third_party/xds/src/main/proto/xds/type/matcher/v3/matcher.proto @@ -2,7 +2,6 @@ syntax = "proto3"; package xds.type.matcher.v3; -import "xds/annotations/v3/status.proto"; import "xds/core/v3/extension.proto"; import "xds/type/matcher/v3/string.proto"; @@ -21,8 +20,6 @@ option go_package = "github.com/cncf/xds/go/xds/type/matcher/v3"; // As an on_no_match might result in another matching tree being evaluated, this process // might repeat several times until the final OnMatch (or no match) is decided. message Matcher { - option (xds.annotations.v3.message_status).work_in_progress = true; - // What to do if a match is successful. message OnMatch { oneof on_match { @@ -38,6 +35,14 @@ message Matcher { // Protocol-specific action to take. core.v3.TypedExtensionConfig action = 2; } + + // If true and the Matcher matches, the action will be taken but the caller + // will behave as if the Matcher did not match. A subsequent matcher or + // on_no_match action will be used instead. + // This field is not supported in all contexts in which the matcher API is + // used. If this field is set in a context in which it's not supported, + // the resource will be rejected. + bool keep_matching = 3; } // A linear list of field matchers. diff --git a/xds/third_party/xds/src/main/proto/xds/type/v3/cel.proto b/xds/third_party/xds/src/main/proto/xds/type/v3/cel.proto index df4f81d90d2..043990401c6 100644 --- a/xds/third_party/xds/src/main/proto/xds/type/v3/cel.proto +++ b/xds/third_party/xds/src/main/proto/xds/type/v3/cel.proto @@ -47,6 +47,13 @@ message CelExpression { // // If set, takes precedence over ``cel_expr_parsed``. cel.expr.CheckedExpr cel_expr_checked = 4; + + // Unparsed expression in string form. For example, ``request.headers['x-env'] == 'prod'`` will + // get ``x-env`` header value and compare it with ``prod``. + // Check the `Common Expression Language `_ for more details. + // + // If set, takes precedence over ``cel_expr_parsed`` and ``cel_expr_checked``. + string cel_expr_string = 5; } // Extracts a string by evaluating a `Common Expression Language From 6935d3a115be45f7ed240fd80087a0a829d966f4 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Thu, 17 Jul 2025 11:35:34 +0530 Subject: [PATCH 322/591] Revert "xds: add "resource_timer_is_transient_failure" server feature (#12063)" (#12228) --- .../java/io/grpc/xds/client/Bootstrapper.java | 9 +- .../io/grpc/xds/client/BootstrapperImpl.java | 14 +- .../java/io/grpc/xds/client/XdsClient.java | 6 +- .../io/grpc/xds/client/XdsClientImpl.java | 20 +-- .../grpc/xds/GrpcXdsClientImplDataTest.java | 2 +- .../grpc/xds/GrpcXdsClientImplTestBase.java | 160 +++--------------- .../java/io/grpc/xds/XdsNameResolverTest.java | 4 +- .../client/CommonBootstrapperTestUtils.java | 2 +- 8 files changed, 41 insertions(+), 176 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java index 4fa75f6b335..90babd1e8d0 100644 --- a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java @@ -63,20 +63,17 @@ public abstract static class ServerInfo { public abstract boolean isTrustedXdsServer(); - public abstract boolean resourceTimerIsTransientError(); - @VisibleForTesting public static ServerInfo create(String target, @Nullable Object implSpecificConfig) { - return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, - false, false, false); + return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, false, false); } @VisibleForTesting public static ServerInfo create( String target, Object implSpecificConfig, boolean ignoreResourceDeletion, - boolean isTrustedXdsServer, boolean resourceTimerIsTransientError) { + boolean isTrustedXdsServer) { return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, - ignoreResourceDeletion, isTrustedXdsServer, resourceTimerIsTransientError); + ignoreResourceDeletion, isTrustedXdsServer); } } diff --git a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java index 423c1a118e8..c00685f1781 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -43,8 +43,6 @@ public abstract class BootstrapperImpl extends Bootstrapper { public static final String GRPC_EXPERIMENTAL_XDS_FALLBACK = "GRPC_EXPERIMENTAL_XDS_FALLBACK"; - public static final String GRPC_EXPERIMENTAL_XDS_DATA_ERROR_HANDLING = - "GRPC_EXPERIMENTAL_XDS_DATA_ERROR_HANDLING"; // Client features. @VisibleForTesting @@ -56,16 +54,10 @@ public abstract class BootstrapperImpl extends Bootstrapper { // Server features. private static final String SERVER_FEATURE_IGNORE_RESOURCE_DELETION = "ignore_resource_deletion"; private static final String SERVER_FEATURE_TRUSTED_XDS_SERVER = "trusted_xds_server"; - private static final String - SERVER_FEATURE_RESOURCE_TIMER_IS_TRANSIENT_ERROR = "resource_timer_is_transient_error"; @VisibleForTesting static boolean enableXdsFallback = GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_FALLBACK, true); - @VisibleForTesting - public static boolean xdsDataErrorHandlingEnabled - = GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_DATA_ERROR_HANDLING, false); - protected final XdsLogger logger; protected FileReader reader = LocalFileReader.INSTANCE; @@ -255,7 +247,6 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo Object implSpecificConfig = getImplSpecificConfig(serverConfig, serverUri); - boolean resourceTimerIsTransientError = false; boolean ignoreResourceDeletion = false; // "For forward compatibility reasons, the client will ignore any entry in the list that it // does not understand, regardless of type." @@ -263,14 +254,11 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo if (serverFeatures != null) { logger.log(XdsLogLevel.INFO, "Server features: {0}", serverFeatures); ignoreResourceDeletion = serverFeatures.contains(SERVER_FEATURE_IGNORE_RESOURCE_DELETION); - resourceTimerIsTransientError = xdsDataErrorHandlingEnabled - && serverFeatures.contains(SERVER_FEATURE_RESOURCE_TIMER_IS_TRANSIENT_ERROR); } servers.add( ServerInfo.create(serverUri, implSpecificConfig, ignoreResourceDeletion, serverFeatures != null - && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER), - resourceTimerIsTransientError)); + && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER))); } return servers.build(); } diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClient.java b/xds/src/main/java/io/grpc/xds/client/XdsClient.java index e2dd4169bca..edbb0b2d74c 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClient.java @@ -199,10 +199,6 @@ public static ResourceMetadata newResourceMetadataDoesNotExist() { return new ResourceMetadata(ResourceMetadataStatus.DOES_NOT_EXIST, "", 0, false, null, null); } - public static ResourceMetadata newResourceMetadataTimeout() { - return new ResourceMetadata(ResourceMetadataStatus.TIMEOUT, "", 0, false, null, null); - } - public static ResourceMetadata newResourceMetadataAcked( Any rawResource, String version, long updateTimeNanos) { checkNotNull(rawResource, "rawResource"); @@ -260,7 +256,7 @@ public UpdateFailureState getErrorState() { * config_dump.proto */ public enum ResourceMetadataStatus { - UNKNOWN, REQUESTED, DOES_NOT_EXIST, ACKED, NACKED, TIMEOUT + UNKNOWN, REQUESTED, DOES_NOT_EXIST, ACKED, NACKED } /** diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index d70cfd841ef..2b25d4db977 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static io.grpc.xds.client.BootstrapperImpl.xdsDataErrorHandlingEnabled; import static io.grpc.xds.client.XdsResourceType.ParsedResource; import static io.grpc.xds.client.XdsResourceType.ValidatedResourceUpdate; @@ -68,7 +67,6 @@ public final class XdsClientImpl extends XdsClient implements ResourceStore { // Longest time to wait, since the subscription to some resource, for concluding its absence. @VisibleForTesting public static final int INITIAL_RESOURCE_FETCH_TIMEOUT_SEC = 15; - public static final int EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC = 30; private final SynchronizationContext syncContext = new SynchronizationContext( new Thread.UncaughtExceptionHandler() { @@ -740,9 +738,6 @@ void restartTimer() { // When client becomes ready, it triggers a restartTimer for all relevant subscribers. return; } - ServerInfo serverInfo = activeCpc.getServerInfo(); - int timeoutSec = xdsDataErrorHandlingEnabled && serverInfo.resourceTimerIsTransientError() - ? EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC : INITIAL_RESOURCE_FETCH_TIMEOUT_SEC; class ResourceNotFound implements Runnable { @Override @@ -766,7 +761,8 @@ public String toString() { respTimer.cancel(); } respTimer = syncContext.schedule( - new ResourceNotFound(), timeoutSec, TimeUnit.SECONDS, timeService); + new ResourceNotFound(), INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS, + timeService); } void stopTimer() { @@ -844,8 +840,6 @@ void onAbsent(@Nullable ProcessingTracker processingTracker, ServerInfo serverIn // Ignore deletion of State of the World resources when this feature is on, // and the resource is reusable. boolean ignoreResourceDeletionEnabled = serverInfo.ignoreResourceDeletion(); - boolean resourceTimerIsTransientError = - xdsDataErrorHandlingEnabled && serverInfo.resourceTimerIsTransientError(); if (ignoreResourceDeletionEnabled && type.isFullStateOfTheWorld() && data != null) { if (!resourceDeletionIgnored) { logger.log(XdsLogLevel.FORCE_WARNING, @@ -860,20 +854,14 @@ void onAbsent(@Nullable ProcessingTracker processingTracker, ServerInfo serverIn if (!absent) { data = null; absent = true; - metadata = resourceTimerIsTransientError ? ResourceMetadata.newResourceMetadataTimeout() : - ResourceMetadata.newResourceMetadataDoesNotExist(); + metadata = ResourceMetadata.newResourceMetadataDoesNotExist(); for (ResourceWatcher watcher : watchers.keySet()) { if (processingTracker != null) { processingTracker.startTask(); } watchers.get(watcher).execute(() -> { try { - if (resourceTimerIsTransientError) { - watcher.onError(Status.UNAVAILABLE.withDescription( - "Timed out waiting for resource " + resource + " from xDS server")); - } else { - watcher.onResourceDoesNotExist(resource); - } + watcher.onResourceDoesNotExist(resource); } finally { if (processingTracker != null) { processingTracker.onComplete(); diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 3650e5d5bb9..6167f491930 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -3549,7 +3549,7 @@ private static Filter buildHttpConnectionManagerFilter(HttpFilter... httpFilters private XdsResourceType.Args getXdsResourceTypeArgs(boolean isTrustedServer) { return new XdsResourceType.Args( - ServerInfo.create("http://td", "", false, isTrustedServer, false), "1.0", null, null, null, null + ServerInfo.create("http://td", "", false, isTrustedServer), "1.0", null, null, null, null ); } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 26b0baec05b..32361684a6d 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -85,7 +85,6 @@ import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.Bootstrapper.CertificateProviderInfo; import io.grpc.xds.client.Bootstrapper.ServerInfo; -import io.grpc.xds.client.BootstrapperImpl; import io.grpc.xds.client.EnvoyProtoData.Node; import io.grpc.xds.client.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.client.Locality; @@ -146,7 +145,7 @@ public abstract class GrpcXdsClientImplTestBase { private static final String SERVER_URI = "trafficdirector.googleapis.com"; - private static final String SERVER_URI_CUSTOM_AUTHORITY = "trafficdirector2.googleapis.com"; + private static final String SERVER_URI_CUSTOME_AUTHORITY = "trafficdirector2.googleapis.com"; private static final String SERVER_URI_EMPTY_AUTHORITY = "trafficdirector3.googleapis.com"; private static final String LDS_RESOURCE = "listener.googleapis.com"; private static final String RDS_RESOURCE = "route-configuration.googleapis.com"; @@ -305,30 +304,6 @@ public long currentTimeNanos() { private final BindableService adsService = createAdsService(); private final BindableService lrsService = createLrsService(); - private XdsTransportFactory xdsTransportFactory = new XdsTransportFactory() { - @Override - public XdsTransport create(ServerInfo serverInfo) { - if (serverInfo.target().equals(SERVER_URI)) { - return new GrpcXdsTransport(channel); - } - if (serverInfo.target().equals(SERVER_URI_CUSTOM_AUTHORITY)) { - if (channelForCustomAuthority == null) { - channelForCustomAuthority = cleanupRule.register( - InProcessChannelBuilder.forName(serverName).directExecutor().build()); - } - return new GrpcXdsTransport(channelForCustomAuthority); - } - if (serverInfo.target().equals(SERVER_URI_EMPTY_AUTHORITY)) { - if (channelForEmptyAuthority == null) { - channelForEmptyAuthority = cleanupRule.register( - InProcessChannelBuilder.forName(serverName).directExecutor().build()); - } - return new GrpcXdsTransport(channelForEmptyAuthority); - } - throw new IllegalArgumentException("Can not create channel for " + serverInfo); - } - }; - @Before public void setUp() throws IOException { when(backoffPolicyProvider.get()).thenReturn(backoffPolicy1, backoffPolicy2); @@ -347,9 +322,32 @@ public void setUp() throws IOException { .start()); channel = cleanupRule.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); + XdsTransportFactory xdsTransportFactory = new XdsTransportFactory() { + @Override + public XdsTransport create(ServerInfo serverInfo) { + if (serverInfo.target().equals(SERVER_URI)) { + return new GrpcXdsTransport(channel); + } + if (serverInfo.target().equals(SERVER_URI_CUSTOME_AUTHORITY)) { + if (channelForCustomAuthority == null) { + channelForCustomAuthority = cleanupRule.register( + InProcessChannelBuilder.forName(serverName).directExecutor().build()); + } + return new GrpcXdsTransport(channelForCustomAuthority); + } + if (serverInfo.target().equals(SERVER_URI_EMPTY_AUTHORITY)) { + if (channelForEmptyAuthority == null) { + channelForEmptyAuthority = cleanupRule.register( + InProcessChannelBuilder.forName(serverName).directExecutor().build()); + } + return new GrpcXdsTransport(channelForEmptyAuthority); + } + throw new IllegalArgumentException("Can not create channel for " + serverInfo); + } + }; xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), - true, false); + true); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -359,7 +357,7 @@ public void setUp() throws IOException { AuthorityInfo.create( "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/%s", ImmutableList.of(Bootstrapper.ServerInfo.create( - SERVER_URI_CUSTOM_AUTHORITY, CHANNEL_CREDENTIALS))), + SERVER_URI_CUSTOME_AUTHORITY, CHANNEL_CREDENTIALS))), "", AuthorityInfo.create( "xdstp:///envoy.config.listener.v3.Listener/%s", @@ -3157,108 +3155,6 @@ public void flowControlAbsent() throws Exception { verify(anotherWatcher).onError(any()); } - @Test - @SuppressWarnings("unchecked") - public void resourceTimerIsTransientError_schedulesExtendedTimeout() { - BootstrapperImpl.xdsDataErrorHandlingEnabled = true; - ServerInfo serverInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, - false, true, true); - BootstrapInfo bootstrapInfo = - Bootstrapper.BootstrapInfo.builder() - .servers(Collections.singletonList(serverInfo)) - .node(NODE) - .authorities(ImmutableMap.of( - "", - AuthorityInfo.create( - "xdstp:///envoy.config.listener.v3.Listener/%s", - ImmutableList.of(Bootstrapper.ServerInfo.create( - SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS))))) - .certProviders(ImmutableMap.of()) - .build(); - xdsClient = new XdsClientImpl( - xdsTransportFactory, - bootstrapInfo, - fakeClock.getScheduledExecutorService(), - backoffPolicyProvider, - fakeClock.getStopwatchSupplier(), - timeProvider, - MessagePrinter.INSTANCE, - new TlsContextManagerImpl(bootstrapInfo), - xdsClientMetricReporter); - ResourceWatcher watcher = mock(ResourceWatcher.class); - String resourceName = "cluster.googleapis.com"; - - xdsClient.watchXdsResource( - XdsClusterResource.getInstance(), - resourceName, - watcher, - fakeClock.getScheduledExecutorService()); - - ScheduledTask task = Iterables.getOnlyElement( - fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - assertThat(task.getDelay(TimeUnit.SECONDS)) - .isEqualTo(XdsClientImpl.EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC); - fakeClock.runDueTasks(); - BootstrapperImpl.xdsDataErrorHandlingEnabled = false; - } - - @Test - @SuppressWarnings("unchecked") - public void resourceTimerIsTransientError_callsOnErrorUnavailable() { - BootstrapperImpl.xdsDataErrorHandlingEnabled = true; - xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), - true, true); - BootstrapInfo bootstrapInfo = - Bootstrapper.BootstrapInfo.builder() - .servers(Collections.singletonList(xdsServerInfo)) - .node(NODE) - .authorities(ImmutableMap.of( - "authority.xds.com", - AuthorityInfo.create( - "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/%s", - ImmutableList.of(Bootstrapper.ServerInfo.create( - SERVER_URI_CUSTOM_AUTHORITY, CHANNEL_CREDENTIALS))), - "", - AuthorityInfo.create( - "xdstp:///envoy.config.listener.v3.Listener/%s", - ImmutableList.of(Bootstrapper.ServerInfo.create( - SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS))))) - .certProviders(ImmutableMap.of("cert-instance-name", - CertificateProviderInfo.create("file-watcher", ImmutableMap.of()))) - .build(); - xdsClient = new XdsClientImpl( - xdsTransportFactory, - bootstrapInfo, - fakeClock.getScheduledExecutorService(), - backoffPolicyProvider, - fakeClock.getStopwatchSupplier(), - timeProvider, - MessagePrinter.INSTANCE, - new TlsContextManagerImpl(bootstrapInfo), - xdsClientMetricReporter); - String timeoutResource = CDS_RESOURCE + "_timeout"; - ResourceWatcher timeoutWatcher = mock(ResourceWatcher.class); - - xdsClient.watchXdsResource( - XdsClusterResource.getInstance(), - timeoutResource, - timeoutWatcher, - fakeClock.getScheduledExecutorService()); - - assertThat(resourceDiscoveryCalls).hasSize(1); - DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); - call.verifyRequest(CDS, ImmutableList.of(timeoutResource), "", "", NODE); - fakeClock.forwardTime(XdsClientImpl.EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - fakeClock.runDueTasks(); - ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Status.class); - verify(timeoutWatcher).onError(errorCaptor.capture()); - Status error = errorCaptor.getValue(); - assertThat(error.getCode()).isEqualTo(Status.Code.UNAVAILABLE); - assertThat(error.getDescription()).isEqualTo( - "Timed out waiting for resource " + timeoutResource + " from xDS server"); - BootstrapperImpl.xdsDataErrorHandlingEnabled = false; - } - private Answer blockUpdate(CyclicBarrier barrier) { return new Answer() { @Override @@ -4324,7 +4220,7 @@ private XdsClientImpl createXdsClient(String serverUri) { private BootstrapInfo buildBootStrap(String serverUri) { ServerInfo xdsServerInfo = ServerInfo.create(serverUri, CHANNEL_CREDENTIALS, - ignoreResourceDeletion(), true, false); + ignoreResourceDeletion(), true); return Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -4334,7 +4230,7 @@ private BootstrapInfo buildBootStrap(String serverUri) { AuthorityInfo.create( "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/%s", ImmutableList.of(Bootstrapper.ServerInfo.create( - SERVER_URI_CUSTOM_AUTHORITY, CHANNEL_CREDENTIALS))), + SERVER_URI_CUSTOME_AUTHORITY, CHANNEL_CREDENTIALS))), "", AuthorityInfo.create( "xdstp:///envoy.config.listener.v3.Listener/%s", diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index e2618df18b1..7015a43f6ed 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -368,13 +368,13 @@ public void resolving_targetAuthorityInAuthoritiesMap() { String serviceAuthority = "[::FFFF:129.144.52.38]:80"; bootstrapInfo = BootstrapInfo.builder() .servers(ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true, true, false))) + "td.googleapis.com", InsecureChannelCredentials.create(), true, true))) .node(Node.newBuilder().build()) .authorities( ImmutableMap.of(targetAuthority, AuthorityInfo.create( "xdstp://" + targetAuthority + "/envoy.config.listener.v3.Listener/%s?foo=1&bar=2", ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true, true, false))))) + "td.googleapis.com", InsecureChannelCredentials.create(), true, true))))) .build(); expectedLdsResourceName = "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/" + "%5B::FFFF:129.144.52.38%5D:80?bar=2&foo=1"; // query param canonified diff --git a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java index 754e903f8a9..485970741c1 100644 --- a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java @@ -203,7 +203,7 @@ public static Bootstrapper.BootstrapInfo buildBootStrap(List serverUris) List serverInfos = new ArrayList<>(); for (String uri : serverUris) { - serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, false, true, false)); + serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, false, true)); } EnvoyProtoData.Node node = EnvoyProtoData.Node.newBuilder().setId("node-id").build(); From 1fc4ab0bb203a7934753d4d29d8d4689a1012390 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 16 Jul 2025 14:58:15 -0700 Subject: [PATCH 323/591] LBs should avoid calling LBs after lb.shutdown() LoadBalancers shouldn't be called after shutdown(), but RingHashLb could have enqueued work to the SynchronizationContext that executed after shutdown(). This commit fixes problems discovered when auditing all LBs usage of the syncContext for that type of problem. Similarly, PickFirstLb could have requested a new connection after shutdown(). We want to avoid that sort of thing too. RingHashLb's test changed from CONNECTING to TRANSIENT_FAILURE to get the latest picker. Because two subchannels have failed it will be in TRANSIENT_FAILURE. Previously the test was using an older picker with out-of-date subchannelView, and the verifyConnection() was too imprecise to notice it was creating the wrong subchannel. As discovered in b/430347751, where ClusterImplLb was seeing a new subchannel being called after the child LB was shutdown (the shutdown itself had been caused by RingHashConfig not implementing equals() and was fixed by a8de9f07ab, which caused ClusterResolverLb to replace its state): ``` java.lang.NullPointerException at io.grpc.xds.ClusterImplLoadBalancer$ClusterImplLbHelper.createClusterLocalityFromAttributes(ClusterImplLoadBalancer.java:322) at io.grpc.xds.ClusterImplLoadBalancer$ClusterImplLbHelper.createSubchannel(ClusterImplLoadBalancer.java:236) at io.grpc.util.ForwardingLoadBalancerHelper.createSubchannel(ForwardingLoadBalancerHelper.java:47) at io.grpc.util.ForwardingLoadBalancerHelper.createSubchannel(ForwardingLoadBalancerHelper.java:47) at io.grpc.internal.PickFirstLeafLoadBalancer.createNewSubchannel(PickFirstLeafLoadBalancer.java:527) at io.grpc.internal.PickFirstLeafLoadBalancer.requestConnection(PickFirstLeafLoadBalancer.java:459) at io.grpc.internal.PickFirstLeafLoadBalancer.acceptResolvedAddresses(PickFirstLeafLoadBalancer.java:174) at io.grpc.xds.LazyLoadBalancer$LazyDelegate.activate(LazyLoadBalancer.java:64) at io.grpc.xds.LazyLoadBalancer$LazyDelegate.requestConnection(LazyLoadBalancer.java:97) at io.grpc.util.ForwardingLoadBalancer.requestConnection(ForwardingLoadBalancer.java:61) at io.grpc.xds.RingHashLoadBalancer$RingHashPicker.lambda$pickSubchannel$0(RingHashLoadBalancer.java:440) at io.grpc.SynchronizationContext.drain(SynchronizationContext.java:96) at io.grpc.SynchronizationContext.execute(SynchronizationContext.java:128) at io.grpc.xds.client.XdsClientImpl$ResourceSubscriber.onData(XdsClientImpl.java:817) ``` --- api/src/main/java/io/grpc/LoadBalancer.java | 4 + .../io/grpc/internal/ManagedChannelImpl.java | 3 + .../grpc/internal/PickFirstLoadBalancer.java | 14 +-- .../grpc/internal/ManagedChannelImplTest.java | 13 +++ .../ShufflingPickFirstLoadBalancer.java | 17 +--- .../java/io/grpc/xds/LazyLoadBalancer.java | 15 +++ .../io/grpc/xds/RingHashLoadBalancer.java | 13 ++- .../io/grpc/xds/LazyLoadBalancerTest.java | 94 +++++++++++++++++++ .../io/grpc/xds/RingHashLoadBalancerTest.java | 4 +- 9 files changed, 145 insertions(+), 32 deletions(-) create mode 100644 xds/src/test/java/io/grpc/xds/LazyLoadBalancerTest.java diff --git a/api/src/main/java/io/grpc/LoadBalancer.java b/api/src/main/java/io/grpc/LoadBalancer.java index 53ba60fdc9c..d2fd8409e01 100644 --- a/api/src/main/java/io/grpc/LoadBalancer.java +++ b/api/src/main/java/io/grpc/LoadBalancer.java @@ -1189,6 +1189,10 @@ public void ignoreRefreshNameResolutionCheck() { * Returns a {@link SynchronizationContext} that runs tasks in the same Synchronization Context * as that the callback methods on the {@link LoadBalancer} interface are run in. * + *

    Work added to the synchronization context might not run immediately, so LB implementations + * must be careful to ensure that any assumptions still hold when it is executed. In particular, + * the LB might have been shut down or subchannels might have changed state. + * *

    Pro-tip: in order to call {@link SynchronizationContext#schedule}, you need to provide a * {@link ScheduledExecutorService}. {@link #getScheduledExecutorService} is provided for your * convenience. diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index e8f106c7775..16b8adbd347 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -1967,6 +1967,9 @@ public void run() { public void requestConnection() { syncContext.throwIfNotInThisSynchronizationContext(); checkState(started, "not started"); + if (shutdown) { + return; + } subchannel.obtainActiveTransport(); } diff --git a/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java b/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java index acef79d3d9f..a23855e67ec 100644 --- a/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java +++ b/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java @@ -134,7 +134,7 @@ private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo SubchannelPicker picker; switch (newState) { case IDLE: - picker = new RequestConnectionPicker(subchannel); + picker = new RequestConnectionPicker(); break; case CONNECTING: // It's safe to use RequestConnectionPicker here, so when coming from IDLE we could leave @@ -197,22 +197,12 @@ public String toString() { /** Picker that requests connection during the first pick, and returns noResult. */ private final class RequestConnectionPicker extends SubchannelPicker { - private final Subchannel subchannel; private final AtomicBoolean connectionRequested = new AtomicBoolean(false); - RequestConnectionPicker(Subchannel subchannel) { - this.subchannel = checkNotNull(subchannel, "subchannel"); - } - @Override public PickResult pickSubchannel(PickSubchannelArgs args) { if (connectionRequested.compareAndSet(false, true)) { - helper.getSynchronizationContext().execute(new Runnable() { - @Override - public void run() { - subchannel.requestConnection(); - } - }); + helper.getSynchronizationContext().execute(PickFirstLoadBalancer.this::requestConnection); } return PickResult.withNoResult(); } diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 6dfba7404c6..efc582703ba 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -1767,6 +1767,19 @@ public void subchannelsNoConnectionShutdown() { any(SocketAddress.class), any(ClientTransportOptions.class), any(ChannelLogger.class)); } + @Test + public void subchannelsRequestConnectionNoopAfterShutdown() { + createChannel(); + Subchannel sub1 = + createSubchannelSafely(helper, addressGroup, Attributes.EMPTY, subchannelStateListener); + + shutdownSafely(helper, sub1); + requestConnectionSafely(helper, sub1); + verify(mockTransportFactory, never()) + .newClientTransport( + any(SocketAddress.class), any(ClientTransportOptions.class), any(ChannelLogger.class)); + } + @Test public void subchannelsNoConnectionShutdownNow() { createChannel(); diff --git a/examples/src/main/java/io/grpc/examples/customloadbalance/ShufflingPickFirstLoadBalancer.java b/examples/src/main/java/io/grpc/examples/customloadbalance/ShufflingPickFirstLoadBalancer.java index 4cf09170c8d..b49c856c4d0 100644 --- a/examples/src/main/java/io/grpc/examples/customloadbalance/ShufflingPickFirstLoadBalancer.java +++ b/examples/src/main/java/io/grpc/examples/customloadbalance/ShufflingPickFirstLoadBalancer.java @@ -122,7 +122,7 @@ private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo SubchannelPicker picker; switch (currentState) { case IDLE: - picker = new RequestConnectionPicker(subchannel); + picker = new RequestConnectionPicker(); break; case CONNECTING: picker = new Picker(PickResult.withNoResult()); @@ -182,24 +182,15 @@ public String toString() { */ private final class RequestConnectionPicker extends SubchannelPicker { - private final Subchannel subchannel; private final AtomicBoolean connectionRequested = new AtomicBoolean(false); - RequestConnectionPicker(Subchannel subchannel) { - this.subchannel = checkNotNull(subchannel, "subchannel"); - } - @Override public PickResult pickSubchannel(PickSubchannelArgs args) { if (connectionRequested.compareAndSet(false, true)) { - helper.getSynchronizationContext().execute(new Runnable() { - @Override - public void run() { - subchannel.requestConnection(); - } - }); + helper.getSynchronizationContext().execute( + ShufflingPickFirstLoadBalancer.this::requestConnection); } return PickResult.withNoResult(); } } -} \ No newline at end of file +} diff --git a/xds/src/main/java/io/grpc/xds/LazyLoadBalancer.java b/xds/src/main/java/io/grpc/xds/LazyLoadBalancer.java index 8ba1cb28c62..b5f09c4ea93 100644 --- a/xds/src/main/java/io/grpc/xds/LazyLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/LazyLoadBalancer.java @@ -99,11 +99,13 @@ public void requestConnection() { @Override public void shutdown() { + delegate = new NoopLoadBalancer(); } private final class LazyPicker extends SubchannelPicker { @Override public PickResult pickSubchannel(PickSubchannelArgs args) { + // activate() is a no-op after shutdown() helper.getSynchronizationContext().execute(LazyDelegate.this::activate); return PickResult.withNoResult(); } @@ -121,4 +123,17 @@ public Factory(LoadBalancer.Factory delegate) { return new LazyLoadBalancer(helper, delegate); } } + + private static final class NoopLoadBalancer extends LoadBalancer { + @Override + public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + return Status.OK; + } + + @Override + public void handleNameResolutionError(Status error) {} + + @Override + public void shutdown() {} + } } diff --git a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java index 96ce5b97024..21ee914ff8f 100644 --- a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java @@ -438,7 +438,9 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { if (subchannelView.connectivityState == IDLE) { syncContext.execute(() -> { - childLbState.getLb().requestConnection(); + if (childLbState.getCurrentState() == IDLE) { + childLbState.getLb().requestConnection(); + } }); return PickResult.withNoResult(); // Indicates that this should be retried after backoff @@ -456,10 +458,11 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { return childLbState.getCurrentPicker().pickSubchannel(args); } if (!requestedConnection && subchannelView.connectivityState == IDLE) { - syncContext.execute( - () -> { - childLbState.getLb().requestConnection(); - }); + syncContext.execute(() -> { + if (childLbState.getCurrentState() == IDLE) { + childLbState.getLb().requestConnection(); + } + }); requestedConnection = true; } } diff --git a/xds/src/test/java/io/grpc/xds/LazyLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/LazyLoadBalancerTest.java new file mode 100644 index 00000000000..c79d048c9d3 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/LazyLoadBalancerTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; + +import io.grpc.CallOptions; +import io.grpc.ConnectivityState; +import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancer.ResolvedAddresses; +import io.grpc.LoadBalancer.SubchannelPicker; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.SynchronizationContext; +import io.grpc.internal.PickSubchannelArgsImpl; +import io.grpc.testing.TestMethodDescriptors; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit test for {@link io.grpc.xds.LazyLoadBalancer}. */ +@RunWith(JUnit4.class) +public final class LazyLoadBalancerTest { + private SynchronizationContext syncContext = + new SynchronizationContext((t, e) -> { + throw new AssertionError(e); + }); + private LoadBalancer.PickSubchannelArgs args = new PickSubchannelArgsImpl( + TestMethodDescriptors.voidMethod(), + new Metadata(), + CallOptions.DEFAULT, + new LoadBalancer.PickDetailsConsumer() {}); + private FakeHelper helper = new FakeHelper(); + + @Test + public void pickerIsNoopAfterEarlyShutdown() { + LazyLoadBalancer lb = new LazyLoadBalancer(helper, new LoadBalancer.Factory() { + @Override + public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) { + throw new AssertionError("unexpected"); + } + }); + lb.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(Arrays.asList()) + .build()); + SubchannelPicker picker = helper.picker; + assertThat(picker).isNotNull(); + lb.shutdown(); + + picker.pickSubchannel(args); + } + + class FakeHelper extends LoadBalancer.Helper { + ConnectivityState state; + SubchannelPicker picker; + + @Override + public ManagedChannel createOobChannel(EquivalentAddressGroup eag, String authority) { + throw new UnsupportedOperationException(); + } + + @Override + public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) { + this.state = newState; + this.picker = newPicker; + } + + @Override + public SynchronizationContext getSynchronizationContext() { + return syncContext; + } + + @Override + public String getAuthority() { + return "localhost"; + } + } +} diff --git a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java index fc5f21156dd..d65cf96c00d 100644 --- a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java @@ -261,7 +261,7 @@ public void aggregateSubchannelStates_connectingReadyIdleFailure() { private void verifyConnection(int times) { for (int i = 0; i < times; i++) { Subchannel connectOnce = connectionRequestedQueue.poll(); - assertWithMessage("Null connection is at (%s) of (%s)", i, times) + assertWithMessage("Expected %s new connections, but found %s", times, i) .that(connectOnce).isNotNull(); clearInvocations(connectOnce); } @@ -648,7 +648,7 @@ public void skipFailingHosts_firstTwoHostsFailed_pickNextFirstReady() { getSubchannel(servers, 2), ConnectivityStateInfo.forTransientFailure( Status.PERMISSION_DENIED.withDescription("permission denied"))); - verify(helper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); + verify(helper).updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); verifyConnection(0); PickResult result = pickerCaptor.getValue().pickSubchannel(args); // activate last subchannel assertThat(result.getStatus().isOk()).isTrue(); From a37d3eb349e048b953633027ed011cda8b68c603 Mon Sep 17 00:00:00 2001 From: George Gensure Date: Thu, 10 Jul 2025 09:49:54 -0400 Subject: [PATCH 324/591] Guarantee missing stream promise delivery In observed cases, whether RST_STREAM or another failure from netty or the server, listeners can fail to be notified when a connection yields a null stream for the selected streamId. This causes hangs in clients, despite deadlines, with no obvious resolution. Tests which relied upon this promise succeeding must now change. --- .../io/grpc/netty/NettyClientHandler.java | 19 ++++++++++++------- .../io/grpc/netty/NettyClientHandlerTest.java | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java index a5fa0f80077..276fa623c38 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java @@ -738,14 +738,19 @@ public void operationComplete(ChannelFuture future) throws Exception { // Attach the client stream to the HTTP/2 stream object as user data. stream.setHttp2Stream(http2Stream); + promise.setSuccess(); + } else { + // Otherwise, the stream has been cancelled and Netty is sending a + // RST_STREAM frame which causes it to purge pending writes from the + // flow-controller and delete the http2Stream. The stream listener has already + // been notified of cancellation so there is nothing to do. + // + // This process has been observed to fail in some circumstances, leaving listeners + // unanswered. Ensure that some exception has been delivered consistent with the + // implied RST_STREAM result above. + Status status = Status.INTERNAL.withDescription("unknown stream for connection"); + promise.setFailure(status.asRuntimeException()); } - // Otherwise, the stream has been cancelled and Netty is sending a - // RST_STREAM frame which causes it to purge pending writes from the - // flow-controller and delete the http2Stream. The stream listener has already - // been notified of cancellation so there is nothing to do. - - // Just forward on the success status to the original promise. - promise.setSuccess(); } else { Throwable cause = future.cause(); if (cause instanceof StreamBufferingEncoder.Http2GoAwayException) { diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java index f8fbeea9b82..dd4fcb4ea6c 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java @@ -268,7 +268,7 @@ public void cancelBufferedStreamShouldChangeClientStreamStatus() throws Exceptio // Cancel the stream. cancelStream(Status.CANCELLED); - assertTrue(createFuture.isSuccess()); + assertFalse(createFuture.isSuccess()); verify(streamListener).closed(eq(Status.CANCELLED), same(PROCESSED), any(Metadata.class)); } @@ -311,7 +311,7 @@ public void cancelWhileBufferedShouldSucceed() throws Exception { ChannelFuture cancelFuture = cancelStream(Status.CANCELLED); assertTrue(cancelFuture.isSuccess()); assertTrue(createFuture.isDone()); - assertTrue(createFuture.isSuccess()); + assertFalse(createFuture.isSuccess()); } /** From 2e96fbf1e851242f8028af2cbc16dbc96e1037ff Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 15 Jul 2025 15:00:24 -0700 Subject: [PATCH 325/591] netty: Associate netty stream eagerly to avoid client hang In #12185, RPCs were randomly hanging. In #12207 this was tracked down to the headers promise completing successfully, but the netty stream was null. This was because the headers write hadn't completed but stream.close() had been called by goingAway(). --- .../io/grpc/netty/NettyClientHandler.java | 13 ++++++++++++ .../io/grpc/netty/NettyClientHandlerTest.java | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java index 276fa623c38..d6bb3790433 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java @@ -773,6 +773,19 @@ public void operationComplete(ChannelFuture future) throws Exception { } } }); + // When the HEADERS are not buffered because of MAX_CONCURRENT_STREAMS in + // StreamBufferingEncoder, the stream is created immediately even if the bytes of the HEADERS + // are delayed because the OS may have too much buffered and isn't accepting the write. The + // write promise is also delayed until flush(). However, we need to associate the netty stream + // with the transport state so that goingAway() and forcefulClose() and able to notify the + // stream of failures. + // + // This leaves a hole when MAX_CONCURRENT_STREAMS is reached, as http2Stream will be null, but + // it is better than nothing. + Http2Stream http2Stream = connection().stream(streamId); + if (http2Stream != null) { + http2Stream.setProperty(streamKey, stream); + } } /** diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java index dd4fcb4ea6c..5a2605eea5e 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java @@ -453,6 +453,26 @@ public void receivedAbruptGoAwayShouldFailRacingQueuedStreamid() throws Exceptio assertTrue(future.isDone()); } + @Test + public void receivedAbruptGoAwayShouldFailRacingQueuedIoStreamid() throws Exception { + // Purposefully avoid flush(), since we want the write to not actually complete. + // EmbeddedChannel doesn't support flow control, so this is the next closest approximation. + ChannelFuture future = channel().write( + newCreateStreamCommand(grpcHeaders, streamTransportState)); + // Read a GOAWAY that indicates our stream can't be sent + channelRead(goAwayFrame(0, 0 /* NO_ERROR */, Unpooled.copiedBuffer("this is a test", UTF_8))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Status.class); + verify(streamListener).closed(captor.capture(), same(REFUSED), + ArgumentMatchers.notNull()); + assertEquals(Status.UNAVAILABLE.getCode(), captor.getValue().getCode()); + assertEquals( + "Abrupt GOAWAY closed sent stream. HTTP/2 error code: NO_ERROR, " + + "debug data: this is a test", + captor.getValue().getDescription()); + assertTrue(future.isDone()); + } + @Test public void receivedGoAway_shouldFailBufferedStreamsExceedingMaxConcurrentStreams() throws Exception { From 80217275db3bd3d565a0167812560119d15d0a83 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Mon, 21 Jul 2025 20:44:08 -0700 Subject: [PATCH 326/591] api: Size Sets and Maps correctly in handling of Metadata values to be exchanged during a call (#12229) Fix HashSet / HashMap initializations to have sufficient capacity allocated based on the number of keys to be inserted, without which it would always lead to a rehash / resize operation. --- api/src/main/java/io/grpc/Metadata.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/io/grpc/Metadata.java b/api/src/main/java/io/grpc/Metadata.java index fba2659776b..8a958d127df 100644 --- a/api/src/main/java/io/grpc/Metadata.java +++ b/api/src/main/java/io/grpc/Metadata.java @@ -22,6 +22,8 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import com.google.common.io.BaseEncoding; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; @@ -32,8 +34,6 @@ import java.util.Arrays; import java.util.BitSet; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -325,7 +325,7 @@ public Set keys() { if (isEmpty()) { return Collections.emptySet(); } - Set ks = new HashSet<>(size); + Set ks = Sets.newHashSetWithExpectedSize(size); for (int i = 0; i < size; i++) { ks.add(new String(name(i), 0 /* hibyte */)); } @@ -526,7 +526,7 @@ public void merge(Metadata other) { public void merge(Metadata other, Set> keys) { Preconditions.checkNotNull(other, "other"); // Use ByteBuffer for equals and hashCode. - Map> asciiKeys = new HashMap<>(keys.size()); + Map> asciiKeys = Maps.newHashMapWithExpectedSize(keys.size()); for (Key key : keys) { asciiKeys.put(ByteBuffer.wrap(key.asciiName()), key); } From 6ff8ecac09aa2a9e84b342d0326eb424a286fb32 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 10 Jul 2025 11:02:48 -0700 Subject: [PATCH 327/591] core: Don't pre-compute DEADLINE_EXCEEDED message for delayed calls The main reason I made a change here was to fix the tense from the deadline "will be exceeded in" to "was exceeded after". But we really don't want to be doing the string formatting unless the deadline is actually exceeded. There were a few more changes to make some variables effectively final. --- .../io/grpc/internal/DelayedClientCall.java | 46 +++++++++---------- .../java/io/grpc/xds/XdsNameResolverTest.java | 2 +- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/DelayedClientCall.java b/core/src/main/java/io/grpc/internal/DelayedClientCall.java index f2b0c9a3f06..253237c3c7d 100644 --- a/core/src/main/java/io/grpc/internal/DelayedClientCall.java +++ b/core/src/main/java/io/grpc/internal/DelayedClientCall.java @@ -96,15 +96,13 @@ private boolean isAbeforeB(@Nullable Deadline a, @Nullable Deadline b) { private ScheduledFuture scheduleDeadlineIfNeeded( ScheduledExecutorService scheduler, @Nullable Deadline deadline) { Deadline contextDeadline = context.getDeadline(); - if (deadline == null && contextDeadline == null) { - return null; - } - long remainingNanos = Long.MAX_VALUE; - if (deadline != null) { + String deadlineName; + long remainingNanos; + if (deadline != null && isAbeforeB(deadline, contextDeadline)) { + deadlineName = "CallOptions"; remainingNanos = deadline.timeRemaining(NANOSECONDS); - } - - if (contextDeadline != null && contextDeadline.timeRemaining(NANOSECONDS) < remainingNanos) { + } else if (contextDeadline != null) { + deadlineName = "Context"; remainingNanos = contextDeadline.timeRemaining(NANOSECONDS); if (logger.isLoggable(Level.FINE)) { StringBuilder builder = @@ -121,29 +119,29 @@ private ScheduledFuture scheduleDeadlineIfNeeded( } logger.fine(builder.toString()); } - } - - long seconds = Math.abs(remainingNanos) / TimeUnit.SECONDS.toNanos(1); - long nanos = Math.abs(remainingNanos) % TimeUnit.SECONDS.toNanos(1); - final StringBuilder buf = new StringBuilder(); - String deadlineName = isAbeforeB(contextDeadline, deadline) ? "Context" : "CallOptions"; - if (remainingNanos < 0) { - buf.append("ClientCall started after "); - buf.append(deadlineName); - buf.append(" deadline was exceeded. Deadline has been exceeded for "); } else { - buf.append("Deadline "); - buf.append(deadlineName); - buf.append(" will be exceeded in "); + return null; } - buf.append(seconds); - buf.append(String.format(Locale.US, ".%09d", nanos)); - buf.append("s. "); /* Cancels the call if deadline exceeded prior to the real call being set. */ class DeadlineExceededRunnable implements Runnable { @Override public void run() { + long seconds = Math.abs(remainingNanos) / TimeUnit.SECONDS.toNanos(1); + long nanos = Math.abs(remainingNanos) % TimeUnit.SECONDS.toNanos(1); + StringBuilder buf = new StringBuilder(); + if (remainingNanos < 0) { + buf.append("ClientCall started after "); + buf.append(deadlineName); + buf.append(" deadline was exceeded. Deadline has been exceeded for "); + } else { + buf.append("Deadline "); + buf.append(deadlineName); + buf.append(" was exceeded after "); + } + buf.append(seconds); + buf.append(String.format(Locale.US, ".%09d", nanos)); + buf.append("s"); cancel( Status.DEADLINE_EXCEEDED.withDescription(buf.toString()), // We should not cancel the call if the realCall is set because there could be a diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 7015a43f6ed..3fa31aedf6a 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -2239,7 +2239,7 @@ public long nanoTime() { assertThat(testCall).isNull(); verifyRpcDelayedThenAborted(observer, 4000L, Status.DEADLINE_EXCEEDED.withDescription( "Deadline exceeded after up to 5000 ns of fault-injected delay:" - + " Deadline CallOptions will be exceeded in 0.000004000s. ")); + + " Deadline CallOptions was exceeded after 0.000004000s")); } @Test From c4256add4d58b1ac0e0bbc340874483d408f9a9c Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 22 Jul 2025 10:00:52 -0700 Subject: [PATCH 328/591] xds: Align PriorityLB child selection with A56 The PriorityLB predates A56. tryNextPriority() now matches ChoosePriority() from the gRFC. The biggest change is waiting on CONNECTING children instead of failing after the failOverTimer fires. The failOverTimer should be used to start lower priorities more eagerly, but shouldn't cause the overall connectivity state to become TRANSIENT_FAILURE on its own. The prior behavior of creating the "Connection timeout for priority" failing picker was particularly strange, because it didn't update child's connectivity state. This previous behavior was creating errors because of the failOverTimer with no way to diagnose what was going wrong. b/428517222 --- .../io/grpc/xds/PriorityLoadBalancer.java | 20 +++++----- .../io/grpc/xds/PriorityLoadBalancerTest.java | 38 +++++++++++++------ 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java b/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java index 845c167a643..c72cef9f637 100644 --- a/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java @@ -154,7 +154,8 @@ private Status tryNextPriority() { ChildLbState child = new ChildLbState(priority, priorityConfigs.get(priority).ignoreReresolution); children.put(priority, child); - updateOverallState(priority, CONNECTING, new FixedResultPicker(PickResult.withNoResult())); + // Child is created in CONNECTING with pending failOverTimer + updateOverallState(priority, child.connectivityState, child.picker); // Calling the child's updateResolvedAddresses() can result in tryNextPriority() being // called recursively. We need to be sure to be done with processing here before it is // called. @@ -173,21 +174,23 @@ private Status tryNextPriority() { } return Status.OK; } - if (child.failOverTimer != null && child.failOverTimer.isPending()) { + if (child.failOverTimer.isPending()) { updateOverallState(priority, child.connectivityState, child.picker); return Status.OK; // Give priority i time to connect. } - if (priority.equals(currentPriority) && child.connectivityState != TRANSIENT_FAILURE) { - // If the current priority is not changed into TRANSIENT_FAILURE, keep using it. + } + for (int i = 0; i < priorityNames.size(); i++) { + String priority = priorityNames.get(i); + ChildLbState child = children.get(priority); + if (child.connectivityState.equals(CONNECTING)) { updateOverallState(priority, child.connectivityState, child.picker); return Status.OK; } } - // TODO(zdapeng): Include error details of each priority. logger.log(XdsLogLevel.DEBUG, "All priority failed"); String lastPriority = priorityNames.get(priorityNames.size() - 1); - SubchannelPicker errorPicker = children.get(lastPriority).picker; - updateOverallState(lastPriority, TRANSIENT_FAILURE, errorPicker); + ChildLbState child = children.get(lastPriority); + updateOverallState(lastPriority, child.connectivityState, child.picker); return Status.OK; } @@ -231,10 +234,7 @@ public void run() { // The child is deactivated. return; } - picker = new FixedResultPicker(PickResult.withError( - Status.UNAVAILABLE.withDescription("Connection timeout for priority " + priority))); logger.log(XdsLogLevel.DEBUG, "Priority {0} failed over to next", priority); - currentPriority = null; // reset currentPriority to guarantee failover happen Status status = tryNextPriority(); if (!status.isOk()) { // A child had a problem with the addresses/config. Request it to be refreshed diff --git a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java index 08d4863d194..ab4c00398f0 100644 --- a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java @@ -376,6 +376,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { assertThat(fooBalancers).hasSize(2); assertThat(fooHelpers).hasSize(2); LoadBalancer balancer1 = Iterables.getLast(fooBalancers); + Helper helper1 = Iterables.getLast(fooHelpers); // p1 timeout, and fails over to p2 fakeClock.forwardTime(10, TimeUnit.SECONDS); @@ -423,14 +424,20 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { LoadBalancer balancer3 = Iterables.getLast(fooBalancers); Helper helper3 = Iterables.getLast(fooHelpers); - // p3 timeout then the channel should go to TRANSIENT_FAILURE + // p3 timeout then the channel should stay in CONNECTING fakeClock.forwardTime(10, TimeUnit.SECONDS); - assertCurrentPickerReturnsError(Status.Code.UNAVAILABLE, "timeout"); + assertCurrentPicker(CONNECTING, PickResult.withNoResult()); - // p3 fails then the picker should have error status updated + // p3 fails then the picker should still be waiting on p1 helper3.updateBalancingState( TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(Status.DATA_LOSS.withDescription("foo")))); + assertCurrentPicker(CONNECTING, PickResult.withNoResult()); + + // p1 fails then the picker should have error status updated to p3 + helper1.updateBalancingState( + TRANSIENT_FAILURE, + new FixedResultPicker(PickResult.withError(Status.DATA_LOSS.withDescription("bar")))); assertCurrentPickerReturnsError(Status.Code.DATA_LOSS, "foo"); // p2 gets back to READY @@ -642,6 +649,7 @@ public void typicalPriorityFailOverFlowWithIdleUpdate() { assertThat(fooBalancers).hasSize(2); assertThat(fooHelpers).hasSize(2); LoadBalancer balancer1 = Iterables.getLast(fooBalancers); + Helper helper1 = Iterables.getLast(fooHelpers); // p1 timeout, and fails over to p2 fakeClock.forwardTime(10, TimeUnit.SECONDS); @@ -677,14 +685,20 @@ public void typicalPriorityFailOverFlowWithIdleUpdate() { LoadBalancer balancer3 = Iterables.getLast(fooBalancers); Helper helper3 = Iterables.getLast(fooHelpers); - // p3 timeout then the channel should go to TRANSIENT_FAILURE + // p3 timeout then the channel should stay in CONNECTING fakeClock.forwardTime(10, TimeUnit.SECONDS); - assertCurrentPickerReturnsError(Status.Code.UNAVAILABLE, "timeout"); + assertCurrentPicker(CONNECTING, PickResult.withNoResult()); - // p3 fails then the picker should have error status updated + // p3 fails then the picker should still be waiting on p1 helper3.updateBalancingState( TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(Status.DATA_LOSS.withDescription("foo")))); + assertCurrentPicker(CONNECTING, PickResult.withNoResult()); + + // p1 fails then the picker should have error status updated to p3 + helper1.updateBalancingState( + TRANSIENT_FAILURE, + new FixedResultPicker(PickResult.withError(Status.DATA_LOSS.withDescription("bar")))); assertCurrentPickerReturnsError(Status.Code.DATA_LOSS, "foo"); // p2 gets back to IDLE @@ -863,15 +877,17 @@ private void assertCurrentPickerReturnsError( } private void assertCurrentPickerPicksSubchannel(Subchannel expectedSubchannelToPick) { - assertLatestConnectivityState(READY); - PickResult pickResult = pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)); - assertThat(pickResult.getSubchannel()).isEqualTo(expectedSubchannelToPick); + assertCurrentPicker(READY, PickResult.withSubchannel(expectedSubchannelToPick)); } private void assertCurrentPickerIsBufferPicker() { - assertLatestConnectivityState(IDLE); + assertCurrentPicker(IDLE, PickResult.withNoResult()); + } + + private void assertCurrentPicker(ConnectivityState state, PickResult result) { + assertLatestConnectivityState(state); PickResult pickResult = pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)); - assertThat(pickResult).isEqualTo(PickResult.withNoResult()); + assertThat(pickResult).isEqualTo(result); } private Object newChildConfig(LoadBalancerProvider provider, Object config) { From 42e1829b3724c0fb20910c0abe70099994856307 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Thu, 24 Jul 2025 11:28:32 +0000 Subject: [PATCH 329/591] xds: Do RLS fallback policy eagar start (#12211) The resource subscription to the fallback target was done only at the time of falling back, which can cause rpcs to fail. This change makes the fallback target to be subscribed and cached earlier, similar to C++ and go gRPC implementations. --- .../java/io/grpc/rls/CachingRlsLbClient.java | 23 ++++------- .../io/grpc/rls/CachingRlsLbClientTest.java | 15 +++++--- .../java/io/grpc/rls/RlsLoadBalancerTest.java | 38 +++++++++++++++---- 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java index 7855468ee61..cc3ac9f516e 100644 --- a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java +++ b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java @@ -132,6 +132,7 @@ final class CachingRlsLbClient { @GuardedBy("lock") private final RefCountedChildPolicyWrapperFactory refCountedChildPolicyWrapperFactory; private final ChannelLogger logger; + private final ChildPolicyWrapper fallbackChildPolicyWrapper; static { MetricInstrumentRegistry metricInstrumentRegistry @@ -226,6 +227,13 @@ private CachingRlsLbClient(Builder builder) { lbPolicyConfig.getLoadBalancingPolicy(), childLbResolvedAddressFactory, childLbHelperProvider, new BackoffRefreshListener()); + // TODO(creamsoup) wait until lb is ready + String defaultTarget = lbPolicyConfig.getRouteLookupConfig().defaultTarget(); + if (defaultTarget != null && !defaultTarget.isEmpty()) { + fallbackChildPolicyWrapper = refCountedChildPolicyWrapperFactory.createOrGet(defaultTarget); + } else { + fallbackChildPolicyWrapper = null; + } gaugeRegistration = helper.getMetricRecorder() .registerBatchCallback(new BatchCallback() { @@ -1022,12 +1030,8 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { } } - private ChildPolicyWrapper fallbackChildPolicyWrapper; - /** Uses Subchannel connected to default target. */ private PickResult useFallback(PickSubchannelArgs args) { - // TODO(creamsoup) wait until lb is ready - startFallbackChildPolicy(); SubchannelPicker picker = fallbackChildPolicyWrapper.getPicker(); if (picker == null) { return PickResult.withNoResult(); @@ -1052,17 +1056,6 @@ private String determineMetricsPickResult(PickResult pickResult) { } } - private void startFallbackChildPolicy() { - String defaultTarget = lbPolicyConfig.getRouteLookupConfig().defaultTarget(); - synchronized (lock) { - if (fallbackChildPolicyWrapper != null) { - return; - } - logger.log(ChannelLogLevel.DEBUG, "starting fallback to {0}", defaultTarget); - fallbackChildPolicyWrapper = refCountedChildPolicyWrapperFactory.createOrGet(defaultTarget); - } - } - // GuardedBy CachingRlsLbClient.lock void close() { synchronized (lock) { // Lock is already held, but ErrorProne can't tell diff --git a/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java b/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java index 7c5df2c96b3..4f086abc4a2 100644 --- a/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java +++ b/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java @@ -191,7 +191,9 @@ public void setUpMockMetricRecorder() { @After public void tearDown() throws Exception { - rlsLbClient.close(); + if (rlsLbClient != null) { + rlsLbClient.close(); + } assertWithMessage( "On client shut down, RlsLoadBalancer must shut down with all its child loadbalancers.") .that(lbProvider.loadBalancers).isEmpty(); @@ -372,12 +374,14 @@ public void get_updatesLbState() throws Exception { ArgumentCaptor pickerCaptor = ArgumentCaptor.forClass(SubchannelPicker.class); ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ConnectivityState.class); - inOrder.verify(helper, times(2)) + inOrder.verify(helper, times(3)) .updateBalancingState(stateCaptor.capture(), pickerCaptor.capture()); assertThat(new HashSet<>(pickerCaptor.getAllValues())).hasSize(1); + // TRANSIENT_FAILURE is because the test setup pretends fallback is not available. assertThat(stateCaptor.getAllValues()) - .containsExactly(ConnectivityState.CONNECTING, ConnectivityState.READY); + .containsExactly(ConnectivityState.TRANSIENT_FAILURE, ConnectivityState.CONNECTING, + ConnectivityState.READY); Metadata headers = new Metadata(); PickResult pickResult = getPickResultForCreate(pickerCaptor, headers); assertThat(pickResult.getStatus().isOk()).isTrue(); @@ -439,7 +443,7 @@ public void timeout_not_changing_picked_subchannel() throws Exception { ArgumentCaptor pickerCaptor = ArgumentCaptor.forClass(SubchannelPicker.class); ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ConnectivityState.class); - verify(helper, times(4)).updateBalancingState(stateCaptor.capture(), pickerCaptor.capture()); + verify(helper, times(5)).updateBalancingState(stateCaptor.capture(), pickerCaptor.capture()); Metadata headers = new Metadata(); PickResult pickResult = getPickResultForCreate(pickerCaptor, headers); @@ -509,7 +513,7 @@ public void get_withAdaptiveThrottler() throws Exception { ArgumentCaptor pickerCaptor = ArgumentCaptor.forClass(SubchannelPicker.class); ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ConnectivityState.class); - inOrder.verify(helper, times(2)) + inOrder.verify(helper, times(3)) .updateBalancingState(stateCaptor.capture(), pickerCaptor.capture()); Metadata headers = new Metadata(); @@ -699,6 +703,7 @@ public void metricGauges() throws ExecutionException, InterruptedException, Time // Shutdown rlsLbClient.close(); + rlsLbClient = null; verify(mockGaugeRegistration).close(); } diff --git a/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java b/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java index f3986cb89d5..354466f3caf 100644 --- a/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java +++ b/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java @@ -201,7 +201,13 @@ public void tearDown() { @Test public void lb_serverStatusCodeConversion() throws Exception { - deliverResolvedAddresses(); + helper.getSynchronizationContext().execute(() -> { + try { + deliverResolvedAddresses(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); InOrder inOrder = inOrder(helper); inOrder.verify(helper) .updateBalancingState(eq(ConnectivityState.CONNECTING), pickerCaptor.capture()); @@ -236,7 +242,13 @@ public void lb_serverStatusCodeConversion() throws Exception { @Test public void lb_working_withDefaultTarget_rlsResponding() throws Exception { - deliverResolvedAddresses(); + helper.getSynchronizationContext().execute(() -> { + try { + deliverResolvedAddresses(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); InOrder inOrder = inOrder(helper); inOrder.verify(helper) .updateBalancingState(eq(ConnectivityState.CONNECTING), pickerCaptor.capture()); @@ -257,7 +269,7 @@ public void lb_working_withDefaultTarget_rlsResponding() throws Exception { inOrder.verifyNoMoreInteractions(); assertThat(res.getStatus().isOk()).isTrue(); - assertThat(subchannels).hasSize(1); + assertThat(subchannels).hasSize(2); // includes fallback sub-channel FakeSubchannel searchSubchannel = subchannels.getLast(); assertThat(subchannelIsReady(searchSubchannel)).isFalse(); @@ -277,7 +289,7 @@ public void lb_working_withDefaultTarget_rlsResponding() throws Exception { // other rls picker itself is ready due to first channel. assertThat(res.getStatus().isOk()).isTrue(); assertThat(subchannelIsReady(res.getSubchannel())).isFalse(); - assertThat(subchannels).hasSize(2); + assertThat(subchannels).hasSize(3); // includes fallback sub-channel FakeSubchannel rescueSubchannel = subchannels.getLast(); // search subchannel is down, rescue subchannel is connecting @@ -393,7 +405,13 @@ public void lb_working_withoutDefaultTarget_noRlsResponse() throws Exception { public void lb_working_withDefaultTarget_noRlsResponse() throws Exception { fakeThrottler.nextResult = true; - deliverResolvedAddresses(); + helper.getSynchronizationContext().execute(() -> { + try { + deliverResolvedAddresses(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); InOrder inOrder = inOrder(helper); inOrder.verify(helper) .updateBalancingState(eq(ConnectivityState.CONNECTING), pickerCaptor.capture()); @@ -535,7 +553,13 @@ public void lb_working_withoutDefaultTarget() throws Exception { @Test public void lb_nameResolutionFailed() throws Exception { - deliverResolvedAddresses(); + helper.getSynchronizationContext().execute(() -> { + try { + deliverResolvedAddresses(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); InOrder inOrder = inOrder(helper); inOrder.verify(helper) .updateBalancingState(eq(ConnectivityState.CONNECTING), pickerCaptor.capture()); @@ -545,7 +569,7 @@ public void lb_nameResolutionFailed() throws Exception { assertThat(subchannelIsReady(res.getSubchannel())).isFalse(); inOrder.verify(helper).createSubchannel(any(CreateSubchannelArgs.class)); - assertThat(subchannels).hasSize(1); + assertThat(subchannels).hasSize(2); // includes fallback sub-channel FakeSubchannel searchSubchannel = subchannels.getLast(); searchSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); From 8f09b968991ab78d565dc1af7b6d1d07c9386795 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 28 Jul 2025 00:00:39 -0700 Subject: [PATCH 330/591] bazel: Use jarjar to avoid xds deps (#12243) Avoiding so many deps will allow us to upgrade the protos without being forced to upgrade to protobuf-java 4.x. It also removes the remaining non-bzlmod dependencies. It'd be really easy to get this wrong, so we do two things 1) mirror the gradle configuration as much as possible, as that sees a lot of testing, and 2) run the fake control plane with the _results_ of jarjar. There's lots of classes that we could mess up, but that at least kicks the tires. XdsTestUtils.buildRouteConfiguration() was moved to ControlPlaneRule to stop the unnecessary circular dependency between the classes and to avoid the many dependencies of XdsTestUtils. I'm totally hacking java_grpc_library to improve the dependency situation. Long-term, I think we will stop building Java libraries with Bazel and require users to rely entirely on Maven Central. That seems to be the direction Bazel is going and it will greatly simplify the problems we've seen with protobuf having a single repository for many languages. So while the hack isn't too bad, I hope we won't have to live with it long-term. --- .github/workflows/testing.yml | 3 + MODULE.bazel | 26 +- WORKSPACE | 9 +- compiler/BUILD.bazel | 1 + examples/MODULE.bazel | 4 - examples/WORKSPACE | 9 +- java_grpc_library.bzl | 27 ++ repositories.bzl | 59 +-- testing-proto/BUILD.bazel | 20 + xds/BUILD.bazel | 363 ++++++++++++++---- xds/build.gradle | 6 +- .../java/io/grpc/xds/ControlPlaneRule.java | 22 +- .../test/java/io/grpc/xds/XdsTestUtils.java | 19 +- 13 files changed, 380 insertions(+), 188 deletions(-) create mode 100644 testing-proto/BUILD.bazel diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 8c934f24f3e..0f099cbcac7 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -102,6 +102,9 @@ jobs: - name: Run bazel build run: bazelisk build //... --enable_bzlmod=${{ matrix.bzlmod }} + - name: Run bazel test + run: bazelisk test //... --enable_bzlmod=${{ matrix.bzlmod }} + - name: Run example bazel build run: bazelisk build //... --enable_bzlmod=${{ matrix.bzlmod }} working-directory: ./examples diff --git a/MODULE.bazel b/MODULE.bazel index ed027757e86..ed7bac31a34 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -46,36 +46,16 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ ] # GRPC_DEPS_END +bazel_dep(name = "bazel_jar_jar", version = "0.1.7") bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") - -# CEL Spec may be removed when cncf/xds MODULE is no longer using protobuf 27.x -bazel_dep(name = "cel-spec", repo_name = "dev_cel", version = "0.15.0") -bazel_dep(name = "grpc", repo_name = "com_github_grpc_grpc", version = "1.56.3.bcr.1") bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") bazel_dep(name = "rules_cc", version = "0.0.9") bazel_dep(name = "rules_java", version = "5.3.5") -bazel_dep(name = "rules_go", repo_name = "io_bazel_rules_go", version = "0.46.0") bazel_dep(name = "rules_jvm_external", version = "6.0") bazel_dep(name = "rules_proto", version = "5.3.0-21.7") -non_module_deps = use_extension("//:repositories.bzl", "grpc_java_repositories_extension") - -use_repo( - non_module_deps, - "com_github_cncf_xds", - "envoy_api", -) - -grpc_repo_deps_ext = use_extension("@com_github_grpc_grpc//bazel:grpc_deps.bzl", "grpc_repo_deps_ext") - -use_repo( - grpc_repo_deps_ext, - "com_envoyproxy_protoc_gen_validate", - "opencensus_proto", -) - maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") maven.install( @@ -202,7 +182,3 @@ maven.override( coordinates = "io.grpc:grpc-util", target = "@io_grpc_grpc_java//util", ) - -switched_rules = use_extension("@com_google_googleapis//:extensions.bzl", "switched_rules") - -switched_rules.use_languages(java = True) diff --git a/WORKSPACE b/WORKSPACE index 7bbfbcc5fa3..227ef332757 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -22,20 +22,19 @@ load("//:repositories.bzl", "grpc_java_repositories") grpc_java_repositories() +load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar_repositories") + +jar_jar_repositories() + load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS") load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") protobuf_deps() -load("@envoy_api//bazel:repositories.bzl", "api_dependencies") - -api_dependencies() - load("@com_google_googleapis//:repository_rules.bzl", "switched_rules_by_language") switched_rules_by_language( name = "com_google_googleapis_imports", - java = True, ) maven_install( diff --git a/compiler/BUILD.bazel b/compiler/BUILD.bazel index 6f66164f155..6d885ef3f69 100644 --- a/compiler/BUILD.bazel +++ b/compiler/BUILD.bazel @@ -18,6 +18,7 @@ cc_binary( java_library( name = "java_grpc_library_deps__do_not_reference", + visibility = ["//xds:__pkg__"], exports = [ "//api", "//protobuf", diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index 382e3943eab..ef057544d5c 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -11,10 +11,6 @@ local_path_override( path = "..", ) -switched_rules = use_extension("@com_google_googleapis//:extensions.bzl", "switched_rules") - -switched_rules.use_languages(java = True) - maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") use_repo(maven, "maven") diff --git a/examples/WORKSPACE b/examples/WORKSPACE index 170e06a90c7..66a713a1a01 100644 --- a/examples/WORKSPACE +++ b/examples/WORKSPACE @@ -28,6 +28,10 @@ load("@io_grpc_grpc_java//:repositories.bzl", "grpc_java_repositories") grpc_java_repositories() +load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar_repositories") + +jar_jar_repositories() + # Protobuf now requires C++14 or higher, which requires Bazel configuration # outside the WORKSPACE. See .bazelrc in this directory. load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS") @@ -35,15 +39,10 @@ load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") protobuf_deps() -load("@envoy_api//bazel:repositories.bzl", "api_dependencies") - -api_dependencies() - load("@com_google_googleapis//:repository_rules.bzl", "switched_rules_by_language") switched_rules_by_language( name = "com_google_googleapis_imports", - java = True, ) maven_install( diff --git a/java_grpc_library.bzl b/java_grpc_library.bzl index e7d133a2769..2caa161b268 100644 --- a/java_grpc_library.bzl +++ b/java_grpc_library.bzl @@ -147,6 +147,33 @@ _java_grpc_library = rule( implementation = _java_rpc_library_impl, ) +# A copy of _java_grpc_library, except with a neverlink=1 _toolchain +INTERNAL_java_grpc_library_for_xds = rule( + attrs = { + "srcs": attr.label_list( + mandatory = True, + allow_empty = False, + providers = [ProtoInfo], + ), + "deps": attr.label_list( + mandatory = True, + allow_empty = False, + providers = [JavaInfo], + ), + "_toolchain": attr.label( + default = Label("//xds:java_grpc_library_toolchain"), + ), + }, + toolchains = ["@bazel_tools//tools/jdk:toolchain_type"], + fragments = ["java"], + outputs = { + "jar": "lib%{name}.jar", + "srcjar": "lib%{name}-src.jar", + }, + provides = [JavaInfo], + implementation = _java_rpc_library_impl, +) + _java_lite_grpc_library = rule( attrs = { "srcs": attr.label_list( diff --git a/repositories.bzl b/repositories.bzl index dd74a48da4b..f9991cac243 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -87,38 +87,11 @@ IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS = { "io.grpc:grpc-util": "@io_grpc_grpc_java//util", } -def grpc_java_repositories(bzlmod = False): +def grpc_java_repositories(): """Imports dependencies for grpc-java.""" - if not bzlmod and not native.existing_rule("dev_cel"): - http_archive( - name = "dev_cel", - strip_prefix = "cel-spec-0.15.0", - sha256 = "3ee09eb69dbe77722e9dee23dc48dc2cd9f765869fcf5ffb1226587c81791a0b", - urls = [ - "https://github.com/google/cel-spec/archive/refs/tags/v0.15.0.tar.gz", - ], - ) - if not native.existing_rule("com_github_cncf_xds"): - http_archive( - name = "com_github_cncf_xds", - strip_prefix = "xds-2ac532fd44436293585084f8d94c6bdb17835af0", - sha256 = "790c4c83b6950bb602fec221f6a529d9f368cdc8852aae7d2592d0d04b015f37", - urls = [ - "https://github.com/cncf/xds/archive/2ac532fd44436293585084f8d94c6bdb17835af0.tar.gz", - ], - ) - if not bzlmod and not native.existing_rule("com_github_grpc_grpc"): - http_archive( - name = "com_github_grpc_grpc", - strip_prefix = "grpc-1.46.0", - sha256 = "67423a4cd706ce16a88d1549297023f0f9f0d695a96dd684adc21e67b021f9bc", - urls = [ - "https://github.com/grpc/grpc/archive/v1.46.0.tar.gz", - ], - ) - if not bzlmod and not native.existing_rule("com_google_protobuf"): + if not native.existing_rule("com_google_protobuf"): com_google_protobuf() - if not bzlmod and not native.existing_rule("com_google_googleapis"): + if not native.existing_rule("com_google_googleapis"): http_archive( name = "com_google_googleapis", sha256 = "49930468563dd48283e8301e8d4e71436bf6d27ac27c235224cc1a098710835d", @@ -127,25 +100,14 @@ def grpc_java_repositories(bzlmod = False): "https://github.com/googleapis/googleapis/archive/ca1372c6d7bcb199638ebfdb40d2b2660bab7b88.tar.gz", ], ) - if not bzlmod and not native.existing_rule("io_bazel_rules_go"): - http_archive( - name = "io_bazel_rules_go", - sha256 = "ab21448cef298740765f33a7f5acee0607203e4ea321219f2a4c85a6e0fb0a27", - urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.32.0/rules_go-v0.32.0.zip", - "https://github.com/bazelbuild/rules_go/releases/download/v0.32.0/rules_go-v0.32.0.zip", - ], - ) if not native.existing_rule("io_grpc_grpc_proto"): io_grpc_grpc_proto() - if not native.existing_rule("envoy_api"): + if not native.existing_rule("bazel_jar_jar"): http_archive( - name = "envoy_api", - sha256 = "ecf71817233eba19cc8b4ee14e126ffd5838065d5b5a92b2506258a42ac55199", - strip_prefix = "data-plane-api-0bc95493c5e88b7b07e62758d23b39341813a827", - urls = [ - "https://github.com/envoyproxy/data-plane-api/archive/0bc95493c5e88b7b07e62758d23b39341813a827.tar.gz", - ], + name = "bazel_jar_jar", + sha256 = "3117f913c732142a795551f530d02c9157b9ea895e6b2de0fbb5af54f03040a5", + strip_prefix = "bazel_jar_jar-0.1.6", + url = "https://github.com/bazeltools/bazel_jar_jar/releases/download/v0.1.6/bazel_jar_jar-v0.1.6.tar.gz", ) def com_google_protobuf(): @@ -166,8 +128,3 @@ def io_grpc_grpc_proto(): strip_prefix = "grpc-proto-4f245d272a28a680606c0739753506880cf33b5f", urls = ["https://github.com/grpc/grpc-proto/archive/4f245d272a28a680606c0739753506880cf33b5f.zip"], ) - -def _grpc_java_repositories_extension(_): - grpc_java_repositories(bzlmod = True) - -grpc_java_repositories_extension = module_extension(implementation = _grpc_java_repositories_extension) diff --git a/testing-proto/BUILD.bazel b/testing-proto/BUILD.bazel new file mode 100644 index 00000000000..985dbd9777e --- /dev/null +++ b/testing-proto/BUILD.bazel @@ -0,0 +1,20 @@ +load("//:java_grpc_library.bzl", "java_grpc_library") + +proto_library( + name = "simpleservice_proto", + srcs = ["src/main/proto/io/grpc/testing/protobuf/simpleservice.proto"], + strip_import_prefix = "src/main/proto/", +) + +java_proto_library( + name = "simpleservice_java_proto", + visibility = ["//xds:__pkg__"], + deps = [":simpleservice_proto"], +) + +java_grpc_library( + name = "simpleservice_java_grpc", + srcs = [":simpleservice_proto"], + visibility = ["//xds:__pkg__"], + deps = [":simpleservice_java_proto"], +) diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index 53fac28b2da..203fa2c3d71 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -1,5 +1,6 @@ +load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar") load("@rules_jvm_external//:defs.bzl", "artifact") -load("//:java_grpc_library.bzl", "java_grpc_library") +load("//:java_grpc_library.bzl", "INTERNAL_java_grpc_library_for_xds", "java_grpc_library", "java_rpc_toolchain") # Mirrors the dependencies included in the artifact on Maven Central for usage # with maven_install's override_targets. Should only be used as a dep for @@ -13,25 +14,11 @@ java_library( ], ) +# Ordinary deps for :xds java_library( - name = "xds", - srcs = glob( - [ - "src/main/java/**/*.java", - "third_party/zero-allocation-hashing/main/java/**/*.java", - ], - exclude = ["src/main/java/io/grpc/xds/orca/**"], - ), - resources = glob([ - "src/main/resources/**", - ]), - visibility = ["//visibility:public"], - deps = [ - ":envoy_service_discovery_v3_java_grpc", - ":envoy_service_load_stats_v3_java_grpc", - ":envoy_service_status_v3_java_grpc", + name = "xds_deps_depend", + exports = [ ":orca", - ":xds_protos_java", "//:auto_value_annotations", "//alts", "//api", @@ -43,7 +30,6 @@ java_library( "//services:metrics_internal", "//stub", "//util", - "@com_google_googleapis//google/rpc:rpc_java_proto", "@com_google_protobuf//:protobuf_java", "@com_google_protobuf//:protobuf_java_util", "@maven//:com_google_auth_google_auth_library_oauth2_http", @@ -58,70 +44,89 @@ java_library( artifact("io.netty:netty-handler"), artifact("io.netty:netty-transport"), ], + runtime_deps = [ + "//compiler:java_grpc_library_deps__do_not_reference", + ], ) -java_proto_library( - name = "xds_protos_java", - deps = [ - "@com_github_cncf_xds//udpa/type/v1:pkg", - "@com_github_cncf_xds//xds/type/v3:pkg", - "@envoy_api//envoy/admin/v3:pkg", - "@envoy_api//envoy/config/cluster/v3:pkg", - "@envoy_api//envoy/config/core/v3:pkg", - "@envoy_api//envoy/config/endpoint/v3:pkg", - "@envoy_api//envoy/config/listener/v3:pkg", - "@envoy_api//envoy/config/rbac/v3:pkg", - "@envoy_api//envoy/config/route/v3:pkg", - "@envoy_api//envoy/extensions/clusters/aggregate/v3:pkg", - "@envoy_api//envoy/extensions/filters/common/fault/v3:pkg", - "@envoy_api//envoy/extensions/filters/http/fault/v3:pkg", - "@envoy_api//envoy/extensions/filters/http/gcp_authn/v3:pkg", - "@envoy_api//envoy/extensions/filters/http/rbac/v3:pkg", - "@envoy_api//envoy/extensions/filters/http/router/v3:pkg", - "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg", - "@envoy_api//envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3:pkg", - "@envoy_api//envoy/extensions/load_balancing_policies/least_request/v3:pkg", - "@envoy_api//envoy/extensions/load_balancing_policies/pick_first/v3:pkg", - "@envoy_api//envoy/extensions/load_balancing_policies/ring_hash/v3:pkg", - "@envoy_api//envoy/extensions/load_balancing_policies/round_robin/v3:pkg", - "@envoy_api//envoy/extensions/load_balancing_policies/wrr_locality/v3:pkg", - "@envoy_api//envoy/extensions/transport_sockets/http_11_proxy/v3:pkg", - "@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg", - "@envoy_api//envoy/service/discovery/v3:pkg", - "@envoy_api//envoy/service/load_stats/v3:pkg", - "@envoy_api//envoy/service/status/v3:pkg", - "@envoy_api//envoy/type/matcher/v3:pkg", - "@envoy_api//envoy/type/v3:pkg", - ], +java_library( + name = "xds_deps_depend_neverlink", + neverlink = 1, + exports = [":xds_deps_depend"], ) -java_grpc_library( - name = "envoy_service_discovery_v3_java_grpc", - srcs = ["@envoy_api//envoy/service/discovery/v3:pkg"], - deps = [":xds_protos_java"], +# Deps to be combined into the :xds jar itself +java_library( + name = "xds_deps_embed", + exports = [ + ":envoy_java_grpc", + ":envoy_java_proto", + ":googleapis_rpc_java_proto", + ":xds_java_proto", + ], ) -java_grpc_library( - name = "envoy_service_load_stats_v3_java_grpc", - srcs = ["@envoy_api//envoy/service/load_stats/v3:pkg"], - deps = [":xds_protos_java"], +java_binary( + name = "xds_notjarjar", + srcs = glob( + [ + "src/main/java/**/*.java", + "third_party/zero-allocation-hashing/main/java/**/*.java", + ], + exclude = ["src/main/java/io/grpc/xds/orca/**"], + ), + main_class = "unused", + resources = glob([ + "src/main/resources/**", + ]), + deps = [ + # Do not add additional dependencies here; add them to one of these two deps instead + ":xds_deps_depend_neverlink", + ":xds_deps_embed", + ], ) -java_grpc_library( - name = "envoy_service_status_v3_java_grpc", - srcs = ["@envoy_api//envoy/service/status/v3:pkg"], - deps = [":xds_protos_java"], +JAR_JAR_RULES = [ + "zap com.google.protobuf.**", # Drop codegen dep + # Keep in sync with build.gradle's shadowJar + "rule com.github.udpa.** io.grpc.xds.shaded.com.github.udpa.@1", + "rule com.github.xds.** io.grpc.xds.shaded.com.github.xds.@1", + "rule com.google.api.expr.** io.grpc.xds.shaded.com.google.api.expr.@1", + "rule com.google.security.** io.grpc.xds.shaded.com.google.security.@1", + "rule dev.cel.expr.** io.grpc.xds.shaded.dev.cel.expr.@1", + "rule envoy.annotations.** io.grpc.xds.shaded.envoy.annotations.@1", + "rule io.envoyproxy.** io.grpc.xds.shaded.io.envoyproxy.@1", + "rule udpa.annotations.** io.grpc.xds.shaded.udpa.annotations.@1", + "rule xds.annotations.** io.grpc.xds.shaded.xds.annotations.@1", +] + +jar_jar( + name = "xds_jarjar", + inline_rules = JAR_JAR_RULES, + input_jar = ":xds_notjarjar_deploy.jar", ) java_library( - name = "orca", - srcs = glob([ - "src/main/java/io/grpc/xds/orca/*.java", - ]), + name = "xds", visibility = ["//visibility:public"], + exports = [":xds_jarjar"], + runtime_deps = [":xds_deps_depend"], +) + +java_proto_library( + name = "googleapis_rpc_java_proto", deps = [ - ":orca_protos_java", - ":xds_service_orca_v3_java_grpc", + "@com_google_googleapis//google/rpc:code_proto", + "@com_google_googleapis//google/rpc:status_proto", + ], +) + +# Ordinary deps for :orca +java_library( + name = "orca_deps_depend", + exports = [ + ":xds_orca_java_grpc", + ":xds_orca_java_proto", "//api", "//context", "//core:internal", @@ -136,16 +141,222 @@ java_library( ], ) +java_library( + name = "orca_deps_depend_neverlink", + neverlink = 1, + exports = [":orca_deps_depend"], +) + +# Deps to be combined into the :orca jar itself +java_library( + name = "orca_deps_embed", + exports = [ + ":xds_orca_java_grpc", + ":xds_orca_java_proto", + ], +) + +java_binary( + name = "orca_notjarjar", + srcs = glob([ + "src/main/java/io/grpc/xds/orca/*.java", + ]), + main_class = "unused", + visibility = ["//visibility:public"], + deps = [ + # Do not add additional dependencies here; add them to one of these two deps instead + ":orca_deps_depend_neverlink", + ":orca_deps_embed", + ], +) + +jar_jar( + name = "orca_jarjar", + inline_rules = JAR_JAR_RULES, + input_jar = ":orca_notjarjar_deploy.jar", +) + +java_library( + name = "orca", + visibility = ["//visibility:public"], + exports = [":orca_jarjar"], + runtime_deps = [":orca_deps_depend"], +) + +java_proto_library( + name = "orca_java_proto", + deps = [":xds_proto"], +) + +java_grpc_library( + name = "orca_java_grpc", + srcs = [":xds_proto"], + deps = [":orca_java_proto"], +) + +proto_library( + name = "cel_spec_proto", + srcs = glob(["third_party/cel-spec/src/main/proto/**/*.proto"]), + strip_import_prefix = "third_party/cel-spec/src/main/proto/", + deps = [ + "@com_google_protobuf//:duration_proto", + "@com_google_protobuf//:empty_proto", + "@com_google_protobuf//:struct_proto", + "@com_google_protobuf//:timestamp_proto", + ], +) + +proto_library( + name = "envoy_proto", + srcs = glob(["third_party/envoy/src/main/proto/**/*.proto"]), + strip_import_prefix = "third_party/envoy/src/main/proto/", + deps = [ + ":googleapis_proto", + ":protoc_gen_validate_proto", + ":xds_proto", + "@com_google_googleapis//google/api:annotations_proto", + "@com_google_googleapis//google/rpc:status_proto", + "@com_google_protobuf//:any_proto", + "@com_google_protobuf//:descriptor_proto", + "@com_google_protobuf//:duration_proto", + "@com_google_protobuf//:empty_proto", + "@com_google_protobuf//:struct_proto", + "@com_google_protobuf//:timestamp_proto", + "@com_google_protobuf//:wrappers_proto", + ], +) + +java_proto_library( + name = "envoy_java_proto", + deps = [":envoy_proto"], +) + +INTERNAL_java_grpc_library_for_xds( + name = "envoy_java_grpc", + srcs = [":envoy_proto"], + deps = [":envoy_java_proto"], +) + +proto_library( + name = "googleapis_proto", + srcs = glob(["third_party/googleapis/src/main/proto/**/*.proto"]), + strip_import_prefix = "third_party/googleapis/src/main/proto/", + deps = [ + "@com_google_protobuf//:duration_proto", + "@com_google_protobuf//:empty_proto", + "@com_google_protobuf//:struct_proto", + "@com_google_protobuf//:timestamp_proto", + ], +) + +proto_library( + name = "protoc_gen_validate_proto", + srcs = glob(["third_party/protoc-gen-validate/src/main/proto/**/*.proto"]), + strip_import_prefix = "third_party/protoc-gen-validate/src/main/proto/", + deps = [ + "@com_google_protobuf//:descriptor_proto", + "@com_google_protobuf//:duration_proto", + "@com_google_protobuf//:timestamp_proto", + ], +) + +proto_library( + name = "xds_proto", + srcs = glob( + ["third_party/xds/src/main/proto/**/*.proto"], + exclude = [ + "third_party/xds/src/main/proto/xds/data/orca/v3/*.proto", + "third_party/xds/src/main/proto/xds/service/orca/v3/*.proto", + ], + ), + strip_import_prefix = "third_party/xds/src/main/proto/", + deps = [ + ":cel_spec_proto", + ":googleapis_proto", + ":protoc_gen_validate_proto", + "@com_google_protobuf//:any_proto", + "@com_google_protobuf//:descriptor_proto", + "@com_google_protobuf//:duration_proto", + "@com_google_protobuf//:struct_proto", + "@com_google_protobuf//:wrappers_proto", + ], +) + java_proto_library( - name = "orca_protos_java", + name = "xds_java_proto", + deps = [":xds_proto"], +) + +proto_library( + name = "xds_orca_proto", + srcs = glob([ + "third_party/xds/src/main/proto/xds/data/orca/v3/*.proto", + "third_party/xds/src/main/proto/xds/service/orca/v3/*.proto", + ]), + strip_import_prefix = "third_party/xds/src/main/proto/", deps = [ - "@com_github_cncf_xds//xds/data/orca/v3:pkg", - "@com_github_cncf_xds//xds/service/orca/v3:pkg", + ":protoc_gen_validate_proto", + "@com_google_protobuf//:duration_proto", ], ) +java_proto_library( + name = "xds_orca_java_proto", + deps = [":xds_orca_proto"], +) + java_grpc_library( - name = "xds_service_orca_v3_java_grpc", - srcs = ["@com_github_cncf_xds//xds/service/orca/v3:pkg"], - deps = [":orca_protos_java"], + name = "xds_orca_java_grpc", + srcs = [":xds_orca_proto"], + deps = [":xds_orca_java_proto"], +) + +java_rpc_toolchain( + name = "java_grpc_library_toolchain", + plugin = "//compiler:grpc_java_plugin", + runtime = [":java_grpc_library_deps"], +) + +java_library( + name = "java_grpc_library_deps", + neverlink = 1, + exports = ["//compiler:java_grpc_library_deps__do_not_reference"], +) + +java_library( + name = "testlib", + testonly = 1, + srcs = [ + "src/test/java/io/grpc/xds/ControlPlaneRule.java", + "src/test/java/io/grpc/xds/DataPlaneRule.java", + "src/test/java/io/grpc/xds/FakeControlPlaneXdsIntegrationTest.java", + "src/test/java/io/grpc/xds/MetadataLoadBalancerProvider.java", + "src/test/java/io/grpc/xds/XdsTestControlPlaneService.java", + "src/test/java/io/grpc/xds/XdsTestLoadReportingService.java", + ], + deps = [ + ":envoy_java_grpc", + ":envoy_java_proto", + ":xds", + ":xds_java_proto", + "//api", + "//core:internal", + "//stub", + "//testing", + "//testing-proto:simpleservice_java_grpc", + "//testing-proto:simpleservice_java_proto", + "//util", + "@com_google_protobuf//java/core", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_guava_guava", + "@maven//:com_google_truth_truth", + "@maven//:junit_junit", + ], +) + +java_test( + name = "FakeControlPlaneXdsIntegrationTest", + size = "small", + test_class = "io.grpc.xds.FakeControlPlaneXdsIntegrationTest", + runtime_deps = [":testlib"], ) diff --git a/xds/build.gradle b/xds/build.gradle index 90ba3709d14..72dea373097 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -17,12 +17,11 @@ sourceSets { srcDir "${projectDir}/third_party/zero-allocation-hashing/main/java" } proto { + srcDir 'third_party/cel-spec/src/main/proto' srcDir 'third_party/envoy/src/main/proto' + srcDir 'third_party/googleapis/src/main/proto' srcDir 'third_party/protoc-gen-validate/src/main/proto' srcDir 'third_party/xds/src/main/proto' - srcDir 'third_party/cel-spec/src/main/proto' - srcDir 'third_party/googleapis/src/main/proto' - srcDir 'third_party/istio/src/main/proto' } } main { @@ -186,6 +185,7 @@ tasks.named("shadowJar").configure { include(project(':grpc-xds')) } // Relocated packages commonly need exclusions in jacocoTestReport and javadoc + // Keep in sync with BUILD.bazel's JAR_JAR_RULES relocate 'com.github.udpa', "${prefixName}.shaded.com.github.udpa" relocate 'com.github.xds', "${prefixName}.shaded.com.github.xds" relocate 'com.google.api.expr', "${prefixName}.shaded.com.google.api.expr" diff --git a/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java b/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java index 11ea957ae35..3665e16b6bf 100644 --- a/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java +++ b/xds/src/test/java/io/grpc/xds/ControlPlaneRule.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; import com.google.protobuf.Message; import com.google.protobuf.UInt32Value; import io.envoyproxy.envoy.config.cluster.v3.Cluster; @@ -44,6 +45,7 @@ import io.envoyproxy.envoy.config.listener.v3.Listener; import io.envoyproxy.envoy.config.route.v3.NonForwardingAction; 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; @@ -237,7 +239,25 @@ void setEdsConfig(String edsName, ClusterLoadAssignment clusterLoadAssignment) { * Builds a new default RDS configuration. */ static RouteConfiguration buildRouteConfiguration(String authority) { - return XdsTestUtils.buildRouteConfiguration(authority, RDS_NAME, CLUSTER_NAME); + return buildRouteConfiguration(authority, RDS_NAME, CLUSTER_NAME); + } + + static RouteConfiguration buildRouteConfiguration(String authority, String rdsName, + String clusterName) { + io.envoyproxy.envoy.config.route.v3.VirtualHost.Builder vhBuilder = + io.envoyproxy.envoy.config.route.v3.VirtualHost.newBuilder() + .setName(rdsName) + .addDomains(authority) + .addRoutes( + Route.newBuilder() + .setMatch( + RouteMatch.newBuilder().setPrefix("/").build()) + .setRoute( + RouteAction.newBuilder().setCluster(clusterName) + .setAutoHostRewrite(BoolValue.newBuilder().setValue(true).build()) + .build())); + io.envoyproxy.envoy.config.route.v3.VirtualHost virtualHost = vhBuilder.build(); + return RouteConfiguration.newBuilder().setName(rdsName).addVirtualHosts(virtualHost).build(); } /** diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java index e85058f0f3f..b1818445bea 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -30,7 +30,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.Any; -import com.google.protobuf.BoolValue; import com.google.protobuf.Message; import com.google.protobuf.util.Durations; import io.envoyproxy.envoy.config.cluster.v3.Cluster; @@ -38,10 +37,7 @@ import io.envoyproxy.envoy.config.endpoint.v3.ClusterStats; import io.envoyproxy.envoy.config.listener.v3.ApiListener; import io.envoyproxy.envoy.config.listener.v3.Listener; -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.extensions.clusters.aggregate.v3.ClusterConfig; import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter; @@ -306,20 +302,7 @@ static Map createMinimalLbEndpointsMap(String ser static RouteConfiguration buildRouteConfiguration(String authority, String rdsName, String clusterName) { - io.envoyproxy.envoy.config.route.v3.VirtualHost.Builder vhBuilder = - io.envoyproxy.envoy.config.route.v3.VirtualHost.newBuilder() - .setName(rdsName) - .addDomains(authority) - .addRoutes( - Route.newBuilder() - .setMatch( - RouteMatch.newBuilder().setPrefix("/").build()) - .setRoute( - RouteAction.newBuilder().setCluster(clusterName) - .setAutoHostRewrite(BoolValue.newBuilder().setValue(true).build()) - .build())); - io.envoyproxy.envoy.config.route.v3.VirtualHost virtualHost = vhBuilder.build(); - return RouteConfiguration.newBuilder().setName(rdsName).addVirtualHosts(virtualHost).build(); + return ControlPlaneRule.buildRouteConfiguration(authority, rdsName, clusterName); } static Cluster buildAggCluster(String name, List childNames) { From c3ef1ab034c8c3c75d2538d7e1c9b5f99583d8bf Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Mon, 28 Jul 2025 20:56:27 +0530 Subject: [PATCH 331/591] xds: Envoy proto sync to (#12224) --- xds/third_party/envoy/import.sh | 4 +- .../envoy/admin/v3/config_dump_shared.proto | 8 + .../envoy/config/accesslog/v3/accesslog.proto | 46 +++-- .../envoy/config/bootstrap/v3/bootstrap.proto | 4 +- .../envoy/config/cluster/v3/cluster.proto | 46 ++++- .../proto/envoy/config/core/v3/address.proto | 16 +- .../proto/envoy/config/core/v3/base.proto | 49 ++++- .../envoy/config/core/v3/health_check.proto | 12 +- .../proto/envoy/config/core/v3/protocol.proto | 68 +++++-- .../envoy/config/core/v3/proxy_protocol.proto | 40 ++++ .../config/core/v3/socket_cmsg_headers.proto | 2 +- .../core/v3/substitution_format_string.proto | 13 +- .../endpoint/v3/endpoint_components.proto | 35 +++- .../envoy/config/listener/v3/listener.proto | 34 +++- .../proto/envoy/config/rbac/v3/rbac.proto | 85 +++++--- .../config/route/v3/route_components.proto | 182 ++++++++++++++--- .../envoy/data/accesslog/v3/accesslog.proto | 49 ++--- .../clusters/aggregate/v3/cluster.proto | 17 ++ .../filters/http/rbac/v3/rbac.proto | 55 ++--- .../filters/http/router/v3/router.proto | 4 +- .../v3/http_connection_manager.proto | 130 ++++++------ .../v3/client_side_weighted_round_robin.proto | 7 + .../common/v3/common.proto | 25 +++ .../transport_sockets/tls/v3/common.proto | 44 +++- .../transport_sockets/tls/v3/secret.proto | 7 +- .../transport_sockets/tls/v3/tls.proto | 119 +++++------ .../service/discovery/v3/discovery.proto | 191 +++++++++++------- .../proto/envoy/service/status/v3/csds.proto | 5 + .../proto/envoy/type/matcher/v3/address.proto | 22 ++ .../envoy/type/matcher/v3/filter_state.proto | 4 + .../envoy/type/matcher/v3/metadata.proto | 32 +-- .../proto/envoy/type/matcher/v3/string.proto | 22 +- .../envoy/type/metadata/v3/metadata.proto | 32 +-- .../xds/src/main/proto/xds/core/v3/cidr.proto | 2 +- 34 files changed, 998 insertions(+), 413 deletions(-) create mode 100644 xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/address.proto diff --git a/xds/third_party/envoy/import.sh b/xds/third_party/envoy/import.sh index 7a6b33871b3..ba657612586 100755 --- a/xds/third_party/envoy/import.sh +++ b/xds/third_party/envoy/import.sh @@ -17,7 +17,7 @@ set -e # import VERSION from the google internal copybara_version.txt for Envoy -VERSION=0b90f64539c88dc3d2a6792dc714e8207bce0c08 +VERSION=1128a52d227efb8c798478d293fdc05e8075ebcd DOWNLOAD_URL="https://github.com/envoyproxy/envoy/archive/${VERSION}.tar.gz" DOWNLOAD_BASE_DIR="envoy-${VERSION}" SOURCE_PROTO_BASE_DIR="${DOWNLOAD_BASE_DIR}/api" @@ -46,6 +46,7 @@ envoy/config/core/v3/http_uri.proto envoy/config/core/v3/protocol.proto envoy/config/core/v3/proxy_protocol.proto envoy/config/core/v3/resolver.proto +envoy/config/core/v3/socket_cmsg_headers.proto envoy/config/core/v3/socket_option.proto envoy/config/core/v3/substitution_format_string.proto envoy/config/core/v3/udp_socket_config.proto @@ -97,6 +98,7 @@ envoy/service/load_stats/v3/lrs.proto envoy/service/rate_limit_quota/v3/rlqs.proto envoy/service/status/v3/csds.proto envoy/type/http/v3/path_transformation.proto +envoy/type/matcher/v3/address.proto envoy/type/matcher/v3/filter_state.proto envoy/type/matcher/v3/http_inputs.proto envoy/type/matcher/v3/metadata.proto diff --git a/xds/third_party/envoy/src/main/proto/envoy/admin/v3/config_dump_shared.proto b/xds/third_party/envoy/src/main/proto/envoy/admin/v3/config_dump_shared.proto index 8de77e18e1f..b34e004d986 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/admin/v3/config_dump_shared.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/admin/v3/config_dump_shared.proto @@ -39,6 +39,14 @@ enum ClientResourceStatus { // Client received this resource and replied with NACK. NACKED = 4; + + // Client received an error from the control plane. The attached config + // dump is the most recent accepted one. If no config is accepted yet, + // the attached config dump will be empty. + RECEIVED_ERROR = 5; + + // Client timed out waiting for the resource from the control plane. + TIMEOUT = 6; } message UpdateFailureState { diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/accesslog/v3/accesslog.proto b/xds/third_party/envoy/src/main/proto/envoy/config/accesslog/v3/accesslog.proto index 5599f8082d3..6753ab6ab52 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/accesslog/v3/accesslog.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/accesslog/v3/accesslog.proto @@ -152,35 +152,38 @@ message TraceableFilter { "envoy.config.filter.accesslog.v2.TraceableFilter"; } -// Filters for random sampling of requests. +// Filters requests based on runtime-configurable sampling rates. message RuntimeFilter { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.accesslog.v2.RuntimeFilter"; - // Runtime key to get an optional overridden numerator for use in the - // ``percent_sampled`` field. If found in runtime, this value will replace the - // default numerator. + // Specifies a key used to look up a custom sampling rate from the runtime configuration. If a value is found for this + // key, it will override the default sampling rate specified in ``percent_sampled``. string runtime_key = 1 [(validate.rules).string = {min_len: 1}]; - // The default sampling percentage. If not specified, defaults to 0% with - // denominator of 100. + // Defines the default sampling percentage when no runtime override is present. If not specified, the default is + // **0%** (with a denominator of 100). type.v3.FractionalPercent percent_sampled = 2; - // By default, sampling pivots on the header - // :ref:`x-request-id` being - // present. If :ref:`x-request-id` - // is present, the filter will consistently sample across multiple hosts based - // on the runtime key value and the value extracted from - // :ref:`x-request-id`. If it is - // missing, or ``use_independent_randomness`` is set to true, the filter will - // randomly sample based on the runtime key value alone. - // ``use_independent_randomness`` can be used for logging kill switches within - // complex nested :ref:`AndFilter - // ` and :ref:`OrFilter - // ` blocks that are easier to - // reason about from a probability perspective (i.e., setting to true will - // cause the filter to behave like an independent random variable when - // composed within logical operator filters). + // Controls how sampling decisions are made. + // + // - Default behavior (``false``): + // + // * Uses the :ref:`x-request-id` as a consistent sampling pivot. + // * When :ref:`x-request-id` is present, sampling will be consistent + // across multiple hosts based on both the ``runtime_key`` and + // :ref:`x-request-id`. + // * Useful for tracking related requests across a distributed system. + // + // - When set to ``true`` or :ref:`x-request-id` is missing: + // + // * Sampling decisions are made randomly based only on the ``runtime_key``. + // * Useful in complex filter configurations (like nested + // :ref:`AndFilter`/ + // :ref:`OrFilter` blocks) where independent probability + // calculations are desired. + // * Can be used to implement logging kill switches with predictable probability distributions. + // bool use_independent_randomness = 3; } @@ -257,6 +260,7 @@ message ResponseFlagFilter { in: "DF" in: "DO" in: "DR" + in: "UDO" } } }]; diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto b/xds/third_party/envoy/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto index 94868f13432..bf65f3df45c 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto @@ -57,9 +57,7 @@ message Bootstrap { // If a network based configuration source is specified for :ref:`cds_config // `, it's necessary // to have some initial cluster definitions available to allow Envoy to know - // how to speak to the management server. These cluster definitions may not - // use :ref:`EDS ` (i.e. they should be static - // IP or DNS-based). + // how to speak to the management server. repeated cluster.v3.Cluster clusters = 2; // These static secrets can be used by :ref:`SdsSecretConfig diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/cluster/v3/cluster.proto b/xds/third_party/envoy/src/main/proto/envoy/config/cluster/v3/cluster.proto index 0d2d6f1918e..51180b1e855 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/cluster/v3/cluster.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/cluster/v3/cluster.proto @@ -942,6 +942,7 @@ message Cluster { // "envoy.filters.network.thrift_proxy". See the extension's documentation for details on // specific options. // [#next-major-version: make this a list of typed extensions.] + // [#extension-category: envoy.upstream_options] map typed_extension_protocol_options = 36; // If the DNS refresh rate is specified and the cluster type is either @@ -953,8 +954,15 @@ message Cluster { // :ref:`STRICT_DNS` // and :ref:`LOGICAL_DNS` // this setting is ignored. - google.protobuf.Duration dns_refresh_rate = 16 - [(validate.rules).duration = {gt {nanos: 1000000}}]; + // This field is deprecated in favor of using the :ref:`cluster_type` + // extension point and configuring it with :ref:`DnsCluster`. + // If :ref:`cluster_type` is configured with + // :ref:`DnsCluster`, this field will be ignored. + google.protobuf.Duration dns_refresh_rate = 16 [ + deprecated = true, + (validate.rules).duration = {gt {nanos: 1000000}}, + (envoy.annotations.deprecated_at_minor_version) = "3.0" + ]; // DNS jitter can be optionally specified if the cluster type is either // :ref:`STRICT_DNS`, @@ -965,7 +973,15 @@ message Cluster { // :ref:`STRICT_DNS` // and :ref:`LOGICAL_DNS` // this setting is ignored. - google.protobuf.Duration dns_jitter = 58; + // This field is deprecated in favor of using the :ref:`cluster_type` + // extension point and configuring it with :ref:`DnsCluster`. + // If :ref:`cluster_type` is configured with + // :ref:`DnsCluster`, this field will be ignored. + google.protobuf.Duration dns_jitter = 58 [ + deprecated = true, + (validate.rules).duration = {gte {}}, + (envoy.annotations.deprecated_at_minor_version) = "3.0" + ]; // If the DNS failure refresh rate is specified and the cluster type is either // :ref:`STRICT_DNS`, @@ -975,16 +991,31 @@ message Cluster { // other than :ref:`STRICT_DNS` and // :ref:`LOGICAL_DNS` this setting is // ignored. - RefreshRate dns_failure_refresh_rate = 44; + // This field is deprecated in favor of using the :ref:`cluster_type` + // extension point and configuring it with :ref:`DnsCluster`. + // If :ref:`cluster_type` is configured with + // :ref:`DnsCluster`, this field will be ignored. + RefreshRate dns_failure_refresh_rate = 44 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; // Optional configuration for setting cluster's DNS refresh rate. If the value is set to true, // cluster's DNS refresh rate will be set to resource record's TTL which comes from DNS // resolution. - bool respect_dns_ttl = 39; + // This field is deprecated in favor of using the :ref:`cluster_type` + // extension point and configuring it with :ref:`DnsCluster`. + // If :ref:`cluster_type` is configured with + // :ref:`DnsCluster`, this field will be ignored. + bool respect_dns_ttl = 39 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; // The DNS IP address resolution policy. If this setting is not specified, the // value defaults to // :ref:`AUTO`. + // For logical and strict dns cluster, this field is deprecated in favor of using the + // :ref:`cluster_type` + // extension point and configuring it with :ref:`DnsCluster`. + // If :ref:`cluster_type` is configured with + // :ref:`DnsCluster`, this field will be ignored. DnsLookupFamily dns_lookup_family = 17 [(validate.rules).enum = {defined_only: true}]; // If DNS resolvers are specified and the cluster type is either @@ -1024,6 +1055,9 @@ message Cluster { // During the transition period when both ``dns_resolution_config`` and ``typed_dns_resolver_config`` exists, // when ``typed_dns_resolver_config`` is in place, Envoy will use it and ignore ``dns_resolution_config``. // When ``typed_dns_resolver_config`` is missing, the default behavior is in place. + // Also note that this field is deprecated for logical dns and strict dns clusters and will be ignored when + // :ref:`cluster_type` is configured with + // :ref:`DnsCluster`. // [#extension-category: envoy.network.dns_resolver] core.v3.TypedExtensionConfig typed_dns_resolver_config = 55; @@ -1311,7 +1345,7 @@ message TrackClusterStats { // If request_response_sizes is true, then the :ref:`histograms // ` tracking header and body sizes - // of requests and responses will be published. + // of requests and responses will be published. Additionally, number of headers in the requests and responses will be tracked. bool request_response_sizes = 2; // If true, some stats will be emitted per-endpoint, similar to the stats in admin ``/clusters`` diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/address.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/address.proto index d8d47882655..56796fc721a 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/address.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/address.proto @@ -50,7 +50,7 @@ message EnvoyInternalAddress { string endpoint_id = 2; } -// [#next-free-field: 7] +// [#next-free-field: 8] message SocketAddress { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.SocketAddress"; @@ -97,6 +97,20 @@ message SocketAddress { // allow both IPv4 and IPv6 connections, with peer IPv4 addresses mapped into // IPv6 space as ``::FFFF:``. bool ipv4_compat = 6; + + // Filepath that specifies the Linux network namespace this socket will be created in (see ``man 7 + // network_namespaces``). If this field is set, Envoy will create the socket in the specified + // network namespace. + // + // .. note:: + // Setting this parameter requires Envoy to run with the ``CAP_NET_ADMIN`` capability. + // + // .. note:: + // Currently only used for Listener sockets. + // + // .. attention:: + // Network namespaces are only configurable on Linux. Otherwise, this field has no effect. + string network_namespace_filepath = 7; } message TcpKeepalive { diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/base.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/base.proto index 57f59373395..978f365d5f9 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/base.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/base.proto @@ -266,7 +266,7 @@ message RuntimeUInt32 { uint32 default_value = 2; // Runtime key to get value for comparison. This value is used if defined. - string runtime_key = 3 [(validate.rules).string = {min_len: 1}]; + string runtime_key = 3; } // Runtime derived percentage with a default when not specified. @@ -275,7 +275,7 @@ message RuntimePercent { type.v3.Percent default_value = 1; // Runtime key to get value for comparison. This value is used if defined. - string runtime_key = 2 [(validate.rules).string = {min_len: 1}]; + string runtime_key = 2; } // Runtime derived double with a default when not specified. @@ -286,7 +286,7 @@ message RuntimeDouble { double default_value = 1; // Runtime key to get value for comparison. This value is used if defined. - string runtime_key = 2 [(validate.rules).string = {min_len: 1}]; + string runtime_key = 2; } // Runtime derived bool with a default when not specified. @@ -300,15 +300,34 @@ message RuntimeFeatureFlag { // Runtime key to get value for comparison. This value is used if defined. The boolean value must // be represented via its // `canonical JSON encoding `_. - string runtime_key = 2 [(validate.rules).string = {min_len: 1}]; + string runtime_key = 2; } +// Please use :ref:`KeyValuePair ` instead. +// [#not-implemented-hide:] message KeyValue { + // The key of the key/value pair. + string key = 1 [ + deprecated = true, + (validate.rules).string = {min_len: 1 max_bytes: 16384}, + (envoy.annotations.deprecated_at_minor_version) = "3.0" + ]; + + // The value of the key/value pair. + // + // The ``bytes`` type is used. This means if JSON or YAML is used to to represent the + // configuration, the value must be base64 encoded. This is unfriendly for users in most + // use scenarios of this message. + // + bytes value = 2 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; +} + +message KeyValuePair { // The key of the key/value pair. string key = 1 [(validate.rules).string = {min_len: 1 max_bytes: 16384}]; // The value of the key/value pair. - bytes value = 2; + google.protobuf.Value value = 2; } // Key/value pair plus option to control append behavior. This is used to specify @@ -339,8 +358,18 @@ message KeyValueAppend { OVERWRITE_IF_EXISTS = 3; } - // Key/value pair entry that this option to append or overwrite. - KeyValue entry = 1 [(validate.rules).message = {required: true}]; + // The single key/value pair record to be appended or overridden. This field must be set. + KeyValuePair record = 3; + + // Key/value pair entry that this option to append or overwrite. This field is deprecated + // and please use :ref:`record ` + // as replacement. + // [#not-implemented-hide:] + KeyValue entry = 1 [ + deprecated = true, + (validate.rules).message = {skip: true}, + (envoy.annotations.deprecated_at_minor_version) = "3.0" + ]; // Describes the action taken to append/overwrite the given value for an existing // key or to only add this key if it's absent. @@ -349,10 +378,12 @@ message KeyValueAppend { // Key/value pair to append or remove. message KeyValueMutation { - // Key/value pair to append or overwrite. Only one of ``append`` or ``remove`` can be set. + // Key/value pair to append or overwrite. Only one of ``append`` or ``remove`` can be set or + // the configuration will be rejected. KeyValueAppend append = 1; - // Key to remove. Only one of ``append`` or ``remove`` can be set. + // Key to remove. Only one of ``append`` or ``remove`` can be set or the configuration will be + // rejected. string remove = 2 [(validate.rules).string = {max_bytes: 16384}]; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/health_check.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/health_check.proto index 821f042bbe6..fd4440d8fa5 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/health_check.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/health_check.proto @@ -375,13 +375,13 @@ message HealthCheck { // The default value for "healthy edge interval" is the same as the default interval. google.protobuf.Duration healthy_edge_interval = 16 [(validate.rules).duration = {gt {}}]; - // .. attention:: - // This field is deprecated in favor of the extension - // :ref:`event_logger ` and - // :ref:`event_log_path ` - // in the file sink extension. - // // Specifies the path to the :ref:`health check event log `. + // + // .. attention:: + // This field is deprecated in favor of the extension + // :ref:`event_logger ` and + // :ref:`event_log_path ` + // in the file sink extension. string event_log_path = 17 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto index 7160cfb641a..edab4cd79c6 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package envoy.config.core.v3; import "envoy/config/core/v3/extension.proto"; +import "envoy/type/matcher/v3/string.proto"; import "envoy/type/v3/percent.proto"; import "google/protobuf/duration.proto"; @@ -63,8 +64,11 @@ message QuicProtocolOptions { // `_ size. Valid values range from // 1 to 16777216 (2^24, maximum supported by QUICHE) and defaults to 16777216 (16 * 1024 * 1024). // - // NOTE: 16384 (2^14) is the minimum window size supported in Google QUIC. If configured smaller than it, we will use 16384 instead. - // QUICHE IETF Quic implementation supports 1 bytes window. We only support increasing the default window size now, so it's also the minimum. + // .. note:: + // + // 16384 (2^14) is the minimum window size supported in Google QUIC. If configured smaller than it, we will use + // 16384 instead. QUICHE IETF Quic implementation supports 1 bytes window. We only support increasing the default + // window size now, so it's also the minimum. // // This field also acts as a soft limit on the number of bytes Envoy will buffer per-stream in the // QUIC stream send and receive buffers. Once the buffer reaches this pointer, watermark callbacks will fire to @@ -76,8 +80,11 @@ message QuicProtocolOptions { // flow-control. Valid values rage from 1 to 25165824 (24MB, maximum supported by QUICHE) and defaults // to 25165824 (24 * 1024 * 1024). // - // NOTE: 16384 (2^14) is the minimum window size supported in Google QUIC. We only support increasing the default - // window size now, so it's also the minimum. + // .. note:: + // + // 16384 (2^14) is the minimum window size supported in Google QUIC. We only support increasing the default + // window size now, so it's also the minimum. + // google.protobuf.UInt32Value initial_connection_window_size = 3 [(validate.rules).uint32 = {lte: 25165824 gte: 1}]; @@ -281,9 +288,13 @@ message HttpProtocolOptions { // :ref:`HTTP Connection Manager // `. // - // Note: currently some protocol codecs impose limits on the maximum size of a single header: - // HTTP/2 (when using nghttp2) limits a single header to around 100kb. - // HTTP/3 limits a single header to around 1024kb. + // .. note:: + // + // Currently some protocol codecs impose limits on the maximum size of a single header. + // + // * HTTP/2 (when using nghttp2) limits a single header to around 100kb. + // * HTTP/3 limits a single header to around 1024kb. + // google.protobuf.UInt32Value max_response_headers_kb = 7 [(validate.rules).uint32 = {lte: 8192 gt: 0}]; @@ -293,9 +304,15 @@ message HttpProtocolOptions { // Action to take when a client request with a header name containing underscore characters is received. // If this setting is not specified, the value defaults to ALLOW. - // Note: upstream responses are not affected by this setting. - // Note: this only affects client headers. It does not affect headers added - // by Envoy filters and does not have any impact if added to cluster config. + // + // .. note:: + // + // Upstream responses are not affected by this setting. + // + // .. note:: + // + // This only affects client headers. It does not affect headers added by Envoy filters and does not have any + // impact if added to cluster config. HeadersWithUnderscoresAction headers_with_underscores_action = 5; // Optional maximum requests for both upstream and downstream connections. @@ -305,7 +322,7 @@ message HttpProtocolOptions { google.protobuf.UInt32Value max_requests_per_connection = 6; } -// [#next-free-field: 11] +// [#next-free-field: 12] message Http1ProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.Http1ProtocolOptions"; @@ -417,6 +434,14 @@ message Http1ProtocolOptions { // ` // to reject custom methods. bool allow_custom_methods = 10 [(xds.annotations.v3.field_status).work_in_progress = true]; + + // Ignore HTTP/1.1 upgrade values matching any of the supplied matchers. + // + // .. note:: + // + // ``h2c`` upgrades are always removed for backwards compatibility, regardless of the + // value in this setting. + repeated type.matcher.v3.StringMatcher ignore_http_11_upgrade = 11; } message KeepaliveSettings { @@ -449,7 +474,7 @@ message KeepaliveSettings { [(validate.rules).duration = {gte {nanos: 1000000}}]; } -// [#next-free-field: 17] +// [#next-free-field: 18] message Http2ProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.Http2ProtocolOptions"; @@ -495,8 +520,10 @@ message Http2ProtocolOptions { // (2^16 - 1, HTTP/2 default) to 2147483647 (2^31 - 1, HTTP/2 maximum) and defaults to 268435456 // (256 * 1024 * 1024). // - // NOTE: 65535 is the initial window size from HTTP/2 spec. We only support increasing the default - // window size now, so it's also the minimum. + // .. note:: + // + // 65535 is the initial window size from HTTP/2 spec. We only support increasing the default window size now, + // so it's also the minimum. // // This field also acts as a soft limit on the number of bytes Envoy will buffer per-stream in the // HTTP/2 codec buffers. Once the buffer reaches this pointer, watermark callbacks will fire to @@ -633,6 +660,9 @@ message Http2ProtocolOptions { // If unset, HTTP/2 codec is selected based on envoy.reloadable_features.http2_use_oghttp2. google.protobuf.BoolValue use_oghttp2_codec = 16 [(xds.annotations.v3.field_status).work_in_progress = true]; + + // Configure the maximum amount of metadata than can be handled per stream. Defaults to 1 MB. + google.protobuf.UInt64Value max_metadata_size = 17; } // [#not-implemented-hide:] @@ -644,7 +674,7 @@ message GrpcProtocolOptions { } // A message which allows using HTTP/3. -// [#next-free-field: 7] +// [#next-free-field: 8] message Http3ProtocolOptions { QuicProtocolOptions quic_protocol_options = 1; @@ -671,6 +701,14 @@ message Http3ProtocolOptions { // docs](https://github.com/envoyproxy/envoy/blob/main/source/docs/h2_metadata.md) for more // information. bool allow_metadata = 6; + + // [#not-implemented-hide:] Hiding until Envoy has full HTTP/3 upstream support. + // Still under implementation. DO NOT USE. + // + // Disables QPACK compression related features for HTTP/3 including: + // No huffman encoding, zero dynamic table capacity and no cookie crumbing. + // This can be useful for trading off CPU vs bandwidth when an upstream HTTP/3 connection multiplexes multiple downstream connections. + bool disable_qpack = 7; } // A message to control transformations to the :scheme header diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/proxy_protocol.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/proxy_protocol.proto index 32747dd2288..564e76cb1e5 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/proxy_protocol.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/proxy_protocol.proto @@ -32,6 +32,15 @@ message ProxyProtocolPassThroughTLVs { repeated uint32 tlv_type = 2 [(validate.rules).repeated = {items {uint32 {lt: 256}}}]; } +// Represents a single Type-Length-Value (TLV) entry. +message TlvEntry { + // The type of the TLV. Must be a uint8 (0-255) as per the Proxy Protocol v2 specification. + uint32 type = 1 [(validate.rules).uint32 = {lt: 256}]; + + // The value of the TLV. Must be at least one byte long. + bytes value = 2 [(validate.rules).bytes = {min_len: 1}]; +} + message ProxyProtocolConfig { enum Version { // PROXY protocol version 1. Human readable format. @@ -47,4 +56,35 @@ message ProxyProtocolConfig { // This config controls which TLVs can be passed to upstream if it is Proxy Protocol // V2 header. If there is no setting for this field, no TLVs will be passed through. ProxyProtocolPassThroughTLVs pass_through_tlvs = 2; + + // This config allows additional TLVs to be included in the upstream PROXY protocol + // V2 header. Unlike ``pass_through_tlvs``, which passes TLVs from the downstream request, + // ``added_tlvs`` provides an extension mechanism for defining new TLVs that are included + // with the upstream request. These TLVs may not be present in the downstream request and + // can be defined at either the transport socket level or the host level to provide more + // granular control over the TLVs that are included in the upstream request. + // + // Host-level TLVs are specified in the ``metadata.typed_filter_metadata`` field under the + // ``envoy.transport_sockets.proxy_protocol`` namespace. + // + // .. literalinclude:: /_configs/repo/proxy_protocol.yaml + // :language: yaml + // :lines: 49-57 + // :linenos: + // :lineno-start: 49 + // :caption: :download:`proxy_protocol.yaml ` + // + // **Precedence behavior**: + // + // - When a TLV is defined at both the host level and the transport socket level, the value + // from the host level configuration takes precedence. This allows users to define default TLVs + // at the transport socket level and override them at the host level. + // - Any TLV defined in the ``pass_through_tlvs`` field will be overridden by either the host-level + // or transport socket-level TLV. + repeated TlvEntry added_tlvs = 3; +} + +message PerHostConfig { + // Enables per-host configuration for Proxy Protocol. + repeated TlvEntry added_tlvs = 1; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/socket_cmsg_headers.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/socket_cmsg_headers.proto index 4c8da0d1e4e..cc3e58e0996 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/socket_cmsg_headers.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/socket_cmsg_headers.proto @@ -25,4 +25,4 @@ message SocketCmsgHeaders { // Expected size of cmsg value. Default is zero. uint32 expected_size = 3; -} \ No newline at end of file +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/substitution_format_string.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/substitution_format_string.proto index abe8afa68ae..3edbf5f5f00 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/substitution_format_string.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/substitution_format_string.proto @@ -22,7 +22,12 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Optional configuration options to be used with json_format. message JsonFormatOptions { // The output JSON string properties will be sorted. - bool sort_properties = 1; + // + // .. note:: + // As the properties are always sorted, this option has no effect and is deprecated. + // + bool sort_properties = 1 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; } // Configuration to use multiple :ref:`command operators ` @@ -101,6 +106,12 @@ message SubstitutionFormatString { // * for ``text_format``, the output of the empty operator is changed from ``-`` to an // empty string, so that empty values are omitted entirely. // * for ``json_format`` the keys with null values are omitted in the output structure. + // + // .. note:: + // This option does not work perfectly with ``json_format`` as keys with ``null`` values + // will still be included in the output. See https://github.com/envoyproxy/envoy/issues/37941 + // for more details. + // bool omit_empty_values = 3; // Specify a ``content_type`` field. diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/endpoint_components.proto b/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/endpoint_components.proto index 6673691105e..eacc555df73 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/endpoint_components.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/endpoint_components.proto @@ -9,6 +9,9 @@ import "envoy/config/core/v3/health_check.proto"; import "google/protobuf/wrappers.proto"; +import "xds/core/v3/collection_entry.proto"; + +import "envoy/annotations/deprecation.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; import "validate/validate.proto"; @@ -133,14 +136,24 @@ message LbEndpoint { google.protobuf.UInt32Value load_balancing_weight = 4 [(validate.rules).uint32 = {gte: 1}]; } +// LbEndpoint list collection. Entries are `LbEndpoint` resources or references. // [#not-implemented-hide:] -// A configuration for a LEDS collection. +message LbEndpointCollection { + xds.core.v3.CollectionEntry entries = 1; +} + +// A configuration for an LEDS collection. message LedsClusterLocalityConfig { // Configuration for the source of LEDS updates for a Locality. core.v3.ConfigSource leds_config = 1; - // The xDS transport protocol glob collection resource name. - // The service is only supported in delta xDS (incremental) mode. + // The name of the LbEndpoint collection resource. + // + // If the name ends in ``/*``, it indicates an LbEndpoint glob collection, + // which is supported only in the xDS incremental protocol variants. + // Otherwise, it indicates an LbEndpointCollection list collection. + // + // Envoy currently supports only glob collections. string leds_collection_name = 2; } @@ -165,18 +178,20 @@ message LocalityLbEndpoints { core.v3.Metadata metadata = 9; // The group of endpoints belonging to the locality specified. - // [#comment:TODO(adisuissa): Once LEDS is implemented this field needs to be - // deprecated and replaced by ``load_balancer_endpoints``.] + // This is ignored if :ref:`leds_cluster_locality_config + // ` is set. repeated LbEndpoint lb_endpoints = 2; - // [#not-implemented-hide:] oneof lb_config { - // The group of endpoints belonging to the locality. - // [#comment:TODO(adisuissa): Once LEDS is implemented the ``lb_endpoints`` field - // needs to be deprecated.] - LbEndpointList load_balancer_endpoints = 7; + // [#not-implemented-hide:] + // Not implemented and deprecated. + LbEndpointList load_balancer_endpoints = 7 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; // LEDS Configuration for the current locality. + // If this is set, the :ref:`lb_endpoints + // ` + // field is ignored. LedsClusterLocalityConfig leds_cluster_locality_config = 8; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/listener.proto b/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/listener.proto index 9381d4eb7ac..ff2f79d1137 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/listener.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/listener.proto @@ -5,6 +5,7 @@ package envoy.config.listener.v3; import "envoy/config/accesslog/v3/accesslog.proto"; import "envoy/config/core/v3/address.proto"; import "envoy/config/core/v3/base.proto"; +import "envoy/config/core/v3/config_source.proto"; import "envoy/config/core/v3/extension.proto"; import "envoy/config/core/v3/socket_option.proto"; import "envoy/config/listener/v3/api_listener.proto"; @@ -53,7 +54,7 @@ message ListenerCollection { repeated xds.core.v3.CollectionEntry entries = 1; } -// [#next-free-field: 36] +// [#next-free-field: 37] message Listener { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.Listener"; @@ -115,6 +116,20 @@ message Listener { message InternalListenerConfig { } + // Configuration for filter chains discovery. + // [#not-implemented-hide:] + message FcdsConfig { + // Optional name to present to the filter chain discovery service. This may be an arbitrary name with arbitrary + // length. If a name is not provided, the listener's name is used. Refer to :ref:`filter_chains `. + // for details on how listener name is determined if unspecified. In addition, this may be a xdstp:// URL. + string name = 1; + + // Configuration for the source of FCDS updates for this listener. + // .. note:: + // This discovery service only supports ``AGGREGATED_GRPC`` API type. + core.v3.ConfigSource config_source = 2; + } + reserved 14, 23; // The unique name by which this listener is known. If no name is provided, @@ -147,6 +162,12 @@ message Listener { // :ref:`FAQ entry `. repeated FilterChain filter_chains = 3; + // Discover filter chains configurations by external service. Dynamic discovery of filter chains is allowed + // while having statically configured filter chains, however, a filter chain name must be unique within a + // listener. If a discovered filter chain matches a name of an existing filter chain, it is discarded. + // [#not-implemented-hide:] + FcdsConfig fcds_config = 36; + // :ref:`Matcher API ` resolving the filter chain name from the // network properties. This matcher is used as a replacement for the filter chain match condition // :ref:`filter_chain_match @@ -247,10 +268,10 @@ message Listener { google.protobuf.BoolValue freebind = 11; // Additional socket options that may not be present in Envoy source code or - // precompiled binaries. The socket options can be updated for a listener when + // precompiled binaries. + // It is not allowed to update the socket options for any existing address if // :ref:`enable_reuse_port ` - // is ``true``. Otherwise, if socket options change during a listener update the update will be rejected - // to make it clear that the options were not updated. + // is ``false`` to avoid the conflict when creating new sockets for the listener. repeated core.v3.SocketOption socket_options = 13; // Whether the listener should accept TCP Fast Open (TFO) connections. @@ -352,6 +373,11 @@ message Listener { // accepted in later event loop iterations. // If no value is provided Envoy will accept all connections pending accept // from the kernel. + // + // .. note:: + // + // It is recommended to lower this value for better overload management and reduced per-event cost. + // Setting it to 1 is a viable option with no noticeable impact on performance. google.protobuf.UInt32Value max_connections_to_accept_per_socket_event = 34 [(validate.rules).uint32 = {gt: 0}]; diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/rbac/v3/rbac.proto b/xds/third_party/envoy/src/main/proto/envoy/config/rbac/v3/rbac.proto index e33a533e25a..cdb1267a2c9 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/rbac/v3/rbac.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/rbac/v3/rbac.proto @@ -206,8 +206,10 @@ message Policy { // metadata should be sourced from, rather than only matching against dynamic metadata. // // The matcher can be configured to look up metadata from: +// // * Dynamic metadata: Runtime metadata added by filters during request processing // * Route metadata: Static metadata configured on the route entry +// message SourcedMetadata { // Metadata matcher configuration that defines what metadata to match against. This includes the filter name, // metadata key path, and expected value. @@ -246,10 +248,14 @@ message Permission { // When any is set, it matches any action. bool any = 3 [(validate.rules).bool = {const: true}]; - // A header (or pseudo-header such as :path or :method) on the incoming HTTP request. Only - // available for HTTP request. - // Note: the pseudo-header :path includes the query and fragment string. Use the ``url_path`` - // field if you want to match the URL path without the query and fragment string. + // A header (or pseudo-header such as ``:path`` or ``:method``) on the incoming HTTP request. Only available + // for HTTP request. + // + // .. note:: + // + // The pseudo-header ``:path`` includes the query and fragment string. Use the ``url_path`` field if you + // want to match the URL path without the query and fragment string. + // route.v3.HeaderMatcher header = 4; // A URL path on the incoming HTTP request. Only available for HTTP. @@ -274,8 +280,7 @@ message Permission { // the value of ``not_rule`` would not match, this permission would match. Permission not_rule = 8; - // The request server from the client's connection request. This is - // typically TLS SNI. + // The request server from the client's connection request. This is typically TLS SNI. // // .. attention:: // @@ -292,8 +297,7 @@ message Permission { // * A :ref:`listener filter ` may // overwrite a connection's requested server name within Envoy. // - // Please refer to :ref:`this FAQ entry ` to learn to - // setup SNI. + // Please refer to :ref:`this FAQ entry ` to learn how to setup SNI. type.matcher.v3.StringMatcher requested_server_name = 9; // Extension for configuring custom matchers for RBAC. @@ -312,7 +316,7 @@ message Permission { // Principal defines an identity or a group of identities for a downstream // subject. -// [#next-free-field: 14] +// [#next-free-field: 15] message Principal { option (udpa.annotations.versioning).previous_message_type = "envoy.config.rbac.v2.Principal"; @@ -326,6 +330,10 @@ message Principal { } // Authentication attributes for a downstream. + // It is recommended to NOT use this type, but instead use + // :ref:`MTlsAuthenticated `, + // configured via :ref:`custom `, + // which should be used for most use cases due to its improved security. message Authenticated { option (udpa.annotations.versioning).previous_message_type = "envoy.config.rbac.v2.Principal.Authenticated"; @@ -334,25 +342,31 @@ message Principal { // The name of the principal. If set, The URI SAN or DNS SAN in that order // is used from the certificate, otherwise the subject field is used. If - // unset, it applies to any user that is authenticated. + // unset, it applies to any user that is allowed by the downstream TLS configuration. + // If :ref:`require_client_certificate ` + // is false or :ref:`trust_chain_verification ` + // is set to :ref:`ACCEPT_UNTRUSTED `, + // then no authentication is required. type.matcher.v3.StringMatcher principal_name = 2; } oneof identifier { option (validate.required) = true; - // A set of identifiers that all must match in order to define the - // downstream. + // A set of identifiers that all must match in order to define the downstream. Set and_ids = 1; - // A set of identifiers at least one must match in order to define the - // downstream. + // A set of identifiers at least one must match in order to define the downstream. Set or_ids = 2; // When any is set, it matches any downstream. bool any = 3 [(validate.rules).bool = {const: true}]; // Authenticated attributes that identify the downstream. + // It is recommended to NOT use this field, but instead use + // :ref:`MTlsAuthenticated `, + // configured via :ref:`custom `, + // which should be used for most use cases due to its improved security. Authenticated authenticated = 4; // A CIDR block that describes the downstream IP. @@ -366,24 +380,33 @@ message Principal { [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; // A CIDR block that describes the downstream remote/origin address. - // Note: This is always the physical peer even if the - // :ref:`remote_ip ` is - // inferred from for example the x-forwarder-for header, proxy protocol, - // etc. + // + // .. note:: + // + // This is always the physical peer even if the + // :ref:`remote_ip ` is inferred from the + // x-forwarder-for header, the proxy protocol, etc. + // core.v3.CidrRange direct_remote_ip = 10; // A CIDR block that describes the downstream remote/origin address. - // Note: This may not be the physical peer and could be different from the - // :ref:`direct_remote_ip - // `. E.g, if the - // remote ip is inferred from for example the x-forwarder-for header, proxy - // protocol, etc. + // + // .. note:: + // + // This may not be the physical peer and could be different from the :ref:`direct_remote_ip + // `. E.g, if the remote ip is inferred from + // the x-forwarder-for header, the proxy protocol, etc. + // core.v3.CidrRange remote_ip = 11; - // A header (or pseudo-header such as :path or :method) on the incoming HTTP - // request. Only available for HTTP request. Note: the pseudo-header :path - // includes the query and fragment string. Use the ``url_path`` field if you - // want to match the URL path without the query and fragment string. + // A header (or pseudo-header such as ``:path`` or ``:method``) on the incoming HTTP request. Only available + // for HTTP request. + // + // .. note:: + // + // The pseudo-header ``:path`` includes the query and fragment string. Use the ``url_path`` field if you + // want to match the URL path without the query and fragment string. + // route.v3.HeaderMatcher header = 6; // A URL path on the incoming HTTP request. Only available for HTTP. @@ -405,6 +428,10 @@ message Principal { // Matches against metadata from either dynamic state or route configuration. Preferred over the // ``metadata`` field as it provides more flexibility in metadata source selection. SourcedMetadata sourced_metadata = 13; + + // Extension for configuring custom principals for RBAC. + // [#extension-category: envoy.rbac.principals] + core.v3.TypedExtensionConfig custom = 14; } } @@ -416,7 +443,7 @@ message Action { // The action to take if the matcher matches. Every action either allows or denies a request, // and can also carry out action-specific operations. // - // Actions: + // **Actions:** // // * ``ALLOW``: If the request gets matched on ALLOW, it is permitted. // * ``DENY``: If the request gets matched on DENY, it is not permitted. @@ -425,7 +452,7 @@ message Action { // ``envoy.common`` will be set to the value ``true``. // * If the request cannot get matched, it will fallback to ``DENY``. // - // Log behavior: + // **Log behavior:** // // If the RBAC matcher contains at least one LOG action, the dynamic // metadata key ``access_log_hint`` will be set based on if the request diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/route/v3/route_components.proto b/xds/third_party/envoy/src/main/proto/envoy/config/route/v3/route_components.proto index ce781d100c9..292e5b93558 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/route/v3/route_components.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/route/v3/route_components.proto @@ -5,6 +5,7 @@ package envoy.config.route.v3; import "envoy/config/core/v3/base.proto"; import "envoy/config/core/v3/extension.proto"; import "envoy/config/core/v3/proxy_protocol.proto"; +import "envoy/type/matcher/v3/filter_state.proto"; import "envoy/type/matcher/v3/metadata.proto"; import "envoy/type/matcher/v3/regex.proto"; import "envoy/type/matcher/v3/string.proto"; @@ -500,6 +501,8 @@ message WeightedCluster { // Configuration for a cluster specifier plugin. message ClusterSpecifierPlugin { // The name of the plugin and its opaque configuration. + // + // [#extension-category: envoy.router.cluster_specifier_plugin] core.v3.TypedExtensionConfig extension = 1 [(validate.rules).message = {required: true}]; // If is_optional is not set or is set to false and the plugin defined by this message is not a @@ -510,7 +513,7 @@ message ClusterSpecifierPlugin { bool is_optional = 2; } -// [#next-free-field: 16] +// [#next-free-field: 17] message RouteMatch { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.RouteMatch"; @@ -661,6 +664,12 @@ message RouteMatch { // If the number of specified dynamic metadata matchers is nonzero, they all must match the // dynamic metadata for a match to occur. repeated type.matcher.v3.MetadataMatcher dynamic_metadata = 13; + + // Specifies a set of filter state matchers on which the route should match. + // The router will check the filter state against all the specified filter state matchers. + // If the number of specified filter state matchers is nonzero, they all must match the + // filter state for a match to occur. + repeated type.matcher.v3.FilterStateMatcher filter_state = 16; } // Cors policy configuration. @@ -815,7 +824,10 @@ message RouteAction { // value, the request will be mirrored. core.v3.RuntimeFractionalPercent runtime_fraction = 3; - // Determines if the trace span should be sampled. Defaults to true. + // Specifies whether the trace span for the shadow request should be sampled. If this field is not explicitly set, + // the shadow request will inherit the sampling decision of its parent span. This ensures consistency with the trace + // sampling policy of the original request and prevents oversampling, especially in scenarios where runtime sampling + // is disabled. google.protobuf.BoolValue trace_sampled = 4; // Disables appending the ``-shadow`` suffix to the shadowed ``Host`` header. Defaults to ``false``. @@ -1149,12 +1161,21 @@ message RouteAction { // [#extension-category: envoy.path.rewrite] core.v3.TypedExtensionConfig path_rewrite_policy = 41; + // If one of the host rewrite specifiers is set and the + // :ref:`suppress_envoy_headers + // ` flag is not + // set to true, the router filter will place the original host header value before + // rewriting into the :ref:`x-envoy-original-host + // ` header. + // + // And if the + // :ref:`append_x_forwarded_host ` + // is set to true, the original host value will also be appended to the + // :ref:`config_http_conn_man_headers_x-forwarded-host` header. + // oneof host_rewrite_specifier { // Indicates that during forwarding, the host header will be swapped with - // this value. Using this option will append the - // :ref:`config_http_conn_man_headers_x-forwarded-host` header if - // :ref:`append_x_forwarded_host ` - // is set. + // this value. string host_rewrite_literal = 6 [(validate.rules).string = {well_known_regex: HTTP_HEADER_VALUE strict: false}]; @@ -1164,18 +1185,12 @@ message RouteAction { // type ``strict_dns`` or ``logical_dns``, // or when :ref:`hostname ` // field is not empty. Setting this to true with other cluster types - // has no effect. Using this option will append the - // :ref:`config_http_conn_man_headers_x-forwarded-host` header if - // :ref:`append_x_forwarded_host ` - // is set. + // has no effect. google.protobuf.BoolValue auto_host_rewrite = 7; // Indicates that during forwarding, the host header will be swapped with the content of given // downstream or :ref:`custom ` header. - // If header value is empty, host header is left intact. Using this option will append the - // :ref:`config_http_conn_man_headers_x-forwarded-host` header if - // :ref:`append_x_forwarded_host ` - // is set. + // If header value is empty, host header is left intact. // // .. attention:: // @@ -1191,10 +1206,6 @@ message RouteAction { // Indicates that during forwarding, the host header will be swapped with // the result of the regex substitution executed on path value with query and fragment removed. // This is useful for transitioning variable content between path segment and subdomain. - // Using this option will append the - // :ref:`config_http_conn_man_headers_x-forwarded-host` header if - // :ref:`append_x_forwarded_host ` - // is set. // // For example with the following config: // @@ -1868,10 +1879,11 @@ message VirtualCluster { // Global rate limiting :ref:`architecture overview `. // Also applies to Local rate limiting :ref:`using descriptors `. +// [#next-free-field: 7] message RateLimit { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.RateLimit"; - // [#next-free-field: 12] + // [#next-free-field: 13] message Action { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.RateLimit.Action"; @@ -1928,9 +1940,48 @@ message RateLimit { // The key to use in the descriptor entry. string descriptor_key = 2 [(validate.rules).string = {min_len: 1}]; - // If set to true, Envoy skips the descriptor while calling rate limiting service - // when header is not present in the request. By default it skips calling the - // rate limiting service if this header is not present in the request. + // Controls the behavior when the specified header is not present in the request. + // + // If set to ``false`` (default): + // + // * Envoy does **NOT** call the rate limiting service for this descriptor. + // * Useful if the header is optional and you prefer to skip rate limiting when it's absent. + // + // If set to ``true``: + // + // * Envoy calls the rate limiting service but omits this descriptor if the header is missing. + // * Useful if you want Envoy to enforce rate limiting even when the header is not present. + // + bool skip_if_absent = 3; + } + + // The following descriptor entry is appended when a query parameter contains a key that matches the + // ``query_parameter_name``: + // + // .. code-block:: cpp + // + // ("", "") + message QueryParameters { + // The name of the query parameter to use for rate limiting. Value of this query parameter is used to populate + // the value of the descriptor entry for the descriptor_key. + string query_parameter_name = 1 [(validate.rules).string = {min_len: 1}]; + + // The key to use when creating the rate limit descriptor entry. his descriptor key will be used to identify the + // rate limit rule in the rate limiting service. + string descriptor_key = 2 [(validate.rules).string = {min_len: 1}]; + + // Controls the behavior when the specified query parameter is not present in the request. + // + // If set to ``false`` (default): + // + // * Envoy does **NOT** call the rate limiting service for this descriptor. + // * Useful if the query parameter is optional and you prefer to skip rate limiting when it's absent. + // + // If set to ``true``: + // + // * Envoy calls the rate limiting service but omits this descriptor if the query parameter is missing. + // * Useful if you want Envoy to enforce rate limiting even when the query parameter is not present. + // bool skip_if_absent = 3; } @@ -2065,9 +2116,19 @@ message RateLimit { // Source of metadata Source source = 4 [(validate.rules).enum = {defined_only: true}]; - // If set to true, Envoy skips the descriptor while calling rate limiting service - // when ``metadata_key`` is empty and ``default_value`` is not set. By default it skips calling the - // rate limiting service in that case. + // Controls the behavior when the specified ``metadata_key`` is empty and ``default_value`` is not set. + // + // If set to ``false`` (default): + // + // * Envoy does **NOT** call the rate limiting service for this descriptor. + // * Useful if the metadata is optional and you prefer to skip rate limiting when it's absent. + // + // If set to ``true``: + // + // * Envoy calls the rate limiting service but omits this descriptor if the ``metadata_key`` is empty and + // ``default_value`` is missing. + // * Useful if you want Envoy to enforce rate limiting even when the metadata is not present. + // bool skip_if_absent = 5; } @@ -2110,6 +2171,9 @@ message RateLimit { // Rate limit on request headers. RequestHeaders request_headers = 3; + // Rate limit on query parameters. + QueryParameters query_parameters = 12; + // Rate limit on remote address. RemoteAddress remote_address = 4; @@ -2168,6 +2232,33 @@ message RateLimit { } } + message HitsAddend { + // Fixed number of hits to add to the rate limit descriptor. + // + // One of the ``number`` or ``format`` fields should be set but not both. + google.protobuf.UInt64Value number = 1 [(validate.rules).uint64 = {lte: 1000000000}]; + + // Substitution format string to extract the number of hits to add to the rate limit descriptor. + // The same :ref:`format specifier ` as used for + // :ref:`HTTP access logging ` applies here. + // + // .. note:: + // + // The format string must contains only single valid substitution field. If the format string + // not meets the requirement, the configuration will be rejected. + // + // The substitution field should generates a non-negative number or string representation of + // a non-negative number. The value of the non-negative number should be less than or equal + // to 1000000000 like the ``number`` field. If the output of the substitution field not meet + // the requirement, this will be treated as an error and the current descriptor will be ignored. + // + // For example, the ``%BYTES_RECEIVED%`` format string will be replaced with the number of bytes + // received in the request. + // + // One of the ``number`` or ``format`` fields should be set but not both. + string format = 2 [(validate.rules).string = {prefix: "%" suffix: "%" ignore_empty: true}]; + } + // Refers to the stage set in the filter. The rate limit configuration only // applies to filters with the same stage number. The default stage number is // 0. @@ -2175,9 +2266,19 @@ message RateLimit { // .. note:: // // The filter supports a range of 0 - 10 inclusively for stage numbers. + // + // .. note:: + // This is not supported if the rate limit action is configured in the ``typed_per_filter_config`` like + // :ref:`VirtualHost.typed_per_filter_config` or + // :ref:`Route.typed_per_filter_config`, etc. google.protobuf.UInt32Value stage = 1 [(validate.rules).uint32 = {lte: 10}]; // The key to be set in runtime to disable this rate limit configuration. + // + // .. note:: + // This is not supported if the rate limit action is configured in the ``typed_per_filter_config`` like + // :ref:`VirtualHost.typed_per_filter_config` or + // :ref:`Route.typed_per_filter_config`, etc. string disable_key = 2; // A list of actions that are to be applied for this rate limit configuration. @@ -2192,7 +2293,38 @@ message RateLimit { // rate limit configuration. If the override value is invalid or cannot be resolved // from metadata, no override is provided. See :ref:`rate limit override // ` for more information. + // + // .. note:: + // This is not supported if the rate limit action is configured in the ``typed_per_filter_config`` like + // :ref:`VirtualHost.typed_per_filter_config` or + // :ref:`Route.typed_per_filter_config`, etc. Override limit = 4; + + // An optional hits addend to be appended to the descriptor produced by this rate limit + // configuration. + // + // .. note:: + // This is only supported if the rate limit action is configured in the ``typed_per_filter_config`` like + // :ref:`VirtualHost.typed_per_filter_config` or + // :ref:`Route.typed_per_filter_config`, etc. + HitsAddend hits_addend = 5; + + // If true, the rate limit request will be applied when the stream completes. The default value is false. + // This is useful when the rate limit budget needs to reflect the response context that is not available + // on the request path. + // + // For example, let's say the upstream service calculates the usage statistics and returns them in the response body + // and we want to utilize these numbers to apply the rate limit action for the subsequent requests. + // Combined with another filter that can set the desired addend based on the response (e.g. Lua filter), + // this can be used to subtract the usage statistics from the rate limit budget. + // + // A rate limit applied on the stream completion is "fire-and-forget" by nature, and rate limit is not enforced by this config. + // In other words, the current request won't be blocked when this is true, but the budget will be updated for the subsequent + // requests based on the action with this field set to true. Users should ensure that the rate limit is enforced by the actions + // applied on the request path, i.e. the ones with this field set to false. + // + // Currently, this is only supported by the HTTP global rate filter. + bool apply_on_stream_done = 6; } // .. attention:: diff --git a/xds/third_party/envoy/src/main/proto/envoy/data/accesslog/v3/accesslog.proto b/xds/third_party/envoy/src/main/proto/envoy/data/accesslog/v3/accesslog.proto index 2e02f1eb455..da029b7da2e 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/data/accesslog/v3/accesslog.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/data/accesslog/v3/accesslog.proto @@ -109,14 +109,16 @@ message AccessLogCommon { double sample_rate = 1 [(validate.rules).double = {lte: 1.0 gt: 0.0}]; // This field is the remote/origin address on which the request from the user was received. - // Note: This may not be the physical peer. E.g, if the remote address is inferred from for - // example the x-forwarder-for header, proxy protocol, etc. + // + // .. note:: + // This may not be the actual peer address. For example, it might be derived from headers like ``x-forwarded-for``, + // the proxy protocol, or similar sources. config.core.v3.Address downstream_remote_address = 2; // This field is the local/destination address on which the request from the user was received. config.core.v3.Address downstream_local_address = 3; - // If the connection is secure,S this field will contain TLS properties. + // If the connection is secure, this field will contain TLS properties. TLSProperties tls_properties = 4; // The time that Envoy started servicing this request. This is effectively the time that the first @@ -128,7 +130,7 @@ message AccessLogCommon { google.protobuf.Duration time_to_last_rx_byte = 6; // Interval between the first downstream byte received and the first upstream byte sent. There may - // by considerable delta between ``time_to_last_rx_byte`` and this value due to filters. + // be considerable delta between ``time_to_last_rx_byte`` and this value due to filters. // Additionally, the same caveats apply as documented in ``time_to_last_downstream_tx_byte`` about // not accounting for kernel socket buffer time, etc. google.protobuf.Duration time_to_first_upstream_tx_byte = 7; @@ -187,7 +189,7 @@ message AccessLogCommon { // If upstream connection failed due to transport socket (e.g. TLS handshake), provides the // failure reason from the transport socket. The format of this field depends on the configured // upstream transport socket. Common TLS failures are in - // :ref:`TLS trouble shooting `. + // :ref:`TLS troubleshooting `. string upstream_transport_failure_reason = 18; // The name of the route @@ -204,7 +206,7 @@ message AccessLogCommon { map filter_state_objects = 21; // A list of custom tags, which annotate logs with additional information. - // To configure this value, users should configure + // To configure this value, see the documentation for // :ref:`custom_tags `. map custom_tags = 22; @@ -225,40 +227,41 @@ message AccessLogCommon { // This could be any format string that could be used to identify one stream. string stream_id = 26; - // If this log entry is final log entry that flushed after the stream completed or - // intermediate log entry that flushed periodically during the stream. - // There may be multiple intermediate log entries and only one final log entry for each - // long-live stream (TCP connection, long-live HTTP2 stream). - // And if it is necessary, unique ID or identifier can be added to the log entry - // :ref:`stream_id ` to - // correlate all these intermediate log entries and final log entry. + // Indicates whether this log entry is the final entry (flushed after the stream completed) or an intermediate entry + // (flushed periodically during the stream). + // + // For long-lived streams (e.g., TCP connections or long-lived HTTP/2 streams), there may be multiple intermediate + // entries and only one final entry. + // + // If needed, a unique identifier (see :ref:`stream_id `) + // can be used to correlate all intermediate and final log entries for the same stream. // // .. attention:: // - // This field is deprecated in favor of ``access_log_type`` for better indication of the - // type of the access log record. + // This field is deprecated in favor of ``access_log_type``, which provides a clearer indication of the log entry + // type. bool intermediate_log_entry = 27 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; // If downstream connection in listener failed due to transport socket (e.g. TLS handshake), provides the // failure reason from the transport socket. The format of this field depends on the configured downstream - // transport socket. Common TLS failures are in :ref:`TLS trouble shooting `. + // transport socket. Common TLS failures are in :ref:`TLS troubleshooting `. string downstream_transport_failure_reason = 28; // For HTTP: Total number of bytes sent to the downstream by the http stream. - // For TCP: Total number of bytes sent to the downstream by the tcp proxy. + // For TCP: Total number of bytes sent to the downstream by the :ref:`TCP Proxy `. uint64 downstream_wire_bytes_sent = 29; // For HTTP: Total number of bytes received from the downstream by the http stream. Envoy over counts sizes of received HTTP/1.1 pipelined requests by adding up bytes of requests in the pipeline to the one currently being processed. - // For TCP: Total number of bytes received from the downstream by the tcp proxy. + // For TCP: Total number of bytes received from the downstream by the :ref:`TCP Proxy `. uint64 downstream_wire_bytes_received = 30; // For HTTP: Total number of bytes sent to the upstream by the http stream. This value accumulates during upstream retries. - // For TCP: Total number of bytes sent to the upstream by the tcp proxy. + // For TCP: Total number of bytes sent to the upstream by the :ref:`TCP Proxy `. uint64 upstream_wire_bytes_sent = 31; // For HTTP: Total number of bytes received from the upstream by the http stream. - // For TCP: Total number of bytes sent to the upstream by the tcp proxy. + // For TCP: Total number of bytes sent to the upstream by the :ref:`TCP Proxy `. uint64 upstream_wire_bytes_received = 32; // The type of the access log, which indicates when the log was recorded. @@ -297,7 +300,7 @@ message ResponseFlags { // Indicates there was no healthy upstream. bool no_healthy_upstream = 2; - // Indicates an there was an upstream request timeout. + // Indicates there was an upstream request timeout. bool upstream_request_timeout = 3; // Indicates local codec level reset was sent on the stream. @@ -358,7 +361,7 @@ message ResponseFlags { // Indicates that a filter configuration is not available. bool no_filter_config_found = 22; - // Indicates that request or connection exceeded the downstream connection duration. + // Indicates that the request or connection exceeded the downstream connection duration. bool duration_timeout = 23; // Indicates there was an HTTP protocol error in the upstream response. @@ -480,7 +483,7 @@ message HTTPRequestProperties { // do not already have a request ID. string request_id = 9; - // Value of the ``X-Envoy-Original-Path`` request header. + // Value of the ``x-envoy-original-path`` request header. string original_path = 10; // Size of the HTTP request headers in bytes. diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/clusters/aggregate/v3/cluster.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/clusters/aggregate/v3/cluster.proto index 4f44ac9cd5c..d23d767f73b 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/clusters/aggregate/v3/cluster.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/clusters/aggregate/v3/cluster.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package envoy.extensions.clusters.aggregate.v3; +import "envoy/config/core/v3/config_source.proto"; + import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; import "validate/validate.proto"; @@ -25,3 +27,18 @@ message ClusterConfig { // appear in this list. repeated string clusters = 1 [(validate.rules).repeated = {min_items: 1}]; } + +// Configures an aggregate cluster whose +// :ref:`ClusterConfig ` +// is to be fetched from a separate xDS resource. +// [#extension: envoy.clusters.aggregate_resource] +// [#not-implemented-hide:] +message AggregateClusterResource { + // Configuration source specifier for the ClusterConfig resource. + // Only the aggregated protocol variants are supported; if configured + // otherwise, the cluster resource will be NACKed. + config.core.v3.ConfigSource config_source = 1 [(validate.rules).message = {required: true}]; + + // The name of the ClusterConfig resource to subscribe to. + string resource_name = 2 [(validate.rules).string = {min_len: 1}]; +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/rbac/v3/rbac.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/rbac/v3/rbac.proto index 649869a255d..6efd47ac58b 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/rbac/v3/rbac.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/rbac/v3/rbac.proto @@ -27,48 +27,53 @@ message RBAC { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.rbac.v2.RBAC"; - // Specify the RBAC rules to be applied globally. - // If absent, no enforcing RBAC policy will be applied. - // If present and empty, DENY. - // If both rules and matcher are configured, rules will be ignored. + // The primary RBAC policy which will be applied globally, to all the incoming requests. + // + // * If absent, no RBAC enforcement occurs. + // * If set but empty, all requests are denied. + // + // .. note:: + // + // When both ``rules`` and ``matcher`` are configured, ``rules`` will be ignored. + // config.rbac.v3.RBAC rules = 1 [(udpa.annotations.field_migrate).oneof_promotion = "rules_specifier"]; // If specified, rules will emit stats with the given prefix. - // This is useful to distinguish the stat when there are more than 1 RBAC filter configured with - // rules. + // This is useful for distinguishing metrics when multiple RBAC filters are configured. string rules_stat_prefix = 6; - // The match tree to use when resolving RBAC action for incoming requests. Requests do not - // match any matcher will be denied. - // If absent, no enforcing RBAC matcher will be applied. - // If present and empty, deny all requests. - xds.type.matcher.v3.Matcher matcher = 4 [ - (udpa.annotations.field_migrate).oneof_promotion = "rules_specifier", - (xds.annotations.v3.field_status).work_in_progress = true - ]; + // Match tree for evaluating RBAC actions on incoming requests. Requests not matching any matcher will be denied. + // + // * If absent, no RBAC enforcement occurs. + // * If set but empty, all requests are denied. + // + xds.type.matcher.v3.Matcher matcher = 4 + [(udpa.annotations.field_migrate).oneof_promotion = "rules_specifier"]; - // Shadow rules are not enforced by the filter (i.e., returning a 403) - // but will emit stats and logs and can be used for rule testing. - // If absent, no shadow RBAC policy will be applied. - // If both shadow rules and shadow matcher are configured, shadow rules will be ignored. + // Shadow policy for testing RBAC rules without enforcing them. These rules generate stats and logs but do not deny + // requests. If absent, no shadow RBAC policy will be applied. + // + // .. note:: + // + // When both ``shadow_rules`` and ``shadow_matcher`` are configured, ``shadow_rules`` will be ignored. + // config.rbac.v3.RBAC shadow_rules = 2 [(udpa.annotations.field_migrate).oneof_promotion = "shadow_rules_specifier"]; - // The match tree to use for emitting stats and logs which can be used for rule testing for - // incoming requests. // If absent, no shadow matcher will be applied. + // Match tree for testing RBAC rules through stats and logs without enforcing them. + // If absent, no shadow matching occurs. xds.type.matcher.v3.Matcher shadow_matcher = 5 [ (udpa.annotations.field_migrate).oneof_promotion = "shadow_rules_specifier", (xds.annotations.v3.field_status).work_in_progress = true ]; // If specified, shadow rules will emit stats with the given prefix. - // This is useful to distinguish the stat when there are more than 1 RBAC filter configured with - // shadow rules. + // This is useful for distinguishing metrics when multiple RBAC filters use shadow rules. string shadow_rules_stat_prefix = 3; - // If track_per_rule_stats is true, counters will be published for each rule and shadow rule. + // If ``track_per_rule_stats`` is ``true``, counters will be published for each rule and shadow rule. bool track_per_rule_stats = 7; } @@ -78,7 +83,7 @@ message RBACPerRoute { reserved 1; - // Override the global configuration of the filter with this new config. - // If absent, the global RBAC policy will be disabled for this route. + // Per-route specific RBAC configuration that overrides the global RBAC configuration. + // If absent, RBAC policy will be disabled for this route. RBAC rbac = 2; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/router/v3/router.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/router/v3/router.proto index 75bca960da1..d3996a96798 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/router/v3/router.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/router/v3/router.proto @@ -119,11 +119,11 @@ message Router { // for more details. bool suppress_grpc_request_failure_code_stats = 7; + // Optional HTTP filters for the upstream HTTP filter chain. + // // .. note:: // Upstream HTTP filters are currently in alpha. // - // Optional HTTP filters for the upstream HTTP filter chain. - // // These filters will be applied for all requests that pass through the router. // They will also be applied to shadowed requests. // Upstream HTTP filters cannot change route or cluster. diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index 5fb9f24cc7c..e0282af86e6 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -99,33 +99,43 @@ message HttpConnectionManager { ALWAYS_FORWARD_ONLY = 4; } - // Determines the action for request that contain %2F, %2f, %5C or %5c sequences in the URI path. + // Determines the action for request that contain ``%2F``, ``%2f``, ``%5C`` or ``%5c`` sequences in the URI path. // This operation occurs before URL normalization and the merge slashes transformations if they were enabled. enum PathWithEscapedSlashesAction { // Default behavior specific to implementation (i.e. Envoy) of this configuration option. // Envoy, by default, takes the KEEP_UNCHANGED action. - // NOTE: the implementation may change the default behavior at-will. + // + // .. note:: + // + // The implementation may change the default behavior at-will. IMPLEMENTATION_SPECIFIC_DEFAULT = 0; // Keep escaped slashes. KEEP_UNCHANGED = 1; // Reject client request with the 400 status. gRPC requests will be rejected with the INTERNAL (13) error code. - // The "httpN.downstream_rq_failed_path_normalization" counter is incremented for each rejected request. + // The ``httpN.downstream_rq_failed_path_normalization`` counter is incremented for each rejected request. REJECT_REQUEST = 2; - // Unescape %2F and %5C sequences and redirect request to the new path if these sequences were present. + // Unescape ``%2F`` and ``%5C`` sequences and redirect request to the new path if these sequences were present. // Redirect occurs after path normalization and merge slashes transformations if they were configured. - // NOTE: gRPC requests will be rejected with the INTERNAL (13) error code. - // This option minimizes possibility of path confusion exploits by forcing request with unescaped slashes to - // traverse all parties: downstream client, intermediate proxies, Envoy and upstream server. - // The "httpN.downstream_rq_redirected_with_normalized_path" counter is incremented for each - // redirected request. + // + // .. note:: + // + // gRPC requests will be rejected with the INTERNAL (13) error code. This option minimizes possibility of path + // confusion exploits by forcing request with unescaped slashes to traverse all parties: downstream client, + // intermediate proxies, Envoy and upstream server. The ``httpN.downstream_rq_redirected_with_normalized_path`` + // counter is incremented for each redirected request. + // UNESCAPE_AND_REDIRECT = 3; - // Unescape %2F and %5C sequences. - // Note: this option should not be enabled if intermediaries perform path based access control as - // it may lead to path confusion vulnerabilities. + // Unescape ``%2F`` and ``%5C`` sequences. + // + // .. note:: + // + // This option should not be enabled if intermediaries perform path based access control as it may lead to path + // confusion vulnerabilities. + // UNESCAPE_AND_FORWARD = 4; } @@ -185,14 +195,6 @@ message HttpConnectionManager { // Configuration for an external tracing provider. // If not specified, no tracing will be performed. - // - // .. attention:: - // Please be aware that ``envoy.tracers.opencensus`` provider can only be configured once - // in Envoy lifetime. - // Any attempts to reconfigure it or to use different configurations for different HCM filters - // will be rejected. - // Such a constraint is inherent to OpenCensus itself. It cannot be overcome without changes - // on OpenCensus side. config.trace.v3.Tracing.Http provider = 9; // Create separate tracing span for each upstream request if true. And if this flag is set to true, @@ -266,13 +268,12 @@ message HttpConnectionManager { // // .. warning:: // - // The current implementation of upgrade headers does not handle - // multi-valued upgrade headers. Support for multi-valued headers may be - // added in the future if needed. + // The current implementation of upgrade headers does not handle multi-valued upgrade headers. Support for + // multi-valued headers may be added in the future if needed. // // .. warning:: - // The current implementation of upgrade headers does not work with HTTP/2 - // upstreams. + // The current implementation of upgrade headers does not work with HTTP/2 upstreams. + // message UpgradeConfig { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager." @@ -304,7 +305,10 @@ message HttpConnectionManager { // `) will apply to the ``:path`` header // destined for the upstream. // - // Note: access logging and tracing will show the original ``:path`` header. + // .. note:: + // + // Access logging and tracing will show the original ``:path`` header. + // message PathNormalizationOptions { // [#not-implemented-hide:] Normalization applies internally before any processing of requests by // HTTP filters, routing, and matching *and* will affect the forwarded ``:path`` header. Defaults @@ -442,23 +446,23 @@ message HttpConnectionManager { Tracing tracing = 7; // Additional settings for HTTP requests handled by the connection manager. These will be - // applicable to both HTTP1 and HTTP2 requests. + // applicable to both HTTP/1.1 and HTTP/2 requests. config.core.v3.HttpProtocolOptions common_http_protocol_options = 35 [(udpa.annotations.security).configure_for_untrusted_downstream = true]; - // If set to true, Envoy will not start a drain timer for downstream HTTP1 connections after - // :ref:`common_http_protocol_options.max_connection_duration - // ` passes. - // Instead, Envoy will wait for the next downstream request, add connection:close to the response - // headers, then close the connection after the stream ends. + // If set to ``true``, Envoy will not initiate an immediate drain timer for downstream HTTP/1 connections + // once :ref:`common_http_protocol_options.max_connection_duration + // ` is exceeded. + // Instead, Envoy will wait until the next downstream request arrives, add a ``connection: close`` header + // to the response, and then gracefully close the connection once the stream has completed. // - // This behavior is compliant with `RFC 9112 section 9.6 `_ + // This behavior adheres to `RFC 9112, Section 9.6 `_. // - // If set to false, ``max_connection_duration`` will cause Envoy to enter the normal drain - // sequence for HTTP1 with Envoy eventually closing the connection (once there are no active - // streams). + // If set to ``false``, exceeding ``max_connection_duration`` triggers Envoy's default drain behavior for HTTP/1, + // where the connection is eventually closed after all active streams finish. // - // Has no effect if ``max_connection_duration`` is unset. Defaults to false. + // This option has no effect if ``max_connection_duration`` is not configured. + // Defaults to ``false``. bool http1_safe_max_connection_duration = 58; // Additional HTTP/1 settings that are passed to the HTTP/1 codec. @@ -496,9 +500,13 @@ message HttpConnectionManager { // The default value can be overridden by setting runtime key ``envoy.reloadable_features.max_request_headers_size_kb``. // Requests that exceed this limit will receive a 431 response. // - // Note: currently some protocol codecs impose limits on the maximum size of a single header: - // HTTP/2 (when using nghttp2) limits a single header to around 100kb. - // HTTP/3 limits a single header to around 1024kb. + // .. note:: + // + // Currently some protocol codecs impose limits on the maximum size of a single header. + // + // * HTTP/2 (when using nghttp2) limits a single header to around 100kb. + // * HTTP/3 limits a single header to around 1024kb. + // google.protobuf.UInt32Value max_request_headers_kb = 29 [(validate.rules).uint32 = {lte: 8192 gt: 0}]; @@ -576,31 +584,34 @@ message HttpConnectionManager { // during which Envoy will wait for the peer to close (i.e., a TCP FIN/RST is received by Envoy // from the downstream connection) prior to Envoy closing the socket associated with that // connection. - // NOTE: This timeout is enforced even when the socket associated with the downstream connection - // is pending a flush of the write buffer. However, any progress made writing data to the socket - // will restart the timer associated with this timeout. This means that the total grace period for - // a socket in this state will be - // +. + // + // .. note:: + // + // This timeout is enforced even when the socket associated with the downstream connection is pending a flush of + // the write buffer. However, any progress made writing data to the socket will restart the timer associated with + // this timeout. This means that the total grace period for a socket in this state will be + // +. // // Delaying Envoy's connection close and giving the peer the opportunity to initiate the close // sequence mitigates a race condition that exists when downstream clients do not drain/process // data in a connection's receive buffer after a remote close has been detected via a socket - // write(). This race leads to such clients failing to process the response code sent by Envoy, + // ``write()``. This race leads to such clients failing to process the response code sent by Envoy, // which could result in erroneous downstream processing. // // If the timeout triggers, Envoy will close the connection's socket. // // The default timeout is 1000 ms if this option is not specified. // - // .. NOTE:: + // .. note:: // To be useful in avoiding the race condition described above, this timeout must be set // to *at least* +<100ms to account for // a reasonable "worst" case processing time for a full iteration of Envoy's event loop>. // - // .. WARNING:: - // A value of 0 will completely disable delayed close processing. When disabled, the downstream + // .. warning:: + // A value of ``0`` will completely disable delayed close processing. When disabled, the downstream // connection's socket will be closed immediately after the write flush is completed or will // never close if the write flush does not complete. + // google.protobuf.Duration delayed_close_timeout = 26; // Configuration for :ref:`HTTP access logs ` @@ -657,20 +668,19 @@ message HttpConnectionManager { // :ref:`config_http_conn_man_headers_x-forwarded-for` for more information. uint32 xff_num_trusted_hops = 19; - // The configuration for the original IP detection extensions. + // Configuration for original IP detection extensions. // - // When configured the extensions will be called along with the request headers - // and information about the downstream connection, such as the directly connected address. - // Each extension will then use these parameters to decide the request's effective remote address. - // If an extension fails to detect the original IP address and isn't configured to reject - // the request, the HCM will try the remaining extensions until one succeeds or rejects - // the request. If the request isn't rejected nor any extension succeeds, the HCM will - // fallback to using the remote address. + // When these extensions are configured, Envoy will invoke them with the incoming request headers and + // details about the downstream connection, including the directly connected address. Each extension uses + // this information to determine the effective remote IP address for the request. If an extension cannot + // identify the original IP address and isn't set to reject the request, Envoy will sequentially attempt + // the remaining extensions until one successfully determines the IP or explicitly rejects the request. + // If all extensions fail without rejection, Envoy defaults to using the directly connected remote address. // - // .. WARNING:: - // Extensions cannot be used in conjunction with :ref:`use_remote_address + // .. warning:: + // These extensions cannot be configured simultaneously with :ref:`use_remote_address // ` - // nor :ref:`xff_num_trusted_hops + // or :ref:`xff_num_trusted_hops // `. // // [#extension-category: envoy.http.original_ip_detection] diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3/client_side_weighted_round_robin.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3/client_side_weighted_round_robin.proto index 9520f6dbd43..f913cb6a257 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3/client_side_weighted_round_robin.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3/client_side_weighted_round_robin.proto @@ -32,6 +32,13 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // weights using eps and qps. The weight of a given endpoint is computed as: // ``qps / (utilization + eps/qps * error_utilization_penalty)``. // +// Note that Envoy will forward the ORCA response headers/trailers from the upstream +// cluster to the downstream client. This means that if the downstream client is also +// configured to use ``client_side_weighted_round_robin`` it will load balance against +// Envoy based on upstream weights. This can happen when Envoy is used as a reverse proxy. +// To avoid this issue you can configure the :ref:`header_mutation filter ` to remove +// the ORCA payload from the response headers/trailers. +// // See the :ref:`load balancing architecture // overview` for more information. // diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/common/v3/common.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/common/v3/common.proto index 51520690a29..7868fb02b1a 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/common/v3/common.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/common/v3/common.proto @@ -8,6 +8,7 @@ import "envoy/type/v3/percent.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/wrappers.proto"; +import "envoy/annotations/deprecation.proto"; import "udpa/annotations/status.proto"; import "validate/validate.proto"; @@ -22,7 +23,25 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; message LocalityLbConfig { // Configuration for :ref:`zone aware routing // `. + // [#next-free-field: 6] message ZoneAwareLbConfig { + // Configures Envoy to always route requests to the local zone regardless of the + // upstream zone structure. In Envoy's default configuration, traffic is distributed proportionally + // across all upstream hosts while trying to maximize local routing when possible. The approach + // with force_local_zone aims to be more predictable and if there are upstream hosts in the local + // zone, they will receive all traffic. + // * :ref:`runtime values `. + // * :ref:`Zone aware routing support `. + message ForceLocalZone { + // Configures the minimum number of upstream hosts in the local zone required when force_local_zone + // is enabled. If the number of upstream hosts in the local zone is less than the specified value, + // Envoy will fall back to the default proportional-based distribution across localities. + // If not specified, the default is 1. + // * :ref:`runtime values `. + // * :ref:`Zone aware routing support `. + google.protobuf.UInt32Value min_size = 1; + } + // Configures percentage of requests that will be considered for zone aware routing // if zone aware routing is configured. If not specified, the default is 100%. // * :ref:`runtime values `. @@ -41,6 +60,12 @@ message LocalityLbConfig { // requests as if all hosts are unhealthy. This can help avoid potentially overwhelming a // failing service. bool fail_traffic_on_panic = 3; + + // If set to true, Envoy will force LocalityDirect routing if a local locality exists. + bool force_locality_direct_routing = 4 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; + + ForceLocalZone force_local_zone = 5; } // Configuration for :ref:`locality weighted load balancing diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto index 46d86b65856..9bc5fb5d029 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto @@ -24,7 +24,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Common TLS configuration] -// [#next-free-field: 6] +// [#next-free-field: 7] message TlsParameters { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.auth.TlsParameters"; @@ -45,6 +45,23 @@ message TlsParameters { TLSv1_3 = 4; } + enum CompliancePolicy { + // FIPS_202205 configures a TLS connection to use: + // + // * TLS 1.2 or 1.3 + // * For TLS 1.2, only ECDHE_[RSA|ECDSA]_WITH_AES_*_GCM_SHA*. + // * For TLS 1.3, only AES-GCM + // * P-256 or P-384 for key agreement. + // * For server signatures, only ``PKCS#1/PSS`` with ``SHA256/384/512``, or ECDSA + // with P-256 or P-384. + // + // .. attention:: + // + // Please refer to `BoringSSL policies `_ + // for details. + FIPS_202205 = 0; + } + // Minimum TLS protocol version. By default, it's ``TLSv1_2`` for both clients and servers. // // TLS protocol versions below TLSv1_2 require setting compatible ciphers with the @@ -157,6 +174,11 @@ message TlsParameters { // rsa_pkcs1_sha1 // ecdsa_sha1 repeated string signature_algorithms = 5; + + // Compliance policies configure various aspects of the TLS based on the given policy. + // The policies are applied last during configuration and may override the other TLS + // parameters, or any previous policy. + repeated CompliancePolicy compliance_policies = 6 [(validate.rules).repeated = {max_items: 1}]; } // BoringSSL private key method configuration. The private key methods are used for external @@ -232,12 +254,13 @@ message TlsCertificate { config.core.v3.WatchedDirectory watched_directory = 7; // BoringSSL private key method provider. This is an alternative to :ref:`private_key - // ` field. This can't be - // marked as ``oneof`` due to API compatibility reasons. Setting both :ref:`private_key - // ` and - // :ref:`private_key_provider - // ` fields will result in an - // error. + // ` field. + // When both :ref:`private_key ` and + // :ref:`private_key_provider ` fields are set, + // ``private_key_provider`` takes precedence. + // If ``private_key_provider`` is unavailable and :ref:`fallback + // ` + // is enabled, ``private_key`` will be used. PrivateKeyProvider private_key_provider = 6; // The password to decrypt the TLS private key. If this field is not set, it is assumed that the @@ -322,6 +345,13 @@ message SubjectAltNameMatcher { // Matcher for SAN value. // + // If the :ref:`san_type ` + // is :ref:`DNS ` + // and the matcher type is :ref:`exact `, DNS wildcards are evaluated + // according to the rules in https://www.rfc-editor.org/rfc/rfc6125#section-6.4.3. + // For example, ``*.example.com`` would match ``test.example.com`` but not ``example.com`` and not + // ``a.b.example.com``. + // // The string matching for OTHER_NAME SAN values depends on their ASN.1 type: // // * OBJECT: Validated against its dotted numeric notation (e.g., "1.2.3.4") diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto index 83ad364c4bf..94660e2da9f 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto @@ -22,8 +22,13 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; message GenericSecret { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.auth.GenericSecret"; - // Secret of generic type and is available to filters. + // Secret of generic type and is available to filters. It is expected + // that only only one of secret and secrets is set. config.core.v3.DataSource secret = 1 [(udpa.annotations.sensitive) = true]; + + // For cases where multiple associated secrets need to be distributed together. It is expected + // that only only one of secret and secrets is set. + map secrets = 2 [(udpa.annotations.sensitive) = true]; } message SdsSecretConfig { diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto index 44b8d269324..b292b18c45d 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto @@ -34,9 +34,8 @@ message UpstreamTlsContext { // // .. attention:: // - // Server certificate verification is not enabled by default. Configure - // :ref:`trusted_ca` to enable - // verification. + // Server certificate verification is not enabled by default. To enable verification, configure + // :ref:`trusted_ca`. CommonTlsContext common_tls_context = 1; // SNI string to use when creating TLS backend connections. @@ -51,14 +50,13 @@ message UpstreamTlsContext { // interacts with other validation options. bool auto_host_sni = 6; - // If true, replace any Subject Alternative Name validations with a validation for a DNS SAN matching - // the SNI value sent. Note that the validation will be against the actual requested SNI, regardless of how it - // is configured. + // If true, replaces any Subject Alternative Name (SAN) validations with a validation for a DNS SAN matching + // the SNI value sent. The validation uses the actual requested SNI, regardless of how the SNI is configured. // - // For the common case where an SNI value is sent and it is expected that the server certificate contains a SAN - // matching that SNI value, this option will do the correct SAN validation. + // For common cases where an SNI value is present and the server certificate should include a corresponding SAN, + // this option ensures the SAN is properly validated. // - // See :ref:`validation configuration ` for how this interacts with + // See the :ref:`validation configuration ` for how this interacts with // other validation options. bool auto_sni_san_validation = 7; @@ -70,16 +68,19 @@ message UpstreamTlsContext { bool allow_renegotiation = 3; // Maximum number of session keys (Pre-Shared Keys for TLSv1.3+, Session IDs and Session Tickets - // for TLSv1.2 and older) to store for the purpose of session resumption. + // for TLSv1.2 and older) to be stored for session resumption. // // Defaults to 1, setting this to 0 disables session resumption. google.protobuf.UInt32Value max_session_keys = 4; - // This field is used to control the enforcement, whereby the handshake will fail if the keyUsage extension - // is present and incompatible with the TLS usage. Currently, the default value is false (i.e., enforcement off) - // but it is expected to be changed to true by default in a future release. - // ``ssl.was_key_usage_invalid`` in :ref:`listener metrics ` will be set for certificate - // configurations that would fail if this option were set to true. + // Controls enforcement of the ``keyUsage`` extension in peer certificates. If set to ``true``, the handshake will fail if + // the ``keyUsage`` is incompatible with TLS usage. + // + // .. note:: + // The default value is ``false`` (i.e., enforcement off). It is expected to change to ``true`` in a future release. + // + // The ``ssl.was_key_usage_invalid`` in :ref:`listener metrics ` metric will be incremented + // for configurations that would fail if this option were enabled. google.protobuf.BoolValue enforce_rsa_key_usage = 5; } @@ -89,24 +90,16 @@ message DownstreamTlsContext { "envoy.api.v2.auth.DownstreamTlsContext"; enum OcspStaplePolicy { - // OCSP responses are optional. If an OCSP response is absent - // or expired, the associated certificate will be used for - // connections without an OCSP staple. + // OCSP responses are optional. If absent or expired, the certificate is used without stapling. LENIENT_STAPLING = 0; - // OCSP responses are optional. If an OCSP response is absent, - // the associated certificate will be used without an - // OCSP staple. If a response is provided but is expired, - // the associated certificate will not be used for - // subsequent connections. If no suitable certificate is found, - // the connection is rejected. + // OCSP responses are optional. If absent, the certificate is used without stapling. If present but expired, + // the certificate is not used for subsequent connections. Connections are rejected if no suitable certificate + // is found. STRICT_STAPLING = 1; - // OCSP responses are required. Configuration will fail if - // a certificate is provided without an OCSP response. If a - // response expires, the associated certificate will not be - // used connections. If no suitable certificate is found, the - // connection is rejected. + // OCSP responses are required. Connections fail if a certificate lacks a valid OCSP response. Expired responses + // prevent certificate use in new connections, and connections are rejected if no suitable certificate is available. MUST_STAPLE = 2; } @@ -139,46 +132,54 @@ message DownstreamTlsContext { bool disable_stateless_session_resumption = 7; } - // If set to true, the TLS server will not maintain a session cache of TLS sessions. (This is - // relevant only for TLSv1.2 and earlier.) + // If ``true``, the TLS server will not maintain a session cache of TLS sessions. + // + // .. note:: + // This applies only to TLSv1.2 and earlier. + // bool disable_stateful_session_resumption = 10; - // If specified, ``session_timeout`` will change the maximum lifetime (in seconds) of the TLS session. - // Currently this value is used as a hint for the `TLS session ticket lifetime (for TLSv1.2) `_. - // Only seconds can be specified (fractional seconds are ignored). + // Maximum lifetime of TLS sessions. If specified, ``session_timeout`` will change the maximum lifetime + // of the TLS session. + // + // This serves as a hint for the `TLS session ticket lifetime (for TLSv1.2) `_. + // Only whole seconds are considered; fractional seconds are ignored. google.protobuf.Duration session_timeout = 6 [(validate.rules).duration = { lt {seconds: 4294967296} gte {} }]; - // Config for whether to use certificates if they do not have - // an accompanying OCSP response or if the response expires at runtime. - // Defaults to LENIENT_STAPLING + // Configuration for handling certificates without an OCSP response or with expired responses. + // + // Defaults to ``LENIENT_STAPLING`` OcspStaplePolicy ocsp_staple_policy = 8 [(validate.rules).enum = {defined_only: true}]; // Multiple certificates are allowed in Downstream transport socket to serve different SNI. - // If the client provides SNI but no such cert matched, it will decide to full scan certificates or not based on this config. - // Defaults to false. See more details in :ref:`Multiple TLS certificates `. + // This option controls the behavior when no matching certificate is found for the received SNI value, + // or no SNI value was sent. If enabled, all certificates will be evaluated for a match for non-SNI criteria + // such as key type and OCSP settings. If disabled, the first provided certificate will be used. + // Defaults to ``false``. See more details in :ref:`Multiple TLS certificates `. google.protobuf.BoolValue full_scan_certs_on_sni_mismatch = 9; - // By default, Envoy as a server uses its preferred cipher during the handshake. - // Setting this to true would allow the downstream client's preferred cipher to be used instead. - // Has no effect when using TLSv1_3. + // If ``true``, the downstream client's preferred cipher is used during the handshake. If ``false``, Envoy + // uses its preferred cipher. + // + // .. note:: + // This has no effect when using TLSv1_3. + // bool prefer_client_ciphers = 11; } // TLS key log configuration. // The key log file format is "format used by NSS for its SSLKEYLOGFILE debugging output" (text taken from openssl man page) message TlsKeyLog { - // The path to save the TLS key log. + // Path to save the TLS key log. string path = 1 [(validate.rules).string = {min_len: 1}]; - // The local IP address that will be used to filter the connection which should save the TLS key log - // If it is not set, any local IP address will be matched. + // Local IP address ranges to filter connections for TLS key logging. If not set, matches any local IP address. repeated config.core.v3.CidrRange local_address_range = 2; - // The remote IP address that will be used to filter the connection which should save the TLS key log - // If it is not set, any remote IP address will be matched. + // Remote IP address ranges to filter connections for TLS key logging. If not set, matches any remote IP address. repeated config.core.v3.CidrRange remote_address_range = 3; } @@ -187,8 +188,8 @@ message TlsKeyLog { message CommonTlsContext { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.auth.CommonTlsContext"; - // Config for Certificate provider to get certificates. This provider should allow certificates to be - // fetched/refreshed over the network asynchronously with respect to the TLS handshake. + // Config for the Certificate Provider to fetch certificates. Certificates are fetched/refreshed asynchronously over + // the network relative to the TLS handshake. // // DEPRECATED: This message is not currently used, but if we ever do need it, we will want to // move it out of CommonTlsContext and into common.proto, similar to the existing @@ -281,7 +282,7 @@ message CommonTlsContext { // fetched/refreshed over the network asynchronously with respect to the TLS handshake. // // The same number and types of certificates as :ref:`tls_certificates ` - // are valid in the the certificates fetched through this setting. + // are valid in the certificates fetched through this setting. // // If ``tls_certificates`` or ``tls_certificate_provider_instance`` are set, this field // is ignored. @@ -319,13 +320,17 @@ message CommonTlsContext { // fetched/refreshed over the network asynchronously with respect to the TLS handshake. SdsSecretConfig validation_context_sds_secret_config = 7; - // Combined certificate validation context holds a default CertificateValidationContext - // and SDS config. When SDS server returns dynamic CertificateValidationContext, both dynamic - // and default CertificateValidationContext are merged into a new CertificateValidationContext - // for validation. This merge is done by Message::MergeFrom(), so dynamic - // CertificateValidationContext overwrites singular fields in default - // CertificateValidationContext, and concatenates repeated fields to default - // CertificateValidationContext, and logical OR is applied to boolean fields. + // Combines the default ``CertificateValidationContext`` with the SDS-provided dynamic context for certificate + // validation. + // + // When the SDS server returns a dynamic ``CertificateValidationContext``, it is merged + // with the default context using ``Message::MergeFrom()``. The merging rules are as follows: + // + // * **Singular Fields:** Dynamic fields override the default singular fields. + // * **Repeated Fields:** Dynamic repeated fields are concatenated with the default repeated fields. + // * **Boolean Fields:** Boolean fields are combined using a logical OR operation. + // + // The resulting ``CertificateValidationContext`` is used to perform certificate validation. CombinedCertificateValidationContext combined_validation_context = 8; // Certificate provider for fetching validation context. 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 index b7270f246de..e1ce827a48f 100644 --- 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 @@ -41,18 +41,29 @@ message ResourceName { DynamicParameterConstraints dynamic_parameter_constraints = 2; } +// [#not-implemented-hide:] +// An error associated with a specific resource name, returned to the +// client by the server. +message ResourceError { + // The name of the resource. + ResourceName resource_name = 1; + + // The error reported for the resource. + google.rpc.Status error_detail = 2; +} + // A DiscoveryRequest requests a set of versioned resources of the same type for // a given Envoy node on some API. // [#next-free-field: 8] 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 + // 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 + // 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; @@ -61,10 +72,10 @@ message DiscoveryRequest { // 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 + // 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. + // which will be explicitly enumerated in ``resource_names``. repeated string resource_names = 3; // [#not-implemented-hide:] @@ -72,21 +83,27 @@ message DiscoveryRequest { // parameters along with each resource name. Clients that populate this // field must be able to handle responses from the server where resources // are wrapped in a Resource message. - // Note that it is legal for a request to have some resources listed - // in ``resource_names`` and others in ``resource_locators``. + // + // .. note:: + // It is legal for a request to have some resources listed + // in ``resource_names`` and others in ``resource_locators``. + // repeated ResourceLocator resource_locators = 7; // Type of the resource that is being requested, e.g. - // "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment". This is implicit + // ``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). + // nonce corresponding to ``DiscoveryResponse`` being ACK/NACKed. See above + // discussion on ``version_info`` and the ``DiscoveryResponse`` nonce comment. This + // may be empty only if: + // + // * This is a non-persistent-stream xDS such as HTTP, or + // * 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 ` @@ -96,7 +113,7 @@ message DiscoveryRequest { google.rpc.Status error_detail = 6; } -// [#next-free-field: 7] +// [#next-free-field: 8] message DiscoveryResponse { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.DiscoveryResponse"; @@ -109,35 +126,46 @@ message DiscoveryResponse { // [#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-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. + // + // * 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 + // + // * ``--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). + // 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 + // 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 + // 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 + // 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; // The control plane instance that sent the response. config.core.v3.ControlPlane control_plane = 6; + + // [#not-implemented-hide:] + // Errors associated with specific resources. Clients are expected to + // remember the most recent error for a given resource across responses; + // the error condition is not considered to be cleared until a response is + // received that contains the resource in the 'resources' field. + repeated ResourceError resource_errors = 7; } // DeltaDiscoveryRequest and DeltaDiscoveryResponse are used in a new gRPC @@ -153,25 +181,28 @@ message DiscoveryResponse { // 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 +// ``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 +// ``DeltaDiscoveryRequest`` plays two independent roles. Any ``DeltaDiscoveryRequest`` +// can be either or both of: +// +// * Informing the server of what resources the client has gained/lost interest in +// (using ``resource_names_subscribe`` and ``resource_names_unsubscribe``), or +// * (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. +// 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: +// 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. +// 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: 10] message DeltaDiscoveryRequest { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.DeltaDiscoveryRequest"; @@ -187,23 +218,24 @@ message DeltaDiscoveryRequest { // 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 + // 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 + // *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. + // .. 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. + // 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; @@ -214,31 +246,40 @@ message DeltaDiscoveryRequest { // [#not-implemented-hide:] // Alternative to ``resource_names_subscribe`` field that allows specifying dynamic parameters // along with each resource name. - // Note that it is legal for a request to have some resources listed - // in ``resource_names_subscribe`` and others in ``resource_locators_subscribe``. + // + // .. note:: + // It is legal for a request to have some resources listed + // in ``resource_names_subscribe`` and others in ``resource_locators_subscribe``. + // repeated ResourceLocator resource_locators_subscribe = 8; // [#not-implemented-hide:] // Alternative to ``resource_names_unsubscribe`` field that allows specifying dynamic parameters // along with each resource name. - // Note that it is legal for a request to have some resources listed - // in ``resource_names_unsubscribe`` and others in ``resource_locators_unsubscribe``. + // + // .. note:: + // It is legal for a request to have some resources listed + // in ``resource_names_unsubscribe`` and others in ``resource_locators_unsubscribe``. + // repeated ResourceLocator resource_locators_unsubscribe = 9; // 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.) + // It will not be populated: + // + // * In the very first stream of a session, since the client will not yet have any resources. + // * 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. + // 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 ` @@ -247,7 +288,7 @@ message DeltaDiscoveryRequest { google.rpc.Status error_detail = 7; } -// [#next-free-field: 9] +// [#next-free-field: 10] message DeltaDiscoveryResponse { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.DeltaDiscoveryResponse"; @@ -256,37 +297,46 @@ message DeltaDiscoveryResponse { string system_version_info = 1; // The response resources. These are typed resources, whose types must match - // the type_url field. + // 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. + // 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. + // Resource names of resources that have been deleted and to be removed from the xDS Client. // Removed resources for missing resources can be ignored. repeated string removed_resources = 6; - // Alternative to removed_resources that allows specifying which variant of + // Alternative to ``removed_resources`` that allows specifying which variant of // a resource is being removed. This variant must be used for any resource // for which dynamic parameter constraints were sent to the client. repeated ResourceName removed_resource_names = 8; - // The nonce provides a way for DeltaDiscoveryRequests to uniquely - // reference a DeltaDiscoveryResponse when (N)ACKing. The nonce is required. + // The nonce provides a way for ``DeltaDiscoveryRequests`` to uniquely + // reference a ``DeltaDiscoveryResponse`` when (N)ACKing. The nonce is required. string nonce = 5; // [#not-implemented-hide:] // The control plane instance that sent the response. config.core.v3.ControlPlane control_plane = 7; + + // [#not-implemented-hide:] + // Errors associated with specific resources. + // + // .. note:: + // A resource in this field with a status of NOT_FOUND should be treated the same as + // a resource listed in the ``removed_resources`` or ``removed_resource_names`` fields. + // + repeated ResourceError resource_errors = 9; } // A set of dynamic parameter constraints associated with a variant of an individual xDS resource. // These constraints determine whether the resource matches a subscription based on the set of // dynamic parameters in the subscription, as specified in the -// :ref:`ResourceLocator.dynamic_parameters` +// :ref:`ResourceLocator.dynamic_parameters ` // field. This allows xDS implementations (clients, servers, and caching proxies) to determine // which variant of a resource is appropriate for a given client. message DynamicParameterConstraints { @@ -340,8 +390,11 @@ message Resource { // [#not-implemented-hide:] message CacheControl { // If true, xDS proxies may not cache this resource. - // Note that this does not apply to clients other than xDS proxies, which must cache resources - // for their own use, regardless of the value of this field. + // + // .. note:: + // This does not apply to clients other than xDS proxies, which must cache resources + // for their own use, regardless of the value of this field. + // bool do_not_cache = 1; } @@ -371,7 +424,7 @@ message Resource { // configuration for the resource will be removed. // // The TTL can be refreshed or changed by sending a response that doesn't change the resource - // version. In this case the resource field does not need to be populated, which allows for + // version. In this case the ``resource`` field does not need to be populated, which allows for // light-weight "heartbeat" updates to keep a resource with a TTL alive. // // The TTL feature is meant to support configurations that should be removed in the event of diff --git a/xds/third_party/envoy/src/main/proto/envoy/service/status/v3/csds.proto b/xds/third_party/envoy/src/main/proto/envoy/service/status/v3/csds.proto index 1c51f2bac37..de62fbf9b0f 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/service/status/v3/csds.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/service/status/v3/csds.proto @@ -72,6 +72,11 @@ enum ClientConfigStatus { // config dump is not the NACKed version, but the most recent accepted one. If // no config is accepted yet, the attached config dump will be empty. CLIENT_NACKED = 3; + + // Client received an error from the control plane. The attached config + // dump is the most recent accepted one. If no config is accepted yet, + // the attached config dump will be empty. + CLIENT_RECEIVED_ERROR = 4; } // Request for client status of clients identified by a list of NodeMatchers. diff --git a/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/address.proto b/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/address.proto new file mode 100644 index 00000000000..8a03a5320af --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/address.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package envoy.type.matcher.v3; + +import "xds/core/v3/cidr.proto"; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.type.matcher.v3"; +option java_outer_classname = "AddressProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3;matcherv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Address Matcher] + +// Match an IP against a repeated CIDR range. This matcher is intended to be +// used in other matchers, for example in the filter state matcher to match a +// filter state object as an IP. +message AddressMatcher { + repeated xds.core.v3.CidrRange ranges = 1; +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/filter_state.proto b/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/filter_state.proto index f813178ae05..8c38a515ae9 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/filter_state.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/filter_state.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package envoy.type.matcher.v3; +import "envoy/type/matcher/v3/address.proto"; import "envoy/type/matcher/v3/string.proto"; import "udpa/annotations/status.proto"; @@ -25,5 +26,8 @@ message FilterStateMatcher { // Matches the filter state object as a string value. StringMatcher string_match = 2; + + // Matches the filter state object as a ip Instance. + AddressMatcher address_match = 3; } } diff --git a/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/metadata.proto b/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/metadata.proto index d3316e88a88..30abde97c09 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/metadata.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/metadata.proto @@ -16,11 +16,11 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Metadata matcher] -// MetadataMatcher provides a general interface to check if a given value is matched in -// :ref:`Metadata `. It uses `filter` and `path` to retrieve the value -// from the Metadata and then check if it's matched to the specified value. +// ``MetadataMatcher`` provides a general interface to check if a given value is matched in +// :ref:`Metadata `. It uses ``filter`` and ``path`` to retrieve the value +// from the ``Metadata`` and then check if it's matched to the specified value. // -// For example, for the following Metadata: +// For example, for the following ``Metadata``: // // .. code-block:: yaml // @@ -41,8 +41,8 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // - string_value: m // - string_value: n // -// The following MetadataMatcher is matched as the path [a, b, c] will retrieve a string value "pro" -// from the Metadata which is matched to the specified prefix match. +// The following ``MetadataMatcher`` is matched as the path ``[a, b, c]`` will retrieve a string value ``pro`` +// from the ``Metadata`` which is matched to the specified prefix match. // // .. code-block:: yaml // @@ -55,7 +55,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // string_match: // prefix: pr // -// The following MetadataMatcher is matched as the code will match one of the string values in the +// The following ``MetadataMatcher`` is matched as the code will match one of the string values in the // list at the path [a, t]. // // .. code-block:: yaml @@ -70,7 +70,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // string_match: // exact: m // -// An example use of MetadataMatcher is specifying additional metadata in envoy.filters.http.rbac to +// An example use of ``MetadataMatcher`` is specifying additional metadata in ``envoy.filters.http.rbac`` to // enforce access control based on dynamic metadata in a request. See :ref:`Permission // ` and :ref:`Principal // `. @@ -79,9 +79,11 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; message MetadataMatcher { option (udpa.annotations.versioning).previous_message_type = "envoy.type.matcher.MetadataMatcher"; - // Specifies the segment in a path to retrieve value from Metadata. - // Note: Currently it's not supported to retrieve a value from a list in Metadata. This means that - // if the segment key refers to a list, it has to be the last segment in a path. + // Specifies the segment in a path to retrieve value from ``Metadata``. + // + // .. note:: + // Currently it's not supported to retrieve a value from a list in ``Metadata``. This means that + // if the segment key refers to a list, it has to be the last segment in a path. message PathSegment { option (udpa.annotations.versioning).previous_message_type = "envoy.type.matcher.MetadataMatcher.PathSegment"; @@ -89,18 +91,18 @@ message MetadataMatcher { oneof segment { option (validate.required) = true; - // If specified, use the key to retrieve the value in a Struct. + // If specified, use the key to retrieve the value in a ``Struct``. string key = 1 [(validate.rules).string = {min_len: 1}]; } } - // The filter name to retrieve the Struct from the Metadata. + // The filter name to retrieve the ``Struct`` from the ``Metadata``. string filter = 1 [(validate.rules).string = {min_len: 1}]; - // The path to retrieve the Value from the Struct. + // The path to retrieve the ``Value`` from the ``Struct``. repeated PathSegment path = 2 [(validate.rules).repeated = {min_items: 1}]; - // The MetadataMatcher is matched if the value retrieved by path is matched to this value. + // The ``MetadataMatcher`` is matched if the value retrieved by path is matched to this value. ValueMatcher value = 3 [(validate.rules).message = {required: true}]; // If true, the match result will be inverted. diff --git a/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/string.proto b/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/string.proto index 10033749acd..56d39565ca5 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/string.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/string.proto @@ -38,7 +38,10 @@ message StringMatcher { string exact = 1; // The input string must have the prefix specified here. - // Note: empty prefix is not allowed, please use regex instead. + // + // .. note:: + // + // Empty prefix match is not allowed, please use ``safe_regex`` instead. // // Examples: // @@ -46,7 +49,10 @@ message StringMatcher { string prefix = 2 [(validate.rules).string = {min_len: 1}]; // The input string must have the suffix specified here. - // Note: empty prefix is not allowed, please use regex instead. + // + // .. note:: + // + // Empty suffix match is not allowed, please use ``safe_regex`` instead. // // Examples: // @@ -57,7 +63,10 @@ message StringMatcher { RegexMatcher safe_regex = 5 [(validate.rules).message = {required: true}]; // The input string must have the substring specified here. - // Note: empty contains match is not allowed, please use regex instead. + // + // .. note:: + // + // Empty contains match is not allowed, please use ``safe_regex`` instead. // // Examples: // @@ -69,9 +78,10 @@ message StringMatcher { xds.core.v3.TypedExtensionConfig custom = 8; } - // If true, indicates the exact/prefix/suffix/contains matching should be case insensitive. This - // has no effect for the safe_regex match. - // For example, the matcher ``data`` will match both input string ``Data`` and ``data`` if set to true. + // If ``true``, indicates the exact/prefix/suffix/contains matching should be case insensitive. This + // has no effect for the ``safe_regex`` match. + // For example, the matcher ``data`` will match both input string ``Data`` and ``data`` if this option + // is set to ``true``. bool ignore_case = 6; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/type/metadata/v3/metadata.proto b/xds/third_party/envoy/src/main/proto/envoy/type/metadata/v3/metadata.proto index 20758577503..d131635bf9f 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/type/metadata/v3/metadata.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/type/metadata/v3/metadata.proto @@ -14,10 +14,10 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Metadata] -// MetadataKey provides a general interface using ``key`` and ``path`` to retrieve value from -// :ref:`Metadata `. +// MetadataKey provides a way to retrieve values from +// :ref:`Metadata ` using a ``key`` and a ``path``. // -// For example, for the following Metadata: +// For example, consider the following Metadata: // // .. code-block:: yaml // @@ -28,7 +28,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // xyz: // hello: envoy // -// The following MetadataKey will retrieve a string value "bar" from the Metadata. +// The following MetadataKey would retrieve the string value "bar" from the Metadata: // // .. code-block:: yaml // @@ -40,8 +40,8 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; message MetadataKey { option (udpa.annotations.versioning).previous_message_type = "envoy.type.metadata.v2.MetadataKey"; - // Specifies the segment in a path to retrieve value from Metadata. - // Currently it is only supported to specify the key, i.e. field name, as one segment of a path. + // Specifies a segment in a path for retrieving values from Metadata. + // Currently, only key-based segments (field names) are supported. message PathSegment { option (udpa.annotations.versioning).previous_message_type = "envoy.type.metadata.v2.MetadataKey.PathSegment"; @@ -49,25 +49,27 @@ message MetadataKey { oneof segment { option (validate.required) = true; - // If specified, use the key to retrieve the value in a Struct. + // If specified, use this key to retrieve the value in a Struct. string key = 1 [(validate.rules).string = {min_len: 1}]; } } - // The key name of Metadata to retrieve the Struct from the metadata. - // Typically, it represents a builtin subsystem or custom extension. + // The key name of the Metadata from which to retrieve the Struct. + // This typically represents a builtin subsystem or custom extension. string key = 1 [(validate.rules).string = {min_len: 1}]; - // The path to retrieve the Value from the Struct. It can be a prefix or a full path, - // e.g. ``[prop, xyz]`` for a struct or ``[prop, foo]`` for a string in the example, - // which depends on the particular scenario. + // The path used to retrieve a specific Value from the Struct. + // This can be either a prefix or a full path, depending on the use case. + // For example, ``[prop, xyz]`` would retrieve a struct or ``[prop, foo]`` would retrieve a string + // in the example above. // - // Note: Due to that only the key type segment is supported, the path can not specify a list - // unless the list is the last segment. + // .. note:: + // Since only key-type segments are supported, a path cannot specify a list + // unless the list is the last segment. repeated PathSegment path = 2 [(validate.rules).repeated = {min_items: 1}]; } -// Describes what kind of metadata. +// Describes different types of metadata sources. message MetadataKind { option (udpa.annotations.versioning).previous_message_type = "envoy.type.metadata.v2.MetadataKind"; diff --git a/xds/third_party/xds/src/main/proto/xds/core/v3/cidr.proto b/xds/third_party/xds/src/main/proto/xds/core/v3/cidr.proto index c40dab2f28f..b8471bc8078 100644 --- a/xds/third_party/xds/src/main/proto/xds/core/v3/cidr.proto +++ b/xds/third_party/xds/src/main/proto/xds/core/v3/cidr.proto @@ -22,4 +22,4 @@ message CidrRange { // Length of prefix, e.g. 0, 32. Defaults to 0 when unset. google.protobuf.UInt32Value prefix_len = 2 [(validate.rules).uint32 = {lte: 128}]; -} +} \ No newline at end of file From 7e982e48a1ee03b509124ea7ad3c4a4464f8a189 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 29 Jul 2025 12:36:39 +0000 Subject: [PATCH 332/591] Xds: Aggregate cluster fixes (A75) (#12186) Instead of representing an aggregate cluster as a single cluster whose priorities come from different underlying clusters, represent an aggregate cluster as an instance of a priority LB policy where each child is a cds LB policy for the underlying cluster. --- .../java/io/grpc/xds/CdsLoadBalancer2.java | 128 +++---- .../io/grpc/xds/CdsLoadBalancerProvider.java | 18 +- .../grpc/xds/ClusterResolverLoadBalancer.java | 82 ++--- .../ClusterResolverLoadBalancerProvider.java | 15 +- .../io/grpc/xds/CdsLoadBalancer2Test.java | 149 +++++--- .../xds/ClusterResolverLoadBalancerTest.java | 337 ++++++------------ 6 files changed, 326 insertions(+), 403 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java index c50f844d388..87963476265 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java @@ -18,10 +18,10 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; +import static io.grpc.xds.XdsLbPolicies.CDS_POLICY_NAME; import static io.grpc.xds.XdsLbPolicies.CLUSTER_RESOLVER_POLICY_NAME; +import static io.grpc.xds.XdsLbPolicies.PRIORITY_POLICY_NAME; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.InternalLogId; import io.grpc.LoadBalancer; @@ -33,6 +33,7 @@ import io.grpc.xds.CdsLoadBalancerProvider.CdsConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; +import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; import io.grpc.xds.XdsClusterResource.CdsUpdate; import io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType; import io.grpc.xds.XdsConfig.Subscription; @@ -41,10 +42,11 @@ import io.grpc.xds.XdsConfig.XdsClusterConfig.EndpointConfig; import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger.XdsLogLevel; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Load balancer for cds_experimental LB policy. One instance per top-level cluster. @@ -55,19 +57,15 @@ final class CdsLoadBalancer2 extends LoadBalancer { private final XdsLogger logger; private final Helper helper; private final LoadBalancerRegistry lbRegistry; + private GracefulSwitchLoadBalancer delegate; // Following fields are effectively final. private String clusterName; private Subscription clusterSubscription; - private LoadBalancer childLb; - CdsLoadBalancer2(Helper helper) { - this(helper, LoadBalancerRegistry.getDefaultRegistry()); - } - - @VisibleForTesting CdsLoadBalancer2(Helper helper, LoadBalancerRegistry lbRegistry) { this.helper = checkNotNull(helper, "helper"); this.lbRegistry = checkNotNull(lbRegistry, "lbRegistry"); + this.delegate = new GracefulSwitchLoadBalancer(helper); logger = XdsLogger.withLogId(InternalLogId.allocate("cds-lb", helper.getAuthority())); logger.log(XdsLogLevel.INFO, "Created"); } @@ -91,7 +89,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { if (clusterSubscription == null) { // Should be impossible, because XdsDependencyManager wouldn't have generated this return fail(Status.INTERNAL.withDescription( - errorPrefix() + "Unable to find non-dynamic root cluster")); + errorPrefix() + "Unable to find non-dynamic cluster")); } // The dynamic cluster must not have loaded yet return Status.OK; @@ -100,42 +98,25 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { return fail(clusterConfigOr.getStatus()); } XdsClusterConfig clusterConfig = clusterConfigOr.getValue(); - List leafNames; - if (clusterConfig.getChildren() instanceof AggregateConfig) { - leafNames = ((AggregateConfig) clusterConfig.getChildren()).getLeafNames(); - } else if (clusterConfig.getChildren() instanceof EndpointConfig) { - leafNames = ImmutableList.of(clusterName); - } else { - return fail(Status.INTERNAL.withDescription( - errorPrefix() + "Unexpected cluster children type: " - + clusterConfig.getChildren().getClass())); - } - if (leafNames.isEmpty()) { - // Should be impossible, because XdsClusterResource validated this - return fail(Status.UNAVAILABLE.withDescription( - errorPrefix() + "Zero leaf clusters for root cluster " + clusterName)); - } - Status noneFoundError = Status.INTERNAL - .withDescription(errorPrefix() + "No leaves and no error; this is a bug"); - List instances = new ArrayList<>(); - for (String leafName : leafNames) { - StatusOr leafConfigOr = xdsConfig.getClusters().get(leafName); - if (!leafConfigOr.hasValue()) { - noneFoundError = leafConfigOr.getStatus(); - continue; - } - if (!(leafConfigOr.getValue().getChildren() instanceof EndpointConfig)) { - noneFoundError = Status.INTERNAL.withDescription( - errorPrefix() + "Unexpected child " + leafName + " cluster children type: " - + leafConfigOr.getValue().getChildren().getClass()); - continue; + NameResolver.ConfigOrError configOrError; + Object gracefulConfig; + if (clusterConfig.getChildren() instanceof EndpointConfig) { + // The LB policy config is provided in service_config.proto/JSON format. + configOrError = + GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig( + Arrays.asList(clusterConfig.getClusterResource().lbPolicyConfig()), + lbRegistry); + if (configOrError.getError() != null) { + // Should be impossible, because XdsClusterResource validated this + return fail(Status.INTERNAL.withDescription( + errorPrefix() + "Unable to parse the LB config: " + configOrError.getError())); } - CdsUpdate result = leafConfigOr.getValue().getClusterResource(); + CdsUpdate result = clusterConfig.getClusterResource(); DiscoveryMechanism instance; if (result.clusterType() == ClusterType.EDS) { instance = DiscoveryMechanism.forEds( - leafName, + clusterName, result.edsServiceName(), result.lrsServerInfo(), result.maxConcurrentRequests(), @@ -144,45 +125,49 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { result.outlierDetection()); } else { instance = DiscoveryMechanism.forLogicalDns( - leafName, + clusterName, result.dnsHostName(), result.lrsServerInfo(), result.maxConcurrentRequests(), result.upstreamTlsContext(), result.filterMetadata()); } - instances.add(instance); - } - if (instances.isEmpty()) { - return fail(noneFoundError); - } - - // The LB policy config is provided in service_config.proto/JSON format. - NameResolver.ConfigOrError configOrError = - GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig( - Arrays.asList(clusterConfig.getClusterResource().lbPolicyConfig()), lbRegistry); - if (configOrError.getError() != null) { - // Should be impossible, because XdsClusterResource validated this + gracefulConfig = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + lbRegistry.getProvider(CLUSTER_RESOLVER_POLICY_NAME), + new ClusterResolverConfig( + instance, + configOrError.getConfig(), + clusterConfig.getClusterResource().isHttp11ProxyAvailable())); + } else if (clusterConfig.getChildren() instanceof AggregateConfig) { + Map priorityChildConfigs = new HashMap<>(); + List leafClusters = ((AggregateConfig) clusterConfig.getChildren()).getLeafNames(); + for (String childCluster: leafClusters) { + priorityChildConfigs.put(childCluster, + new PriorityChildConfig( + GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + lbRegistry.getProvider(CDS_POLICY_NAME), + new CdsConfig(childCluster)), + false)); + } + gracefulConfig = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + lbRegistry.getProvider(PRIORITY_POLICY_NAME), + new PriorityLoadBalancerProvider.PriorityLbConfig( + Collections.unmodifiableMap(priorityChildConfigs), leafClusters)); + } else { return fail(Status.INTERNAL.withDescription( - errorPrefix() + "Unable to parse the LB config: " + configOrError.getError())); + errorPrefix() + "Unexpected cluster children type: " + + clusterConfig.getChildren().getClass())); } - ClusterResolverConfig config = new ClusterResolverConfig( - Collections.unmodifiableList(instances), - configOrError.getConfig(), - clusterConfig.getClusterResource().isHttp11ProxyAvailable()); - if (childLb == null) { - childLb = lbRegistry.getProvider(CLUSTER_RESOLVER_POLICY_NAME).newLoadBalancer(helper); - } - return childLb.acceptResolvedAddresses( - resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(config).build()); + return delegate.acceptResolvedAddresses( + resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(gracefulConfig).build()); } @Override public void handleNameResolutionError(Status error) { logger.log(XdsLogLevel.WARNING, "Received name resolution error: {0}", error); - if (childLb != null) { - childLb.handleNameResolutionError(error); + if (delegate != null) { + delegate.handleNameResolutionError(error); } else { helper.updateBalancingState( TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error))); @@ -192,10 +177,8 @@ public void handleNameResolutionError(Status error) { @Override public void shutdown() { logger.log(XdsLogLevel.INFO, "Shutdown"); - if (childLb != null) { - childLb.shutdown(); - childLb = null; - } + delegate.shutdown(); + delegate = new GracefulSwitchLoadBalancer(helper); if (clusterSubscription != null) { clusterSubscription.close(); clusterSubscription = null; @@ -204,10 +187,7 @@ public void shutdown() { @CheckReturnValue // don't forget to return up the stack after the fail call private Status fail(Status error) { - if (childLb != null) { - childLb.shutdown(); - childLb = null; - } + delegate.shutdown(); helper.updateBalancingState( TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error))); return Status.OK; // XdsNameResolver isn't a polling NR, so this value doesn't matter diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java index 9b242822f6c..875af9089ed 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java @@ -23,6 +23,7 @@ import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancerProvider; +import io.grpc.LoadBalancerRegistry; import io.grpc.NameResolver.ConfigOrError; import io.grpc.Status; import io.grpc.internal.JsonUtil; @@ -51,9 +52,24 @@ public String getPolicyName() { return XdsLbPolicies.CDS_POLICY_NAME; } + private final LoadBalancerRegistry loadBalancerRegistry; + + public CdsLoadBalancerProvider() { + this.loadBalancerRegistry = null; + } + + public CdsLoadBalancerProvider(LoadBalancerRegistry loadBalancerRegistry) { + this.loadBalancerRegistry = loadBalancerRegistry; + } + @Override public LoadBalancer newLoadBalancer(Helper helper) { - return new CdsLoadBalancer2(helper); + LoadBalancerRegistry loadBalancerRegistry = this.loadBalancerRegistry; + if (loadBalancerRegistry == null) { + loadBalancerRegistry = LoadBalancerRegistry.getDefaultRegistry(); + } + + return new CdsLoadBalancer2(helper, loadBalancerRegistry); } @Override diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index 080760303bf..e333c46750c 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -168,8 +168,8 @@ public LoadBalancer newLoadBalancer(Helper helper) { */ private final class ClusterResolverLbState extends LoadBalancer { private final Helper helper; - private final List clusters = new ArrayList<>(); - private final Map clusterStates = new HashMap<>(); + private ClusterState clusterState; + private String cluster; private Object endpointLbConfig; private ResolvedAddresses resolvedAddresses; private LoadBalancer childLb; @@ -185,21 +185,18 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { ClusterResolverConfig config = (ClusterResolverConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); endpointLbConfig = config.lbConfig; - for (DiscoveryMechanism instance : config.discoveryMechanisms) { - clusters.add(instance.cluster); - ClusterState state; - if (instance.type == DiscoveryMechanism.Type.EDS) { - state = new EdsClusterState(instance.cluster, instance.edsServiceName, - instance.lrsServerInfo, instance.maxConcurrentRequests, instance.tlsContext, - instance.filterMetadata, instance.outlierDetection); - } else { // logical DNS - state = new LogicalDnsClusterState(instance.cluster, instance.dnsHostName, - instance.lrsServerInfo, instance.maxConcurrentRequests, instance.tlsContext, - instance.filterMetadata); - } - clusterStates.put(instance.cluster, state); - state.start(); - } + DiscoveryMechanism instance = config.discoveryMechanism; + cluster = instance.cluster; + if (instance.type == DiscoveryMechanism.Type.EDS) { + clusterState = new EdsClusterState(instance.cluster, instance.edsServiceName, + instance.lrsServerInfo, instance.maxConcurrentRequests, instance.tlsContext, + instance.filterMetadata, instance.outlierDetection); + } else { // logical DNS + clusterState = new LogicalDnsClusterState(instance.cluster, instance.dnsHostName, + instance.lrsServerInfo, instance.maxConcurrentRequests, instance.tlsContext, + instance.filterMetadata); + } + clusterState.start(); return Status.OK; } @@ -215,9 +212,7 @@ public void handleNameResolutionError(Status error) { @Override public void shutdown() { - for (ClusterState state : clusterStates.values()) { - state.shutdown(); - } + clusterState.shutdown(); if (childLb != null) { childLb.shutdown(); } @@ -229,24 +224,21 @@ private void handleEndpointResourceUpdate() { List priorities = new ArrayList<>(); // totally ordered priority list Status endpointNotFound = Status.OK; - for (String cluster : clusters) { - ClusterState state = clusterStates.get(cluster); - // Propagate endpoints to the child LB policy only after all clusters have been resolved. - if (!state.resolved && state.status.isOk()) { - return; - } - if (state.result != null) { - addresses.addAll(state.result.addresses); - priorityChildConfigs.putAll(state.result.priorityChildConfigs); - priorities.addAll(state.result.priorities); - } else { - endpointNotFound = state.status; - } + // Propagate endpoints to the child LB policy only after all clusters have been resolved. + if (!clusterState.resolved && clusterState.status.isOk()) { + return; + } + if (clusterState.result != null) { + addresses.addAll(clusterState.result.addresses); + priorityChildConfigs.putAll(clusterState.result.priorityChildConfigs); + priorities.addAll(clusterState.result.priorities); + } else { + endpointNotFound = clusterState.status; } if (addresses.isEmpty()) { if (endpointNotFound.isOk()) { endpointNotFound = Status.UNAVAILABLE.withDescription( - "No usable endpoint from cluster(s): " + clusters); + "No usable endpoint from cluster: " + cluster); } else { endpointNotFound = Status.UNAVAILABLE.withCause(endpointNotFound.getCause()) @@ -274,22 +266,12 @@ private void handleEndpointResourceUpdate() { } private void handleEndpointResolutionError() { - boolean allInError = true; - Status error = null; - for (String cluster : clusters) { - ClusterState state = clusterStates.get(cluster); - if (state.status.isOk()) { - allInError = false; - } else { - error = state.status; - } - } - if (allInError) { + if (!clusterState.status.isOk()) { if (childLb != null) { - childLb.handleNameResolutionError(error); + childLb.handleNameResolutionError(clusterState.status); } else { helper.updateBalancingState( - TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error))); + TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(clusterState.status))); } } } @@ -306,10 +288,8 @@ private RefreshableHelper(Helper delegate) { @Override public void refreshNameResolution() { - for (ClusterState state : clusterStates.values()) { - if (state instanceof LogicalDnsClusterState) { - ((LogicalDnsClusterState) state).refresh(); - } + if (clusterState instanceof LogicalDnsClusterState) { + ((LogicalDnsClusterState) clusterState).refresh(); } } diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java index 27a884b1f4f..48101cd9c54 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java @@ -30,7 +30,6 @@ import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.client.Bootstrapper.ServerInfo; -import java.util.List; import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; @@ -70,15 +69,15 @@ public LoadBalancer newLoadBalancer(Helper helper) { } static final class ClusterResolverConfig { - // Ordered list of clusters to be resolved. - final List discoveryMechanisms; + // Cluster to be resolved. + final DiscoveryMechanism discoveryMechanism; // GracefulSwitch configuration final Object lbConfig; private final boolean isHttp11ProxyAvailable; - ClusterResolverConfig(List discoveryMechanisms, Object lbConfig, + ClusterResolverConfig(DiscoveryMechanism discoveryMechanism, Object lbConfig, boolean isHttp11ProxyAvailable) { - this.discoveryMechanisms = checkNotNull(discoveryMechanisms, "discoveryMechanisms"); + this.discoveryMechanism = checkNotNull(discoveryMechanism, "discoveryMechanism"); this.lbConfig = checkNotNull(lbConfig, "lbConfig"); this.isHttp11ProxyAvailable = isHttp11ProxyAvailable; } @@ -89,7 +88,7 @@ boolean isHttp11ProxyAvailable() { @Override public int hashCode() { - return Objects.hash(discoveryMechanisms, lbConfig, isHttp11ProxyAvailable); + return Objects.hash(discoveryMechanism, lbConfig, isHttp11ProxyAvailable); } @Override @@ -101,7 +100,7 @@ public boolean equals(Object o) { return false; } ClusterResolverConfig that = (ClusterResolverConfig) o; - return discoveryMechanisms.equals(that.discoveryMechanisms) + return discoveryMechanism.equals(that.discoveryMechanism) && lbConfig.equals(that.lbConfig) && isHttp11ProxyAvailable == that.isHttp11ProxyAvailable; } @@ -109,7 +108,7 @@ public boolean equals(Object o) { @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("discoveryMechanisms", discoveryMechanisms) + .add("discoveryMechanism", discoveryMechanism) .add("lbConfig", lbConfig) .add("isHttp11ProxyAvailable", isHttp11ProxyAvailable) .toString(); diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java index 258e2909203..2059022c203 100644 --- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java +++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static io.grpc.xds.XdsLbPolicies.CLUSTER_RESOLVER_POLICY_NAME; +import static io.grpc.xds.XdsLbPolicies.PRIORITY_POLICY_NAME; import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS; import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_EDS; import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_LDS; @@ -27,6 +28,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.github.xds.type.v3.TypedStruct; import com.google.common.collect.ImmutableMap; @@ -157,7 +159,9 @@ public void setUp() throws Exception { new LeastRequestLoadBalancerProvider())); lbRegistry.register(new FakeLoadBalancerProvider("wrr_locality_experimental", new WrrLocalityLoadBalancerProvider())); - loadBalancer = new CdsLoadBalancer2(helper, lbRegistry); + CdsLoadBalancerProvider cdsLoadBalancerProvider = new CdsLoadBalancerProvider(lbRegistry); + lbRegistry.register(cdsLoadBalancerProvider); + loadBalancer = (CdsLoadBalancer2) cdsLoadBalancerProvider.newLoadBalancer(helper); cleanupRule.register(InProcessServerBuilder .forName("control-plane.example.com") @@ -169,6 +173,8 @@ public void setUp() throws Exception { SynchronizationContext syncContext = new SynchronizationContext((t, e) -> { throw new AssertionError(e); }); + when(helper.getSynchronizationContext()).thenReturn(syncContext); + when(helper.getScheduledExecutorService()).thenReturn(fakeClock.getScheduledExecutorService()); NameResolver.Args nameResolverArgs = NameResolver.Args.newBuilder() .setDefaultPort(8080) @@ -246,13 +252,12 @@ public void discoverTopLevelEdsCluster() { FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.name).isEqualTo(CLUSTER_RESOLVER_POLICY_NAME); ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).isEqualTo( - Arrays.asList( - DiscoveryMechanism.forEds( - CLUSTER, EDS_SERVICE_NAME, lrsServerInfo, 100L, upstreamTlsContext, - Collections.emptyMap(), io.grpc.xds.EnvoyServerProtoData.OutlierDetection.create( - null, null, null, null, SuccessRateEjection.create(null, null, null, null), - FailurePercentageEjection.create(null, null, null, null))))); + assertThat(childLbConfig.discoveryMechanism).isEqualTo( + DiscoveryMechanism.forEds( + CLUSTER, EDS_SERVICE_NAME, lrsServerInfo, 100L, upstreamTlsContext, + Collections.emptyMap(), io.grpc.xds.EnvoyServerProtoData.OutlierDetection.create( + null, null, null, null, SuccessRateEjection.create(null, null, null, null), + FailurePercentageEjection.create(null, null, null, null)))); assertThat( GracefulSwitchLoadBalancerAccessor.getChildProvider(childLbConfig.lbConfig).getPolicyName()) .isEqualTo("wrr_locality_experimental"); @@ -296,11 +301,10 @@ public void discoverTopLevelLogicalDnsCluster() { FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.name).isEqualTo(CLUSTER_RESOLVER_POLICY_NAME); ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).isEqualTo( - Arrays.asList( - DiscoveryMechanism.forLogicalDns( - CLUSTER, "dns.example.com:1111", lrsServerInfo, 100L, upstreamTlsContext, - Collections.emptyMap()))); + assertThat(childLbConfig.discoveryMechanism).isEqualTo( + DiscoveryMechanism.forLogicalDns( + CLUSTER, "dns.example.com:1111", lrsServerInfo, 100L, upstreamTlsContext, + Collections.emptyMap())); assertThat( GracefulSwitchLoadBalancerAccessor.getChildProvider(childLbConfig.lbConfig).getPolicyName()) .isEqualTo("wrr_locality_experimental"); @@ -332,10 +336,9 @@ public void nonAggregateCluster_resourceUpdate() { assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).isEqualTo( - Arrays.asList( + assertThat(childLbConfig.discoveryMechanism).isEqualTo( DiscoveryMechanism.forEds( - CLUSTER, EDS_SERVICE_NAME, null, 100L, null, Collections.emptyMap(), null))); + CLUSTER, EDS_SERVICE_NAME, null, 100L, null, Collections.emptyMap(), null)); cluster = EDS_CLUSTER.toBuilder() .setCircuitBreakers(CircuitBreakers.newBuilder() @@ -348,10 +351,9 @@ public void nonAggregateCluster_resourceUpdate() { assertThat(childBalancers).hasSize(1); childBalancer = Iterables.getOnlyElement(childBalancers); childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).isEqualTo( - Arrays.asList( + assertThat(childLbConfig.discoveryMechanism).isEqualTo( DiscoveryMechanism.forEds( - CLUSTER, EDS_SERVICE_NAME, null, 200L, null, Collections.emptyMap(), null))); + CLUSTER, EDS_SERVICE_NAME, null, 200L, null, Collections.emptyMap(), null)); } @Test @@ -363,10 +365,9 @@ public void nonAggregateCluster_resourceRevoked() { assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).isEqualTo( - Arrays.asList( + assertThat(childLbConfig.discoveryMechanism).isEqualTo( DiscoveryMechanism.forEds( - CLUSTER, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null))); + CLUSTER, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null)); controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of()); @@ -395,10 +396,9 @@ public void dynamicCluster() { assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanisms).isEqualTo( - Arrays.asList( + assertThat(childLbConfig.discoveryMechanism).isEqualTo( DiscoveryMechanism.forEds( - clusterName, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null))); + clusterName, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null)); assertThat(this.lastXdsConfig.getClusters()).containsKey(clusterName); shutdownLoadBalancer(); @@ -406,7 +406,12 @@ public void dynamicCluster() { } @Test - public void discoverAggregateCluster() { + public void discoverAggregateCluster_createsPriorityLbPolicy() { + lbRegistry.register(new FakeLoadBalancerProvider(PRIORITY_POLICY_NAME)); + CdsLoadBalancerProvider cdsLoadBalancerProvider = new CdsLoadBalancerProvider(lbRegistry); + lbRegistry.register(cdsLoadBalancerProvider); + loadBalancer = (CdsLoadBalancer2) cdsLoadBalancerProvider.newLoadBalancer(helper); + String cluster1 = "cluster-01.googleapis.com"; String cluster2 = "cluster-02.googleapis.com"; String cluster3 = "cluster-03.googleapis.com"; @@ -459,20 +464,77 @@ public void discoverAggregateCluster() { verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(childBalancer.name).isEqualTo(CLUSTER_RESOLVER_POLICY_NAME); - ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - // Clusters are resolved recursively, later duplicates removed: [cluster3, cluster4, cluster2] - assertThat(childLbConfig.discoveryMechanisms).isEqualTo( - Arrays.asList( - DiscoveryMechanism.forEds( - cluster3, EDS_SERVICE_NAME, null, 100L, null, Collections.emptyMap(), null), - DiscoveryMechanism.forEds( - cluster4, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null), - DiscoveryMechanism.forLogicalDns( - cluster2, "dns.example.com:1111", null, null, null, Collections.emptyMap()))); + assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); + PriorityLoadBalancerProvider.PriorityLbConfig childLbConfig = + (PriorityLoadBalancerProvider.PriorityLbConfig) childBalancer.config; + assertThat(childLbConfig.priorities).hasSize(3); + assertThat(childLbConfig.priorities.get(0)).isEqualTo(cluster3); + assertThat(childLbConfig.priorities.get(1)).isEqualTo(cluster4); + assertThat(childLbConfig.priorities.get(2)).isEqualTo(cluster2); + assertThat(childLbConfig.childConfigs).hasSize(3); + PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig childConfig3 = + childLbConfig.childConfigs.get(cluster3); assertThat( - GracefulSwitchLoadBalancerAccessor.getChildProvider(childLbConfig.lbConfig).getPolicyName()) - .isEqualTo("ring_hash_experimental"); // dominated by top-level cluster's config + GracefulSwitchLoadBalancerAccessor.getChildProvider(childConfig3.childConfig) + .getPolicyName()) + .isEqualTo("cds_experimental"); + PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig childConfig4 = + childLbConfig.childConfigs.get(cluster4); + assertThat( + GracefulSwitchLoadBalancerAccessor.getChildProvider(childConfig4.childConfig) + .getPolicyName()) + .isEqualTo("cds_experimental"); + PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig childConfig2 = + childLbConfig.childConfigs.get(cluster2); + assertThat( + GracefulSwitchLoadBalancerAccessor.getChildProvider(childConfig2.childConfig) + .getPolicyName()) + .isEqualTo("cds_experimental"); + } + + @Test + // Both priorities will get tried using real priority LB policy. + public void discoverAggregateCluster_testChildCdsLbPolicyParsing() { + lbRegistry.register(new PriorityLoadBalancerProvider()); + CdsLoadBalancerProvider cdsLoadBalancerProvider = new CdsLoadBalancerProvider(lbRegistry); + lbRegistry.register(cdsLoadBalancerProvider); + loadBalancer = (CdsLoadBalancer2) cdsLoadBalancerProvider.newLoadBalancer(helper); + + String cluster1 = "cluster-01.googleapis.com"; + String cluster2 = "cluster-02.googleapis.com"; + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + // CLUSTER (aggr.) -> [cluster1 (EDS), cluster2 (EDS)] + CLUSTER, Cluster.newBuilder() + .setName(CLUSTER) + .setClusterType(Cluster.CustomClusterType.newBuilder() + .setName("envoy.clusters.aggregate") + .setTypedConfig(Any.pack(ClusterConfig.newBuilder() + .addClusters(cluster1) + .addClusters(cluster2) + .build()))) + .build(), + cluster1, EDS_CLUSTER.toBuilder().setName(cluster1).build(), + cluster2, EDS_CLUSTER.toBuilder().setName(cluster2).build())); + startXdsDepManager(); + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); + assertThat(childBalancers).hasSize(2); + ClusterResolverConfig cluster1ResolverConfig = + (ClusterResolverConfig) childBalancers.get(0).config; + assertThat(cluster1ResolverConfig.discoveryMechanism.cluster) + .isEqualTo("cluster-01.googleapis.com"); + assertThat(cluster1ResolverConfig.discoveryMechanism.type) + .isEqualTo(DiscoveryMechanism.Type.EDS); + assertThat(cluster1ResolverConfig.discoveryMechanism.edsServiceName) + .isEqualTo("backend-service-1.googleapis.com"); + ClusterResolverConfig cluster2ResolverConfig = + (ClusterResolverConfig) childBalancers.get(1).config; + assertThat(cluster2ResolverConfig.discoveryMechanism.cluster) + .isEqualTo("cluster-02.googleapis.com"); + assertThat(cluster2ResolverConfig.discoveryMechanism.type) + .isEqualTo(DiscoveryMechanism.Type.EDS); + assertThat(cluster2ResolverConfig.discoveryMechanism.edsServiceName) + .isEqualTo("backend-service-1.googleapis.com"); } @Test @@ -500,6 +562,11 @@ public void aggregateCluster_noChildren() { @Test public void aggregateCluster_noNonAggregateClusterExits_returnErrorPicker() { + lbRegistry.register(new PriorityLoadBalancerProvider()); + CdsLoadBalancerProvider cdsLoadBalancerProvider = new CdsLoadBalancerProvider(lbRegistry); + lbRegistry.register(cdsLoadBalancerProvider); + loadBalancer = (CdsLoadBalancer2) cdsLoadBalancerProvider.newLoadBalancer(helper); + String cluster1 = "cluster-01.googleapis.com"; controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( // CLUSTER (aggr.) -> [cluster1 (missing)] @@ -550,8 +617,8 @@ public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_fallThroug loadBalancer.handleNameResolutionError(Status.UNAVAILABLE.withDescription("unreachable")); assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(childBalancer.upstreamError.getDescription()).isEqualTo("unreachable"); - verify(helper, never()).updateBalancingState( - any(ConnectivityState.class), any(SubchannelPicker.class)); + verify(helper).updateBalancingState( + eq(ConnectivityState.CONNECTING), any(SubchannelPicker.class)); } @Test diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index 395662cd93c..982677d24da 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -245,7 +245,7 @@ public void tearDown() { @Test public void edsClustersWithRingHashEndpointLbPolicy() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanism1), ringHash, false); + edsDiscoveryMechanism1, ringHash, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertThat(childBalancers).isEmpty(); @@ -311,7 +311,7 @@ public void edsClustersWithRingHashEndpointLbPolicy() { @Test public void edsClustersWithLeastRequestEndpointLbPolicy() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanism1), leastRequest, false); + edsDiscoveryMechanism1, leastRequest, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertThat(childBalancers).isEmpty(); @@ -358,7 +358,7 @@ public void edsClustersWithLeastRequestEndpointLbPolicy() { @Test public void edsClustersEndpointHostname_addedToAddressAttribute() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection), leastRequest, false); + edsDiscoveryMechanismWithOutlierDetection, leastRequest, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertThat(childBalancers).isEmpty(); @@ -385,7 +385,7 @@ public void edsClustersEndpointHostname_addedToAddressAttribute() { @Test public void endpointAddressRewritten_whenProxyMetadataIsInEndpointMetadata() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection), leastRequest, true); + edsDiscoveryMechanismWithOutlierDetection, leastRequest, true); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertThat(childBalancers).isEmpty(); @@ -404,7 +404,7 @@ public void endpointAddressRewritten_whenProxyMetadataIsInEndpointMetadata() { LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( Arrays.asList( LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, - "hostname1", endpointMetadata)), + "hostname1", endpointMetadata)), 100 /* localityWeight */, 1 /* priority */, localityMetadata); xdsClient.deliverClusterLoadAssignment( @@ -432,7 +432,7 @@ public void endpointAddressRewritten_whenProxyMetadataIsInEndpointMetadata() { @Test public void endpointAddressRewritten_whenProxyMetadataIsInLocalityMetadata() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection), leastRequest, true); + edsDiscoveryMechanismWithOutlierDetection, leastRequest, true); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertThat(childBalancers).isEmpty(); @@ -451,7 +451,7 @@ public void endpointAddressRewritten_whenProxyMetadataIsInLocalityMetadata() { LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( Arrays.asList( LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, - "hostname2", endpointMetadata)), + "hostname2", endpointMetadata)), 100 /* localityWeight */, 1 /* priority */, localityMetadata); xdsClient.deliverClusterLoadAssignment( @@ -479,48 +479,36 @@ public void endpointAddressRewritten_whenProxyMetadataIsInLocalityMetadata() { @Test public void onlyEdsClusters_receivedEndpoints() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), roundRobin, false); + edsDiscoveryMechanism2, roundRobin, false); deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1, EDS_SERVICE_NAME2); + assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME2); assertThat(childBalancers).isEmpty(); // CLUSTER1 has priority 1 (priority3), which has locality 2, which has endpoint3. // CLUSTER2 has priority 1 (priority1) and 2 (priority2); priority1 has locality1, // which has endpoint1 and endpoint2; priority2 has locality3, which has endpoint4. EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); - EquivalentAddressGroup endpoint3 = makeAddress("endpoint-addr-3"); EquivalentAddressGroup endpoint4 = makeAddress("endpoint-addr-4"); LocalityLbEndpoints localityLbEndpoints1 = - LocalityLbEndpoints.create( - Arrays.asList( - LbEndpoint.create(endpoint1, 100, - true, "hostname1", ImmutableMap.of()), - LbEndpoint.create(endpoint2, 100, - true, "hostname1", ImmutableMap.of())), - 70 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - LocalityLbEndpoints localityLbEndpoints2 = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint3, 100, true, - "hostname2", ImmutableMap.of())), - 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); + LocalityLbEndpoints.create( + Arrays.asList( + LbEndpoint.create(endpoint1, 100, + true, "hostname1", ImmutableMap.of()), + LbEndpoint.create(endpoint2, 100, + true, "hostname1", ImmutableMap.of())), + 70 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); LocalityLbEndpoints localityLbEndpoints3 = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint4, 100, true, - "hostname3", ImmutableMap.of())), + "hostname3", ImmutableMap.of())), 20 /* localityWeight */, 2 /* priority */, ImmutableMap.of()); String priority1 = CLUSTER2 + "[child1]"; String priority2 = CLUSTER2 + "[child2]"; - String priority3 = CLUSTER1 + "[child1]"; // CLUSTER2: locality1 with priority 1 and locality3 with priority 2. xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME2, - ImmutableMap.of(locality1, localityLbEndpoints1, locality3, localityLbEndpoints3)); - assertThat(childBalancers).isEmpty(); // not created until all clusters resolved - - // CLUSTER1: locality2 with priority 1. - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, Collections.singletonMap(locality2, localityLbEndpoints2)); + EDS_SERVICE_NAME2, + ImmutableMap.of(locality1, localityLbEndpoints1, locality3, localityLbEndpoints3)); // Endpoints of all clusters have been resolved. assertThat(childBalancers).hasSize(1); @@ -528,12 +516,12 @@ public void onlyEdsClusters_receivedEndpoints() { assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); PriorityLbConfig priorityLbConfig = (PriorityLbConfig) childBalancer.config; assertThat(priorityLbConfig.priorities) - .containsExactly(priority3, priority1, priority2).inOrder(); + .containsExactly(priority1, priority2).inOrder(); PriorityChildConfig priorityChildConfig1 = priorityLbConfig.childConfigs.get(priority1); assertThat(priorityChildConfig1.ignoreReresolution).isTrue(); assertThat(GracefulSwitchLoadBalancerAccessor.getChildProvider(priorityChildConfig1.childConfig) - .getPolicyName()) + .getPolicyName()) .isEqualTo(CLUSTER_IMPL_POLICY_NAME); ClusterImplConfig clusterImplConfig1 = (ClusterImplConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig1.childConfig); @@ -548,7 +536,7 @@ public void onlyEdsClusters_receivedEndpoints() { PriorityChildConfig priorityChildConfig2 = priorityLbConfig.childConfigs.get(priority2); assertThat(priorityChildConfig2.ignoreReresolution).isTrue(); assertThat(GracefulSwitchLoadBalancerAccessor.getChildProvider(priorityChildConfig2.childConfig) - .getPolicyName()) + .getPolicyName()) .isEqualTo(CLUSTER_IMPL_POLICY_NAME); ClusterImplConfig clusterImplConfig2 = (ClusterImplConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig2.childConfig); @@ -560,15 +548,6 @@ public void onlyEdsClusters_receivedEndpoints() { GracefulSwitchLoadBalancerAccessor.getChildProvider(wrrLocalityConfig2.childConfig); assertThat(childProvider2.getPolicyName()).isEqualTo("round_robin"); - PriorityChildConfig priorityChildConfig3 = priorityLbConfig.childConfigs.get(priority3); - assertThat(priorityChildConfig3.ignoreReresolution).isTrue(); - assertThat(GracefulSwitchLoadBalancerAccessor.getChildProvider(priorityChildConfig3.childConfig) - .getPolicyName()) - .isEqualTo(CLUSTER_IMPL_POLICY_NAME); - ClusterImplConfig clusterImplConfig3 = (ClusterImplConfig) - GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig3.childConfig); - assertClusterImplConfig(clusterImplConfig3, CLUSTER1, EDS_SERVICE_NAME1, LRS_SERVER_INFO, 100L, - tlsContext, Collections.emptyList(), WRR_LOCALITY_POLICY_NAME); WrrLocalityConfig wrrLocalityConfig3 = (WrrLocalityConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(clusterImplConfig1.childConfig); LoadBalancerProvider childProvider3 = @@ -595,7 +574,7 @@ public void onlyEdsClusters_receivedEndpoints() { private void verifyEdsPriorityNames(List want, Map... updates) { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism2), roundRobin, false); + edsDiscoveryMechanism2, roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME2); assertThat(childBalancers).isEmpty(); @@ -617,7 +596,7 @@ private void verifyEdsPriorityNames(List want, public void edsUpdatePriorityName_twoPriorities() { verifyEdsPriorityNames(Arrays.asList(CLUSTER2 + "[child1]", CLUSTER2 + "[child2]"), ImmutableMap.of(locality1, createEndpoints(1), - locality2, createEndpoints(2) + locality2, createEndpoints(2) )); } @@ -653,8 +632,8 @@ public void edsUpdatePriorityName_mergeTwoPriorities() { locality3, createEndpoints(3), locality2, createEndpoints(2)), ImmutableMap.of(locality1, createEndpoints(2), - locality3, createEndpoints(1), - locality2, createEndpoints(1) + locality3, createEndpoints(1), + locality2, createEndpoints(1) )); } @@ -662,76 +641,64 @@ private LocalityLbEndpoints createEndpoints(int priority) { return LocalityLbEndpoints.create( Arrays.asList( LbEndpoint.create(makeAddress("endpoint-addr-1"), 100, - true, "hostname1", ImmutableMap.of()), + true, "hostname1", ImmutableMap.of()), LbEndpoint.create(makeAddress("endpoint-addr-2"), 100, - true, "hostname2", ImmutableMap.of())), + true, "hostname2", ImmutableMap.of())), 70 /* localityWeight */, priority /* priority */, ImmutableMap.of()); } @Test public void onlyEdsClusters_resourceNeverExist_returnErrorPicker() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), roundRobin, false); + edsDiscoveryMechanism1, roundRobin, false); deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1, EDS_SERVICE_NAME2); + assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertThat(childBalancers).isEmpty(); reset(helper); xdsClient.deliverResourceNotFound(EDS_SERVICE_NAME1); - verify(helper, never()).updateBalancingState( - any(ConnectivityState.class), any(SubchannelPicker.class)); // wait for CLUSTER2's results - xdsClient.deliverResourceNotFound(EDS_SERVICE_NAME2); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); assertPicker( pickerCaptor.getValue(), Status.UNAVAILABLE.withDescription( - "No usable endpoint from cluster(s): " + Arrays.asList(CLUSTER1, CLUSTER2)), + "No usable endpoint from cluster: " + CLUSTER1), null); } @Test - public void onlyEdsClusters_allResourcesRevoked_shutDownChildLbPolicy() { + public void edsCluster_resourcesRevoked_shutDownChildLbPolicy() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), roundRobin, false); + edsDiscoveryMechanism1, roundRobin, false); deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1, EDS_SERVICE_NAME2); + assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertThat(childBalancers).isEmpty(); reset(helper); EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint1, 100, true, - "hostname1", ImmutableMap.of())), + "hostname1", ImmutableMap.of())), 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - LocalityLbEndpoints localityLbEndpoints2 = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint2, 100, true, - "hostname2", ImmutableMap.of())), - 20 /* localityWeight */, 2 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints1)); - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME2, Collections.singletonMap(locality2, localityLbEndpoints2)); assertThat(childBalancers).hasSize(1); // child LB policy created FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(((PriorityLbConfig) childBalancer.config).priorities).hasSize(2); - assertAddressesEqual(Arrays.asList(endpoint1, endpoint2), childBalancer.addresses); + assertThat(((PriorityLbConfig) childBalancer.config).priorities).hasSize(1); + assertAddressesEqual(Arrays.asList(endpoint1), childBalancer.addresses); - xdsClient.deliverResourceNotFound(EDS_SERVICE_NAME2); xdsClient.deliverResourceNotFound(EDS_SERVICE_NAME1); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); Status expectedError = Status.UNAVAILABLE.withDescription( - "No usable endpoint from cluster(s): " + Arrays.asList(CLUSTER1, CLUSTER2)); + "No usable endpoint from cluster: " + CLUSTER1); assertPicker(pickerCaptor.getValue(), expectedError, null); } @Test public void handleEdsResource_ignoreUnhealthyEndpoints() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanism1), roundRobin, false); + edsDiscoveryMechanism1, roundRobin, false); deliverLbConfig(config); EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); @@ -739,9 +706,9 @@ public void handleEdsResource_ignoreUnhealthyEndpoints() { LocalityLbEndpoints.create( Arrays.asList( LbEndpoint.create(endpoint1, 100, false /* isHealthy */, - "hostname1", ImmutableMap.of()), + "hostname1", ImmutableMap.of()), LbEndpoint.create(endpoint2, 100, true /* isHealthy */, - "hostname2", ImmutableMap.of())), + "hostname2", ImmutableMap.of())), 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); @@ -753,19 +720,19 @@ public void handleEdsResource_ignoreUnhealthyEndpoints() { @Test public void handleEdsResource_ignoreLocalitiesWithNoHealthyEndpoints() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanism1), roundRobin, false); + edsDiscoveryMechanism1, roundRobin, false); deliverLbConfig(config); EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint1, 100, false /* isHealthy */, - "hostname1", ImmutableMap.of())), + "hostname1", ImmutableMap.of())), 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); LocalityLbEndpoints localityLbEndpoints2 = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint2, 100, true /* isHealthy */, - "hostname2", ImmutableMap.of())), + "hostname2", ImmutableMap.of())), 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, @@ -780,20 +747,20 @@ public void handleEdsResource_ignoreLocalitiesWithNoHealthyEndpoints() { @Test public void handleEdsResource_ignorePrioritiesWithNoHealthyEndpoints() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanism1), roundRobin, false); + edsDiscoveryMechanism1, roundRobin, false); deliverLbConfig(config); EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints1 = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint1, 100, false /* isHealthy */, - "hostname1", ImmutableMap.of())), + "hostname1", ImmutableMap.of())), 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); LocalityLbEndpoints localityLbEndpoints2 = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint2, 200, true /* isHealthy */, - "hostname2", ImmutableMap.of())), - 10 /* localityWeight */, 2 /* priority */, ImmutableMap.of()); + "hostname2", ImmutableMap.of())), + 10 /* localityWeight */, 2 /* priority */, ImmutableMap.of()); String priority2 = CLUSTER1 + "[child2]"; xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, @@ -806,7 +773,7 @@ public void handleEdsResource_ignorePrioritiesWithNoHealthyEndpoints() { @Test public void handleEdsResource_noHealthyEndpoint() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanism1), roundRobin, false); + edsDiscoveryMechanism1, roundRobin, false); deliverLbConfig(config); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); LocalityLbEndpoints localityLbEndpoints = @@ -823,7 +790,7 @@ public void handleEdsResource_noHealthyEndpoint() { assertPicker( pickerCaptor.getValue(), Status.UNAVAILABLE.withDescription( - "No usable endpoint from cluster(s): " + Collections.singleton(CLUSTER1)), + "No usable endpoint from cluster: " + CLUSTER1), null); } @@ -841,7 +808,7 @@ public void oldListenerCallback_onlyLogicalDnsCluster_endpointsResolved() { void do_onlyLogicalDnsCluster_endpointsResolved() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin, false); + logicalDnsDiscoveryMechanism, roundRobin, false); deliverLbConfig(config); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); @@ -857,7 +824,7 @@ void do_onlyLogicalDnsCluster_endpointsResolved() { PriorityChildConfig priorityChildConfig = priorityLbConfig.childConfigs.get(priority); assertThat(priorityChildConfig.ignoreReresolution).isFalse(); assertThat(GracefulSwitchLoadBalancerAccessor.getChildProvider(priorityChildConfig.childConfig) - .getPolicyName()) + .getPolicyName()) .isEqualTo(CLUSTER_IMPL_POLICY_NAME); ClusterImplConfig clusterImplConfig = (ClusterImplConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig.childConfig); @@ -873,7 +840,7 @@ void do_onlyLogicalDnsCluster_endpointsResolved() { @Test public void onlyLogicalDnsCluster_handleRefreshNameResolution() { ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin, false); + logicalDnsDiscoveryMechanism, roundRobin, false); deliverLbConfig(config); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); @@ -900,9 +867,9 @@ public void oldListenerCallback_resolutionError_backoffAndRefresh() { void do_onlyLogicalDnsCluster_resolutionError_backoffAndRefresh() { InOrder inOrder = Mockito.inOrder(helper, backoffPolicyProvider, - backoffPolicy1, backoffPolicy2); + backoffPolicy1, backoffPolicy2); ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin, false); + logicalDnsDiscoveryMechanism, roundRobin, false); deliverLbConfig(config); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); @@ -948,7 +915,7 @@ void do_onlyLogicalDnsCluster_resolutionError_backoffAndRefresh() { public void onlyLogicalDnsCluster_refreshNameResolutionRaceWithResolutionError() { InOrder inOrder = Mockito.inOrder(backoffPolicyProvider, backoffPolicy1, backoffPolicy2); ClusterResolverConfig config = new ClusterResolverConfig( - Collections.singletonList(logicalDnsDiscoveryMechanism), roundRobin, false); + logicalDnsDiscoveryMechanism, roundRobin, false); deliverLbConfig(config); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); @@ -984,119 +951,22 @@ public void onlyLogicalDnsCluster_refreshNameResolutionRaceWithResolutionError() } @Test - public void edsClustersAndLogicalDnsCluster_receivedEndpoints() { + public void resolutionErrorAfterChildLbCreated_propagateError() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); + edsDiscoveryMechanism1, roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); - assertThat(childBalancers).isEmpty(); - EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); // DNS endpoint - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); // DNS endpoint - EquivalentAddressGroup endpoint3 = makeAddress("endpoint-addr-3"); // EDS endpoint - resolver.deliverEndpointAddresses(Arrays.asList(endpoint1, endpoint2)); - LocalityLbEndpoints localityLbEndpoints = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint3, 100, true, - "hostname3", ImmutableMap.of())), - 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); - - assertThat(childBalancers).hasSize(1); - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(((PriorityLbConfig) childBalancer.config).priorities) - .containsExactly(CLUSTER1 + "[child1]", CLUSTER_DNS + "[child0]").inOrder(); - assertAddressesEqual(Arrays.asList(endpoint3, endpoint1, endpoint2), - childBalancer.addresses); // ordered by cluster then addresses - assertAddressesEqual(AddressFilter.filter(AddressFilter.filter( - childBalancer.addresses, CLUSTER1 + "[child1]"), - "{region=\"test-region-1\", zone=\"test-zone-1\", sub_zone=\"test-subzone-1\"}"), - Collections.singletonList(endpoint3)); - assertAddressesEqual(AddressFilter.filter(AddressFilter.filter( - childBalancer.addresses, CLUSTER_DNS + "[child0]"), - "{region=\"\", zone=\"\", sub_zone=\"\"}"), - Arrays.asList(endpoint1, endpoint2)); - } - - @Test - public void noEdsResourceExists_useDnsResolutionResults() { - ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); - assertThat(childBalancers).isEmpty(); - reset(helper); - xdsClient.deliverResourceNotFound(EDS_SERVICE_NAME1); - verify(helper, never()).updateBalancingState( - any(ConnectivityState.class), any(SubchannelPicker.class)); // wait for DNS results - - EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); - resolver.deliverEndpointAddresses(Arrays.asList(endpoint1, endpoint2)); - assertThat(childBalancers).hasSize(1); - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - String priority = Iterables.getOnlyElement( - ((PriorityLbConfig) childBalancer.config).priorities); - assertThat(priority).isEqualTo(CLUSTER_DNS + "[child0]"); - assertAddressesEqual(Arrays.asList(endpoint1, endpoint2), childBalancer.addresses); - } - - @Test - public void edsResourceRevoked_dnsResolutionError_shutDownChildLbPolicyAndReturnErrorPicker() { - ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); reset(helper); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint, 100, true, - "hostname1", ImmutableMap.of())), + "hostname1", ImmutableMap.of())), 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); - resolver.deliverError(Status.UNKNOWN.withDescription("I am lost")); - assertThat(childBalancers).hasSize(1); - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(((PriorityLbConfig) childBalancer.config).priorities) - .containsExactly(CLUSTER1 + "[child1]"); - assertAddressesEqual(Collections.singletonList(endpoint), childBalancer.addresses); - assertThat(childBalancer.shutdown).isFalse(); - xdsClient.deliverResourceNotFound(EDS_SERVICE_NAME1); - assertThat(childBalancer.shutdown).isTrue(); - verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - assertPicker(pickerCaptor.getValue(), - Status.UNAVAILABLE.withDescription("I am lost"), null); - } + EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); - @Test - public void resolutionErrorAfterChildLbCreated_propagateErrorIfAllClustersEncounterError() { - ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); - assertThat(childBalancers).isEmpty(); - reset(helper); - EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); - LocalityLbEndpoints localityLbEndpoints = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint, 100, true, - "hostname1", ImmutableMap.of())), - 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); - assertThat(childBalancers).isEmpty(); // not created until all clusters resolved. - - resolver.deliverError(Status.UNKNOWN.withDescription("I am lost")); - - // DNS resolution failed, but there are EDS endpoints can be used. assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); // child LB created assertThat(childBalancer.upstreamError).isNull(); // should not propagate error to child LB @@ -1104,60 +974,53 @@ public void resolutionErrorAfterChildLbCreated_propagateErrorIfAllClustersEncoun xdsClient.deliverError(Status.RESOURCE_EXHAUSTED.withDescription("out of memory")); assertThat(childBalancer.upstreamError).isNotNull(); // last cluster's (DNS) error propagated - assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Code.UNKNOWN); - assertThat(childBalancer.upstreamError.getDescription()).isEqualTo("I am lost"); + assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Code.UNAVAILABLE); + assertThat(childBalancer.upstreamError.getDescription()) + .isEqualTo("Unable to load EDS backend-service-foo.googleapis.com. xDS server returned: " + + "RESOURCE_EXHAUSTED: out of memory"); assertThat(childBalancer.shutdown).isFalse(); verify(helper, never()).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), any(SubchannelPicker.class)); } @Test - public void resolutionErrorBeforeChildLbCreated_returnErrorPickerIfAllClustersEncounterError() { + public void resolutionErrorBeforeChildLbCreated_returnErrorPicker() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); + edsDiscoveryMechanism1, roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); reset(helper); - xdsClient.deliverError(Status.UNIMPLEMENTED.withDescription("not found")); + xdsClient.deliverError(Status.RESOURCE_EXHAUSTED.withDescription("OOM")); assertThat(childBalancers).isEmpty(); - verify(helper, never()).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), any(SubchannelPicker.class)); // wait for DNS - Status dnsError = Status.UNKNOWN.withDescription("I am lost"); - resolver.deliverError(dnsError); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - assertPicker( - pickerCaptor.getValue(), - Status.UNAVAILABLE.withDescription(dnsError.getDescription()), - null); + PickResult result = pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)); + Status actualStatus = result.getStatus(); + assertThat(actualStatus.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(actualStatus.getDescription()).contains("RESOURCE_EXHAUSTED: OOM"); } @Test - public void resolutionErrorBeforeChildLbCreated_edsOnly_returnErrorPicker() { + public void handleNameResolutionErrorFromUpstream_eds_beforeChildLbCreated_returnErrorPicker() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1), roundRobin, false); + edsDiscoveryMechanism1, roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertThat(childBalancers).isEmpty(); reset(helper); - xdsClient.deliverError(Status.RESOURCE_EXHAUSTED.withDescription("OOM")); - assertThat(childBalancers).isEmpty(); + Status upstreamError = Status.UNAVAILABLE.withDescription("unreachable"); + loadBalancer.handleNameResolutionError(upstreamError); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - PickResult result = pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)); - Status actualStatus = result.getStatus(); - assertThat(actualStatus.getCode()).isEqualTo(Status.Code.UNAVAILABLE); - assertThat(actualStatus.getDescription()).contains("RESOURCE_EXHAUSTED: OOM"); + assertPicker(pickerCaptor.getValue(), upstreamError, null); } @Test - public void handleNameResolutionErrorFromUpstream_beforeChildLbCreated_returnErrorPicker() { + public void handleNameResolutionErrorFromUpstream_lDns_beforeChildLbCreated_returnErrorPicker() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); + logicalDnsDiscoveryMechanism, roundRobin, false); deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); reset(helper); @@ -1169,16 +1032,14 @@ public void handleNameResolutionErrorFromUpstream_beforeChildLbCreated_returnErr } @Test - public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_fallThrough() { + public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_eds_fallThrough() { ClusterResolverConfig config = new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, logicalDnsDiscoveryMechanism), roundRobin, false); + edsDiscoveryMechanism1, roundRobin, false); deliverLbConfig(config); assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); assertThat(childBalancers).isEmpty(); reset(helper); EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( Collections.singletonList(LbEndpoint.create(endpoint1, 100, true, @@ -1186,12 +1047,34 @@ public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_fallThroug 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); xdsClient.deliverClusterLoadAssignment( EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); + assertThat(childBalancers).hasSize(1); + FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); + assertThat(((PriorityLbConfig) childBalancer.config).priorities) + .containsExactly(CLUSTER1 + "[child1]"); + assertAddressesEqual(Arrays.asList(endpoint1), childBalancer.addresses); + + loadBalancer.handleNameResolutionError(Status.UNAVAILABLE.withDescription("unreachable")); + assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Code.UNAVAILABLE); + assertThat(childBalancer.upstreamError.getDescription()).isEqualTo("unreachable"); + verify(helper, never()).updateBalancingState( + any(ConnectivityState.class), any(SubchannelPicker.class)); + } + + @Test + public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_logicalDns_fallThrough() { + ClusterResolverConfig config = new ClusterResolverConfig( + logicalDnsDiscoveryMechanism, roundRobin, false); + deliverLbConfig(config); + FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); + assertThat(childBalancers).isEmpty(); + reset(helper); + EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); resolver.deliverEndpointAddresses(Collections.singletonList(endpoint2)); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(((PriorityLbConfig) childBalancer.config).priorities) - .containsExactly(CLUSTER1 + "[child1]", CLUSTER_DNS + "[child0]"); - assertAddressesEqual(Arrays.asList(endpoint1, endpoint2), childBalancer.addresses); + .containsExactly(CLUSTER_DNS + "[child0]"); + assertAddressesEqual(Arrays.asList(endpoint2), childBalancer.addresses); loadBalancer.handleNameResolutionError(Status.UNAVAILABLE.withDescription("unreachable")); assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Code.UNAVAILABLE); @@ -1205,19 +1088,17 @@ public void config_equalsTester() { new EqualsTester() .addEqualityGroup( new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanism1), leastRequest, false), + edsDiscoveryMechanism1, leastRequest, false), new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanism1), leastRequest, false)) + edsDiscoveryMechanism1, leastRequest, false)) .addEqualityGroup(new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanism1), roundRobin, false)) + edsDiscoveryMechanism1, roundRobin, false)) .addEqualityGroup(new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanism1), leastRequest, true)) + edsDiscoveryMechanism1, leastRequest, true)) .addEqualityGroup(new ClusterResolverConfig( - Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection), + edsDiscoveryMechanismWithOutlierDetection, leastRequest, false)) - .addEqualityGroup(new ClusterResolverConfig( - Arrays.asList(edsDiscoveryMechanism1, edsDiscoveryMechanism2), leastRequest, false)) .testEquals(); } From 28f14255ce0da1586abaa281343c190d0117bc58 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 8 Jul 2025 14:22:50 -0700 Subject: [PATCH 333/591] Update README etc to reference 1.74.0 --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index a925d1535f5..e57b79e6bf7 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.73.0/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.73.0/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.74.0/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.74.0/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.73.0 + 1.74.0 runtime io.grpc grpc-protobuf - 1.73.0 + 1.74.0 io.grpc grpc-stub - 1.73.0 + 1.74.0 org.apache.tomcat @@ -79,18 +79,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.73.0' -implementation 'io.grpc:grpc-protobuf:1.73.0' -implementation 'io.grpc:grpc-stub:1.73.0' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.74.0' +implementation 'io.grpc:grpc-protobuf:1.74.0' +implementation 'io.grpc:grpc-stub:1.74.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.73.0' -implementation 'io.grpc:grpc-protobuf-lite:1.73.0' -implementation 'io.grpc:grpc-stub:1.73.0' +implementation 'io.grpc:grpc-okhttp:1.74.0' +implementation 'io.grpc:grpc-protobuf-lite:1.74.0' +implementation 'io.grpc:grpc-stub:1.74.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` @@ -99,7 +99,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.73.0 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.74.0 Development snapshots are available in [Sonatypes's snapshot repository](https://central.sonatype.com/repository/maven-snapshots/). @@ -131,7 +131,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.73.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.74.0:exe:${os.detected.classifier} @@ -161,7 +161,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.73.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0' } } generateProtoTasks { @@ -194,7 +194,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.73.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0' } } generateProtoTasks { From ba0a7329da9f52cb4520d7f8d5676edf197e8cff Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Tue, 29 Jul 2025 09:33:09 -0700 Subject: [PATCH 334/591] stub: simplify BlockingClientCall infinite blocking (#12217) Move deadline computation into overloads with finite timeouts. Blocking calls without timeouts now do not have to read the clock. --- .../java/io/grpc/stub/BlockingClientCall.java | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/stub/src/main/java/io/grpc/stub/BlockingClientCall.java b/stub/src/main/java/io/grpc/stub/BlockingClientCall.java index 58881ef0592..b62bd4322a9 100644 --- a/stub/src/main/java/io/grpc/stub/BlockingClientCall.java +++ b/stub/src/main/java/io/grpc/stub/BlockingClientCall.java @@ -87,7 +87,7 @@ public final class BlockingClientCall { */ public RespT read() throws InterruptedException, StatusException { try { - return read(true, 0, TimeUnit.NANOSECONDS); + return read(true, 0); } catch (TimeoutException e) { throw new AssertionError("should never happen", e); } @@ -106,16 +106,14 @@ public RespT read() throws InterruptedException, StatusException { */ public RespT read(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException, StatusException { - return read(false, timeout, unit); + long endNanoTime = System.nanoTime() + unit.toNanos(timeout); + return read(false, endNanoTime); } - private RespT read(boolean waitForever, long timeout, TimeUnit unit) + private RespT read(boolean waitForever, long endNanoTime) throws InterruptedException, TimeoutException, StatusException { - long start = System.nanoTime(); - long end = start + unit.toNanos(timeout); - Predicate> predicate = BlockingClientCall::skipWaitingForRead; - executor.waitAndDrainWithTimeout(waitForever, end, predicate, this); + executor.waitAndDrainWithTimeout(waitForever, endNanoTime, predicate, this); RespT bufferedValue = buffer.poll(); if (logger.isLoggable(Level.FINER)) { @@ -182,7 +180,7 @@ public boolean hasNext() throws InterruptedException, StatusException { */ public boolean write(ReqT request) throws InterruptedException, StatusException { try { - return write(true, request, Integer.MAX_VALUE, TimeUnit.DAYS); + return write(true, request, 0); } catch (TimeoutException e) { throw new RuntimeException(e); // should never happen } @@ -211,21 +209,20 @@ public boolean write(ReqT request) throws InterruptedException, StatusException */ public boolean write(ReqT request, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException, StatusException { - return write(false, request, timeout, unit); + long endNanoTime = System.nanoTime() + unit.toNanos(timeout); + return write(false, request, endNanoTime); } - private boolean write(boolean waitForever, ReqT request, long timeout, TimeUnit unit) + private boolean write(boolean waitForever, ReqT request, long endNanoTime) throws InterruptedException, TimeoutException, StatusException { if (writeClosed) { throw new IllegalStateException("Writes cannot be done after calling halfClose or cancel"); } - long end = System.nanoTime() + unit.toNanos(timeout); - Predicate> predicate = (x) -> x.call.isReady() || x.closedStatus != null; - executor.waitAndDrainWithTimeout(waitForever, end, predicate, this); + executor.waitAndDrainWithTimeout(waitForever, endNanoTime, predicate, this); Status savedClosedStatus = closedStatus; if (savedClosedStatus == null) { call.sendMessage(request); From 36fe276a5032264e8dca7ef7731add0479395987 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Wed, 30 Jul 2025 17:54:45 +0530 Subject: [PATCH 335/591] xds: add "resource_timer_is_transient_failure" server feature (#12249) --- .../main/java/io/grpc/xds/CsdsService.java | 2 + .../java/io/grpc/xds/client/Bootstrapper.java | 9 +- .../io/grpc/xds/client/BootstrapperImpl.java | 14 +- .../java/io/grpc/xds/client/XdsClient.java | 6 +- .../io/grpc/xds/client/XdsClientImpl.java | 18 +- .../java/io/grpc/xds/CsdsServiceTest.java | 2 + .../grpc/xds/GrpcXdsClientImplDataTest.java | 2 +- .../grpc/xds/GrpcXdsClientImplTestBase.java | 160 +++++++++++++++--- .../java/io/grpc/xds/XdsNameResolverTest.java | 4 +- .../client/CommonBootstrapperTestUtils.java | 2 +- 10 files changed, 178 insertions(+), 41 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/CsdsService.java b/xds/src/main/java/io/grpc/xds/CsdsService.java index a296beb45d0..8c2fe333c15 100644 --- a/xds/src/main/java/io/grpc/xds/CsdsService.java +++ b/xds/src/main/java/io/grpc/xds/CsdsService.java @@ -249,6 +249,8 @@ static ClientResourceStatus metadataStatusToClientStatus(ResourceMetadataStatus return ClientResourceStatus.ACKED; case NACKED: return ClientResourceStatus.NACKED; + case TIMEOUT: + return ClientResourceStatus.TIMEOUT; default: throw new AssertionError("Unexpected ResourceMetadataStatus: " + status); } diff --git a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java index 90babd1e8d0..4fa75f6b335 100644 --- a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java @@ -63,17 +63,20 @@ public abstract static class ServerInfo { public abstract boolean isTrustedXdsServer(); + public abstract boolean resourceTimerIsTransientError(); + @VisibleForTesting public static ServerInfo create(String target, @Nullable Object implSpecificConfig) { - return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, false, false); + return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, + false, false, false); } @VisibleForTesting public static ServerInfo create( String target, Object implSpecificConfig, boolean ignoreResourceDeletion, - boolean isTrustedXdsServer) { + boolean isTrustedXdsServer, boolean resourceTimerIsTransientError) { return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, - ignoreResourceDeletion, isTrustedXdsServer); + ignoreResourceDeletion, isTrustedXdsServer, resourceTimerIsTransientError); } } diff --git a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java index c00685f1781..423c1a118e8 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -43,6 +43,8 @@ public abstract class BootstrapperImpl extends Bootstrapper { public static final String GRPC_EXPERIMENTAL_XDS_FALLBACK = "GRPC_EXPERIMENTAL_XDS_FALLBACK"; + public static final String GRPC_EXPERIMENTAL_XDS_DATA_ERROR_HANDLING = + "GRPC_EXPERIMENTAL_XDS_DATA_ERROR_HANDLING"; // Client features. @VisibleForTesting @@ -54,10 +56,16 @@ public abstract class BootstrapperImpl extends Bootstrapper { // Server features. private static final String SERVER_FEATURE_IGNORE_RESOURCE_DELETION = "ignore_resource_deletion"; private static final String SERVER_FEATURE_TRUSTED_XDS_SERVER = "trusted_xds_server"; + private static final String + SERVER_FEATURE_RESOURCE_TIMER_IS_TRANSIENT_ERROR = "resource_timer_is_transient_error"; @VisibleForTesting static boolean enableXdsFallback = GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_FALLBACK, true); + @VisibleForTesting + public static boolean xdsDataErrorHandlingEnabled + = GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_DATA_ERROR_HANDLING, false); + protected final XdsLogger logger; protected FileReader reader = LocalFileReader.INSTANCE; @@ -247,6 +255,7 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo Object implSpecificConfig = getImplSpecificConfig(serverConfig, serverUri); + boolean resourceTimerIsTransientError = false; boolean ignoreResourceDeletion = false; // "For forward compatibility reasons, the client will ignore any entry in the list that it // does not understand, regardless of type." @@ -254,11 +263,14 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo if (serverFeatures != null) { logger.log(XdsLogLevel.INFO, "Server features: {0}", serverFeatures); ignoreResourceDeletion = serverFeatures.contains(SERVER_FEATURE_IGNORE_RESOURCE_DELETION); + resourceTimerIsTransientError = xdsDataErrorHandlingEnabled + && serverFeatures.contains(SERVER_FEATURE_RESOURCE_TIMER_IS_TRANSIENT_ERROR); } servers.add( ServerInfo.create(serverUri, implSpecificConfig, ignoreResourceDeletion, serverFeatures != null - && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER))); + && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER), + resourceTimerIsTransientError)); } return servers.build(); } diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClient.java b/xds/src/main/java/io/grpc/xds/client/XdsClient.java index edbb0b2d74c..e2dd4169bca 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClient.java @@ -199,6 +199,10 @@ public static ResourceMetadata newResourceMetadataDoesNotExist() { return new ResourceMetadata(ResourceMetadataStatus.DOES_NOT_EXIST, "", 0, false, null, null); } + public static ResourceMetadata newResourceMetadataTimeout() { + return new ResourceMetadata(ResourceMetadataStatus.TIMEOUT, "", 0, false, null, null); + } + public static ResourceMetadata newResourceMetadataAcked( Any rawResource, String version, long updateTimeNanos) { checkNotNull(rawResource, "rawResource"); @@ -256,7 +260,7 @@ public UpdateFailureState getErrorState() { * config_dump.proto */ public enum ResourceMetadataStatus { - UNKNOWN, REQUESTED, DOES_NOT_EXIST, ACKED, NACKED + UNKNOWN, REQUESTED, DOES_NOT_EXIST, ACKED, NACKED, TIMEOUT } /** diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index 2b25d4db977..d3384cbbe4e 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -67,6 +67,7 @@ public final class XdsClientImpl extends XdsClient implements ResourceStore { // Longest time to wait, since the subscription to some resource, for concluding its absence. @VisibleForTesting public static final int INITIAL_RESOURCE_FETCH_TIMEOUT_SEC = 15; + public static final int EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC = 30; private final SynchronizationContext syncContext = new SynchronizationContext( new Thread.UncaughtExceptionHandler() { @@ -738,6 +739,9 @@ void restartTimer() { // When client becomes ready, it triggers a restartTimer for all relevant subscribers. return; } + ServerInfo serverInfo = activeCpc.getServerInfo(); + int timeoutSec = serverInfo.resourceTimerIsTransientError() + ? EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC : INITIAL_RESOURCE_FETCH_TIMEOUT_SEC; class ResourceNotFound implements Runnable { @Override @@ -761,8 +765,7 @@ public String toString() { respTimer.cancel(); } respTimer = syncContext.schedule( - new ResourceNotFound(), INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS, - timeService); + new ResourceNotFound(), timeoutSec, TimeUnit.SECONDS, timeService); } void stopTimer() { @@ -854,14 +857,21 @@ void onAbsent(@Nullable ProcessingTracker processingTracker, ServerInfo serverIn if (!absent) { data = null; absent = true; - metadata = ResourceMetadata.newResourceMetadataDoesNotExist(); + metadata = serverInfo.resourceTimerIsTransientError() + ? ResourceMetadata.newResourceMetadataTimeout() + : ResourceMetadata.newResourceMetadataDoesNotExist(); for (ResourceWatcher watcher : watchers.keySet()) { if (processingTracker != null) { processingTracker.startTask(); } watchers.get(watcher).execute(() -> { try { - watcher.onResourceDoesNotExist(resource); + if (serverInfo.resourceTimerIsTransientError()) { + watcher.onError(Status.UNAVAILABLE.withDescription( + "Timed out waiting for resource " + resource + " from xDS server")); + } else { + watcher.onResourceDoesNotExist(resource); + } } finally { if (processingTracker != null) { processingTracker.onComplete(); diff --git a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java index 2c0aae24e52..573aef7ca1e 100644 --- a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java +++ b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java @@ -366,6 +366,8 @@ public void metadataStatusToClientStatus() { .isEqualTo(ClientResourceStatus.ACKED); assertThat(CsdsService.metadataStatusToClientStatus(ResourceMetadataStatus.NACKED)) .isEqualTo(ClientResourceStatus.NACKED); + assertThat(CsdsService.metadataStatusToClientStatus(ResourceMetadataStatus.TIMEOUT)) + .isEqualTo(ClientResourceStatus.TIMEOUT); } @Test diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 6167f491930..3650e5d5bb9 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -3549,7 +3549,7 @@ private static Filter buildHttpConnectionManagerFilter(HttpFilter... httpFilters private XdsResourceType.Args getXdsResourceTypeArgs(boolean isTrustedServer) { return new XdsResourceType.Args( - ServerInfo.create("http://td", "", false, isTrustedServer), "1.0", null, null, null, null + ServerInfo.create("http://td", "", false, isTrustedServer, false), "1.0", null, null, null, null ); } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 32361684a6d..89668485b96 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -85,6 +85,7 @@ import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.Bootstrapper.CertificateProviderInfo; import io.grpc.xds.client.Bootstrapper.ServerInfo; +import io.grpc.xds.client.BootstrapperImpl; import io.grpc.xds.client.EnvoyProtoData.Node; import io.grpc.xds.client.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.client.Locality; @@ -145,7 +146,7 @@ public abstract class GrpcXdsClientImplTestBase { private static final String SERVER_URI = "trafficdirector.googleapis.com"; - private static final String SERVER_URI_CUSTOME_AUTHORITY = "trafficdirector2.googleapis.com"; + private static final String SERVER_URI_CUSTOM_AUTHORITY = "trafficdirector2.googleapis.com"; private static final String SERVER_URI_EMPTY_AUTHORITY = "trafficdirector3.googleapis.com"; private static final String LDS_RESOURCE = "listener.googleapis.com"; private static final String RDS_RESOURCE = "route-configuration.googleapis.com"; @@ -304,6 +305,30 @@ public long currentTimeNanos() { private final BindableService adsService = createAdsService(); private final BindableService lrsService = createLrsService(); + private XdsTransportFactory xdsTransportFactory = new XdsTransportFactory() { + @Override + public XdsTransport create(ServerInfo serverInfo) { + if (serverInfo.target().equals(SERVER_URI)) { + return new GrpcXdsTransport(channel); + } + if (serverInfo.target().equals(SERVER_URI_CUSTOM_AUTHORITY)) { + if (channelForCustomAuthority == null) { + channelForCustomAuthority = cleanupRule.register( + InProcessChannelBuilder.forName(serverName).directExecutor().build()); + } + return new GrpcXdsTransport(channelForCustomAuthority); + } + if (serverInfo.target().equals(SERVER_URI_EMPTY_AUTHORITY)) { + if (channelForEmptyAuthority == null) { + channelForEmptyAuthority = cleanupRule.register( + InProcessChannelBuilder.forName(serverName).directExecutor().build()); + } + return new GrpcXdsTransport(channelForEmptyAuthority); + } + throw new IllegalArgumentException("Can not create channel for " + serverInfo); + } + }; + @Before public void setUp() throws IOException { when(backoffPolicyProvider.get()).thenReturn(backoffPolicy1, backoffPolicy2); @@ -322,32 +347,9 @@ public void setUp() throws IOException { .start()); channel = cleanupRule.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); - XdsTransportFactory xdsTransportFactory = new XdsTransportFactory() { - @Override - public XdsTransport create(ServerInfo serverInfo) { - if (serverInfo.target().equals(SERVER_URI)) { - return new GrpcXdsTransport(channel); - } - if (serverInfo.target().equals(SERVER_URI_CUSTOME_AUTHORITY)) { - if (channelForCustomAuthority == null) { - channelForCustomAuthority = cleanupRule.register( - InProcessChannelBuilder.forName(serverName).directExecutor().build()); - } - return new GrpcXdsTransport(channelForCustomAuthority); - } - if (serverInfo.target().equals(SERVER_URI_EMPTY_AUTHORITY)) { - if (channelForEmptyAuthority == null) { - channelForEmptyAuthority = cleanupRule.register( - InProcessChannelBuilder.forName(serverName).directExecutor().build()); - } - return new GrpcXdsTransport(channelForEmptyAuthority); - } - throw new IllegalArgumentException("Can not create channel for " + serverInfo); - } - }; xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), - true); + true, false); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -357,7 +359,7 @@ public XdsTransport create(ServerInfo serverInfo) { AuthorityInfo.create( "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/%s", ImmutableList.of(Bootstrapper.ServerInfo.create( - SERVER_URI_CUSTOME_AUTHORITY, CHANNEL_CREDENTIALS))), + SERVER_URI_CUSTOM_AUTHORITY, CHANNEL_CREDENTIALS))), "", AuthorityInfo.create( "xdstp:///envoy.config.listener.v3.Listener/%s", @@ -3155,6 +3157,108 @@ public void flowControlAbsent() throws Exception { verify(anotherWatcher).onError(any()); } + @Test + public void resourceTimerIsTransientError_schedulesExtendedTimeout() { + BootstrapperImpl.xdsDataErrorHandlingEnabled = true; + ServerInfo serverInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, + false, true, true); + BootstrapInfo bootstrapInfo = + Bootstrapper.BootstrapInfo.builder() + .servers(Collections.singletonList(serverInfo)) + .node(NODE) + .authorities(ImmutableMap.of( + "", + AuthorityInfo.create( + "xdstp:///envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS))))) + .certProviders(ImmutableMap.of()) + .build(); + xdsClient = new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + timeProvider, + MessagePrinter.INSTANCE, + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); + @SuppressWarnings("unchecked") + ResourceWatcher watcher = mock(ResourceWatcher.class); + String resourceName = "cluster.googleapis.com"; + + xdsClient.watchXdsResource( + XdsClusterResource.getInstance(), + resourceName, + watcher, + fakeClock.getScheduledExecutorService()); + + ScheduledTask task = Iterables.getOnlyElement( + fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); + assertThat(task.getDelay(TimeUnit.SECONDS)) + .isEqualTo(XdsClientImpl.EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC); + fakeClock.runDueTasks(); + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + } + + @Test + public void resourceTimerIsTransientError_callsOnErrorUnavailable() { + BootstrapperImpl.xdsDataErrorHandlingEnabled = true; + xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), + true, true); + BootstrapInfo bootstrapInfo = + Bootstrapper.BootstrapInfo.builder() + .servers(Collections.singletonList(xdsServerInfo)) + .node(NODE) + .authorities(ImmutableMap.of( + "authority.xds.com", + AuthorityInfo.create( + "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_CUSTOM_AUTHORITY, CHANNEL_CREDENTIALS))), + "", + AuthorityInfo.create( + "xdstp:///envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS))))) + .certProviders(ImmutableMap.of("cert-instance-name", + CertificateProviderInfo.create("file-watcher", ImmutableMap.of()))) + .build(); + xdsClient = new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + timeProvider, + MessagePrinter.INSTANCE, + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); + String timeoutResource = CDS_RESOURCE + "_timeout"; + @SuppressWarnings("unchecked") + ResourceWatcher timeoutWatcher = mock(ResourceWatcher.class); + + xdsClient.watchXdsResource( + XdsClusterResource.getInstance(), + timeoutResource, + timeoutWatcher, + fakeClock.getScheduledExecutorService()); + + assertThat(resourceDiscoveryCalls).hasSize(1); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + call.verifyRequest(CDS, ImmutableList.of(timeoutResource), "", "", NODE); + fakeClock.forwardTime(XdsClientImpl.EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); + fakeClock.runDueTasks(); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Status.class); + verify(timeoutWatcher).onError(errorCaptor.capture()); + Status error = errorCaptor.getValue(); + assertThat(error.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(error.getDescription()).isEqualTo( + "Timed out waiting for resource " + timeoutResource + " from xDS server"); + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + } + private Answer blockUpdate(CyclicBarrier barrier) { return new Answer() { @Override @@ -4220,7 +4324,7 @@ private XdsClientImpl createXdsClient(String serverUri) { private BootstrapInfo buildBootStrap(String serverUri) { ServerInfo xdsServerInfo = ServerInfo.create(serverUri, CHANNEL_CREDENTIALS, - ignoreResourceDeletion(), true); + ignoreResourceDeletion(), true, false); return Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -4230,7 +4334,7 @@ private BootstrapInfo buildBootStrap(String serverUri) { AuthorityInfo.create( "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/%s", ImmutableList.of(Bootstrapper.ServerInfo.create( - SERVER_URI_CUSTOME_AUTHORITY, CHANNEL_CREDENTIALS))), + SERVER_URI_CUSTOM_AUTHORITY, CHANNEL_CREDENTIALS))), "", AuthorityInfo.create( "xdstp:///envoy.config.listener.v3.Listener/%s", diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 3fa31aedf6a..b50bdfce01f 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -368,13 +368,13 @@ public void resolving_targetAuthorityInAuthoritiesMap() { String serviceAuthority = "[::FFFF:129.144.52.38]:80"; bootstrapInfo = BootstrapInfo.builder() .servers(ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true, true))) + "td.googleapis.com", InsecureChannelCredentials.create(), true, true, false))) .node(Node.newBuilder().build()) .authorities( ImmutableMap.of(targetAuthority, AuthorityInfo.create( "xdstp://" + targetAuthority + "/envoy.config.listener.v3.Listener/%s?foo=1&bar=2", ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true, true))))) + "td.googleapis.com", InsecureChannelCredentials.create(), true, true, false))))) .build(); expectedLdsResourceName = "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/" + "%5B::FFFF:129.144.52.38%5D:80?bar=2&foo=1"; // query param canonified diff --git a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java index 485970741c1..754e903f8a9 100644 --- a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java @@ -203,7 +203,7 @@ public static Bootstrapper.BootstrapInfo buildBootStrap(List serverUris) List serverInfos = new ArrayList<>(); for (String uri : serverUris) { - serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, false, true)); + serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, false, true, false)); } EnvoyProtoData.Node node = EnvoyProtoData.Node.newBuilder().setId("node-id").build(); From 6ffcbd927e4bf0f7b08bb42b587cb0e1f6d2ae7c Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 29 Jul 2025 13:12:44 -0700 Subject: [PATCH 336/591] Bump Gradle to 8.14.3 and upgrade plugins The syntax changes adding `=` were to address: https://docs.gradle.org/8.14.3/userguide/upgrading_version_8.html#groovy_space_assignment_syntax --- android-interop-testing/build.gradle | 4 ++-- android/build.gradle | 4 ++-- binder/build.gradle | 8 ++++---- build.gradle | 10 +++++----- census/build.gradle | 2 +- cronet/build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- rls/build.gradle | 2 +- settings.gradle | 8 ++++---- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/android-interop-testing/build.gradle b/android-interop-testing/build.gradle index 4f775d734e9..72b6ac0a302 100644 --- a/android-interop-testing/build.gradle +++ b/android-interop-testing/build.gradle @@ -10,7 +10,7 @@ repositories { } android { - namespace 'io.grpc.android.integrationtest' + namespace = 'io.grpc.android.integrationtest' sourceSets { main { java { @@ -41,7 +41,7 @@ android { versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - multiDexEnabled true + multiDexEnabled = true } buildTypes { debug { minifyEnabled false } diff --git a/android/build.gradle b/android/build.gradle index 3c717da18a3..723be882982 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,7 +7,7 @@ plugins { description = 'gRPC: Android' android { - namespace 'io.grpc.android' + namespace = 'io.grpc.android' compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -20,7 +20,7 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - lintOptions { abortOnError true } + lintOptions { abortOnError = true } publishing { singleVariant('release') { withSourcesJar() diff --git a/binder/build.gradle b/binder/build.gradle index 7ac23750a2a..dc9df6b04de 100644 --- a/binder/build.gradle +++ b/binder/build.gradle @@ -6,7 +6,7 @@ plugins { description = 'gRPC BinderChannel' android { - namespace 'io.grpc.binder' + namespace = 'io.grpc.binder' compileSdkVersion 34 compileOptions { sourceCompatibility 1.8 @@ -18,16 +18,16 @@ android { versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - multiDexEnabled true + multiDexEnabled = true } - lintOptions { abortOnError false } + lintOptions { abortOnError = false } publishing { singleVariant('release') { withSourcesJar() withJavadocJar() } } - testFixtures { enable true } + testFixtures { enable = true } } repositories { diff --git a/build.gradle b/build.gradle index d47687421b0..bedc8a1bdf1 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ subprojects { repositories { maven { // The google mirror is less flaky than mavenCentral() - url "https://maven-central.storage-download.googleapis.com/maven2/" + url = "https://maven-central.storage-download.googleapis.com/maven2/" metadataSources { mavenPom() ignoreGradleMetadataRedirection() @@ -241,9 +241,9 @@ subprojects { tasks.named("test").configure { testLogging { exceptionFormat = 'full' - showExceptions true - showCauses true - showStackTraces true + showExceptions = true + showCauses = true + showStackTraces = true } maxHeapSize = '1500m' } @@ -410,7 +410,7 @@ subprojects { } signing { - required false + required = false sign publishing.publications.maven } diff --git a/census/build.gradle b/census/build.gradle index 0993488ff83..c7cb02c15a0 100644 --- a/census/build.gradle +++ b/census/build.gradle @@ -40,7 +40,7 @@ dependencies { } tasks.named("javadoc").configure { - failOnError false // no public or protected classes found to document + failOnError = false // no public or protected classes found to document exclude 'io/grpc/census/internal/**' exclude 'io/grpc/census/Internal*' } diff --git a/cronet/build.gradle b/cronet/build.gradle index 1a327f9f966..3cc86201298 100644 --- a/cronet/build.gradle +++ b/cronet/build.gradle @@ -11,7 +11,7 @@ repositories { } android { - namespace 'io.grpc.cronet' + namespace = 'io.grpc.cronet' compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 94113f200e6..d4081da476b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/rls/build.gradle b/rls/build.gradle index 1dbcd91ec3c..1193ab3d4bc 100644 --- a/rls/build.gradle +++ b/rls/build.gradle @@ -50,7 +50,7 @@ tasks.named("compileJava").configure { tasks.named("javadoc").configure { // Do not publish javadoc since currently there is no public API. - failOnError false // no public or protected classes found to document + failOnError = false // no public or protected classes found to document exclude 'io/grpc/lookup/v1/**' exclude 'io/grpc/rls/*Provider.java' exclude 'io/grpc/rls/internal/**' diff --git a/settings.gradle b/settings.gradle index e30849dbe2b..22a49f0c3be 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,7 +9,7 @@ pluginManagement { // https://github.com/GoogleCloudPlatform/appengine-plugins/releases id "com.google.cloud.tools.appengine" version "2.8.0" // https://github.com/GoogleContainerTools/jib/blob/master/jib-gradle-plugin/CHANGELOG.md - id "com.google.cloud.tools.jib" version "3.4.4" + id "com.google.cloud.tools.jib" version "3.4.5" // https://github.com/google/osdetector-gradle-plugin/tags id "com.google.osdetector" version "1.7.3" // https://github.com/google/protobuf-gradle-plugin/releases @@ -21,11 +21,11 @@ pluginManagement { // https://github.com/melix/japicmp-gradle-plugin/blob/master/CHANGELOG.txt id "me.champeau.gradle.japicmp" version "0.4.2" // https://github.com/melix/jmh-gradle-plugin/releases - id "me.champeau.jmh" version "0.7.2" + id "me.champeau.jmh" version "0.7.3" // https://github.com/tbroyer/gradle-errorprone-plugin/releases - id "net.ltgt.errorprone" version "4.1.0" + id "net.ltgt.errorprone" version "4.3.0" // https://github.com/xvik/gradle-animalsniffer-plugin/releases - id "ru.vyarus.animalsniffer" version "2.0.0" + id "ru.vyarus.animalsniffer" version "2.0.1" } resolutionStrategy { eachPlugin { From d947c80f996a95ee78b69fa78e6085c322dd1a29 Mon Sep 17 00:00:00 2001 From: apolcyn Date: Wed, 30 Jul 2025 09:31:02 -0700 Subject: [PATCH 337/591] interop-testing: make soak test use logger rather than writing to stderr directly --- .../java/io/grpc/testing/integration/SoakClient.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/SoakClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/SoakClient.java index 935586cfbdd..e119c826f09 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/SoakClient.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/SoakClient.java @@ -39,6 +39,7 @@ import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; import org.HdrHistogram.Histogram; /** @@ -48,6 +49,8 @@ * https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md#rpc_soak */ final class SoakClient { + private static final Logger logger = Logger.getLogger(SoakClient.class.getName()); + private static class SoakIterationResult { public SoakIterationResult(long latencyMs, Status status) { this.latencyMs = latencyMs; @@ -171,7 +174,7 @@ public static void performSoakTest( iterationsDone += threadResult.getIterationsDone(); latencies.add(threadResult.getLatencies()); } - System.err.println( + logger.info( String.format( Locale.US, "(server_uri: %s) soak test ran: %d / %d iterations. total failures: %d. " @@ -244,14 +247,16 @@ private static void executeSoakTestInThread( if (!result.getStatus().equals(Status.OK)) { threadResults.threadFailures++; logStr.append(String.format(" failed: %s", result.getStatus())); + logger.warning(logStr.toString()); } else if (result.getLatencyMs() > maxAcceptablePerIterationLatencyMs) { threadResults.threadFailures++; logStr.append( " exceeds max acceptable latency: " + maxAcceptablePerIterationLatencyMs); + logger.warning(logStr.toString()); } else { logStr.append(" succeeded"); + logger.info(logStr.toString()); } - System.err.println(logStr.toString()); threadResults.iterationsDone++; threadResults.getLatencies().recordValue(result.getLatencyMs()); long remainingNs = earliestNextStartNs - System.nanoTime(); From 8b46ad58c38020b8c1ed5e9a2d7850fd3a9033fb Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 5 Aug 2025 16:30:39 +0000 Subject: [PATCH 338/591] Start 1.76.0 development cycle (#12258) --- MODULE.bazel | 2 +- build.gradle | 2 +- .../src/test/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/test/golden/TestService.java.txt | 2 +- core/src/main/java/io/grpc/internal/GrpcUtil.java | 2 +- examples/MODULE.bazel | 2 +- examples/android/clientcache/app/build.gradle | 10 +++++----- examples/android/helloworld/app/build.gradle | 8 ++++---- examples/android/routeguide/app/build.gradle | 8 ++++---- examples/android/strictmode/app/build.gradle | 8 ++++---- examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 4 ++-- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 4 ++-- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 4 ++-- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 4 ++-- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 4 ++-- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- 34 files changed, 55 insertions(+), 55 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index ed7bac31a34..5a2a9a86ea0 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "grpc-java", compatibility_level = 0, repo_name = "io_grpc_grpc_java", - version = "1.75.0-SNAPSHOT", # CURRENT_GRPC_VERSION + version = "1.76.0-SNAPSHOT", # CURRENT_GRPC_VERSION ) # GRPC_DEPS_START diff --git a/build.gradle b/build.gradle index bedc8a1bdf1..50c9df4c5c8 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.75.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.76.0-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index e56beda4b68..22d3491be95 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.75.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.76.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index f43fe748c46..22978149759 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.75.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.76.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index 658a6785474..2ad4a01d9e6 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -219,7 +219,7 @@ public byte[] parseAsciiString(byte[] serialized) { public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); - public static final String IMPLEMENTATION_VERSION = "1.75.0-SNAPSHOT"; // CURRENT_GRPC_VERSION + public static final String IMPLEMENTATION_VERSION = "1.76.0-SNAPSHOT"; // CURRENT_GRPC_VERSION /** * The default timeout in nanos for a keepalive ping request. diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index ef057544d5c..5e3e62a74e3 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,5 +1,5 @@ bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") -bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.75.0-SNAPSHOT") # CURRENT_GRPC_VERSION +bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.76.0-SNAPSHOT") # CURRENT_GRPC_VERSION bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") bazel_dep(name = "rules_jvm_external", version = "6.0") diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index bf6064658b7..a9716ea1f62 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -34,7 +34,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -54,12 +54,12 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.5' - testImplementation 'io.grpc:grpc-testing:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + testImplementation 'io.grpc:grpc-testing:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index e15fec725a3..2fd2d2fa950 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index f961edbbcb2..371e277bdc6 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index 6bc1d5d7edb..02327041d34 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -33,7 +33,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -53,8 +53,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/build.gradle b/examples/build.gradle index 253fca272c7..4f72a279a91 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index 57e531fb564..2c51ad88d4f 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index 96569ac31b7..b127afad1b1 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index 90a8b2975c6..e455d2f82b2 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -6,13 +6,13 @@ jar - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT example-debug https://github.com/grpc/grpc-java UTF-8 - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index 66056756523..d4082b44927 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index 7525d06c375..3067989f9eb 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -6,13 +6,13 @@ jar - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT example-dualstack https://github.com/grpc/grpc-java UTF-8 - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 31f9dc41150..5e0c5a279e2 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index df716875791..2f3dbbed7a0 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,13 +6,13 @@ jar - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index e28e9929137..c28ecf835e1 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 3c3efab4c51..87ab50a29eb 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 4c9e6662ac3..65dc8dad198 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' dependencies { diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index 74c97622f9e..a00b1a48761 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,13 +6,13 @@ jar - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index 3013c6c6a72..cc6d5ac8685 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index 98497ec41be..cee41f42010 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,13 +7,13 @@ jar - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index ad6e65300d0..e54dfab80d6 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.5' def protocVersion = protobufVersion diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index 1cea56171cd..1908a245b90 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -7,13 +7,13 @@ jar - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT example-oauth https://github.com/grpc/grpc-java UTF-8 - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT 3.25.5 3.25.5 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index 4f5e9e70a11..04c9d2429bc 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' def openTelemetryVersion = '1.40.0' def openTelemetryPrometheusVersion = '1.40.0-alpha' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index 926c7b314e3..6602e6c0d75 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index 66f5374cf3a..55d445cfdf4 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index 0d971942bc0..881ebd0e885 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -15,7 +15,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index ec32a485ef9..a04add5b0e4 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index 99ba741cbf7..a075d97a549 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,13 +6,13 @@ jar - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT example-tls https://github.com/grpc/grpc-java UTF-8 - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT 3.25.5 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index b2d945b1d5e..7393887e110 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.75.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.5' dependencies { diff --git a/examples/pom.xml b/examples/pom.xml index 57b7a9c6b79..baa1ececb6a 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,13 +6,13 @@ jar - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT examples https://github.com/grpc/grpc-java UTF-8 - 1.75.0-SNAPSHOT + 1.76.0-SNAPSHOT 3.25.5 3.25.5 From a40c8cf5a49077b48ec2428c8ddb420b987945af Mon Sep 17 00:00:00 2001 From: camel Date: Tue, 5 Aug 2025 16:47:45 -0700 Subject: [PATCH 339/591] binder: Let apps call SecurityPolicy.checkAuthorization() by PeerUid (#12257) This allows a server with access to PeerUid to check additional application-layer security policy *after* the call itself is authorized by the transport layer. Cross cutting application-layer checks could be done from a ServerInterceptor (RPC method level policy, say). Checks based on the substance of a request message could be done by the individual RPC method implementations themselves. --- .../io/grpc/binder/AsyncSecurityPolicy.java | 21 +++++++++++++++++++ .../java/io/grpc/binder/SecurityPolicy.java | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/binder/src/main/java/io/grpc/binder/AsyncSecurityPolicy.java b/binder/src/main/java/io/grpc/binder/AsyncSecurityPolicy.java index 5b17ad35977..9594c644e0c 100644 --- a/binder/src/main/java/io/grpc/binder/AsyncSecurityPolicy.java +++ b/binder/src/main/java/io/grpc/binder/AsyncSecurityPolicy.java @@ -67,4 +67,25 @@ public final Status checkAuthorization(int uid) { * authorized. */ public abstract ListenableFuture checkAuthorizationAsync(int uid); + + /** + * Decides whether the given Android UID is authorized, without providing its raw integer value. + * + *

    Calling this is equivalent to calling {@link SecurityPolicy#checkAuthorization(int)}, except + * the caller provides a {@link PeerUid} wrapper instead of the raw integer uid (known only to the + * transport). This allows a server to check additional application-layer security policy for + * itself *after* the call itself is authorized by the transport layer. Cross cutting application- + * layer checks could be done from a {@link io.grpc.ServerInterceptor}. Checks based on the + * substance of a request message could be done by the individual RPC method implementations + * themselves. + * + *

    See #checkAuthorizationAsync(int) for details on the semantics. See {@link + * PeerUids#newPeerIdentifyingServerInterceptor()} for how to get a {@link PeerUid}. + * + * @param uid The Android UID to authenticate. + * @return A gRPC {@link Status} object, with OK indicating authorized. + */ + public final ListenableFuture checkAuthorizationAsync(PeerUid uid) { + return checkAuthorizationAsync(uid.getUid()); + } } diff --git a/binder/src/main/java/io/grpc/binder/SecurityPolicy.java b/binder/src/main/java/io/grpc/binder/SecurityPolicy.java index 261e5223a0f..3ad8903407f 100644 --- a/binder/src/main/java/io/grpc/binder/SecurityPolicy.java +++ b/binder/src/main/java/io/grpc/binder/SecurityPolicy.java @@ -53,4 +53,25 @@ protected SecurityPolicy() {} * @return A gRPC {@link Status} object, with OK indicating authorized. */ public abstract Status checkAuthorization(int uid); + + /** + * Decides whether the given Android UID is authorized, without providing its raw integer value. + * + *

    Calling this is equivalent to calling {@link SecurityPolicy#checkAuthorization(int)}, except + * the caller provides a {@link PeerUid} wrapper instead of the raw integer uid (known only to the + * transport). This allows a server to check additional application-layer security policy for + * itself *after* the call itself is authorized by the transport layer. Cross cutting application- + * layer checks could be done from a {@link io.grpc.ServerInterceptor}. Checks based on the + * substance of a request message could be done by the individual RPC method implementations + * themselves. + * + *

    See #checkAuthorizationAsync(int) for details on the semantics. See {@link + * PeerUids#newPeerIdentifyingServerInterceptor()} for how to get a {@link PeerUid}. + * + * @param uid The Android UID to authenticate. + * @return A gRPC {@link Status} object, with OK indicating authorized. + */ + public final Status checkAuthorization(PeerUid uid) { + return checkAuthorization(uid.getUid()); + } } From 7040417eeecb69aa4f012e6ee01da249abbca6d4 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Wed, 6 Aug 2025 12:24:33 +0530 Subject: [PATCH 340/591] stub: use the closedTrailers in StatusException (#12259) --- .../java/io/grpc/stub/BlockingClientCall.java | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/stub/src/main/java/io/grpc/stub/BlockingClientCall.java b/stub/src/main/java/io/grpc/stub/BlockingClientCall.java index b62bd4322a9..2c896c7208a 100644 --- a/stub/src/main/java/io/grpc/stub/BlockingClientCall.java +++ b/stub/src/main/java/io/grpc/stub/BlockingClientCall.java @@ -29,6 +29,7 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; @@ -69,7 +70,7 @@ public final class BlockingClientCall { private final ThreadSafeThreadlessExecutor executor; private boolean writeClosed; - private volatile Status closedStatus; // null if not closed + private AtomicReference closeState = new AtomicReference<>(); BlockingClientCall(ClientCall call, ThreadSafeThreadlessExecutor executor) { this.call = call; @@ -120,22 +121,22 @@ private RespT read(boolean waitForever, long endNanoTime) logger.finer("Client Blocking read had value: " + bufferedValue); } - Status currentClosedStatus; + CloseState currentCloseState; if (bufferedValue != null) { call.request(1); return bufferedValue; - } else if ((currentClosedStatus = closedStatus) == null) { + } else if ((currentCloseState = closeState.get()) == null) { throw new IllegalStateException( "The message disappeared... are you reading from multiple threads?"); - } else if (!currentClosedStatus.isOk()) { - throw currentClosedStatus.asException(); + } else if (!currentCloseState.status.isOk()) { + throw currentCloseState.status.asException(currentCloseState.trailers); } else { return null; } } boolean skipWaitingForRead() { - return closedStatus != null || !buffer.isEmpty(); + return closeState.get() != null || !buffer.isEmpty(); } /** @@ -148,11 +149,11 @@ boolean skipWaitingForRead() { * @throws StatusException If the stream was closed in an error state */ public boolean hasNext() throws InterruptedException, StatusException { - executor.waitAndDrain((x) -> !x.buffer.isEmpty() || x.closedStatus != null, this); + executor.waitAndDrain((x) -> !x.buffer.isEmpty() || x.closeState.get() != null, this); - Status currentClosedStatus = closedStatus; - if (currentClosedStatus != null && !currentClosedStatus.isOk()) { - throw currentClosedStatus.asException(); + CloseState currentCloseState = closeState.get(); + if (currentCloseState != null && !currentCloseState.status.isOk()) { + throw currentCloseState.status.asException(currentCloseState.trailers); } return !buffer.isEmpty(); @@ -221,17 +222,16 @@ private boolean write(boolean waitForever, ReqT request, long endNanoTime) } Predicate> predicate = - (x) -> x.call.isReady() || x.closedStatus != null; + (x) -> x.call.isReady() || x.closeState.get() != null; executor.waitAndDrainWithTimeout(waitForever, endNanoTime, predicate, this); - Status savedClosedStatus = closedStatus; - if (savedClosedStatus == null) { + CloseState savedCloseState = closeState.get(); + if (savedCloseState == null || savedCloseState.status == null) { call.sendMessage(request); return true; - } else if (savedClosedStatus.isOk()) { + } else if (savedCloseState.status.isOk()) { return false; } else { - // Propagate any errors returned from the server - throw savedClosedStatus.asException(); + throw savedCloseState.status.asException(savedCloseState.trailers); } } @@ -274,7 +274,8 @@ public void halfClose() { @VisibleForTesting Status getClosedStatus() { drainQuietly(); - return closedStatus; + CloseState state = closeState.get(); + return (state == null) ? null : state.status; } /** @@ -317,7 +318,7 @@ boolean isWriteReady() { * @return True if writes haven't been closed and the server hasn't closed the stream */ private boolean isWriteLegal() { - return !writeClosed && closedStatus == null; + return !writeClosed && closeState.get() == null; } ClientCall.Listener getListener() { @@ -335,15 +336,25 @@ private void drainQuietly() { private final class QueuingListener extends ClientCall.Listener { @Override public void onMessage(RespT value) { - Preconditions.checkState(closedStatus == null, "ClientCall already closed"); + Preconditions.checkState(closeState.get() == null, "ClientCall already closed"); buffer.add(value); } @Override public void onClose(Status status, Metadata trailers) { - Preconditions.checkState(closedStatus == null, "ClientCall already closed"); - closedStatus = status; + CloseState newCloseState = new CloseState(status, trailers); + boolean wasSet = closeState.compareAndSet(null, newCloseState); + Preconditions.checkState(wasSet, "ClientCall already closed"); } } + private static final class CloseState { + final Status status; + final Metadata trailers; + + CloseState(Status status, Metadata trailers) { + this.status = Preconditions.checkNotNull(status, "status"); + this.trailers = trailers; + } + } } From f30964ab82efab6c92354bea1b9b8f6a9c6dddbe Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 6 Aug 2025 11:01:45 -0700 Subject: [PATCH 341/591] Bump versions of dependencies (#12252) Notably, protobuf to 3.25.8, opentelemetry to 1.52.0. Protobuf in Bazel has 25.5 in the BCR and it seems better to align the WORKSPACE with that version. But we can't actually use 25.5 in BCR because it is incompatible with Bazel 7. --- MODULE.bazel | 7 +++--- examples/MODULE.bazel | 1 - examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 2 +- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 2 +- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 2 +- .../build.gradle | 6 ++--- .../example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 2 +- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 6 ++--- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 2 +- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- gradle/libs.versions.toml | 24 ++++++++++--------- repositories.bzl | 10 ++++---- 28 files changed, 53 insertions(+), 51 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 5a2a9a86ea0..5a731723c33 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,7 +8,7 @@ module( # GRPC_DEPS_START IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.android:annotations:4.1.1.4", - "com.google.api.grpc:proto-google-common-protos:2.51.0", + "com.google.api.grpc:proto-google-common-protos:2.59.2", "com.google.auth:google-auth-library-credentials:1.24.1", "com.google.auth:google-auth-library-oauth2-http:1.24.1", "com.google.auto.value:auto-value-annotations:1.11.0", @@ -19,7 +19,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:33.3.1-android", "com.google.re2j:re2j:1.8", - "com.google.s2a.proto.v2:s2a-proto:0.1.1", + "com.google.s2a.proto.v2:s2a-proto:0.1.2", "com.google.truth:truth:1.4.2", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day @@ -50,7 +50,8 @@ bazel_dep(name = "bazel_jar_jar", version = "0.1.7") bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") -bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") +# Protobuf 25.5+ is incompatible with Bazel 7 with bzlmod +bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "24.4") bazel_dep(name = "rules_cc", version = "0.0.9") bazel_dep(name = "rules_java", version = "5.3.5") bazel_dep(name = "rules_jvm_external", version = "6.0") diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index 5e3e62a74e3..8bd50810c46 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,4 +1,3 @@ -bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.76.0-SNAPSHOT") # CURRENT_GRPC_VERSION bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") diff --git a/examples/build.gradle b/examples/build.gradle index 4f72a279a91..507b87df4db 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.5' +def protobufVersion = '3.25.8' def protocVersion = protobufVersion dependencies { diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index 2c51ad88d4f..33ea02a5875 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.5' +def protocVersion = '3.25.8' dependencies { // grpc-alts transitively depends on grpc-netty-shaded, grpc-protobuf, and grpc-stub diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index b127afad1b1..ed01bbb2636 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.5' +def protobufVersion = '3.25.8' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index e455d2f82b2..90ce766d8a9 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -13,7 +13,7 @@ UTF-8 1.76.0-SNAPSHOT - 3.25.5 + 3.25.8 1.8 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index d4082b44927..fa9db23f987 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -24,7 +24,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.5' +def protobufVersion = '3.25.8' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index 3067989f9eb..b70a3eca3d8 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -13,7 +13,7 @@ UTF-8 1.76.0-SNAPSHOT - 3.25.5 + 3.25.8 1.8 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 5e0c5a279e2..52d945196fa 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.5' +def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index 2f3dbbed7a0..68a98c526a5 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -13,7 +13,7 @@ UTF-8 1.76.0-SNAPSHOT - 3.25.5 + 3.25.8 1.8 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index c28ecf835e1..816ef9e6742 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -23,9 +23,9 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.5' -def openTelemetryVersion = '1.40.0' -def openTelemetryPrometheusVersion = '1.40.0-alpha' +def protocVersion = '3.25.8' +def openTelemetryVersion = '1.52.0' +def openTelemetryPrometheusVersion = '1.52.0-alpha' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 87ab50a29eb..432dceb6730 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.5' +def protocVersion = '3.25.8' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 65dc8dad198..7345d873e4f 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.5' +def protobufVersion = '3.25.8' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index a00b1a48761..00209657e1d 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -13,7 +13,7 @@ UTF-8 1.76.0-SNAPSHOT - 3.25.5 + 3.25.8 1.8 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index cc6d5ac8685..b14040ad58f 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.5' +def protobufVersion = '3.25.8' def protocVersion = protobufVersion dependencies { diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index cee41f42010..8dce2a71032 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -14,8 +14,8 @@ UTF-8 1.76.0-SNAPSHOT - 3.25.5 - 3.25.5 + 3.25.8 + 3.25.8 1.8 1.8 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index e54dfab80d6..521d8b082ce 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.25.5' +def protobufVersion = '3.25.8' def protocVersion = protobufVersion dependencies { diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index 1908a245b90..2907d062053 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -14,8 +14,8 @@ UTF-8 1.76.0-SNAPSHOT - 3.25.5 - 3.25.5 + 3.25.8 + 3.25.8 1.8 1.8 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index 04c9d2429bc..482f5766bee 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -22,9 +22,9 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.5' -def openTelemetryVersion = '1.40.0' -def openTelemetryPrometheusVersion = '1.40.0-alpha' +def protocVersion = '3.25.8' +def openTelemetryVersion = '1.52.0' +def openTelemetryPrometheusVersion = '1.52.0-alpha' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index 6602e6c0d75..2716adf7de1 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -17,7 +17,7 @@ java { } def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.5' +def protocVersion = '3.25.8' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index 55d445cfdf4..2b48cb30b5f 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -17,7 +17,7 @@ java { } def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.5' +def protocVersion = '3.25.8' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index 881ebd0e885..bbdb65349ca 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -16,7 +16,7 @@ java { } def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.5' +def protocVersion = '3.25.8' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}", diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index a04add5b0e4..aeb769a479b 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.5' +def protocVersion = '3.25.8' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index a075d97a549..575400c3608 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -13,7 +13,7 @@ UTF-8 1.76.0-SNAPSHOT - 3.25.5 + 3.25.8 1.8 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index 7393887e110..6c78db3513c 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.25.5' +def protocVersion = '3.25.8' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/pom.xml b/examples/pom.xml index baa1ececb6a..f81346c913c 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -13,8 +13,8 @@ UTF-8 1.76.0-SNAPSHOT - 3.25.5 - 3.25.5 + 3.25.8 + 3.25.8 1.8 1.8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ea4c8b5fa1..9407b2a8774 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,14 +6,16 @@ nettytcnative = '2.0.70.Final' opencensus = "0.31.1" # Not upgrading to 4.x as it is not yet ABI compatible. # https://github.com/protocolbuffers/protobuf/issues/17247 -protobuf = "3.25.5" +protobuf = "3.25.8" [libraries] android-annotations = "com.google.android:annotations:4.1.1.4" +# androidx-annotation 1.9.1+ uses Kotlin and requires Android Gradle Plugin 9+ androidx-annotation = "androidx.annotation:annotation:1.9.0" # 1.15.0 requires libraries and applications that depend on it to compile against # version 35 or later of the Android APIs. androidx-core = "androidx.core:core:1.13.1" +# androidx-lifecycle 2.9+ requires Java 17 androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.8.7" androidx-lifecycle-service = "androidx.lifecycle:lifecycle-service:2.8.7" androidx-test-core = "androidx.test:core:1.6.1" @@ -36,13 +38,13 @@ cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31" errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.30.0" # error-prone 2.32.0+ require Java 17+ errorprone-core = "com.google.errorprone:error_prone_core:2.31.0" -google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.51.0" +google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.59.2" # google-auth-library 1.25.0+ requires error_prone_annotations 2.31.0+, which # breaks the Android build google-auth-credentials = "com.google.auth:google-auth-library-credentials:1.24.1" google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.24.1" # Release notes: https://cloud.google.com/logging/docs/release-notes -google-cloud-logging = "com.google.cloud:google-cloud-logging:3.21.2" +google-cloud-logging = "com.google.cloud:google-cloud-logging:3.23.1" # 2.12.1 requires error_prone_annotations:2.36.0 but we are stuck with 2.30.0 gson = "com.google.code.gson:gson:2.11.0" # 33.4.0 requires com.google.errorprone:error_prone_annotations:2.36.0 but we are stuck with 2.30.0 (see above) @@ -61,7 +63,7 @@ javax-annotation = "org.apache.tomcat:annotations-api:6.0.53" javax-servlet-api = "javax.servlet:javax.servlet-api:4.0.1" # 12.0.0+ require Java 17+ jetty-client = "org.eclipse.jetty:jetty-client:11.0.24" -jetty-http2-server = "org.eclipse.jetty.http2:jetty-http2-server:12.0.16" +jetty-http2-server = "org.eclipse.jetty.http2:jetty-http2-server:12.0.23" jetty-http2-server10 = "org.eclipse.jetty.http2:http2-server:10.0.20" jetty-servlet = "org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.16" jetty-servlet10 = "org.eclipse.jetty:jetty-servlet:10.0.20" @@ -91,19 +93,19 @@ opencensus-contrib-grpc-metrics = { module = "io.opencensus:opencensus-contrib-g opencensus-exporter-stats-stackdriver = { module = "io.opencensus:opencensus-exporter-stats-stackdriver", version.ref = "opencensus" } opencensus-exporter-trace-stackdriver = { module = "io.opencensus:opencensus-exporter-trace-stackdriver", version.ref = "opencensus" } opencensus-impl = { module = "io.opencensus:opencensus-impl", version.ref = "opencensus" } -opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.46.0" -opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.46.0-alpha" -opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.43.0-alpha" -opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.46.0" -opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.46.0" +opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.52.0" +opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.52.0-alpha" +opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.48.0-alpha" +opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.52.0" +opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.52.0" perfmark-api = "io.perfmark:perfmark-api:0.27.0" protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobuf" } protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" } protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } re2j = "com.google.re2j:re2j:1.8" -robolectric = "org.robolectric:robolectric:4.14.1" -s2a-proto = "com.google.s2a.proto.v2:s2a-proto:0.1.1" +robolectric = "org.robolectric:robolectric:4.15.1" +s2a-proto = "com.google.s2a.proto.v2:s2a-proto:0.1.2" signature-android = "net.sf.androidscents.signature:android-api-level-21:5.0.1_r2" signature-java = "org.codehaus.mojo.signature:java18:1.0" # 11.0.0+ require Java 17+ diff --git a/repositories.bzl b/repositories.bzl index f9991cac243..5a760be5a43 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -12,7 +12,7 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # GRPC_DEPS_START IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.android:annotations:4.1.1.4", - "com.google.api.grpc:proto-google-common-protos:2.51.0", + "com.google.api.grpc:proto-google-common-protos:2.59.2", "com.google.auth:google-auth-library-credentials:1.24.1", "com.google.auth:google-auth-library-oauth2-http:1.24.1", "com.google.auto.value:auto-value-annotations:1.11.0", @@ -23,7 +23,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:33.3.1-android", "com.google.re2j:re2j:1.8", - "com.google.s2a.proto.v2:s2a-proto:0.1.1", + "com.google.s2a.proto.v2:s2a-proto:0.1.2", "com.google.truth:truth:1.4.2", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day @@ -116,9 +116,9 @@ def com_google_protobuf(): # This statement defines the @com_google_protobuf repo. http_archive( name = "com_google_protobuf", - sha256 = "9bd87b8280ef720d3240514f884e56a712f2218f0d693b48050c836028940a42", - strip_prefix = "protobuf-25.1", - urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v25.1/protobuf-25.1.tar.gz"], + sha256 = "3cf7d5b17c4ff04fe9f038104e9d0cae6da09b8ce271c13e44f8ac69f51e4e0f", + strip_prefix = "protobuf-25.5", + urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v25.5/protobuf-25.5.tar.gz"], ) def io_grpc_grpc_proto(): From efcdebb904a7768f1c83ac61a3b114234b647cc9 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 7 Aug 2025 08:38:44 -0700 Subject: [PATCH 342/591] Introduce a NameResolver for Android's `intent:` URIs (#12248) Let grpc-binder clients find on-device services by [implicit Intent](https://developer.android.com/guide/components/intents-filters#Types) target URI, lifting the need to hard code a server's package name. --- .../java/io/grpc/NameResolverRegistry.java | 5 + binder/src/androidTest/AndroidManifest.xml | 2 + .../grpc/binder/BinderChannelSmokeTest.java | 19 +- .../java/io/grpc/binder/ApiConstants.java | 12 + .../io/grpc/binder/BinderChannelBuilder.java | 2 + .../BinderClientTransportFactory.java | 4 + .../binder/internal/IntentNameResolver.java | 299 ++++++++++ .../internal/IntentNameResolverProvider.java | 77 +++ .../io/grpc/binder/internal/SystemApis.java | 60 ++ .../IntentNameResolverProviderTest.java | 115 ++++ .../internal/IntentNameResolverTest.java | 531 ++++++++++++++++++ 11 files changed, 1117 insertions(+), 9 deletions(-) create mode 100644 binder/src/main/java/io/grpc/binder/internal/IntentNameResolver.java create mode 100644 binder/src/main/java/io/grpc/binder/internal/IntentNameResolverProvider.java create mode 100644 binder/src/main/java/io/grpc/binder/internal/SystemApis.java create mode 100644 binder/src/test/java/io/grpc/binder/internal/IntentNameResolverProviderTest.java create mode 100644 binder/src/test/java/io/grpc/binder/internal/IntentNameResolverTest.java diff --git a/api/src/main/java/io/grpc/NameResolverRegistry.java b/api/src/main/java/io/grpc/NameResolverRegistry.java index 2648f8de1aa..26eb5552b9b 100644 --- a/api/src/main/java/io/grpc/NameResolverRegistry.java +++ b/api/src/main/java/io/grpc/NameResolverRegistry.java @@ -166,6 +166,11 @@ static List> getHardCodedClasses() { } catch (ClassNotFoundException e) { logger.log(Level.FINE, "Unable to find DNS NameResolver", e); } + try { + list.add(Class.forName("io.grpc.binder.internal.IntentNameResolverProvider")); + } catch (ClassNotFoundException e) { + logger.log(Level.FINE, "Unable to find IntentNameResolverProvider", e); + } return Collections.unmodifiableList(list); } diff --git a/binder/src/androidTest/AndroidManifest.xml b/binder/src/androidTest/AndroidManifest.xml index b6d71574410..44f21e104d9 100644 --- a/binder/src/androidTest/AndroidManifest.xml +++ b/binder/src/androidTest/AndroidManifest.xml @@ -11,11 +11,13 @@ + + diff --git a/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java b/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java index 79f7b98f045..e3a8c58bf88 100644 --- a/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import androidx.test.core.app.ApplicationProvider; @@ -39,7 +40,6 @@ import io.grpc.ManagedChannel; import io.grpc.Metadata; import io.grpc.MethodDescriptor; -import io.grpc.NameResolverRegistry; import io.grpc.ServerCall; import io.grpc.ServerCall.Listener; import io.grpc.ServerCallHandler; @@ -49,7 +49,6 @@ import io.grpc.Status.Code; import io.grpc.StatusRuntimeException; import io.grpc.internal.GrpcUtil; -import io.grpc.internal.testing.FakeNameResolverProvider; import io.grpc.stub.ClientCalls; import io.grpc.stub.MetadataUtils; import io.grpc.stub.ServerCalls; @@ -77,7 +76,6 @@ public final class BinderChannelSmokeTest { private static final int SLIGHTLY_MORE_THAN_ONE_BLOCK = 16 * 1024 + 100; private static final String MSG = "Some text which will be repeated many many times"; - private static final String SERVER_TARGET_URI = "fake://server"; private static final Metadata.Key POISON_KEY = ParcelableUtils.metadataKey("poison-bin", PoisonParcelable.CREATOR); @@ -99,7 +97,6 @@ public final class BinderChannelSmokeTest { .setType(MethodDescriptor.MethodType.BIDI_STREAMING) .build(); - FakeNameResolverProvider fakeNameResolverProvider; ManagedChannel channel; AtomicReference headersCapture = new AtomicReference<>(); AtomicReference clientUidCapture = new AtomicReference<>(); @@ -138,8 +135,6 @@ public void setUp() throws Exception { PeerUids.newPeerIdentifyingServerInterceptor()); AndroidComponentAddress serverAddress = HostServices.allocateService(appContext); - fakeNameResolverProvider = new FakeNameResolverProvider(SERVER_TARGET_URI, serverAddress); - NameResolverRegistry.getDefaultRegistry().register(fakeNameResolverProvider); HostServices.configureService( serverAddress, HostServices.serviceParamsBuilder() @@ -166,7 +161,6 @@ public void setUp() throws Exception { @After public void tearDown() throws Exception { channel.shutdownNow(); - NameResolverRegistry.getDefaultRegistry().deregister(fakeNameResolverProvider); HostServices.awaitServiceShutdown(); } @@ -235,7 +229,11 @@ public void testStreamingCallOptionHeaders() throws Exception { @Test public void testConnectViaTargetUri() throws Exception { - channel = BinderChannelBuilder.forTarget(SERVER_TARGET_URI, appContext).build(); + // Compare with the mapping in AndroidManifest.xml. + channel = + BinderChannelBuilder.forTarget( + "intent://authority/path#Intent;action=action1;scheme=scheme;end;", appContext) + .build(); assertThat(doCall("Hello").get()).isEqualTo("Hello"); } @@ -245,7 +243,10 @@ public void testConnectViaIntentFilter() throws Exception { channel = BinderChannelBuilder.forAddress( AndroidComponentAddress.forBindIntent( - new Intent().setAction("action1").setPackage(appContext.getPackageName())), + new Intent() + .setAction("action1") + .setData(Uri.parse("scheme://authority/path")) + .setPackage(appContext.getPackageName())), appContext) .build(); assertThat(doCall("Hello").get()).isEqualTo("Hello"); diff --git a/binder/src/main/java/io/grpc/binder/ApiConstants.java b/binder/src/main/java/io/grpc/binder/ApiConstants.java index 462586311c6..fbf4be6b7ce 100644 --- a/binder/src/main/java/io/grpc/binder/ApiConstants.java +++ b/binder/src/main/java/io/grpc/binder/ApiConstants.java @@ -34,6 +34,18 @@ private ApiConstants() {} */ public static final String ACTION_BIND = "grpc.io.action.BIND"; + /** + * Gives a {@link NameResolver} access to its Channel's "source" {@link android.content.Context}, + * the entry point to almost every other Android API. + * + *

    This argument is set automatically by {@link BinderChannelBuilder}. Any value passed to + * {@link io.grpc.ManagedChannelBuilder#setNameResolverArg} will be ignored. + * + *

    See {@link BinderChannelBuilder#forTarget(String, android.content.Context)} for more. + */ + public static final NameResolver.Args.Key SOURCE_ANDROID_CONTEXT = + NameResolver.Args.Key.create("source-android-context"); + /** * Specifies the Android user in which target URIs should be resolved. * diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java index 233ec6eac4f..18928339fbd 100644 --- a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java +++ b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java @@ -321,6 +321,8 @@ public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) { public ManagedChannel build() { transportFactoryBuilder.setOffloadExecutorPool( managedChannelImplBuilder.getOffloadExecutorPool()); + setNameResolverArg( + ApiConstants.SOURCE_ANDROID_CONTEXT, transportFactoryBuilder.getSourceContext()); return super.build(); } } diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java index ef00f70e35d..85a0ddd35b7 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java @@ -142,6 +142,10 @@ public Builder setSourceContext(Context sourceContext) { return this; } + public Context getSourceContext() { + return sourceContext; + } + public Builder setOffloadExecutorPool(ObjectPool offloadExecutorPool) { this.offloadExecutorPool = checkNotNull(offloadExecutorPool, "offloadExecutorPool"); return this; diff --git a/binder/src/main/java/io/grpc/binder/internal/IntentNameResolver.java b/binder/src/main/java/io/grpc/binder/internal/IntentNameResolver.java new file mode 100644 index 00000000000..ce3e2a96a42 --- /dev/null +++ b/binder/src/main/java/io/grpc/binder/internal/IntentNameResolver.java @@ -0,0 +1,299 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.grpc.binder.internal; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static io.grpc.binder.internal.SystemApis.createContextAsUser; + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Build; +import android.os.UserHandle; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.Attributes; +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusOr; +import io.grpc.SynchronizationContext; +import io.grpc.binder.AndroidComponentAddress; +import io.grpc.binder.ApiConstants; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import javax.annotation.Nullable; + +/** + * A {@link NameResolver} that resolves Android-standard "intent:" target URIs to the list of {@link + * AndroidComponentAddress} that match it by manifest intent filter. + */ +final class IntentNameResolver extends NameResolver { + private final Intent targetIntent; // Never mutated. + @Nullable private final UserHandle targetUser; // null means same user that hosts this process. + private final Context targetUserContext; + private final Executor offloadExecutor; + private final Executor sequentialExecutor; + private final SynchronizationContext syncContext; + private final ServiceConfigParser serviceConfigParser; + + // Accessed only on `sequentialExecutor` + @Nullable private PackageChangeReceiver receiver; // != null when registered + + // Accessed only on 'syncContext'. + private boolean shutdown; + private boolean queryNeeded; + @Nullable private Listener2 listener; // != null after start(). + @Nullable private ListenableFuture queryResultFuture; // != null when querying. + + @EquivalentAddressGroup.Attr + private static final Attributes CONSTANT_EAG_ATTRS = + Attributes.newBuilder() + // Servers discovered in PackageManager are especially untrusted. After all, any app can + // declare any intent filter it wants! Require pre-authorization so that unauthorized apps + // don't even get a chance to run onCreate()/onBind(). + .set(ApiConstants.PRE_AUTH_SERVER_OVERRIDE, true) + .build(); + + IntentNameResolver(Intent targetIntent, Args args) { + this.targetIntent = targetIntent; + this.targetUser = args.getArg(ApiConstants.TARGET_ANDROID_USER); + Context context = + checkNotNull(args.getArg(ApiConstants.SOURCE_ANDROID_CONTEXT), "SOURCE_ANDROID_CONTEXT") + .getApplicationContext(); + this.targetUserContext = + targetUser != null ? createContextForTargetUserOrThrow(context, targetUser) : context; + // This Executor is nominally optional but all grpc-java Channels provide it since 1.25. + this.offloadExecutor = + checkNotNull(args.getOffloadExecutor(), "NameResolver.Args.getOffloadExecutor()"); + // Ensures start()'s work runs before resolve()'s' work, and both run before shutdown()'s. + this.sequentialExecutor = MoreExecutors.newSequentialExecutor(offloadExecutor); + this.syncContext = args.getSynchronizationContext(); + this.serviceConfigParser = args.getServiceConfigParser(); + } + + private static Context createContextForTargetUserOrThrow(Context context, UserHandle targetUser) { + try { + return createContextAsUser(context, targetUser, /* flags= */ 0); // @SystemApi since R. + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException( + "TARGET_ANDROID_USER NameResolver.Arg requires SDK_INT >= R and @SystemApi visibility"); + } + } + + @Override + public void start(Listener2 listener) { + checkState(this.listener == null, "Already started!"); + checkState(!shutdown, "Resolver is shutdown"); + this.listener = checkNotNull(listener); + sequentialExecutor.execute(this::registerReceiver); + resolve(); + } + + @Override + public void refresh() { + checkState(listener != null, "Not started!"); + resolve(); + } + + private void resolve() { + syncContext.throwIfNotInThisSynchronizationContext(); + + if (shutdown) { + return; + } + + // We can't block here in 'syncContext' so we offload PackageManager queries to an Executor. + // But offloading complicates things a bit because other calls can arrive while we wait for the + // results. We keep 'listener' up-to-date with the latest state in PackageManager by doing: + // 1. Only one query-and-report-to-listener operation at a time. + // 2. At least one query-and-report-to-listener AFTER every PackageManager state change. + if (queryResultFuture == null) { + queryResultFuture = Futures.submit(this::queryPackageManager, sequentialExecutor); + queryResultFuture.addListener(this::onQueryComplete, syncContext); + } else { + // There's already a query in-flight but (2) says we need at least one more. Our sequential + // Executor would be enough to ensure (1) but we also don't want a backlog of work to build up + // if things change rapidly. Just make a note to start a new query when this one finishes. + queryNeeded = true; + } + } + + private void onQueryComplete() { + syncContext.throwIfNotInThisSynchronizationContext(); + checkState(queryResultFuture != null); + checkState(queryResultFuture.isDone()); + + // Capture non-final `listener` here while we're on 'syncContext'. + Listener2 listener = checkNotNull(this.listener); + Futures.addCallback( + queryResultFuture, // Already isDone() so this execute()s immediately. + new FutureCallback() { + @Override + public void onSuccess(ResolutionResult result) { + listener.onResult2(result); + } + + @Override + public void onFailure(Throwable t) { + listener.onResult2( + ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromStatus(Status.fromThrowable(t))) + .build()); + } + }, + syncContext); // Already on 'syncContext' but addCallback() is faster than try/get/catch. + queryResultFuture = null; + + if (queryNeeded) { + // One or more resolve() requests arrived while we were working on the last one. Just one + // follow-on query can subsume all of them. + queryNeeded = false; + resolve(); + } + } + + @Override + public String getServiceAuthority() { + return "localhost"; + } + + @Override + public void shutdown() { + syncContext.throwIfNotInThisSynchronizationContext(); + if (!shutdown) { + shutdown = true; + sequentialExecutor.execute(this::maybeUnregisterReceiver); + } + } + + private ResolutionResult queryPackageManager() throws StatusException { + List queryResults = queryIntentServices(targetIntent); + + // Avoid a spurious UnsafeIntentLaunchViolation later. Since S, Android's StrictMode is very + // conservative, marking any Intent parsed from a string as suspicious and complaining when you + // bind to it. But all this is pointless with grpc-binder, which already goes even further by + // not trusting addresses at all! Instead, we rely on SecurityPolicy, which won't allow a + // connection to an unauthorized server UID no matter how you got there. + Intent prototypeBindIntent = sanitize(targetIntent); + + // Model each matching android.app.Service as an EAG (server) with a single address. + List addresses = new ArrayList<>(); + for (ResolveInfo resolveInfo : queryResults) { + prototypeBindIntent.setComponent( + new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name)); + addresses.add( + new EquivalentAddressGroup( + AndroidComponentAddress.newBuilder() + .setBindIntent(prototypeBindIntent) // Makes a copy. + .setTargetUser(targetUser) + .build(), + CONSTANT_EAG_ATTRS)); + } + + return ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromValue(addresses)) + // Empty service config means we get the default 'pick_first' load balancing policy. + .setServiceConfig(serviceConfigParser.parseServiceConfig(ImmutableMap.of())) + .build(); + } + + private List queryIntentServices(Intent intent) throws StatusException { + int flags = 0; + if (Build.VERSION.SDK_INT >= 29) { + // Don't match direct-boot-unaware Services that can't presently be created. We'll query again + // after the user is unlocked. The MATCH_DIRECT_BOOT_AUTO behavior is actually the default but + // being explicit here avoids an android.os.strictmode.ImplicitDirectBootViolation. + flags |= PackageManager.MATCH_DIRECT_BOOT_AUTO; + } + + List intentServices = + targetUserContext.getPackageManager().queryIntentServices(intent, flags); + if (intentServices == null || intentServices.isEmpty()) { + // Must be the same as when ServiceBinding's call to bindService() returns false. + throw Status.UNIMPLEMENTED + .withDescription("Service not found for intent " + intent) + .asException(); + } + return intentServices; + } + + // Returns a new Intent with the same action, data and categories as 'input'. + private static Intent sanitize(Intent input) { + Intent output = new Intent(); + output.setAction(input.getAction()); + output.setData(input.getData()); + + Set categories = input.getCategories(); + if (categories != null) { + for (String category : categories) { + output.addCategory(category); + } + } + // Don't bother copying extras and flags since AndroidComponentAddress (rightly) ignores them. + // Don't bother copying package or ComponentName either, since we're about to set that. + return output; + } + + final class PackageChangeReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + // Get off the main thread and into the correct SynchronizationContext. + syncContext.executeLater(IntentNameResolver.this::resolve); + offloadExecutor.execute(syncContext::drain); + } + } + + @SuppressLint("UnprotectedReceiver") // All of these are protected system broadcasts. + private void registerReceiver() { + checkState(receiver == null, "Already registered!"); + receiver = new PackageChangeReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addDataScheme("package"); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_REPLACED); + + targetUserContext.registerReceiver(receiver, filter); + + if (Build.VERSION.SDK_INT >= 24) { + // Clients running in direct boot mode must refresh() when the user is unlocked because + // that's when `directBootAware=false` services become visible in queryIntentServices() + // results. ACTION_BOOT_COMPLETED would work too but it's delivered with lower priority. + targetUserContext.registerReceiver(receiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED)); + } + } + + private void maybeUnregisterReceiver() { + if (receiver != null) { // NameResolver API contract appears to allow shutdown without start(). + targetUserContext.unregisterReceiver(receiver); + receiver = null; + } + } +} diff --git a/binder/src/main/java/io/grpc/binder/internal/IntentNameResolverProvider.java b/binder/src/main/java/io/grpc/binder/internal/IntentNameResolverProvider.java new file mode 100644 index 00000000000..67859045dba --- /dev/null +++ b/binder/src/main/java/io/grpc/binder/internal/IntentNameResolverProvider.java @@ -0,0 +1,77 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.grpc.binder.internal; + +import static android.content.Intent.URI_INTENT_SCHEME; + +import android.content.Intent; +import com.google.common.collect.ImmutableSet; +import io.grpc.NameResolver; +import io.grpc.NameResolver.Args; +import io.grpc.NameResolverProvider; +import io.grpc.binder.AndroidComponentAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Objects; +import javax.annotation.Nullable; + +/** + * A {@link NameResolverProvider} that handles Android-standard "intent:" target URIs, resolving + * them to the list of {@link AndroidComponentAddress} that match by manifest intent filter. + */ +public final class IntentNameResolverProvider extends NameResolverProvider { + + static final String ANDROID_INTENT_SCHEME = "intent"; + + @Override + public String getDefaultScheme() { + return ANDROID_INTENT_SCHEME; + } + + @Nullable + @Override + public NameResolver newNameResolver(URI targetUri, final Args args) { + if (Objects.equals(targetUri.getScheme(), ANDROID_INTENT_SCHEME)) { + return new IntentNameResolver(parseUriArg(targetUri), args); + } else { + return null; + } + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public int priority() { + return 3; // Lower than DNS so we don't accidentally become the default scheme for a registry. + } + + @Override + public ImmutableSet> getProducedSocketAddressTypes() { + return ImmutableSet.of(AndroidComponentAddress.class); + } + + private static Intent parseUriArg(URI targetUri) { + try { + return Intent.parseUri(targetUri.toString(), URI_INTENT_SCHEME); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/binder/src/main/java/io/grpc/binder/internal/SystemApis.java b/binder/src/main/java/io/grpc/binder/internal/SystemApis.java new file mode 100644 index 00000000000..a4feec86a11 --- /dev/null +++ b/binder/src/main/java/io/grpc/binder/internal/SystemApis.java @@ -0,0 +1,60 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.grpc.binder.internal; + +import android.content.Context; +import android.os.UserHandle; +import java.lang.reflect.Method; + +/** + * A collection of static methods that wrap hidden Android "System APIs." + * + *

    grpc-java can't call Android methods marked @SystemApi directly, even though many of our users + * are "system apps" entitled to do so. Being a library built outside the Android source tree, these + * "non-SDK" elements simply don't exist from our compiler's perspective. Instead we resort to + * reflection but use the static wrappers found here to keep call sites readable and type safe. + * + *

    Modern Android's JRE also limits the visibility of these methods at *runtime*. Only certain + * privileged apps installed on the system image app can call them, even using reflection, and this + * wrapper doesn't change that. Callers are responsible for ensuring that the host app actually has + * the ability to call @SystemApis and all methods throw {@link ReflectiveOperationException} as a + * reminder to do that. See + * https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces for more. + */ +final class SystemApis { + private static volatile Method createContextAsUserMethod; + + // Not to be instantiated. + private SystemApis() {} + + /** + * Returns a new Context object whose methods act as if they were running in the given user. + * + * @throws ReflectiveOperationException if SDK_INT < R or host app lacks @SystemApi visibility + */ + public static Context createContextAsUser(Context context, UserHandle userHandle, int flags) + throws ReflectiveOperationException { + if (createContextAsUserMethod == null) { + synchronized (SystemApis.class) { + if (createContextAsUserMethod == null) { + createContextAsUserMethod = + Context.class.getMethod("createContextAsUser", UserHandle.class, int.class); + } + } + } + return (Context) createContextAsUserMethod.invoke(context, userHandle, flags); + } +} diff --git a/binder/src/test/java/io/grpc/binder/internal/IntentNameResolverProviderTest.java b/binder/src/test/java/io/grpc/binder/internal/IntentNameResolverProviderTest.java new file mode 100644 index 00000000000..aa75ba84b09 --- /dev/null +++ b/binder/src/test/java/io/grpc/binder/internal/IntentNameResolverProviderTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.grpc.binder.internal; + +import static android.os.Looper.getMainLooper; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Application; +import androidx.core.content.ContextCompat; +import androidx.test.core.app.ApplicationProvider; +import io.grpc.NameResolver; +import io.grpc.NameResolver.ResolutionResult; +import io.grpc.NameResolver.ServiceConfigParser; +import io.grpc.NameResolverProvider; +import io.grpc.SynchronizationContext; +import io.grpc.binder.ApiConstants; +import java.net.URI; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoTestRule; +import org.robolectric.RobolectricTestRunner; + +/** A test for IntentNameResolverProvider. */ +@RunWith(RobolectricTestRunner.class) +public final class IntentNameResolverProviderTest { + + private final Application appContext = ApplicationProvider.getApplicationContext(); + private final SynchronizationContext syncContext = newSynchronizationContext(); + private final NameResolver.Args args = newNameResolverArgs(); + + private NameResolverProvider provider; + + @Rule public MockitoTestRule mockitoTestRule = MockitoJUnit.testRule(this); + @Mock public NameResolver.Listener2 mockListener; + @Captor public ArgumentCaptor resultCaptor; + + @Before + public void setUp() { + provider = new IntentNameResolverProvider(); + } + + @Test + public void testProviderScheme_returnsIntentScheme() throws Exception { + assertThat(provider.getDefaultScheme()) + .isEqualTo(IntentNameResolverProvider.ANDROID_INTENT_SCHEME); + } + + @Test + public void testNoResolverForUnknownScheme_returnsNull() throws Exception { + assertThat(provider.newNameResolver(new URI("random://uri"), args)).isNull(); + } + + @Test + public void testResolutionWithBadUri_throwsIllegalArg() throws Exception { + assertThrows( + IllegalArgumentException.class, + () -> provider.newNameResolver(new URI("intent:xxx#Intent;e.x=1;end;"), args)); + } + + @Test + public void testResolverForIntentScheme_returnsResolver() throws Exception { + URI uri = new URI("intent://authority/path#Intent;action=action;scheme=scheme;end"); + NameResolver resolver = provider.newNameResolver(uri, args); + assertThat(resolver).isNotNull(); + assertThat(resolver.getServiceAuthority()).isEqualTo("localhost"); + syncContext.execute(() -> resolver.start(mockListener)); + shadowOf(getMainLooper()).idle(); + verify(mockListener).onResult2(resultCaptor.capture()); + assertThat(resultCaptor.getValue().getAddressesOrError()).isNotNull(); + syncContext.execute(resolver::shutdown); + shadowOf(getMainLooper()).idle(); + } + + /** Returns a new test-specific {@link NameResolver.Args} instance. */ + private NameResolver.Args newNameResolverArgs() { + return NameResolver.Args.newBuilder() + .setDefaultPort(-1) + .setProxyDetector((target) -> null) // No proxies here. + .setSynchronizationContext(syncContext) + .setOffloadExecutor(ContextCompat.getMainExecutor(appContext)) + .setServiceConfigParser(mock(ServiceConfigParser.class)) + .setArg(ApiConstants.SOURCE_ANDROID_CONTEXT, appContext) + .build(); + } + + private static SynchronizationContext newSynchronizationContext() { + return new SynchronizationContext( + (thread, exception) -> { + throw new AssertionError(exception); + }); + } +} diff --git a/binder/src/test/java/io/grpc/binder/internal/IntentNameResolverTest.java b/binder/src/test/java/io/grpc/binder/internal/IntentNameResolverTest.java new file mode 100644 index 00000000000..b1bfcd4fd56 --- /dev/null +++ b/binder/src/test/java/io/grpc/binder/internal/IntentNameResolverTest.java @@ -0,0 +1,531 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.grpc.binder.internal; + +import static android.content.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_REPLACED; +import static android.os.Looper.getMainLooper; +import static android.os.Process.myUserHandle; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +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.robolectric.Shadows.shadowOf; + +import android.app.Application; +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ServiceInfo; +import android.net.Uri; +import android.os.UserHandle; +import android.os.UserManager; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.test.core.app.ApplicationProvider; +import com.google.common.collect.ImmutableList; +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import io.grpc.NameResolver.ResolutionResult; +import io.grpc.NameResolver.ServiceConfigParser; +import io.grpc.Status; +import io.grpc.StatusOr; +import io.grpc.SynchronizationContext; +import io.grpc.binder.AndroidComponentAddress; +import io.grpc.binder.ApiConstants; +import java.lang.Thread.UncaughtExceptionHandler; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoTestRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowPackageManager; + +/** A test for IntentNameResolverProvider. */ +@RunWith(RobolectricTestRunner.class) +public final class IntentNameResolverTest { + + private static final ComponentName SOME_COMPONENT_NAME = + new ComponentName("com.foo.bar", "SomeComponent"); + private static final ComponentName ANOTHER_COMPONENT_NAME = + new ComponentName("org.blah", "AnotherComponent"); + private final Application appContext = ApplicationProvider.getApplicationContext(); + private final SynchronizationContext syncContext = newSynchronizationContext(); + private final NameResolver.Args args = newNameResolverArgs().build(); + + private final ShadowPackageManager shadowPackageManager = + shadowOf(appContext.getPackageManager()); + + @Rule public MockitoTestRule mockitoTestRule = MockitoJUnit.testRule(this); + @Mock public NameResolver.Listener2 mockListener; + @Captor public ArgumentCaptor resultCaptor; + + @Test + public void testResolverForIntentScheme_returnsResolverWithLocalHostAuthority() throws Exception { + NameResolver resolver = newNameResolver(newIntent()); + assertThat(resolver).isNotNull(); + assertThat(resolver.getServiceAuthority()).isEqualTo("localhost"); + } + + @Test + public void testResolutionWithoutServicesAvailable_returnsUnimplemented() throws Exception { + NameResolver nameResolver = newNameResolver(newIntent()); + syncContext.execute(() -> nameResolver.start(mockListener)); + shadowOf(getMainLooper()).idle(); + verify(mockListener).onResult2(resultCaptor.capture()); + assertThat(resultCaptor.getValue().getAddressesOrError().getStatus().getCode()) + .isEqualTo(Status.UNIMPLEMENTED.getCode()); + } + + @Test + public void testResolutionWithMultipleServicesAvailable_returnsAndroidComponentAddresses() + throws Exception { + Intent intent = newIntent(); + IntentFilter serviceIntentFilter = newFilterMatching(intent); + + shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter); + + // Adds another valid Service + shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter); + + NameResolver nameResolver = newNameResolver(intent); + syncContext.execute(() -> nameResolver.start(mockListener)); + shadowOf(getMainLooper()).idle(); + + verify(mockListener, never()).onError(any()); + verify(mockListener).onResult2(resultCaptor.capture()); + assertThat(getAddressesOrThrow(resultCaptor.getValue())) + .containsExactly( + toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)), + toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME))); + + syncContext.execute(nameResolver::shutdown); + shadowOf(getMainLooper()).idle(); + } + + @Test + public void testExplicitResolutionByComponent_returnsRestrictedResults() throws Exception { + Intent intent = newIntent(); + IntentFilter serviceIntentFilter = newFilterMatching(intent); + + shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter); + shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter); + + NameResolver nameResolver = + newNameResolver(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME)); + syncContext.execute(() -> nameResolver.start(mockListener)); + shadowOf(getMainLooper()).idle(); + + verify(mockListener, never()).onError(any()); + verify(mockListener).onResult2(resultCaptor.capture()); + assertThat(getAddressesOrThrow(resultCaptor.getValue())) + .containsExactly(toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME))); + + syncContext.execute(nameResolver::shutdown); + shadowOf(getMainLooper()).idle(); + } + + @Test + public void testExplicitResolutionByPackage_returnsRestrictedResults() throws Exception { + Intent intent = newIntent(); + IntentFilter serviceIntentFilter = newFilterMatching(intent); + + shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter); + shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter); + + NameResolver nameResolver = + newNameResolver(intent.cloneFilter().setPackage(ANOTHER_COMPONENT_NAME.getPackageName())); + syncContext.execute(() -> nameResolver.start(mockListener)); + shadowOf(getMainLooper()).idle(); + + verify(mockListener, never()).onError(any()); + verify(mockListener).onResult2(resultCaptor.capture()); + assertThat(getAddressesOrThrow(resultCaptor.getValue())) + .containsExactly(toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME))); + + syncContext.execute(nameResolver::shutdown); + shadowOf(getMainLooper()).idle(); + } + + @Test + public void testResolution_setsPreAuthEagAttribute() throws Exception { + Intent intent = newIntent(); + IntentFilter serviceIntentFilter = newFilterMatching(intent); + + shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter); + + NameResolver nameResolver = newNameResolver(intent); + syncContext.execute(() -> nameResolver.start(mockListener)); + shadowOf(getMainLooper()).idle(); + + verify(mockListener).onResult2(resultCaptor.capture()); + assertThat(getAddressesOrThrow(resultCaptor.getValue())) + .containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME))); + assertThat( + getEagsOrThrow(resultCaptor.getValue()).stream() + .map(EquivalentAddressGroup::getAttributes) + .collect(toImmutableList()) + .get(0) + .get(ApiConstants.PRE_AUTH_SERVER_OVERRIDE)) + .isTrue(); + + syncContext.execute(nameResolver::shutdown); + shadowOf(getMainLooper()).idle(); + } + + @Test + public void testServiceRemoved_pushesUpdatedAndroidComponentAddresses() throws Exception { + Intent intent = newIntent(); + IntentFilter serviceIntentFilter = newFilterMatching(intent); + + shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter); + shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter); + + NameResolver nameResolver = newNameResolver(intent); + syncContext.execute(() -> nameResolver.start(mockListener)); + shadowOf(getMainLooper()).idle(); + + verify(mockListener, never()).onError(any()); + verify(mockListener).onResult2(resultCaptor.capture()); + assertThat(getAddressesOrThrow(resultCaptor.getValue())) + .containsExactly( + toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)), + toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME))); + + shadowPackageManager.removeService(ANOTHER_COMPONENT_NAME); + broadcastPackageChange(ACTION_PACKAGE_REPLACED, ANOTHER_COMPONENT_NAME.getPackageName()); + shadowOf(getMainLooper()).idle(); + + verify(mockListener, never()).onError(any()); + verify(mockListener, times(2)).onResult2(resultCaptor.capture()); + assertThat(getAddressesOrThrow(resultCaptor.getValue())) + .containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME))); + + syncContext.execute(nameResolver::shutdown); + shadowOf(getMainLooper()).idle(); + + verifyNoMoreInteractions(mockListener); + assertThat(shadowOf(appContext).getRegisteredReceivers()).isEmpty(); + } + + @Test + @Config(sdk = 30) + public void testTargetAndroidUser_pushesUpdatedAddresses() throws Exception { + Intent intent = newIntent(); + IntentFilter serviceIntentFilter = newFilterMatching(intent); + + NameResolver nameResolver = + newNameResolver( + intent, + newNameResolverArgs().setArg(ApiConstants.TARGET_ANDROID_USER, myUserHandle()).build()); + syncContext.execute(() -> nameResolver.start(mockListener)); + shadowOf(getMainLooper()).idle(); + verify(mockListener).onResult2(resultCaptor.capture()); + assertThat(resultCaptor.getValue().getAddressesOrError().getStatus().getCode()) + .isEqualTo(Status.UNIMPLEMENTED.getCode()); + + shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter); + broadcastPackageChange(ACTION_PACKAGE_ADDED, SOME_COMPONENT_NAME.getPackageName()); + shadowOf(getMainLooper()).idle(); + + verify(mockListener, never()).onError(any()); + verify(mockListener, times(2)).onResult2(resultCaptor.capture()); + assertThat(getAddressesOrThrow(resultCaptor.getValue())) + .containsExactly( + ImmutableList.of( + AndroidComponentAddress.newBuilder() + .setTargetUser(myUserHandle()) + .setBindIntent(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)) + .build())); + + syncContext.execute(nameResolver::shutdown); + shadowOf(getMainLooper()).idle(); + + verifyNoMoreInteractions(mockListener); + assertThat(shadowOf(appContext).getRegisteredReceivers()).isEmpty(); + } + + @Test + @Config(sdk = 29) + public void testTargetAndroidUser_notSupported_throwsWithHelpfulMessage() throws Exception { + NameResolver.Args args = + newNameResolverArgs().setArg(ApiConstants.TARGET_ANDROID_USER, myUserHandle()).build(); + IllegalArgumentException iae = + assertThrows(IllegalArgumentException.class, () -> newNameResolver(newIntent(), args)); + assertThat(iae.getMessage()).contains("TARGET_ANDROID_USER"); + assertThat(iae.getMessage()).contains("SDK_INT >= R"); + } + + @Test + @Config(sdk = 29) + public void testServiceAppearsUponBootComplete_pushesUpdatedAndroidComponentAddresses() + throws Exception { + Intent intent = newIntent(); + IntentFilter serviceIntentFilter = newFilterMatching(intent); + + // Suppose this directBootAware=true Service appears in PackageManager before a user unlock. + shadowOf(appContext.getSystemService(UserManager.class)).setUserUnlocked(false); + ServiceInfo someServiceInfo = shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME); + someServiceInfo.directBootAware = true; + shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter); + + NameResolver nameResolver = newNameResolver(intent); + syncContext.execute(() -> nameResolver.start(mockListener)); + shadowOf(getMainLooper()).idle(); + + verify(mockListener, never()).onError(any()); + verify(mockListener).onResult2(resultCaptor.capture()); + assertThat(getAddressesOrThrow(resultCaptor.getValue())) + .containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME))); + + // TODO(b/331618070): Robolectric doesn't yet support ServiceInfo.directBootAware filtering. + // Simulate support by waiting for a user unlock to add this !directBootAware Service. + ServiceInfo anotherServiceInfo = + shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME); + anotherServiceInfo.directBootAware = false; + shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter); + + shadowOf(appContext.getSystemService(UserManager.class)).setUserUnlocked(true); + broadcastUserUnlocked(myUserHandle()); + shadowOf(getMainLooper()).idle(); + + verify(mockListener, never()).onError(any()); + verify(mockListener, times(2)).onResult2(resultCaptor.capture()); + assertThat(getAddressesOrThrow(resultCaptor.getValue())) + .containsExactly( + toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)), + toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME))); + + syncContext.execute(nameResolver::shutdown); + shadowOf(getMainLooper()).idle(); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void testRefresh_returnsSameAndroidComponentAddresses() throws Exception { + Intent intent = newIntent(); + IntentFilter serviceIntentFilter = newFilterMatching(intent); + + shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter); + shadowPackageManager.addServiceIfNotPresent(ANOTHER_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(ANOTHER_COMPONENT_NAME, serviceIntentFilter); + + NameResolver nameResolver = newNameResolver(intent); + syncContext.execute(() -> nameResolver.start(mockListener)); + shadowOf(getMainLooper()).idle(); + + verify(mockListener, never()).onError(any()); + verify(mockListener).onResult2(resultCaptor.capture()); + assertThat(getAddressesOrThrow(resultCaptor.getValue())) + .containsExactly( + toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)), + toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME))); + + syncContext.execute(nameResolver::refresh); + shadowOf(getMainLooper()).idle(); + verify(mockListener, never()).onError(any()); + verify(mockListener, times(2)).onResult2(resultCaptor.capture()); + assertThat(getAddressesOrThrow(resultCaptor.getValue())) + .containsExactly( + toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME)), + toAddressList(intent.cloneFilter().setComponent(ANOTHER_COMPONENT_NAME))); + + syncContext.execute(nameResolver::shutdown); + shadowOf(getMainLooper()).idle(); + assertThat(shadowOf(appContext).getRegisteredReceivers()).isEmpty(); + } + + @Test + public void testRefresh_collapsesMultipleRequestsIntoOneLookup() throws Exception { + Intent intent = newIntent(); + IntentFilter serviceIntentFilter = newFilterMatching(intent); + + shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, serviceIntentFilter); + + NameResolver nameResolver = newNameResolver(intent); + syncContext.execute(() -> nameResolver.start(mockListener)); // Should kick off the 1st lookup. + syncContext.execute(nameResolver::refresh); // Should queue a lookup to run when 1st finishes. + syncContext.execute(nameResolver::refresh); // Should be ignored since a lookup is already Q'd. + syncContext.execute(nameResolver::refresh); // Also ignored. + shadowOf(getMainLooper()).idle(); + + verify(mockListener, never()).onError(any()); + verify(mockListener, times(2)).onResult2(resultCaptor.capture()); + assertThat(getAddressesOrThrow(resultCaptor.getValue())) + .containsExactly(toAddressList(intent.cloneFilter().setComponent(SOME_COMPONENT_NAME))); + + syncContext.execute(nameResolver::shutdown); + shadowOf(getMainLooper()).idle(); + } + + private void broadcastPackageChange(String action, String pkgName) { + Intent broadcast = new Intent(); + broadcast.setAction(action); + broadcast.setData(Uri.parse("package:" + pkgName)); + appContext.sendBroadcast(broadcast); + } + + private void broadcastUserUnlocked(UserHandle userHandle) { + Intent unlockedBroadcast = new Intent(Intent.ACTION_USER_UNLOCKED); + unlockedBroadcast.putExtra(Intent.EXTRA_USER, userHandle); + appContext.sendBroadcast(unlockedBroadcast); + } + + @Test + public void testResolutionOnResultThrows_onErrorNotCalled() throws Exception { + RetainingUncaughtExceptionHandler exceptionHandler = new RetainingUncaughtExceptionHandler(); + SynchronizationContext syncContext = new SynchronizationContext(exceptionHandler); + Intent intent = newIntent(); + shadowPackageManager.addServiceIfNotPresent(SOME_COMPONENT_NAME); + shadowPackageManager.addIntentFilterForService(SOME_COMPONENT_NAME, newFilterMatching(intent)); + + @SuppressWarnings("serial") + class SomeRuntimeException extends RuntimeException {} + doThrow(SomeRuntimeException.class).when(mockListener).onResult2(any()); + + NameResolver nameResolver = + newNameResolver( + intent, newNameResolverArgs().setSynchronizationContext(syncContext).build()); + syncContext.execute(() -> nameResolver.start(mockListener)); + shadowOf(getMainLooper()).idle(); + + verify(mockListener).onResult2(any()); + verify(mockListener, never()).onError(any()); + assertThat(exceptionHandler.uncaught).hasSize(1); + assertThat(exceptionHandler.uncaught.get(0)).isInstanceOf(SomeRuntimeException.class); + } + + private static Intent newIntent() { + Intent intent = new Intent(); + intent.setAction("test.action"); + intent.setData(Uri.parse("grpc:ServiceName")); + return intent; + } + + private static IntentFilter newFilterMatching(Intent intent) { + IntentFilter filter = new IntentFilter(); + if (intent.getAction() != null) { + filter.addAction(intent.getAction()); + } + Uri data = intent.getData(); + if (data != null) { + if (data.getScheme() != null) { + filter.addDataScheme(data.getScheme()); + } + if (data.getSchemeSpecificPart() != null) { + filter.addDataSchemeSpecificPart(data.getSchemeSpecificPart(), 0); + } + } + Set categories = intent.getCategories(); + if (categories != null) { + for (String category : categories) { + filter.addCategory(category); + } + } + return filter; + } + + private static List getEagsOrThrow(ResolutionResult result) { + StatusOr> eags = result.getAddressesOrError(); + if (!eags.hasValue()) { + throw eags.getStatus().asRuntimeException(); + } + return eags.getValue(); + } + + // Extracts just the addresses from 'result's EquivalentAddressGroups. + private static ImmutableList> getAddressesOrThrow(ResolutionResult result) { + return getEagsOrThrow(result).stream() + .map(EquivalentAddressGroup::getAddresses) + .collect(toImmutableList()); + } + + // Converts given Intents to a list of ACAs, for convenient comparison with getAddressesOrThrow(). + private static ImmutableList toAddressList(Intent... bindIntents) { + ImmutableList.Builder builder = ImmutableList.builder(); + for (Intent bindIntent : bindIntents) { + builder.add(AndroidComponentAddress.forBindIntent(bindIntent)); + } + return builder.build(); + } + + private NameResolver newNameResolver(Intent targetIntent) { + return newNameResolver(targetIntent, args); + } + + private NameResolver newNameResolver(Intent targetIntent, NameResolver.Args args) { + return new IntentNameResolver(targetIntent, args); + } + + /** Returns a new test-specific {@link NameResolver.Args} instance. */ + private NameResolver.Args.Builder newNameResolverArgs() { + return NameResolver.Args.newBuilder() + .setDefaultPort(-1) + .setProxyDetector((target) -> null) // No proxies here. + .setSynchronizationContext(syncContext) + .setOffloadExecutor(ContextCompat.getMainExecutor(appContext)) + .setArg(ApiConstants.SOURCE_ANDROID_CONTEXT, appContext) + .setServiceConfigParser(mock(ServiceConfigParser.class)); + } + + /** + * Returns a test {@link SynchronizationContext}. + * + *

    Exceptions will cause the test to fail with {@link AssertionError}. + */ + private static SynchronizationContext newSynchronizationContext() { + return new SynchronizationContext( + (thread, exception) -> { + throw new AssertionError(exception); + }); + } + + static final class RetainingUncaughtExceptionHandler implements UncaughtExceptionHandler { + final ArrayList uncaught = new ArrayList<>(); + + @Override + public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { + uncaught.add(e); + } + } +} From 06707f7c38068d9e933f6d03811255f481a0abe9 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 5 Aug 2025 14:46:51 -0700 Subject: [PATCH 343/591] xds: Use a different log name for XdsClientImpl and ControlPlaneClient Seems like a good time to stop hating ourselves, as that seems to be the only reason to use the same string. --- xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java index d05942914dc..82a0e2f9220 100644 --- a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java +++ b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java @@ -109,7 +109,7 @@ final class ControlPlaneClient { this.backoffPolicyProvider = checkNotNull(backoffPolicyProvider, "backoffPolicyProvider"); this.messagePrinter = checkNotNull(messagePrinter, "messagePrinter"); stopwatch = checkNotNull(stopwatchSupplier, "stopwatchSupplier").get(); - logId = InternalLogId.allocate("xds-client", serverInfo.target()); + logId = InternalLogId.allocate("xds-cp-client", serverInfo.target()); logger = XdsLogger.withLogId(logId); logger.log(XdsLogLevel.INFO, "Created"); } From f50726d32e216746642513add28e086094ce5506 Mon Sep 17 00:00:00 2001 From: Sangamesh Date: Tue, 12 Aug 2025 03:48:01 +0530 Subject: [PATCH 344/591] android: Clean up android lint and other warnings (#12143) Worked on clearing the lint warnings (OldTargetApi, ObsoleteSdkInt, InlinedApi, NewApi) Fixes #12142 --- android/build.gradle | 2 +- .../grpc/android/AndroidChannelBuilder.java | 2 - binder/build.gradle | 2 +- .../java/io/grpc/binder/SecurityPolicies.java | 2 - .../grpc/binder/internal/ServiceBinding.java | 25 +++++++---- .../binder/internal/ServiceBindingTest.java | 45 +++++++++++++++++++ cronet/build.gradle | 2 +- lint.xml | 8 ++++ 8 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 lint.xml diff --git a/android/build.gradle b/android/build.gradle index 723be882982..d0ae3b9c886 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -14,7 +14,7 @@ android { } compileSdkVersion 34 defaultConfig { - minSdkVersion 21 + minSdkVersion 22 targetSdkVersion 33 versionCode 1 versionName "1.0" diff --git a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java index e56ce5fc405..54b38bc3bd3 100644 --- a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java +++ b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java @@ -217,7 +217,6 @@ private void configureNetworkMonitoring() { connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback); unregisterRunnable = new Runnable() { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void run() { connectivityManager.unregisterNetworkCallback(defaultNetworkCallback); @@ -231,7 +230,6 @@ public void run() { context.registerReceiver(networkReceiver, networkIntentFilter); unregisterRunnable = new Runnable() { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void run() { context.unregisterReceiver(networkReceiver); diff --git a/binder/build.gradle b/binder/build.gradle index dc9df6b04de..d1302ddfed0 100644 --- a/binder/build.gradle +++ b/binder/build.gradle @@ -13,7 +13,7 @@ android { targetCompatibility 1.8 } defaultConfig { - minSdkVersion 21 + minSdkVersion 22 targetSdkVersion 33 versionCode 1 versionName "1.0" diff --git a/binder/src/main/java/io/grpc/binder/SecurityPolicies.java b/binder/src/main/java/io/grpc/binder/SecurityPolicies.java index 05e8c43da79..c0f6fe81989 100644 --- a/binder/src/main/java/io/grpc/binder/SecurityPolicies.java +++ b/binder/src/main/java/io/grpc/binder/SecurityPolicies.java @@ -184,7 +184,6 @@ public Status checkAuthorization(int uid) { * Creates {@link SecurityPolicy} which checks if the app is a device owner app. See {@link * DevicePolicyManager}. */ - @RequiresApi(18) public static io.grpc.binder.SecurityPolicy isDeviceOwner(Context applicationContext) { DevicePolicyManager devicePolicyManager = (DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE); @@ -199,7 +198,6 @@ public static io.grpc.binder.SecurityPolicy isDeviceOwner(Context applicationCon * Creates {@link SecurityPolicy} which checks if the app is a profile owner app. See {@link * DevicePolicyManager}. */ - @RequiresApi(21) public static SecurityPolicy isProfileOwner(Context applicationContext) { DevicePolicyManager devicePolicyManager = (DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE); diff --git a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java index f0cbe9ec56b..42d69d27a2e 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -193,18 +193,27 @@ private static Status bindInternal( bindResult = context.bindService(bindIntent, conn, flags); break; case BIND_SERVICE_AS_USER: - bindResult = context.bindServiceAsUser(bindIntent, conn, flags, targetUserHandle); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + bindResult = context.bindServiceAsUser(bindIntent, conn, flags, targetUserHandle); + } else { + return Status.INTERNAL.withDescription("Cross user Channel requires Android R+"); + } break; case DEVICE_POLICY_BIND_SEVICE_ADMIN: DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); - bindResult = - devicePolicyManager.bindDeviceAdminServiceAsUser( - channelCredentials.getDevicePolicyAdminComponentName(), - bindIntent, - conn, - flags, - targetUserHandle); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + bindResult = + devicePolicyManager.bindDeviceAdminServiceAsUser( + channelCredentials.getDevicePolicyAdminComponentName(), + bindIntent, + conn, + flags, + targetUserHandle); + } else { + return Status.INTERNAL.withDescription( + "Device policy admin binding requires Android R+"); + } break; } if (!bindResult) { diff --git a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java index bd51c522d15..2079b0eed2c 100644 --- a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java @@ -29,6 +29,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ServiceInfo; +import android.os.Build; import android.os.IBinder; import android.os.Parcel; import android.os.UserHandle; @@ -327,6 +328,50 @@ public void testResolveNonExistentServiceWithTargetUserThrows() throws Exception assertThat(statusException.getStatus().getDescription()).contains("12345"); } + @Test + @Config(sdk = 30) + public void testBindService_doesNotThrowInternalErrorWhenSdkAtLeastR() { + UserHandle userHandle = generateUserHandle(/* userId= */ 12345); + binding = newBuilder().setTargetUserHandle(userHandle).build(); + binding.bind(); + shadowOf(getMainLooper()).idle(); + + assertThat(Build.VERSION.SDK_INT).isEqualTo(Build.VERSION_CODES.R); + assertThat(observer.unboundReason).isNull(); + } + + @Test + @Config(sdk = 28) + public void testBindServiceAsUser_returnsErrorWhenSdkBelowR() { + UserHandle userHandle = generateUserHandle(/* userId= */ 12345); + binding = newBuilder().setTargetUserHandle(userHandle).build(); + binding.bind(); + shadowOf(getMainLooper()).idle(); + + assertThat(observer.unboundReason.getCode()).isEqualTo(Code.INTERNAL); + assertThat(observer.unboundReason.getDescription()) + .isEqualTo("Cross user Channel requires Android R+"); + } + + @Test + @Config(sdk = 28) + public void testDevicePolicyBlind_returnsErrorWhenSdkBelowR() { + String deviceAdminClassName = "DevicePolicyAdmin"; + ComponentName adminComponent = new ComponentName(appContext, deviceAdminClassName); + allowBindDeviceAdminForUser(appContext, adminComponent, 10); + binding = + newBuilder() + .setTargetUserHandle(UserHandle.getUserHandleForUid(10)) + .setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent)) + .build(); + binding.bind(); + shadowOf(getMainLooper()).idle(); + + assertThat(observer.unboundReason.getCode()).isEqualTo(Code.INTERNAL); + assertThat(observer.unboundReason.getDescription()) + .isEqualTo("Device policy admin binding requires Android R+"); + } + @Test @Config(sdk = 30) public void testBindWithDeviceAdmin() throws Exception { diff --git a/cronet/build.gradle b/cronet/build.gradle index 3cc86201298..0715b4129bf 100644 --- a/cronet/build.gradle +++ b/cronet/build.gradle @@ -14,7 +14,7 @@ android { namespace = 'io.grpc.cronet' compileSdkVersion 33 defaultConfig { - minSdkVersion 21 + minSdkVersion 22 targetSdkVersion 33 versionCode 1 versionName "1.0" diff --git a/lint.xml b/lint.xml new file mode 100644 index 00000000000..93e2f603108 --- /dev/null +++ b/lint.xml @@ -0,0 +1,8 @@ + + + + + From 95d16d85c84b7bc587adaa349fa58c8fa80e1503 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 13 Aug 2025 12:59:57 -0700 Subject: [PATCH 345/591] Upgrade to Netty 4.1.124.Final This implicitly disables NettyAdaptiveCumulator (#11284), which can have a performance impact. We delayed upgrading Netty to give time to rework the optimization, but we've gone too long already without upgrading which causes problems for vulnerability tracking. --- MODULE.bazel | 24 +++++++++---------- SECURITY.md | 3 ++- gradle/libs.versions.toml | 4 ++-- .../io/grpc/netty/NettyClientHandlerTest.java | 3 +-- repositories.bzl | 24 +++++++++---------- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 5a731723c33..7d49c4e4b49 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -23,20 +23,20 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.truth:truth:1.4.2", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day - "io.netty:netty-buffer:4.1.110.Final", - "io.netty:netty-codec-http2:4.1.110.Final", - "io.netty:netty-codec-http:4.1.110.Final", - "io.netty:netty-codec-socks:4.1.110.Final", - "io.netty:netty-codec:4.1.110.Final", - "io.netty:netty-common:4.1.110.Final", - "io.netty:netty-handler-proxy:4.1.110.Final", - "io.netty:netty-handler:4.1.110.Final", - "io.netty:netty-resolver:4.1.110.Final", + "io.netty:netty-buffer:4.1.124.Final", + "io.netty:netty-codec-http2:4.1.124.Final", + "io.netty:netty-codec-http:4.1.124.Final", + "io.netty:netty-codec-socks:4.1.124.Final", + "io.netty:netty-codec:4.1.124.Final", + "io.netty:netty-common:4.1.124.Final", + "io.netty:netty-handler-proxy:4.1.124.Final", + "io.netty:netty-handler:4.1.124.Final", + "io.netty:netty-resolver:4.1.124.Final", "io.netty:netty-tcnative-boringssl-static:2.0.70.Final", "io.netty:netty-tcnative-classes:2.0.70.Final", - "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.110.Final", - "io.netty:netty-transport-native-unix-common:4.1.110.Final", - "io.netty:netty-transport:4.1.110.Final", + "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.124.Final", + "io.netty:netty-transport-native-unix-common:4.1.124.Final", + "io.netty:netty-transport:4.1.124.Final", "io.opencensus:opencensus-api:0.31.0", "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", diff --git a/SECURITY.md b/SECURITY.md index 48d6e0919a1..cd02fbe3e18 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -400,7 +400,8 @@ grpc-netty version | netty-handler version | netty-tcnative-boringssl-static ver 1.59.x | 4.1.97.Final | 2.0.61.Final 1.60.x-1.66.x | 4.1.100.Final | 2.0.61.Final 1.67.x-1.70.x | 4.1.110.Final | 2.0.65.Final -1.71.x- | 4.1.110.Final | 2.0.70.Final +1.71.x-1.74.x | 4.1.110.Final | 2.0.70.Final +1.75.x- | 4.1.124.Final | 2.0.72.Final _(grpc-netty-shaded avoids issues with keeping these versions in sync.)_ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9407b2a8774..540657b9ddb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -netty = '4.1.110.Final' +netty = '4.1.124.Final' # Keep the following references of tcnative version in sync whenever it's updated: # SECURITY.md -nettytcnative = '2.0.70.Final' +nettytcnative = '2.0.72.Final' opencensus = "0.31.1" # Not upgrading to 4.x as it is not yet ABI compatible. # https://github.com/protocolbuffers/protobuf/issues/17247 diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java index 5a2605eea5e..c5289296ed0 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java @@ -28,7 +28,6 @@ import static io.grpc.netty.Utils.STATUS_OK; import static io.grpc.netty.Utils.TE_HEADER; import static io.grpc.netty.Utils.TE_TRAILERS; -import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -276,7 +275,7 @@ public void cancelBufferedStreamShouldChangeClientStreamStatus() throws Exceptio public void createStreamShouldSucceed() throws Exception { createStream(); verifyWrite().writeHeaders(eq(ctx()), eq(STREAM_ID), eq(grpcHeaders), eq(0), - eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), any(ChannelPromise.class)); + eq(false), any(ChannelPromise.class)); } @Test diff --git a/repositories.bzl b/repositories.bzl index 5a760be5a43..47609ae7671 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -27,20 +27,20 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.truth:truth:1.4.2", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day - "io.netty:netty-buffer:4.1.110.Final", - "io.netty:netty-codec-http2:4.1.110.Final", - "io.netty:netty-codec-http:4.1.110.Final", - "io.netty:netty-codec-socks:4.1.110.Final", - "io.netty:netty-codec:4.1.110.Final", - "io.netty:netty-common:4.1.110.Final", - "io.netty:netty-handler-proxy:4.1.110.Final", - "io.netty:netty-handler:4.1.110.Final", - "io.netty:netty-resolver:4.1.110.Final", + "io.netty:netty-buffer:4.1.124.Final", + "io.netty:netty-codec-http2:4.1.124.Final", + "io.netty:netty-codec-http:4.1.124.Final", + "io.netty:netty-codec-socks:4.1.124.Final", + "io.netty:netty-codec:4.1.124.Final", + "io.netty:netty-common:4.1.124.Final", + "io.netty:netty-handler-proxy:4.1.124.Final", + "io.netty:netty-handler:4.1.124.Final", + "io.netty:netty-resolver:4.1.124.Final", "io.netty:netty-tcnative-boringssl-static:2.0.70.Final", "io.netty:netty-tcnative-classes:2.0.70.Final", - "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.110.Final", - "io.netty:netty-transport-native-unix-common:4.1.110.Final", - "io.netty:netty-transport:4.1.110.Final", + "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.124.Final", + "io.netty:netty-transport-native-unix-common:4.1.124.Final", + "io.netty:netty-transport:4.1.124.Final", "io.opencensus:opencensus-api:0.31.0", "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", From 6462ef9a11980e168c21d90bbc7245c728fd1a7a Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 14 Aug 2025 15:26:55 -0700 Subject: [PATCH 346/591] netty: Count sent RST_STREAMs against limit Http2RstCounterEncoder has to be constructed before NettyServerHandler/Http2ConnectionHandler so it must be static. Thus the code/counters were moved into RstStreamCounter which then can be constructed earlier and shared. This depends on Netty 4.1.124 for a bug fix to actually call the encoder: https://github.com/netty/netty/commit/be53dc3c9acd9af2e20d0c3c07cd77115a594cf1 --- .../io/grpc/netty/NettyServerHandler.java | 126 +++++++++++++----- .../io/grpc/netty/NettyServerHandlerTest.java | 44 ++++++ 2 files changed, 135 insertions(+), 35 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java index 668c3b5c90c..48f1aae91a1 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java @@ -60,6 +60,7 @@ import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http2.DecoratingHttp2ConnectionEncoder; import io.netty.handler.codec.http2.DecoratingHttp2FrameWriter; import io.netty.handler.codec.http2.DefaultHttp2Connection; import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder; @@ -83,6 +84,7 @@ import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2HeadersDecoder; import io.netty.handler.codec.http2.Http2InboundFrameLogger; +import io.netty.handler.codec.http2.Http2LifecycleManager; import io.netty.handler.codec.http2.Http2OutboundFrameLogger; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Stream; @@ -125,13 +127,11 @@ class NettyServerHandler extends AbstractNettyHandler { private final long keepAliveTimeoutInNanos; private final long maxConnectionAgeInNanos; private final long maxConnectionAgeGraceInNanos; - private final int maxRstCount; - private final long maxRstPeriodNanos; + private final RstStreamCounter rstStreamCounter; private final List streamTracerFactories; private final TransportTracer transportTracer; private final KeepAliveEnforcer keepAliveEnforcer; private final Attributes eagAttributes; - private final Ticker ticker; /** Incomplete attributes produced by negotiator. */ private Attributes negotiationAttributes; private InternalChannelz.Security securityInfo; @@ -149,8 +149,6 @@ class NettyServerHandler extends AbstractNettyHandler { private ScheduledFuture maxConnectionAgeMonitor; @CheckForNull private GracefulShutdown gracefulShutdown; - private int rstCount; - private long lastRstNanoTime; static NettyServerHandler newHandler( ServerTransportListener transportListener, @@ -251,6 +249,12 @@ static NettyServerHandler newHandler( final KeepAliveEnforcer keepAliveEnforcer = new KeepAliveEnforcer( permitKeepAliveWithoutCalls, permitKeepAliveTimeInNanos, TimeUnit.NANOSECONDS); + if (ticker == null) { + ticker = Ticker.systemTicker(); + } + + RstStreamCounter rstStreamCounter + = new RstStreamCounter(maxRstCount, maxRstPeriodNanos, ticker); // Create the local flow controller configured to auto-refill the connection window. connection.local().flowController( new DefaultHttp2LocalFlowController(connection, DEFAULT_WINDOW_UPDATE_RATIO, true)); @@ -258,6 +262,7 @@ static NettyServerHandler newHandler( Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(connection, frameWriter); encoder = new Http2ControlFrameLimitEncoder(encoder, 10000); + encoder = new Http2RstCounterEncoder(encoder, rstStreamCounter); Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader); @@ -266,10 +271,6 @@ static NettyServerHandler newHandler( settings.maxConcurrentStreams(maxStreams); settings.maxHeaderListSize(maxHeaderListSize); - if (ticker == null) { - ticker = Ticker.systemTicker(); - } - return new NettyServerHandler( channelUnused, connection, @@ -286,8 +287,7 @@ static NettyServerHandler newHandler( maxConnectionAgeInNanos, maxConnectionAgeGraceInNanos, keepAliveEnforcer, autoFlowControl, - maxRstCount, - maxRstPeriodNanos, + rstStreamCounter, eagAttributes, ticker); } @@ -310,8 +310,7 @@ private NettyServerHandler( long maxConnectionAgeGraceInNanos, final KeepAliveEnforcer keepAliveEnforcer, boolean autoFlowControl, - int maxRstCount, - long maxRstPeriodNanos, + RstStreamCounter rstStreamCounter, Attributes eagAttributes, Ticker ticker) { super( @@ -363,12 +362,9 @@ public void onStreamClosed(Http2Stream stream) { this.maxConnectionAgeInNanos = maxConnectionAgeInNanos; this.maxConnectionAgeGraceInNanos = maxConnectionAgeGraceInNanos; this.keepAliveEnforcer = checkNotNull(keepAliveEnforcer, "keepAliveEnforcer"); - this.maxRstCount = maxRstCount; - this.maxRstPeriodNanos = maxRstPeriodNanos; + this.rstStreamCounter = rstStreamCounter; this.eagAttributes = checkNotNull(eagAttributes, "eagAttributes"); - this.ticker = checkNotNull(ticker, "ticker"); - this.lastRstNanoTime = ticker.read(); streamKey = encoder.connection().newKey(); this.transportListener = checkNotNull(transportListener, "transportListener"); this.streamTracerFactories = checkNotNull(streamTracerFactories, "streamTracerFactories"); @@ -575,24 +571,9 @@ private void onDataRead(int streamId, ByteBuf data, int padding, boolean endOfSt } private void onRstStreamRead(int streamId, long errorCode) throws Http2Exception { - if (maxRstCount > 0) { - long now = ticker.read(); - if (now - lastRstNanoTime > maxRstPeriodNanos) { - lastRstNanoTime = now; - rstCount = 1; - } else { - rstCount++; - if (rstCount > maxRstCount) { - throw new Http2Exception(Http2Error.ENHANCE_YOUR_CALM, "too_many_rststreams") { - @SuppressWarnings("UnsynchronizedOverridesSynchronized") // No memory accesses - @Override - public Throwable fillInStackTrace() { - // Avoid the CPU cycles, since the resets may be a CPU consumption attack - return this; - } - }; - } - } + Http2Exception tooManyRstStream = rstStreamCounter.countRstStream(); + if (tooManyRstStream != null) { + throw tooManyRstStream; } try { @@ -1180,6 +1161,81 @@ public ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId, Http2 } } + private static final class Http2RstCounterEncoder extends DecoratingHttp2ConnectionEncoder { + private final RstStreamCounter rstStreamCounter; + private Http2LifecycleManager lifecycleManager; + + Http2RstCounterEncoder(Http2ConnectionEncoder encoder, RstStreamCounter rstStreamCounter) { + super(encoder); + this.rstStreamCounter = rstStreamCounter; + } + + @Override + public void lifecycleManager(Http2LifecycleManager lifecycleManager) { + this.lifecycleManager = lifecycleManager; + super.lifecycleManager(lifecycleManager); + } + + @Override + public ChannelFuture writeRstStream( + ChannelHandlerContext ctx, int streamId, long errorCode, ChannelPromise promise) { + ChannelFuture future = super.writeRstStream(ctx, streamId, errorCode, promise); + // We want to count "induced" RST_STREAM, where the server sent a reset because of a malformed + // frame. + boolean normalRst + = errorCode == Http2Error.NO_ERROR.code() || errorCode == Http2Error.CANCEL.code(); + if (!normalRst) { + Http2Exception tooManyRstStream = rstStreamCounter.countRstStream(); + if (tooManyRstStream != null) { + lifecycleManager.onError(ctx, true, tooManyRstStream); + ctx.close(); + } + } + return future; + } + } + + private static final class RstStreamCounter { + private final int maxRstCount; + private final long maxRstPeriodNanos; + private final Ticker ticker; + private int rstCount; + private long lastRstNanoTime; + + RstStreamCounter(int maxRstCount, long maxRstPeriodNanos, Ticker ticker) { + checkArgument(maxRstCount >= 0, "maxRstCount must be non-negative: %s", maxRstCount); + this.maxRstCount = maxRstCount; + this.maxRstPeriodNanos = maxRstPeriodNanos; + this.ticker = checkNotNull(ticker, "ticker"); + this.lastRstNanoTime = ticker.read(); + } + + /** Returns non-{@code null} when the connection should be killed by the caller. */ + private Http2Exception countRstStream() { + if (maxRstCount == 0) { + return null; + } + long now = ticker.read(); + if (now - lastRstNanoTime > maxRstPeriodNanos) { + lastRstNanoTime = now; + rstCount = 1; + } else { + rstCount++; + if (rstCount > maxRstCount) { + return new Http2Exception(Http2Error.ENHANCE_YOUR_CALM, "too_many_rststreams") { + @SuppressWarnings("UnsynchronizedOverridesSynchronized") // No memory accesses + @Override + public Throwable fillInStackTrace() { + // Avoid the CPU cycles, since the resets may be a CPU consumption attack + return this; + } + }; + } + } + return null; + } + } + private static class ServerChannelLogger extends ChannelLogger { private static final Logger log = Logger.getLogger(ChannelLogger.class.getName()); diff --git a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java index 54c1375eef2..28217937adc 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java @@ -1304,6 +1304,8 @@ public void maxRstCount_exceedsLimit_fails() throws Exception { } private void rapidReset(int burstSize) throws Exception { + when(streamTracerFactory.newServerStreamTracer(anyString(), any(Metadata.class))) + .thenAnswer((args) -> new TestServerStreamTracer()); Http2Headers headers = new DefaultHttp2Headers() .method(HTTP_METHOD) .set(CONTENT_TYPE_HEADER, new AsciiString("application/grpc", UTF_8)) @@ -1323,6 +1325,48 @@ private void rapidReset(int burstSize) throws Exception { } } + @Test + public void maxRstCountSent_withinLimit_succeeds() throws Exception { + maxRstCount = 10; + maxRstPeriodNanos = TimeUnit.MILLISECONDS.toNanos(100); + manualSetUp(); + madeYouReset(maxRstCount); + + assertTrue(channel().isOpen()); + } + + @Test + public void maxRstCountSent_exceedsLimit_fails() throws Exception { + maxRstCount = 10; + maxRstPeriodNanos = TimeUnit.MILLISECONDS.toNanos(100); + manualSetUp(); + assertThrows(ClosedChannelException.class, () -> madeYouReset(maxRstCount + 1)); + + assertFalse(channel().isOpen()); + } + + private void madeYouReset(int burstSize) throws Exception { + when(streamTracerFactory.newServerStreamTracer(anyString(), any(Metadata.class))) + .thenAnswer((args) -> new TestServerStreamTracer()); + Http2Headers headers = new DefaultHttp2Headers() + .method(HTTP_METHOD) + .set(CONTENT_TYPE_HEADER, new AsciiString("application/grpc", UTF_8)) + .set(TE_HEADER, TE_TRAILERS) + .path(new AsciiString("/foo/bar")); + int streamId = 1; + long rpcTimeNanos = maxRstPeriodNanos / 2 / burstSize; + for (int period = 0; period < 3; period++) { + for (int i = 0; i < burstSize; i++) { + channelRead(headersFrame(streamId, headers)); + channelRead(windowUpdate(streamId, 0)); + streamId += 2; + fakeClock().forwardNanos(rpcTimeNanos); + } + while (channel().readOutbound() != null) {} + fakeClock().forwardNanos(maxRstPeriodNanos - rpcTimeNanos * burstSize + 1); + } + } + private void createStream() throws Exception { Http2Headers headers = new DefaultHttp2Headers() .method(HTTP_METHOD) From 437e03dc98d98f6a2875daf8ac40b00de9002612 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 19 Aug 2025 00:23:47 -0700 Subject: [PATCH 347/591] xds: Avoid PriorityLb re-enabling timer on duplicate CONNECTING (#12289) Since c4256add4 we no longer fabricate a TRANSIENT_FAILURE update from children. However, previously that would have set seenReadyOrIdleSinceTransientFailure = false and prevented future timer creation. If a LB policy gives extraneous updates with state CONNECTING, then it was possible to re-create failOverTimer which would then wait the 10 seconds for the child to finish CONNECTING. We only want to give the child one opportunity after transitioning out of READY/IDLE. https://github.com/grpc/proposal/pull/509 --- .../io/grpc/xds/PriorityLoadBalancer.java | 3 +- .../io/grpc/xds/PriorityLoadBalancerTest.java | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java b/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java index c72cef9f637..6e4566de76d 100644 --- a/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java @@ -320,13 +320,14 @@ public void updateBalancingState(final ConnectivityState newState, if (!children.containsKey(priority)) { return; } + ConnectivityState oldState = connectivityState; connectivityState = newState; picker = newPicker; if (deletionTimer != null && deletionTimer.isPending()) { return; } - if (newState.equals(CONNECTING)) { + if (newState.equals(CONNECTING) && !oldState.equals(newState)) { if (!failOverTimer.isPending() && seenReadyOrIdleSinceTransientFailure) { failOverTimer = syncContext.schedule(new FailOverTask(), 10, TimeUnit.SECONDS, executor); diff --git a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java index ab4c00398f0..beb568be9ce 100644 --- a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -70,6 +71,7 @@ import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; import org.mockito.Captor; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -554,6 +556,55 @@ public void connectingResetFailOverIfSeenReadyOrIdleSinceTransientFailure() { assertThat(fooHelpers).hasSize(2); } + @Test + public void failoverTimerNotRestartedOnDupConnecting() { + InOrder inOrder = inOrder(helper); + PriorityChildConfig priorityChildConfig0 = + new PriorityChildConfig(newChildConfig(fooLbProvider, new Object()), true); + PriorityChildConfig priorityChildConfig1 = + new PriorityChildConfig(newChildConfig(fooLbProvider, new Object()), true); + PriorityLbConfig priorityLbConfig = + new PriorityLbConfig( + ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1), + ImmutableList.of("p0", "p1")); + priorityLb.acceptResolvedAddresses( + ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.of()) + .setLoadBalancingPolicyConfig(priorityLbConfig) + .build()); + // Nothing important about this verify, other than to provide a baseline + inOrder.verify(helper) + .updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult())); + assertThat(fooBalancers).hasSize(1); + assertThat(fooHelpers).hasSize(1); + Helper helper0 = Iterables.getOnlyElement(fooHelpers); + + // Cause seenReadyOrIdleSinceTransientFailure = true + helper0.updateBalancingState(IDLE, EMPTY_PICKER); + inOrder.verify(helper) + .updateBalancingState(eq(IDLE), pickerReturns(PickResult.withNoResult())); + helper0.updateBalancingState(CONNECTING, EMPTY_PICKER); + + // p0 keeps repeating CONNECTING, failover happens + fakeClock.forwardTime(5, TimeUnit.SECONDS); + helper0.updateBalancingState(CONNECTING, EMPTY_PICKER); + fakeClock.forwardTime(5, TimeUnit.SECONDS); + assertThat(fooBalancers).hasSize(2); + assertThat(fooHelpers).hasSize(2); + inOrder.verify(helper, times(2)) + .updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult())); + Helper helper1 = Iterables.getLast(fooHelpers); + + // p0 keeps repeating CONNECTING, no reset of failover timer + helper1.updateBalancingState(IDLE, EMPTY_PICKER); // Stop timer for p1 + inOrder.verify(helper) + .updateBalancingState(eq(IDLE), pickerReturns(PickResult.withNoResult())); + helper0.updateBalancingState(CONNECTING, EMPTY_PICKER); + fakeClock.forwardTime(10, TimeUnit.SECONDS); + inOrder.verify(helper, never()) + .updateBalancingState(eq(CONNECTING), any()); + } + @Test public void readyToConnectDoesNotFailOverButUpdatesPicker() { PriorityChildConfig priorityChildConfig0 = From 43bef65cf90517a4aa5a35f5e3cec0863f624edc Mon Sep 17 00:00:00 2001 From: Jiri Kaplan Date: Mon, 28 Jul 2025 16:16:33 +0200 Subject: [PATCH 348/591] netty: Support BCJSSE provider in GrpcSslContexts --- netty/src/main/java/io/grpc/netty/GrpcSslContexts.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/GrpcSslContexts.java b/netty/src/main/java/io/grpc/netty/GrpcSslContexts.java index 04a290165d7..f1f2c8aed71 100644 --- a/netty/src/main/java/io/grpc/netty/GrpcSslContexts.java +++ b/netty/src/main/java/io/grpc/netty/GrpcSslContexts.java @@ -84,6 +84,7 @@ private GrpcSslContexts() {} private static final String SUN_PROVIDER_NAME = "SunJSSE"; private static final String IBM_PROVIDER_NAME = "IBMJSSE2"; private static final String OPENJSSE_PROVIDER_NAME = "OpenJSSE"; + private static final String BCJSSE_PROVIDER_NAME = "BCJSSE"; /** * Creates an SslContextBuilder with ciphers and APN appropriate for gRPC. @@ -199,7 +200,8 @@ public static SslContextBuilder configure(SslContextBuilder builder, Provider jd jdkProvider.getName() + " selected, but Java 9+ and Jetty NPN/ALPN unavailable"); } } else if (IBM_PROVIDER_NAME.equals(jdkProvider.getName()) - || OPENJSSE_PROVIDER_NAME.equals(jdkProvider.getName())) { + || OPENJSSE_PROVIDER_NAME.equals(jdkProvider.getName()) + || BCJSSE_PROVIDER_NAME.equals(jdkProvider.getName())) { if (JettyTlsUtil.isJava9AlpnAvailable()) { apc = ALPN; } else { @@ -255,7 +257,8 @@ private static Provider findJdkProvider() { return provider; } } else if (IBM_PROVIDER_NAME.equals(provider.getName()) - || OPENJSSE_PROVIDER_NAME.equals(provider.getName())) { + || OPENJSSE_PROVIDER_NAME.equals(provider.getName()) + || BCJSSE_PROVIDER_NAME.equals(provider.getName())) { if (JettyTlsUtil.isJava9AlpnAvailable()) { return provider; } From 2039266ebc904866523c3151575b530481c0bed6 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 19 Aug 2025 21:41:52 +0530 Subject: [PATCH 349/591] xds: xdsClient caches transient error for new watchers (#12262) --- .../io/grpc/xds/client/XdsClientImpl.java | 10 ++++++++++ .../grpc/xds/GrpcXdsClientImplTestBase.java | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index d3384cbbe4e..4c6823f844a 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -676,6 +676,8 @@ private final class ResourceSubscriber { private ResourceMetadata metadata; @Nullable private String errorDescription; + @Nullable + private Status lastError; ResourceSubscriber(XdsResourceType type, String resource) { syncContext.throwIfNotInThisSynchronizationContext(); @@ -712,11 +714,16 @@ void addWatcher(ResourceWatcher watcher, Executor watcherExecutor) { watchers.put(watcher, watcherExecutor); T savedData = data; boolean savedAbsent = absent; + Status savedError = lastError; watcherExecutor.execute(() -> { if (errorDescription != null) { watcher.onError(Status.INVALID_ARGUMENT.withDescription(errorDescription)); return; } + if (savedError != null) { + watcher.onError(savedError); + return; + } if (savedData != null) { notifyWatcher(watcher, savedData); } else if (savedAbsent) { @@ -808,6 +815,7 @@ void onData(ParsedResource parsedResource, String version, long updateTime, this.metadata = ResourceMetadata .newResourceMetadataAcked(parsedResource.getRawResource(), version, updateTime); absent = false; + lastError = null; if (resourceDeletionIgnored) { logger.log(XdsLogLevel.FORCE_INFO, "xds server {0}: server returned new version " + "of resource for which we previously ignored a deletion: type {1} name {2}", @@ -857,6 +865,7 @@ void onAbsent(@Nullable ProcessingTracker processingTracker, ServerInfo serverIn if (!absent) { data = null; absent = true; + lastError = null; metadata = serverInfo.resourceTimerIsTransientError() ? ResourceMetadata.newResourceMetadataTimeout() : ResourceMetadata.newResourceMetadataDoesNotExist(); @@ -894,6 +903,7 @@ void onError(Status error, @Nullable ProcessingTracker tracker) { Status errorAugmented = Status.fromCode(error.getCode()) .withDescription(description + "nodeID: " + bootstrapInfo.node().getId()) .withCause(error.getCause()); + this.lastError = errorAugmented; for (ResourceWatcher watcher : watchers.keySet()) { if (tracker != null) { diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 89668485b96..19266b0d289 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -285,6 +285,8 @@ public long currentTimeNanos() { @Mock private ResourceWatcher ldsResourceWatcher; @Mock + private ResourceWatcher ldsResourceWatcher2; + @Mock private ResourceWatcher rdsResourceWatcher; @Mock private ResourceWatcher cdsResourceWatcher; @@ -694,6 +696,24 @@ public void ldsResourceUpdated_withXdstpResourceName_withUnknownAuthority() { assertThat(resourceDiscoveryCalls.poll()).isNull(); } + @Test + public void ldsResource_onError_cachedForNewWatcher() { + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, + ldsResourceWatcher); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + call.sendCompleted(); + verify(ldsResourceWatcher).onError(errorCaptor.capture()); + Status initialError = errorCaptor.getValue(); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, + ldsResourceWatcher2); + ArgumentCaptor secondErrorCaptor = ArgumentCaptor.forClass(Status.class); + verify(ldsResourceWatcher2).onError(secondErrorCaptor.capture()); + Status cachedError = secondErrorCaptor.getValue(); + + assertThat(cachedError).isEqualTo(initialError); + assertThat(resourceDiscoveryCalls.poll()).isNull(); + } + @Test public void ldsResponseErrorHandling_allResourcesFailedUnpack() { DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE, From afdbecb235a4e7d88524e0c4532bfe1a7c097676 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 21 Aug 2025 16:08:58 -0700 Subject: [PATCH 350/591] binder: Move BinderTransport's inner classes to the top level (#12303) BinderTransport.java was getting too long and deeply nested. This is a pure refactor with no behavior changes. --- .../internal/BinderClientTransportTest.java | 7 +- .../binder/internal/BinderTransportTest.java | 3 +- .../internal/ActiveTransportTracker.java | 4 +- .../internal/BinderClientTransport.java | 441 +++++++++++++++ .../BinderClientTransportFactory.java | 4 +- .../io/grpc/binder/internal/BinderServer.java | 4 +- .../internal/BinderServerTransport.java | 126 +++++ .../grpc/binder/internal/BinderTransport.java | 513 +----------------- .../java/io/grpc/binder/internal/Inbound.java | 5 +- .../internal/BinderServerTransportTest.java | 4 +- .../BinderClientTransportBuilder.java | 6 +- 11 files changed, 587 insertions(+), 530 deletions(-) create mode 100644 binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java create mode 100644 binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java index fe2fd587453..0038a054854 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java @@ -100,7 +100,7 @@ public final class BinderClientTransportTest { .build(); AndroidComponentAddress serverAddress; - BinderTransport.BinderClientTransport transport; + BinderClientTransport transport; BlockingSecurityPolicy blockingSecurityPolicy = new BlockingSecurityPolicy(); private final ObjectPool executorServicePool = @@ -178,7 +178,7 @@ public BinderClientTransportBuilder setPreAuthorizeServer(boolean preAuthorizeSe return this; } - public BinderTransport.BinderClientTransport build() { + public BinderClientTransport build() { return factoryBuilder .buildClientTransportFactory() .newClientTransport(serverAddress, new ClientTransportOptions(), null); @@ -502,8 +502,7 @@ public void testAsyncSecurityPolicyCancelledUponExternalTermination() throws Exc } private static void startAndAwaitReady( - BinderTransport.BinderClientTransport transport, TestTransportListener transportListener) - throws Exception { + BinderClientTransport transport, TestTransportListener transportListener) throws Exception { transport.start(transportListener).run(); transportListener.awaitReady(); } diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderTransportTest.java index fc9a383d572..7932cabde89 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderTransportTest.java @@ -106,8 +106,7 @@ protected ManagedClientTransport newClientTransport(InternalServer server) { options.setEagAttributes(eagAttrs()); options.setChannelLogger(transportLogger()); - return new BinderTransport.BinderClientTransport( - builder.buildClientTransportFactory(), addr, options); + return new BinderClientTransport(builder.buildClientTransportFactory(), addr, options); } @Test diff --git a/binder/src/main/java/io/grpc/binder/internal/ActiveTransportTracker.java b/binder/src/main/java/io/grpc/binder/internal/ActiveTransportTracker.java index 2bfa9fea4cb..01505bfd509 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ActiveTransportTracker.java +++ b/binder/src/main/java/io/grpc/binder/internal/ActiveTransportTracker.java @@ -11,8 +11,8 @@ import io.grpc.internal.ServerTransportListener; /** - * Tracks which {@link BinderTransport.BinderServerTransport} are currently active and allows - * invoking a {@link Runnable} only once all transports are terminated. + * Tracks which {@link BinderServerTransport} are currently active and allows invoking a {@link + * Runnable} only once all transports are terminated. */ final class ActiveTransportTracker implements ServerListener { private final ServerListener delegate; diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java new file mode 100644 index 00000000000..95bd531aa41 --- /dev/null +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -0,0 +1,441 @@ +/* + * 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.binder.internal; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.content.pm.ServiceInfo; +import android.os.Binder; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Process; +import com.google.common.base.Ticker; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.grpc.Attributes; +import io.grpc.CallOptions; +import io.grpc.ClientStreamTracer; +import io.grpc.Grpc; +import io.grpc.Internal; +import io.grpc.InternalLogId; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.SecurityLevel; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.binder.AndroidComponentAddress; +import io.grpc.binder.AsyncSecurityPolicy; +import io.grpc.binder.InboundParcelablePolicy; +import io.grpc.binder.SecurityPolicy; +import io.grpc.internal.ClientStream; +import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; +import io.grpc.internal.ConnectionClientTransport; +import io.grpc.internal.FailingClientStream; +import io.grpc.internal.GrpcAttributes; +import io.grpc.internal.GrpcUtil; +import io.grpc.internal.ManagedClientTransport; +import io.grpc.internal.ObjectPool; +import io.grpc.internal.StatsTraceContext; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** Concrete client-side transport implementation. */ +@ThreadSafe +@Internal +public final class BinderClientTransport extends BinderTransport + implements ConnectionClientTransport, Bindable.Observer { + + private final ObjectPool offloadExecutorPool; + private final Executor offloadExecutor; + private final SecurityPolicy securityPolicy; + private final Bindable serviceBinding; + + /** Number of ongoing calls which keep this transport "in-use". */ + private final AtomicInteger numInUseStreams; + + private final long readyTimeoutMillis; + private final PingTracker pingTracker; + private final boolean preAuthorizeServer; + + @Nullable private ManagedClientTransport.Listener clientTransportListener; + + @GuardedBy("this") + private int latestCallId = FIRST_CALL_ID; + + @GuardedBy("this") + private ScheduledFuture readyTimeoutFuture; // != null iff timeout scheduled. + + @GuardedBy("this") + @Nullable + private ListenableFuture authResultFuture; // null before we check auth. + + @GuardedBy("this") + @Nullable + private ListenableFuture preAuthResultFuture; // null before we pre-auth. + + /** + * Constructs a new transport instance. + * + * @param factory parameters common to all a Channel's transports + * @param targetAddress the fully resolved and load-balanced server address + * @param options other parameters that can vary as transports come and go within a Channel + */ + public BinderClientTransport( + BinderClientTransportFactory factory, + AndroidComponentAddress targetAddress, + ClientTransportOptions options) { + super( + factory.scheduledExecutorPool, + buildClientAttributes( + options.getEagAttributes(), + factory.sourceContext, + targetAddress, + factory.inboundParcelablePolicy), + factory.binderDecorator, + buildLogId(factory.sourceContext, targetAddress)); + this.offloadExecutorPool = factory.offloadExecutorPool; + this.securityPolicy = factory.securityPolicy; + this.offloadExecutor = offloadExecutorPool.getObject(); + this.readyTimeoutMillis = factory.readyTimeoutMillis; + Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE); + this.preAuthorizeServer = + preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; + numInUseStreams = new AtomicInteger(); + pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); + + serviceBinding = + new ServiceBinding( + factory.mainThreadExecutor, + factory.sourceContext, + factory.channelCredentials, + targetAddress.asBindIntent(), + targetAddress.getTargetUser() != null + ? targetAddress.getTargetUser() + : factory.defaultTargetUserHandle, + factory.bindServiceFlags.toInteger(), + this); + } + + @Override + void releaseExecutors() { + super.releaseExecutors(); + offloadExecutorPool.returnObject(offloadExecutor); + } + + @Override + public synchronized void onBound(IBinder binder) { + sendSetupTransaction(binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + } + + @Override + public synchronized void onUnbound(Status reason) { + shutdownInternal(reason, true); + } + + @CheckReturnValue + @Override + public synchronized Runnable start(Listener clientTransportListener) { + this.clientTransportListener = checkNotNull(clientTransportListener); + return () -> { + synchronized (BinderClientTransport.this) { + if (inState(TransportState.NOT_STARTED)) { + setState(TransportState.SETUP); + try { + if (preAuthorizeServer) { + preAuthorize(serviceBinding.resolve()); + } else { + serviceBinding.bind(); + } + } catch (StatusException e) { + shutdownInternal(e.getStatus(), true); + return; + } + if (readyTimeoutMillis >= 0) { + readyTimeoutFuture = + getScheduledExecutorService() + .schedule( + BinderClientTransport.this::onReadyTimeout, + readyTimeoutMillis, + MILLISECONDS); + } + } + } + }; + } + + @GuardedBy("this") + private void preAuthorize(ServiceInfo serviceInfo) { + // It's unlikely, but the identity/existence of this Service could change by the time we + // actually connect. It doesn't matter though, because: + // - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer + // will eventually retry using a new transport instance that will see the Service's new state. + // - If pre-auth succeeds (but would fail against the server's new state), we might give an + // unauthorized server a chance to run, but the connection will still fail by SecurityPolicy + // check later in handshake. Pre-auth remains effective at mitigating abuse because malware + // can't typically control the exact timing of its installation. + preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid); + Futures.addCallback( + preAuthResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + handlePreAuthResult(result); + } + + @Override + public void onFailure(Throwable t) { + handleAuthResult(t); + } + }, + offloadExecutor); + } + + private synchronized void handlePreAuthResult(Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else { + serviceBinding.bind(); + } + } + } + + private synchronized void onReadyTimeout() { + if (inState(TransportState.SETUP)) { + readyTimeoutFuture = null; + shutdownInternal( + Status.DEADLINE_EXCEEDED.withDescription( + "Connect timeout " + readyTimeoutMillis + "ms lapsed"), + true); + } + } + + @Override + public synchronized ClientStream newStream( + final MethodDescriptor method, + final Metadata headers, + final CallOptions callOptions, + ClientStreamTracer[] tracers) { + if (!inState(TransportState.READY)) { + return newFailingClientStream( + isShutdown() + ? shutdownStatus + : Status.INTERNAL.withDescription("newStream() before transportReady()"), + attributes, + headers, + tracers); + } + + int callId = latestCallId++; + if (latestCallId == LAST_CALL_ID) { + latestCallId = FIRST_CALL_ID; + } + StatsTraceContext statsTraceContext = + StatsTraceContext.newClientContext(tracers, attributes, headers); + Inbound.ClientInbound inbound = + new Inbound.ClientInbound( + this, attributes, callId, GrpcUtil.shouldBeCountedForInUse(callOptions)); + if (ongoingCalls.putIfAbsent(callId, inbound) != null) { + Status failure = Status.INTERNAL.withDescription("Clashing call IDs"); + shutdownInternal(failure, true); + return newFailingClientStream(failure, attributes, headers, tracers); + } else { + if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) { + clientTransportListener.transportInUse(true); + } + Outbound.ClientOutbound outbound = + new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext); + if (method.getType().clientSendsOneMessage()) { + return new SingleMessageClientStream(inbound, outbound, attributes); + } else { + return new MultiMessageClientStream(inbound, outbound, attributes); + } + } + } + + @Override + protected void unregisterInbound(Inbound inbound) { + if (inbound.countsForInUse() && numInUseStreams.decrementAndGet() == 0) { + clientTransportListener.transportInUse(false); + } + super.unregisterInbound(inbound); + } + + @Override + public void ping(final PingCallback callback, Executor executor) { + pingTracker.startPing(callback, executor); + } + + @Override + public synchronized void shutdown(Status reason) { + checkNotNull(reason, "reason"); + shutdownInternal(reason, false); + } + + @Override + public synchronized void shutdownNow(Status reason) { + checkNotNull(reason, "reason"); + shutdownInternal(reason, true); + } + + @Override + @GuardedBy("this") + void notifyShutdown(Status status) { + clientTransportListener.transportShutdown(status); + } + + @Override + @GuardedBy("this") + void notifyTerminated() { + if (numInUseStreams.getAndSet(0) > 0) { + clientTransportListener.transportInUse(false); + } + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; + } + if (preAuthResultFuture != null) { + preAuthResultFuture.cancel(false); // No effect if already complete. + } + if (authResultFuture != null) { + authResultFuture.cancel(false); // No effect if already complete. + } + serviceBinding.unbind(); + clientTransportListener.transportTerminated(); + } + + @Override + @GuardedBy("this") + protected void handleSetupTransport(Parcel parcel) { + int remoteUid = Binder.getCallingUid(); + attributes = setSecurityAttrs(attributes, remoteUid); + if (inState(TransportState.SETUP)) { + int version = parcel.readInt(); + IBinder binder = parcel.readStrongBinder(); + if (version != WIRE_FORMAT_VERSION) { + shutdownInternal(Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true); + } else if (binder == null) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); + } else { + authResultFuture = checkServerAuthorizationAsync(remoteUid); + Futures.addCallback( + authResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + handleAuthResult(binder, result); + } + + @Override + public void onFailure(Throwable t) { + handleAuthResult(t); + } + }, + offloadExecutor); + } + } + } + + private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { + return (securityPolicy instanceof AsyncSecurityPolicy) + ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) + : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); + } + + private synchronized void handleAuthResult(IBinder binder, Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + } else { + // Check state again, since a failure inside setOutgoingBinder (or a callback it + // triggers), could have shut us down. + if (!isShutdown()) { + setState(TransportState.READY); + attributes = clientTransportListener.filterTransport(attributes); + clientTransportListener.transportReady(); + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; + } + } + } + } + } + + private synchronized void handleAuthResult(Throwable t) { + shutdownInternal( + Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true); + } + + @GuardedBy("this") + @Override + protected void handlePingResponse(Parcel parcel) { + pingTracker.onPingResponse(parcel.readInt()); + } + + private static ClientStream newFailingClientStream( + Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) { + StatsTraceContext statsTraceContext = + StatsTraceContext.newClientContext(tracers, attributes, headers); + statsTraceContext.clientOutboundHeaders(); + return new FailingClientStream(failure, tracers); + } + + private static InternalLogId buildLogId( + Context sourceContext, AndroidComponentAddress targetAddress) { + return InternalLogId.allocate( + BinderClientTransport.class, + sourceContext.getClass().getSimpleName() + "->" + targetAddress); + } + + private static Attributes buildClientAttributes( + Attributes eagAttrs, + Context sourceContext, + AndroidComponentAddress targetAddress, + InboundParcelablePolicy inboundParcelablePolicy) { + return Attributes.newBuilder() + .set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.NONE) // Trust noone for now. + .set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs) + .set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, AndroidComponentAddress.forContext(sourceContext)) + .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, targetAddress) + .set(INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy) + .build(); + } + + private static Attributes setSecurityAttrs(Attributes attributes, int uid) { + return attributes.toBuilder() + .set(REMOTE_UID, uid) + .set( + GrpcAttributes.ATTR_SECURITY_LEVEL, + uid == Process.myUid() + ? SecurityLevel.PRIVACY_AND_INTEGRITY + : SecurityLevel.INTEGRITY) // TODO: Have the SecrityPolicy decide this. + .build(); + } +} diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java index 85a0ddd35b7..3f51452c90c 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java @@ -83,12 +83,12 @@ private BinderClientTransportFactory(Builder builder) { } @Override - public BinderTransport.BinderClientTransport newClientTransport( + public BinderClientTransport newClientTransport( SocketAddress addr, ClientTransportOptions options, ChannelLogger channelLogger) { if (closed) { throw new IllegalStateException("The transport factory is closed."); } - return new BinderTransport.BinderClientTransport(this, (AndroidComponentAddress) addr, options); + return new BinderClientTransport(this, (AndroidComponentAddress) addr, options); } @Override diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderServer.java b/binder/src/main/java/io/grpc/binder/internal/BinderServer.java index 6b8347390b9..fca8e3d88e1 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderServer.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderServer.java @@ -178,8 +178,8 @@ public synchronized boolean handleTransaction(int code, Parcel parcel) { serverPolicyChecker, checkNotNull(executor, "Not started?")); // Create a new transport and let our listener know about it. - BinderTransport.BinderServerTransport transport = - new BinderTransport.BinderServerTransport( + BinderServerTransport transport = + new BinderServerTransport( executorServicePool, attrsBuilder.build(), streamTracerFactories, diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java new file mode 100644 index 00000000000..1c345249735 --- /dev/null +++ b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java @@ -0,0 +1,126 @@ +/* + * 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.binder.internal; + +import android.os.IBinder; +import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.grpc.Attributes; +import io.grpc.Grpc; +import io.grpc.Internal; +import io.grpc.InternalLogId; +import io.grpc.Metadata; +import io.grpc.ServerStreamTracer; +import io.grpc.Status; +import io.grpc.internal.ObjectPool; +import io.grpc.internal.ServerStream; +import io.grpc.internal.ServerTransport; +import io.grpc.internal.ServerTransportListener; +import io.grpc.internal.StatsTraceContext; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import javax.annotation.Nullable; + +/** Concrete server-side transport implementation. */ +@Internal +public final class BinderServerTransport extends BinderTransport implements ServerTransport { + + private final List streamTracerFactories; + @Nullable private ServerTransportListener serverTransportListener; + + /** + * Constructs a new transport instance. + * + * @param binderDecorator used to decorate 'callbackBinder', for fault injection. + */ + public BinderServerTransport( + ObjectPool executorServicePool, + Attributes attributes, + List streamTracerFactories, + OneWayBinderProxy.Decorator binderDecorator, + IBinder callbackBinder) { + super(executorServicePool, attributes, binderDecorator, buildLogId(attributes)); + this.streamTracerFactories = streamTracerFactories; + // TODO(jdcormie): Plumb in the Server's executor() and use it here instead. + setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService())); + } + + public synchronized void setServerTransportListener( + ServerTransportListener serverTransportListener) { + this.serverTransportListener = serverTransportListener; + if (isShutdown()) { + setState(TransportState.SHUTDOWN_TERMINATED); + notifyTerminated(); + releaseExecutors(); + } else { + sendSetupTransaction(); + // Check we're not shutdown again, since a failure inside sendSetupTransaction (or a callback + // it triggers), could have shut us down. + if (!isShutdown()) { + setState(TransportState.READY); + attributes = serverTransportListener.transportReady(attributes); + } + } + } + + StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) { + return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers); + } + + synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) { + if (isShutdown()) { + return Status.UNAVAILABLE.withDescription("transport is shutdown"); + } else { + serverTransportListener.streamCreated(stream, methodName, headers); + return Status.OK; + } + } + + @Override + @GuardedBy("this") + void notifyShutdown(Status status) { + // Nothing to do. + } + + @Override + @GuardedBy("this") + void notifyTerminated() { + if (serverTransportListener != null) { + serverTransportListener.transportTerminated(); + } + } + + @Override + public synchronized void shutdown() { + shutdownInternal(Status.OK, false); + } + + @Override + public synchronized void shutdownNow(Status reason) { + shutdownInternal(reason, true); + } + + @Override + @Nullable + @GuardedBy("this") + protected Inbound createInbound(int callId) { + return new Inbound.ServerInbound(this, attributes, callId); + } + + private static InternalLogId buildLogId(Attributes attributes) { + return InternalLogId.allocate( + BinderServerTransport.class, "from " + attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR)); + } +} diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index d87cfb74044..0fe131a0728 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -19,67 +19,32 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.util.concurrent.Futures.immediateFuture; -import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import android.content.Context; -import android.content.pm.ServiceInfo; -import android.os.Binder; import android.os.DeadObjectException; import android.os.IBinder; import android.os.Parcel; -import android.os.Process; import android.os.RemoteException; import android.os.TransactionTooLargeException; import androidx.annotation.BinderThread; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Ticker; import com.google.common.base.Verify; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; -import io.grpc.CallOptions; -import io.grpc.ClientStreamTracer; -import io.grpc.Grpc; import io.grpc.Internal; import io.grpc.InternalChannelz.SocketStats; import io.grpc.InternalLogId; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.SecurityLevel; -import io.grpc.ServerStreamTracer; import io.grpc.Status; import io.grpc.StatusException; -import io.grpc.binder.AndroidComponentAddress; -import io.grpc.binder.AsyncSecurityPolicy; import io.grpc.binder.InboundParcelablePolicy; -import io.grpc.binder.SecurityPolicy; -import io.grpc.internal.ClientStream; -import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; -import io.grpc.internal.ConnectionClientTransport; -import io.grpc.internal.FailingClientStream; -import io.grpc.internal.GrpcAttributes; -import io.grpc.internal.GrpcUtil; -import io.grpc.internal.ManagedClientTransport; import io.grpc.internal.ObjectPool; -import io.grpc.internal.ServerStream; -import io.grpc.internal.ServerTransport; -import io.grpc.internal.ServerTransportListener; -import io.grpc.internal.StatsTraceContext; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -169,10 +134,10 @@ public abstract class BinderTransport implements IBinder.DeathRecipient { private static final int RESERVED_TRANSACTIONS = 1000; /** The first call ID we can use. */ - private static final int FIRST_CALL_ID = IBinder.FIRST_CALL_TRANSACTION + RESERVED_TRANSACTIONS; + static final int FIRST_CALL_ID = IBinder.FIRST_CALL_TRANSACTION + RESERVED_TRANSACTIONS; /** The last call ID we can use. */ - private static final int LAST_CALL_ID = IBinder.LAST_CALL_TRANSACTION; + static final int LAST_CALL_ID = IBinder.LAST_CALL_TRANSACTION; /** The states of this transport. */ protected enum TransportState { @@ -218,7 +183,7 @@ protected enum TransportState { // Only read/written on @BinderThread. private long acknowledgedIncomingBytes; - private BinderTransport( + protected BinderTransport( ObjectPool executorServicePool, Attributes attributes, OneWayBinderProxy.Decorator binderDecorator, @@ -559,478 +524,6 @@ final void handleAcknowledgedBytes(long numBytes) { } } - /** Concrete client-side transport implementation. */ - @ThreadSafe - @Internal - public static final class BinderClientTransport extends BinderTransport - implements ConnectionClientTransport, Bindable.Observer { - - private final ObjectPool offloadExecutorPool; - private final Executor offloadExecutor; - private final SecurityPolicy securityPolicy; - private final Bindable serviceBinding; - - /** Number of ongoing calls which keep this transport "in-use". */ - private final AtomicInteger numInUseStreams; - - private final long readyTimeoutMillis; - private final PingTracker pingTracker; - private final boolean preAuthorizeServer; - - @Nullable private ManagedClientTransport.Listener clientTransportListener; - - @GuardedBy("this") - private int latestCallId = FIRST_CALL_ID; - - @GuardedBy("this") - private ScheduledFuture readyTimeoutFuture; // != null iff timeout scheduled. - @GuardedBy("this") - @Nullable private ListenableFuture authResultFuture; // null before we check auth. - - @GuardedBy("this") - @Nullable - private ListenableFuture preAuthResultFuture; // null before we pre-auth. - - /** - * Constructs a new transport instance. - * - * @param factory parameters common to all a Channel's transports - * @param targetAddress the fully resolved and load-balanced server address - * @param options other parameters that can vary as transports come and go within a Channel - */ - public BinderClientTransport( - BinderClientTransportFactory factory, - AndroidComponentAddress targetAddress, - ClientTransportOptions options) { - super( - factory.scheduledExecutorPool, - buildClientAttributes( - options.getEagAttributes(), - factory.sourceContext, - targetAddress, - factory.inboundParcelablePolicy), - factory.binderDecorator, - buildLogId(factory.sourceContext, targetAddress)); - this.offloadExecutorPool = factory.offloadExecutorPool; - this.securityPolicy = factory.securityPolicy; - this.offloadExecutor = offloadExecutorPool.getObject(); - this.readyTimeoutMillis = factory.readyTimeoutMillis; - Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE); - this.preAuthorizeServer = - preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; - numInUseStreams = new AtomicInteger(); - pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); - - serviceBinding = - new ServiceBinding( - factory.mainThreadExecutor, - factory.sourceContext, - factory.channelCredentials, - targetAddress.asBindIntent(), - targetAddress.getTargetUser() != null - ? targetAddress.getTargetUser() - : factory.defaultTargetUserHandle, - factory.bindServiceFlags.toInteger(), - this); - } - - @Override - void releaseExecutors() { - super.releaseExecutors(); - offloadExecutorPool.returnObject(offloadExecutor); - } - - @Override - public synchronized void onBound(IBinder binder) { - sendSetupTransaction( - binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); - } - - @Override - public synchronized void onUnbound(Status reason) { - shutdownInternal(reason, true); - } - - @CheckReturnValue - @Override - public synchronized Runnable start(ManagedClientTransport.Listener clientTransportListener) { - this.clientTransportListener = checkNotNull(clientTransportListener); - return () -> { - synchronized (BinderClientTransport.this) { - if (inState(TransportState.NOT_STARTED)) { - setState(TransportState.SETUP); - try { - if (preAuthorizeServer) { - preAuthorize(serviceBinding.resolve()); - } else { - serviceBinding.bind(); - } - } catch (StatusException e) { - shutdownInternal(e.getStatus(), true); - return; - } - if (readyTimeoutMillis >= 0) { - readyTimeoutFuture = - getScheduledExecutorService() - .schedule( - BinderClientTransport.this::onReadyTimeout, - readyTimeoutMillis, - MILLISECONDS); - } - } - } - }; - } - - @GuardedBy("this") - private void preAuthorize(ServiceInfo serviceInfo) { - // It's unlikely, but the identity/existence of this Service could change by the time we - // actually connect. It doesn't matter though, because: - // - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer - // will eventually retry using a new transport instance that will see the Service's new state. - // - If pre-auth succeeds (but would fail against the server's new state), we might give an - // unauthorized server a chance to run, but the connection will still fail by SecurityPolicy - // check later in handshake. Pre-auth remains effective at mitigating abuse because malware - // can't typically control the exact timing of its installation. - preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid); - Futures.addCallback( - preAuthResultFuture, - new FutureCallback() { - @Override - public void onSuccess(Status result) { - handlePreAuthResult(result); - } - - @Override - public void onFailure(Throwable t) { - handleAuthResult(t); - } - }, - offloadExecutor); - } - - private synchronized void handlePreAuthResult(Status authorization) { - if (inState(TransportState.SETUP)) { - if (!authorization.isOk()) { - shutdownInternal(authorization, true); - } else { - serviceBinding.bind(); - } - } - } - - private synchronized void onReadyTimeout() { - if (inState(TransportState.SETUP)) { - readyTimeoutFuture = null; - shutdownInternal( - Status.DEADLINE_EXCEEDED.withDescription( - "Connect timeout " + readyTimeoutMillis + "ms lapsed"), - true); - } - } - - @Override - public synchronized ClientStream newStream( - final MethodDescriptor method, - final Metadata headers, - final CallOptions callOptions, - ClientStreamTracer[] tracers) { - if (!inState(TransportState.READY)) { - return newFailingClientStream( - isShutdown() - ? shutdownStatus - : Status.INTERNAL.withDescription("newStream() before transportReady()"), - attributes, - headers, - tracers); - } - - int callId = latestCallId++; - if (latestCallId == LAST_CALL_ID) { - latestCallId = FIRST_CALL_ID; - } - StatsTraceContext statsTraceContext = - StatsTraceContext.newClientContext(tracers, attributes, headers); - Inbound.ClientInbound inbound = - new Inbound.ClientInbound( - this, attributes, callId, GrpcUtil.shouldBeCountedForInUse(callOptions)); - if (ongoingCalls.putIfAbsent(callId, inbound) != null) { - Status failure = Status.INTERNAL.withDescription("Clashing call IDs"); - shutdownInternal(failure, true); - return newFailingClientStream(failure, attributes, headers, tracers); - } else { - if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) { - clientTransportListener.transportInUse(true); - } - Outbound.ClientOutbound outbound = - new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext); - if (method.getType().clientSendsOneMessage()) { - return new SingleMessageClientStream(inbound, outbound, attributes); - } else { - return new MultiMessageClientStream(inbound, outbound, attributes); - } - } - } - - @Override - protected void unregisterInbound(Inbound inbound) { - if (inbound.countsForInUse() && numInUseStreams.decrementAndGet() == 0) { - clientTransportListener.transportInUse(false); - } - super.unregisterInbound(inbound); - } - - @Override - public void ping(final PingCallback callback, Executor executor) { - pingTracker.startPing(callback, executor); - } - - @Override - public synchronized void shutdown(Status reason) { - checkNotNull(reason, "reason"); - shutdownInternal(reason, false); - } - - @Override - public synchronized void shutdownNow(Status reason) { - checkNotNull(reason, "reason"); - shutdownInternal(reason, true); - } - - @Override - @GuardedBy("this") - void notifyShutdown(Status status) { - clientTransportListener.transportShutdown(status); - } - - @Override - @GuardedBy("this") - void notifyTerminated() { - if (numInUseStreams.getAndSet(0) > 0) { - clientTransportListener.transportInUse(false); - } - if (readyTimeoutFuture != null) { - readyTimeoutFuture.cancel(false); - readyTimeoutFuture = null; - } - if (preAuthResultFuture != null) { - preAuthResultFuture.cancel(false); // No effect if already complete. - } - if (authResultFuture != null) { - authResultFuture.cancel(false); // No effect if already complete. - } - serviceBinding.unbind(); - clientTransportListener.transportTerminated(); - } - - @Override - @GuardedBy("this") - protected void handleSetupTransport(Parcel parcel) { - int remoteUid = Binder.getCallingUid(); - attributes = setSecurityAttrs(attributes, remoteUid); - if (inState(TransportState.SETUP)) { - int version = parcel.readInt(); - IBinder binder = parcel.readStrongBinder(); - if (version != WIRE_FORMAT_VERSION) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true); - } else if (binder == null) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); - } else { - authResultFuture = checkServerAuthorizationAsync(remoteUid); - Futures.addCallback( - authResultFuture, - new FutureCallback() { - @Override - public void onSuccess(Status result) { - handleAuthResult(binder, result); - } - - @Override - public void onFailure(Throwable t) { - handleAuthResult(t); - } - }, - offloadExecutor); - } - } - } - - private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { - return (securityPolicy instanceof AsyncSecurityPolicy) - ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) - : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); - } - - private synchronized void handleAuthResult(IBinder binder, Status authorization) { - if (inState(TransportState.SETUP)) { - if (!authorization.isOk()) { - shutdownInternal(authorization, true); - } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); - } else { - // Check state again, since a failure inside setOutgoingBinder (or a callback it - // triggers), could have shut us down. - if (!isShutdown()) { - setState(TransportState.READY); - attributes = clientTransportListener.filterTransport(attributes); - clientTransportListener.transportReady(); - if (readyTimeoutFuture != null) { - readyTimeoutFuture.cancel(false); - readyTimeoutFuture = null; - } - } - } - } - } - - private synchronized void handleAuthResult(Throwable t) { - shutdownInternal( - Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true); - } - - @GuardedBy("this") - @Override - protected void handlePingResponse(Parcel parcel) { - pingTracker.onPingResponse(parcel.readInt()); - } - - private static ClientStream newFailingClientStream( - Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) { - StatsTraceContext statsTraceContext = - StatsTraceContext.newClientContext(tracers, attributes, headers); - statsTraceContext.clientOutboundHeaders(); - return new FailingClientStream(failure, tracers); - } - - private static InternalLogId buildLogId( - Context sourceContext, AndroidComponentAddress targetAddress) { - return InternalLogId.allocate( - BinderClientTransport.class, - sourceContext.getClass().getSimpleName() + "->" + targetAddress); - } - - private static Attributes buildClientAttributes( - Attributes eagAttrs, - Context sourceContext, - AndroidComponentAddress targetAddress, - InboundParcelablePolicy inboundParcelablePolicy) { - return Attributes.newBuilder() - .set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.NONE) // Trust noone for now. - .set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs) - .set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, AndroidComponentAddress.forContext(sourceContext)) - .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, targetAddress) - .set(INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy) - .build(); - } - - private static Attributes setSecurityAttrs(Attributes attributes, int uid) { - return attributes.toBuilder() - .set(REMOTE_UID, uid) - .set( - GrpcAttributes.ATTR_SECURITY_LEVEL, - uid == Process.myUid() - ? SecurityLevel.PRIVACY_AND_INTEGRITY - : SecurityLevel.INTEGRITY) // TODO: Have the SecrityPolicy decide this. - .build(); - } - } - - /** Concrete server-side transport implementation. */ - @Internal - public static final class BinderServerTransport extends BinderTransport - implements ServerTransport { - - private final List streamTracerFactories; - @Nullable private ServerTransportListener serverTransportListener; - - /** - * Constructs a new transport instance. - * - * @param binderDecorator used to decorate 'callbackBinder', for fault injection. - */ - public BinderServerTransport( - ObjectPool executorServicePool, - Attributes attributes, - List streamTracerFactories, - OneWayBinderProxy.Decorator binderDecorator, - IBinder callbackBinder) { - super(executorServicePool, attributes, binderDecorator, buildLogId(attributes)); - this.streamTracerFactories = streamTracerFactories; - // TODO(jdcormie): Plumb in the Server's executor() and use it here instead. - setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService())); - } - - public synchronized void setServerTransportListener( - ServerTransportListener serverTransportListener) { - this.serverTransportListener = serverTransportListener; - if (isShutdown()) { - setState(TransportState.SHUTDOWN_TERMINATED); - notifyTerminated(); - releaseExecutors(); - } else { - sendSetupTransaction(); - // Check we're not shutdown again, since a failure inside sendSetupTransaction (or a - // callback it triggers), could have shut us down. - if (!isShutdown()) { - setState(TransportState.READY); - attributes = serverTransportListener.transportReady(attributes); - } - } - } - - StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) { - return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers); - } - - synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) { - if (isShutdown()) { - return Status.UNAVAILABLE.withDescription("transport is shutdown"); - } else { - serverTransportListener.streamCreated(stream, methodName, headers); - return Status.OK; - } - } - - @Override - @GuardedBy("this") - void notifyShutdown(Status status) { - // Nothing to do. - } - - @Override - @GuardedBy("this") - void notifyTerminated() { - if (serverTransportListener != null) { - serverTransportListener.transportTerminated(); - } - } - - @Override - public synchronized void shutdown() { - shutdownInternal(Status.OK, false); - } - - @Override - public synchronized void shutdownNow(Status reason) { - shutdownInternal(reason, true); - } - - @Override - @Nullable - @GuardedBy("this") - protected Inbound createInbound(int callId) { - return new Inbound.ServerInbound(this, attributes, callId); - } - - private static InternalLogId buildLogId(Attributes attributes) { - return InternalLogId.allocate( - BinderServerTransport.class, "from " + attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR)); - } - } - private static void checkTransition(TransportState current, TransportState next) { switch (next) { case SETUP: diff --git a/binder/src/main/java/io/grpc/binder/internal/Inbound.java b/binder/src/main/java/io/grpc/binder/internal/Inbound.java index 50654297c74..9b9dfeef5ce 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Inbound.java +++ b/binder/src/main/java/io/grpc/binder/internal/Inbound.java @@ -610,10 +610,9 @@ protected void deliverCloseAbnormal(Status status) { // Server-side inbound transactions. static final class ServerInbound extends Inbound { - private final BinderTransport.BinderServerTransport serverTransport; + private final BinderServerTransport serverTransport; - ServerInbound( - BinderTransport.BinderServerTransport transport, Attributes attributes, int callId) { + ServerInbound(BinderServerTransport transport, Attributes attributes, int callId) { super(transport, attributes, callId); this.serverTransport = transport; } diff --git a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java index d47106d1d35..e7e73e6d4b0 100644 --- a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java @@ -56,12 +56,12 @@ public final class BinderServerTransportTest { @Mock IBinder mockBinder; - BinderTransport.BinderServerTransport transport; + BinderServerTransport transport; @Before public void setUp() throws Exception { transport = - new BinderTransport.BinderServerTransport( + new BinderServerTransport( new FixedObjectPool<>(executorService), Attributes.EMPTY, ImmutableList.of(), diff --git a/binder/src/testFixtures/java/io/grpc/binder/internal/BinderClientTransportBuilder.java b/binder/src/testFixtures/java/io/grpc/binder/internal/BinderClientTransportBuilder.java index e98daeec3ed..f732ff64663 100644 --- a/binder/src/testFixtures/java/io/grpc/binder/internal/BinderClientTransportBuilder.java +++ b/binder/src/testFixtures/java/io/grpc/binder/internal/BinderClientTransportBuilder.java @@ -24,8 +24,8 @@ import java.net.SocketAddress; /** - * Helps unit tests create {@link BinderTransport.BinderClientTransport} instances without having to - * mention irrelevant details (go/tott/719). + * Helps unit tests create {@link BinderClientTransport} instances without having to mention + * irrelevant details (go/tott/719). */ public class BinderClientTransportBuilder { private BinderClientTransportFactory factory; @@ -54,7 +54,7 @@ public BinderClientTransportBuilder setFactory(BinderClientTransportFactory fact return this; } - public BinderTransport.BinderClientTransport build() { + public BinderClientTransport build() { return factory.newClientTransport( checkNotNull(serverAddress), checkNotNull(options), checkNotNull(channelLogger)); } From 028afbe35252ec2bc028c447381adbf88769ccdf Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 22 Aug 2025 08:07:51 -0700 Subject: [PATCH 351/591] xds: Implement equals in WRRLBConfig Just an is a8de9f0, lack of equals causes cluster_resolver to consider every update a different configuration and restart itself. Handling NaN should really be prevented with validation, but it looks like that would lead to yak shaving at the moment. b/435208946 --- .../xds/WeightedRoundRobinLoadBalancer.java | 27 ++++++++++++++ .../WeightedRoundRobinLoadBalancerTest.java | 35 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index c3d36f7cdc8..6cf3189d587 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -48,6 +48,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; @@ -738,6 +739,32 @@ private WeightedRoundRobinLoadBalancerConfig(long blackoutPeriodNanos, this.errorUtilizationPenalty = errorUtilizationPenalty; } + @Override + public boolean equals(Object o) { + if (!(o instanceof WeightedRoundRobinLoadBalancerConfig)) { + return false; + } + WeightedRoundRobinLoadBalancerConfig that = (WeightedRoundRobinLoadBalancerConfig) o; + return this.blackoutPeriodNanos == that.blackoutPeriodNanos + && this.weightExpirationPeriodNanos == that.weightExpirationPeriodNanos + && this.enableOobLoadReport == that.enableOobLoadReport + && this.oobReportingPeriodNanos == that.oobReportingPeriodNanos + && this.weightUpdatePeriodNanos == that.weightUpdatePeriodNanos + // Float.compare considers NaNs equal + && Float.compare(this.errorUtilizationPenalty, that.errorUtilizationPenalty) == 0; + } + + @Override + public int hashCode() { + return Objects.hash( + blackoutPeriodNanos, + weightExpirationPeriodNanos, + enableOobLoadReport, + oobReportingPeriodNanos, + weightUpdatePeriodNanos, + errorUtilizationPenalty); + } + static final class Builder { long blackoutPeriodNanos = 10_000_000_000L; // 10s long weightExpirationPeriodNanos = 180_000_000_000L; //3min diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index bf23c65def4..868bc811a70 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -35,6 +35,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.testing.EqualsTester; import com.google.protobuf.Duration; import io.grpc.Attributes; import io.grpc.CallOptions; @@ -215,6 +216,40 @@ public void pickChildLbTF() throws Exception { weightedPicker.pickSubchannel(mockArgs); } + @Test + public void config_equalsTester() { + WeightedRoundRobinLoadBalancerConfig defaults = + WeightedRoundRobinLoadBalancerConfig.newBuilder().build(); + new EqualsTester() + .addEqualityGroup( + WeightedRoundRobinLoadBalancerConfig.newBuilder().build(), + WeightedRoundRobinLoadBalancerConfig.newBuilder().build(), + WeightedRoundRobinLoadBalancerConfig.newBuilder() + .setBlackoutPeriodNanos(defaults.blackoutPeriodNanos).build()) + .addEqualityGroup( + WeightedRoundRobinLoadBalancerConfig.newBuilder() + .setBlackoutPeriodNanos(5).build()) + .addEqualityGroup( + WeightedRoundRobinLoadBalancerConfig.newBuilder() + .setWeightExpirationPeriodNanos(5).build()) + .addEqualityGroup( + WeightedRoundRobinLoadBalancerConfig.newBuilder() + .setEnableOobLoadReport(true).build()) + .addEqualityGroup( + WeightedRoundRobinLoadBalancerConfig.newBuilder() + .setOobReportingPeriodNanos(5).build()) + .addEqualityGroup( + WeightedRoundRobinLoadBalancerConfig.newBuilder() + .setWeightUpdatePeriodNanos(5).build()) + .addEqualityGroup( + WeightedRoundRobinLoadBalancerConfig.newBuilder() + .setErrorUtilizationPenalty(0.5F).build()) + .addEqualityGroup( + WeightedRoundRobinLoadBalancerConfig.newBuilder() + .setErrorUtilizationPenalty(Float.NaN).build()) + .testEquals(); + } + @Test public void wrrLifeCycle() { syncContext.execute(() -> wrr.acceptResolvedAddresses(ResolvedAddresses.newBuilder() From c7202c0db5065cf9364086850a36115a843bbb97 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Sun, 24 Aug 2025 18:21:52 +0530 Subject: [PATCH 352/591] Bump readme (#12305) --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e57b79e6bf7..d7e57a9f80c 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.74.0/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.74.0/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.75.0/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.75.0/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.74.0 + 1.75.0 runtime io.grpc grpc-protobuf - 1.74.0 + 1.75.0 io.grpc grpc-stub - 1.74.0 + 1.75.0 org.apache.tomcat @@ -79,18 +79,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.74.0' -implementation 'io.grpc:grpc-protobuf:1.74.0' -implementation 'io.grpc:grpc-stub:1.74.0' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.75.0' +implementation 'io.grpc:grpc-protobuf:1.75.0' +implementation 'io.grpc:grpc-stub:1.75.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.74.0' -implementation 'io.grpc:grpc-protobuf-lite:1.74.0' -implementation 'io.grpc:grpc-stub:1.74.0' +implementation 'io.grpc:grpc-okhttp:1.75.0' +implementation 'io.grpc:grpc-protobuf-lite:1.75.0' +implementation 'io.grpc:grpc-stub:1.75.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` @@ -99,7 +99,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.74.0 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.75.0 Development snapshots are available in [Sonatypes's snapshot repository](https://central.sonatype.com/repository/maven-snapshots/). @@ -131,7 +131,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.74.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.75.0:exe:${os.detected.classifier} @@ -161,7 +161,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0' } } generateProtoTasks { @@ -194,7 +194,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.74.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0' } } generateProtoTasks { From f5b111708b2bddb8f0fe65bbc7f8a4edb3d0757f Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:56:56 +0200 Subject: [PATCH 353/591] servlet: fix unpark() of onWritePossible() thread This should fix #12268 --- gradle/libs.versions.toml | 2 +- .../servlet/AsyncServletOutputStreamWriter.java | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 540657b9ddb..e4eb4f82ddb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,7 +112,7 @@ signature-java = "org.codehaus.mojo.signature:java18:1.0" tomcat-embed-core = "org.apache.tomcat.embed:tomcat-embed-core:10.1.31" tomcat-embed-core9 = "org.apache.tomcat.embed:tomcat-embed-core:9.0.89" truth = "com.google.truth:truth:1.4.4" -undertow-servlet22 = "io.undertow:undertow-servlet:2.2.32.Final" +undertow-servlet22 = "io.undertow:undertow-servlet:2.2.37.Final" undertow-servlet = "io.undertow:undertow-servlet:2.3.18.Final" # Do not update: Pinned to the last version supporting Java 8. diff --git a/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java b/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java index 8c0c6ec6512..3c8d3d07571 100644 --- a/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java +++ b/servlet/src/main/java/io/grpc/servlet/AsyncServletOutputStreamWriter.java @@ -26,9 +26,9 @@ import io.grpc.InternalLogId; import io.grpc.servlet.ServletServerStream.ServletTransportState; import java.io.IOException; -import java.time.Duration; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; import java.util.function.BiFunction; @@ -128,7 +128,7 @@ public void finest(String str, Object... params) { log.fine("call completed"); }); }; - this.isReady = () -> outputStream.isReady(); + this.isReady = outputStream::isReady; } /** @@ -173,7 +173,9 @@ void complete() { /** Called from the container thread {@link javax.servlet.WriteListener#onWritePossible()}. */ void onWritePossible() throws IOException { log.finest("onWritePossible: ENTRY. The servlet output stream becomes ready"); - assureReadyAndDrainedTurnsFalse(); + if (writeState.get().readyAndDrained) { + assureReadyAndDrainedTurnsFalse(); + } while (isReady.getAsBoolean()) { WriteState curState = writeState.get(); @@ -200,11 +202,9 @@ private void assureReadyAndDrainedTurnsFalse() { // readyAndDrained should have been set to false already. // Just in case due to a race condition readyAndDrained is still true at this moment and is // being set to false by runOrBuffer() concurrently. + parkingThread = Thread.currentThread(); while (writeState.get().readyAndDrained) { - parkingThread = Thread.currentThread(); - // Try to sleep for an extremely long time to avoid writeState being changed at exactly - // the time when sleep time expires (in extreme scenario, such as #9917). - LockSupport.parkNanos(Duration.ofHours(1).toNanos()); // should return immediately + LockSupport.parkNanos(TimeUnit.MINUTES.toNanos(1)); // should return immediately } parkingThread = null; } @@ -254,7 +254,7 @@ interface ActionItem { @VisibleForTesting // Lincheck test can not run with java.util.logging dependency. interface Log { default boolean isLoggable(Level level) { - return false; + return false; } default void fine(String str, Object...params) {} From 2ba5c3d2a3655e31d0aa2388fea386f343f70eac Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:05:06 +0200 Subject: [PATCH 354/591] servlet: skip AsyncContext.complete() when already completed (#12296) Currently `AsyncContext.complete()` is called multiple times in some flows, e.g. when DEADLINE_EXCEEDED, and that results in IllegalStateException from servlet container (Tomcat). If counting IllegalStateException from the tests of the servlet module here - the number of those is reduced significantly, a few cases still left, would bet addressed separately. --- .../java/io/grpc/servlet/ServletAdapter.java | 4 ++- .../io/grpc/servlet/ServletServerStream.java | 31 ++++++------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java b/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java index 5a567916f99..4bfe8949776 100644 --- a/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java +++ b/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java @@ -215,7 +215,9 @@ private static final class GrpcAsyncListener implements AsyncListener { } @Override - public void onComplete(AsyncEvent event) {} + public void onComplete(AsyncEvent event) { + stream.asyncCompleted = true; + } @Override public void onTimeout(AsyncEvent event) { diff --git a/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java b/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java index 5dd20567c08..caab5caa8a9 100644 --- a/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java +++ b/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java @@ -30,7 +30,6 @@ import io.grpc.InternalLogId; import io.grpc.Metadata; import io.grpc.Status; -import io.grpc.Status.Code; import io.grpc.internal.AbstractServerStream; import io.grpc.internal.GrpcUtil; import io.grpc.internal.SerializingExecutor; @@ -43,8 +42,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -58,12 +55,15 @@ final class ServletServerStream extends AbstractServerStream { private final ServletTransportState transportState; private final Sink sink = new Sink(); - private final AsyncContext asyncCtx; private final HttpServletResponse resp; private final Attributes attributes; private final String authority; private final InternalLogId logId; private final AsyncServletOutputStreamWriter writer; + /** + * If the async servlet operation has been completed. + */ + volatile boolean asyncCompleted = false; ServletServerStream( AsyncContext asyncCtx, @@ -78,7 +78,6 @@ final class ServletServerStream extends AbstractServerStream { this.attributes = attributes; this.authority = authority; this.logId = logId; - this.asyncCtx = asyncCtx; this.resp = (HttpServletResponse) asyncCtx.getResponse(); this.writer = new AsyncServletOutputStreamWriter( asyncCtx, transportState, logId); @@ -292,24 +291,14 @@ public void writeTrailers(Metadata trailers, boolean headersSent, Status status) @Override public void cancel(Status status) { - if (resp.isCommitted() && Code.DEADLINE_EXCEEDED == status.getCode()) { - return; // let the servlet timeout, the container will sent RST_STREAM automatically - } transportState.runOnTransportThread(() -> transportState.transportReportStatus(status)); - // There is no way to RST_STREAM with CANCEL code, so write trailers instead - close(Status.CANCELLED.withDescription("Servlet stream cancelled") - .withCause(status.asRuntimeException()), - new Metadata()); - CountDownLatch countDownLatch = new CountDownLatch(1); - transportState.runOnTransportThread(() -> { - asyncCtx.complete(); - countDownLatch.countDown(); - }); - try { - countDownLatch.await(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + if (asyncCompleted) { + logger.fine("ignore cancel as already completed"); + return; } + // There is no way to RST_STREAM with CANCEL code, so write trailers instead + close(status, new Metadata()); + // close() calls writeTrailers(), which calls AsyncContext.complete() } } From 8ac5599b92ea777183eb732d25a3087351d67460 Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Tue, 26 Aug 2025 01:25:46 +0200 Subject: [PATCH 355/591] servlet: fix threadingTest and update lincheck (#12306) Seems like previously it was not testing all the flows but only: the write/flush calls are added to the queue by `runOrBuffer` because `readyAndDrained` is initially `false`. So, * `isReady()` is never called * `isReadyReturnedFalse` never set to true * `maybeOnWritePossible()` does nothing All that makes me think that #9917 is caused by some bugs in older versions of llincheck, can be closed now. A more real simulation happens if calling `onWritePossible` first via `initialOnWritePossible`, and then it has found a race condition in `AsyncServletOutputStreamWriterConcurrencyTest.isReady()`, if `maybeOnWritePossible()` is executed before `return isReady`. Additionally updated lincheck, * it does not need jvmArgs now * is compatible with java 8 again * changed asserts from Truth to junit, as lincheck was trying to use `com.google.common.truth.Subject.equals(Object)` It still does not detect #12268, but might be needs more time. --- build.gradle | 2 +- gradle/libs.versions.toml | 3 +- servlet/build.gradle | 15 ++--- ...vletOutputStreamWriterConcurrencyTest.java | 64 +++++++++++-------- 4 files changed, 43 insertions(+), 41 deletions(-) diff --git a/build.gradle b/build.gradle index 50c9df4c5c8..366edf1b9e5 100644 --- a/build.gradle +++ b/build.gradle @@ -238,7 +238,7 @@ subprojects { // At a test failure, log the stack trace to the console so that we don't // have to open the HTML in a browser. - tasks.named("test").configure { + tasks.withType(Test).configureEach { testLogging { exceptionFormat = 'full' showExceptions = true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e4eb4f82ddb..a374ba5aa73 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,8 +69,7 @@ jetty-servlet = "org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.16" jetty-servlet10 = "org.eclipse.jetty:jetty-servlet:10.0.20" jsr305 = "com.google.code.findbugs:jsr305:3.0.2" junit = "junit:junit:4.13.2" -# 2.17+ require Java 11+ (not mentioned in release notes) -lincheck = "org.jetbrains.kotlinx:lincheck-jvm:2.16" +lincheck = "org.jetbrains.lincheck:lincheck:3.2" # Update notes / 2023-07-19 sergiitk: # Couldn't update to 5.4.0, updated to the last in 4.x line. Version 5.x breaks some tests. # Error log: https://github.com/grpc/grpc-java/pull/10359#issuecomment-1632834435 diff --git a/servlet/build.gradle b/servlet/build.gradle index fd5abb6f0e5..7f9cd04a57c 100644 --- a/servlet/build.gradle +++ b/servlet/build.gradle @@ -43,7 +43,7 @@ dependencies { testImplementation libraries.javax.servlet.api threadingTestImplementation project(':grpc-servlet'), - libraries.truth, + libraries.junit, libraries.javax.servlet.api, libraries.lincheck @@ -69,19 +69,12 @@ dependencies { libraries.protobuf.java } -tasks.named("test").configure { - if (JavaVersion.current().isJava9Compatible()) { - jvmArgs += [ - // required for Lincheck - '--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED', - '--add-exports=java.base/jdk.internal.util=ALL-UNNAMED', - ] - } -} - tasks.register('threadingTest', Test) { classpath = sourceSets.threadingTest.runtimeClasspath testClassesDirs = sourceSets.threadingTest.output.classesDirs + jacoco { + enabled = false + } } tasks.named("assemble").configure { diff --git a/servlet/src/threadingTest/java/io/grpc/servlet/AsyncServletOutputStreamWriterConcurrencyTest.java b/servlet/src/threadingTest/java/io/grpc/servlet/AsyncServletOutputStreamWriterConcurrencyTest.java index 61da2bf4c69..ebe1df8c3f3 100644 --- a/servlet/src/threadingTest/java/io/grpc/servlet/AsyncServletOutputStreamWriterConcurrencyTest.java +++ b/servlet/src/threadingTest/java/io/grpc/servlet/AsyncServletOutputStreamWriterConcurrencyTest.java @@ -16,23 +16,23 @@ package io.grpc.servlet; -import static com.google.common.truth.Truth.assertWithMessage; -import static org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedStrategyGuaranteeKt.forClasses; +import static org.jetbrains.lincheck.datastructures.ManagedStrategyGuaranteeKt.forClasses; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import io.grpc.servlet.AsyncServletOutputStreamWriter.ActionItem; import io.grpc.servlet.AsyncServletOutputStreamWriter.Log; import java.io.IOException; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; -import org.jetbrains.kotlinx.lincheck.LinChecker; -import org.jetbrains.kotlinx.lincheck.annotations.OpGroupConfig; -import org.jetbrains.kotlinx.lincheck.annotations.Operation; -import org.jetbrains.kotlinx.lincheck.annotations.Param; -import org.jetbrains.kotlinx.lincheck.paramgen.BooleanGen; import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingCTest; -import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions; -import org.jetbrains.kotlinx.lincheck.verifier.VerifierState; +import org.jetbrains.lincheck.datastructures.BooleanGen; +import org.jetbrains.lincheck.datastructures.ModelCheckingOptions; +import org.jetbrains.lincheck.datastructures.Operation; +import org.jetbrains.lincheck.datastructures.Param; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -50,17 +50,19 @@ * operations are linearizable in each interleave scenario. */ @ModelCheckingCTest -@OpGroupConfig(name = "update", nonParallel = true) -@OpGroupConfig(name = "write", nonParallel = true) @Param(name = "keepReady", gen = BooleanGen.class) @RunWith(JUnit4.class) -public class AsyncServletOutputStreamWriterConcurrencyTest extends VerifierState { +public class AsyncServletOutputStreamWriterConcurrencyTest { private static final int OPERATIONS_PER_THREAD = 6; private final AsyncServletOutputStreamWriter writer; private final boolean[] keepReadyArray = new boolean[OPERATIONS_PER_THREAD]; private volatile boolean isReady; + /** + * The container initiates the first call shortly after {@code startAsync}. + */ + private final AtomicBoolean initialOnWritePossible = new AtomicBoolean(true); // when isReadyReturnedFalse, writer.onWritePossible() will be called. private volatile boolean isReadyReturnedFalse; private int producerIndex; @@ -71,17 +73,15 @@ public class AsyncServletOutputStreamWriterConcurrencyTest extends VerifierState public AsyncServletOutputStreamWriterConcurrencyTest() { BiFunction writeAction = (bytes, numBytes) -> () -> { - assertWithMessage("write should only be called while isReady() is true") - .that(isReady) - .isTrue(); + assertTrue("write should only be called while isReady() is true", isReady); // The byte to be written must equal to consumerIndex, otherwise execution order is wrong - assertWithMessage("write in wrong order").that(bytes[0]).isEqualTo((byte) consumerIndex); + assertEquals("write in wrong order", bytes[0], (byte) consumerIndex); bytesWritten++; writeOrFlush(); }; ActionItem flushAction = () -> { - assertWithMessage("flush must only be called while isReady() is true").that(isReady).isTrue(); + assertTrue("flush must only be called while isReady() is true", isReady); writeOrFlush(); }; @@ -102,12 +102,13 @@ private void writeOrFlush() { } private boolean isReady() { - if (!isReady) { - assertWithMessage("isReady() already returned false, onWritePossible() will be invoked") - .that(isReadyReturnedFalse).isFalse(); + boolean copyOfIsReady = isReady; + if (!copyOfIsReady) { + assertFalse("isReady() already returned false, onWritePossible() will be invoked", + isReadyReturnedFalse); isReadyReturnedFalse = true; } - return isReady; + return copyOfIsReady; } /** @@ -118,7 +119,7 @@ private boolean isReady() { * the ServletOutputStream should become unready if keepReady == false. */ // @com.google.errorprone.annotations.Keep - @Operation(group = "write") + @Operation(nonParallelGroup = "write") public void write(@Param(name = "keepReady") boolean keepReady) throws IOException { keepReadyArray[producerIndex] = keepReady; writer.writeBytes(new byte[]{(byte) producerIndex}, 1); @@ -133,7 +134,7 @@ public void write(@Param(name = "keepReady") boolean keepReady) throws IOExcepti * the ServletOutputStream should become unready if keepReady == false. */ // @com.google.errorprone.annotations.Keep // called by lincheck reflectively - @Operation(group = "write") + @Operation(nonParallelGroup = "write") public void flush(@Param(name = "keepReady") boolean keepReady) throws IOException { keepReadyArray[producerIndex] = keepReady; writer.flush(); @@ -142,9 +143,12 @@ public void flush(@Param(name = "keepReady") boolean keepReady) throws IOExcepti /** If the writer is not ready, let it turn ready and call writer.onWritePossible(). */ // @com.google.errorprone.annotations.Keep // called by lincheck reflectively - @Operation(group = "update") + @Operation(nonParallelGroup = "update") public void maybeOnWritePossible() throws IOException { - if (isReadyReturnedFalse) { + if (initialOnWritePossible.compareAndSet(true, false)) { + isReady = true; + writer.onWritePossible(); + } else if (isReadyReturnedFalse) { isReadyReturnedFalse = false; isReady = true; writer.onWritePossible(); @@ -152,7 +156,13 @@ public void maybeOnWritePossible() throws IOException { } @Override - protected Object extractState() { + public final boolean equals(Object o) { + return o instanceof AsyncServletOutputStreamWriterConcurrencyTest + && bytesWritten == ((AsyncServletOutputStreamWriterConcurrencyTest) o).bytesWritten; + } + + @Override + public int hashCode() { return bytesWritten; } @@ -169,6 +179,6 @@ public void linCheck() { AtomicReference.class.getName()) .allMethods() .treatAsAtomic()); - LinChecker.check(AsyncServletOutputStreamWriterConcurrencyTest.class, options); + options.check(AsyncServletOutputStreamWriterConcurrencyTest.class); } } From ca8f4dfa6d76b3fe9dafcea6e43a4428ec5c167d Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 26 Aug 2025 03:27:49 -0700 Subject: [PATCH 356/591] binder: Improve error descriptions for ServiceConnection callbacks (#12263) Non-experts don't really know what these ServiceConnection callback names mean (eg b/437170499). Use the Status description to explain them a bit. Compare to https://github.com/grpc/grpc-java/pull/11628 --- .../grpc/binder/internal/ServiceBinding.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java index 42d69d27a2e..3885afb19a8 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -356,19 +356,32 @@ public void onServiceConnected(ComponentName className, IBinder binder) { @Override @MainThread public void onServiceDisconnected(ComponentName name) { - unbindInternal(Status.UNAVAILABLE.withDescription("onServiceDisconnected: " + name)); + unbindInternal( + Status.UNAVAILABLE.withDescription( + "Server process crashed, exited or was killed (onServiceDisconnected): " + name)); } @Override @MainThread public void onNullBinding(ComponentName name) { - unbindInternal(Status.UNIMPLEMENTED.withDescription("onNullBinding: " + name)); + unbindInternal( + Status.UNIMPLEMENTED.withDescription( + "Remote Service returned null from onBind() for " + + bindIntent + + " (onNullBinding): " + + name)); } @Override @MainThread public void onBindingDied(ComponentName name) { - unbindInternal(Status.UNAVAILABLE.withDescription("onBindingDied: " + name)); + unbindInternal( + Status.UNAVAILABLE.withDescription( + "Remote Service component " + + name.getClassName() + + " was disabled, or its package " + + name.getPackageName() + + " was disabled, force-stopped, replaced or uninstalled (onBindingDied).")); } @VisibleForTesting From 695014adfd33aa8facb721e358a485d08a23b76f Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 14 Aug 2025 07:23:01 -0700 Subject: [PATCH 357/591] api: Reduce allocations of Attributes.Builder Modifying existing Attributes was virtually guaranteed to allocate within build(), just because `base` was not considered for the new map size. Discard was also allocation-heavy because it often created a new map. Using a regular copy-on-write approach is enough to avoid the unnecessary allocations in both cases. This was noticed in a profile that included xds's AddressFilter.setPathFilter(), where Attributes.Builder.build() allocated 6x the memory of Attributes.Builder.data(). b/435208946#comment41 --- api/src/main/java/io/grpc/Attributes.java | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/api/src/main/java/io/grpc/Attributes.java b/api/src/main/java/io/grpc/Attributes.java index de00e63554c..c8550d176b4 100644 --- a/api/src/main/java/io/grpc/Attributes.java +++ b/api/src/main/java/io/grpc/Attributes.java @@ -215,6 +215,7 @@ public int hashCode() { * The helper class to build an Attributes instance. */ public static final class Builder { + // Exactly one of base and newdata will be set private Attributes base; private IdentityHashMap, Object> newdata; @@ -225,8 +226,11 @@ private Builder(Attributes base) { private IdentityHashMap, Object> data(int size) { if (newdata == null) { - newdata = new IdentityHashMap<>(size); + newdata = new IdentityHashMap<>(base.data.size() + size); + newdata.putAll(base.data); + base = null; } + assert base == null; return newdata; } @@ -243,12 +247,11 @@ public Builder set(Key key, T value) { * @return this */ public Builder discard(Key key) { - if (base.data.containsKey(key)) { - IdentityHashMap, Object> newBaseData = new IdentityHashMap<>(base.data); - newBaseData.remove(key); - base = new Attributes(newBaseData); - } - if (newdata != null) { + if (base != null) { + if (base.data.containsKey(key)) { + data(0).remove(key); + } + } else { newdata.remove(key); } return this; @@ -264,11 +267,6 @@ public Builder setAll(Attributes other) { */ public Attributes build() { if (newdata != null) { - for (Map.Entry, Object> entry : base.data.entrySet()) { - if (!newdata.containsKey(entry.getKey())) { - newdata.put(entry.getKey(), entry.getValue()); - } - } base = new Attributes(newdata); newdata = null; } From afef4fe097f695db76f59f0d4fedc04eb2c1815b Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Thu, 28 Aug 2025 05:57:10 +0200 Subject: [PATCH 358/591] servlet: extract ServletServerStream.serializeHeaders() method (#12299) --- .../io/grpc/servlet/ServletServerStream.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java b/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java index caab5caa8a9..0182f302698 100644 --- a/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java +++ b/servlet/src/main/java/io/grpc/servlet/ServletServerStream.java @@ -42,6 +42,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.Supplier; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -122,9 +123,13 @@ private void writeHeadersToServletResponse(Metadata metadata) { resp.setStatus(HttpServletResponse.SC_OK); resp.setContentType(CONTENT_TYPE_GRPC); + serializeHeaders(metadata, resp::addHeader); + } + + private static void serializeHeaders(Metadata metadata, BiConsumer consumer) { byte[][] serializedHeaders = TransportFrameUtil.toHttp2Headers(metadata); for (int i = 0; i < serializedHeaders.length; i += 2) { - resp.addHeader( + consumer.accept( new String(serializedHeaders[i], StandardCharsets.US_ASCII), new String(serializedHeaders[i + 1], StandardCharsets.US_ASCII)); } @@ -277,13 +282,8 @@ public void writeTrailers(Metadata trailers, boolean headersSent, Status status) if (!headersSent) { writeHeadersToServletResponse(trailers); } else { - byte[][] serializedHeaders = TransportFrameUtil.toHttp2Headers(trailers); - for (int i = 0; i < serializedHeaders.length; i += 2) { - String key = new String(serializedHeaders[i], StandardCharsets.US_ASCII); - String newValue = new String(serializedHeaders[i + 1], StandardCharsets.US_ASCII); - trailerSupplier.get().computeIfPresent(key, (k, v) -> v + "," + newValue); - trailerSupplier.get().putIfAbsent(key, newValue); - } + serializeHeaders(trailers, + (k, v) -> trailerSupplier.get().merge(k, v, (oldV, newV) -> oldV + "," + newV)); } writer.complete(); From c643e68aa1faf7f1aabf8e789030c44323fc4f77 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 28 Aug 2025 01:06:48 -0700 Subject: [PATCH 359/591] binder: REMOTE_UID must hold exactly the uid passed to the SecurityPolicy and never change (#12314) `attributes = setSecurityAttrs(attributes, remoteUid);` should not run for : - a malformed SETUP_TRANSPORT transaction - a rogue SETUP_TRANSPORT transaction that arrives post-TransportState.SETUP --- .../internal/BinderClientTransport.java | 2 +- .../RobolectricBinderTransportTest.java | 42 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 95bd531aa41..82c9e17b871 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -330,7 +330,6 @@ void notifyTerminated() { @GuardedBy("this") protected void handleSetupTransport(Parcel parcel) { int remoteUid = Binder.getCallingUid(); - attributes = setSecurityAttrs(attributes, remoteUid); if (inState(TransportState.SETUP)) { int version = parcel.readInt(); IBinder binder = parcel.readStrongBinder(); @@ -340,6 +339,7 @@ protected void handleSetupTransport(Parcel parcel) { shutdownInternal( Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); } else { + attributes = setSecurityAttrs(attributes, remoteUid); authResultFuture = checkServerAuthorizationAsync(remoteUid); Futures.addCallback( authResultFuture, diff --git a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java index 7275c47d51c..8f1209f389b 100644 --- a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java @@ -16,7 +16,11 @@ package io.grpc.binder.internal; +import static android.os.Process.myUid; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.binder.internal.BinderTransport.REMOTE_UID; +import static io.grpc.binder.internal.BinderTransport.SETUP_TRANSPORT; +import static io.grpc.binder.internal.BinderTransport.WIRE_FORMAT_VERSION; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; @@ -28,6 +32,8 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.ServiceInfo; +import android.os.Binder; +import android.os.Parcel; import androidx.test.core.app.ApplicationProvider; import androidx.test.core.content.pm.ApplicationInfoBuilder; import androidx.test.core.content.pm.PackageInfoBuilder; @@ -38,9 +44,11 @@ import io.grpc.binder.AndroidComponentAddress; import io.grpc.binder.ApiConstants; import io.grpc.binder.AsyncSecurityPolicy; +import io.grpc.binder.SecurityPolicies; import io.grpc.binder.internal.SettableAsyncSecurityPolicy.AuthRequest; import io.grpc.internal.AbstractTransportTest; import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; +import io.grpc.internal.ConnectionClientTransport; import io.grpc.internal.GrpcUtil; import io.grpc.internal.InternalServer; import io.grpc.internal.ManagedClientTransport; @@ -109,7 +117,7 @@ public static ImmutableList data() { public void setUp() { serverAppInfo = ApplicationInfoBuilder.newBuilder().setPackageName("the.server.package").build(); - serverAppInfo.uid = android.os.Process.myUid(); + serverAppInfo.uid = myUid(); serverPkgInfo = PackageInfoBuilder.newBuilder() .setPackageName(serverAppInfo.packageName) @@ -264,6 +272,38 @@ public void eagAttributeCanOverrideChannelPreAuthServerSetting() throws Exceptio verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportReady(); } + @Test + public void clientIgnoresDuplicateSetupTransaction() throws Exception { + server.start(serverListener); + client = + newClientTransportBuilder() + .setFactory( + newClientTransportFactoryBuilder() + .setSecurityPolicy(SecurityPolicies.internalOnly()) + .buildClientTransportFactory()) + .build(); + runIfNotNull(client.start(mockClientTransportListener)); + verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportReady(); + + assertThat(((ConnectionClientTransport) client).getAttributes().get(REMOTE_UID)) + .isEqualTo(myUid()); + + Parcel setupParcel = Parcel.obtain(); + try { + setupParcel.writeInt(WIRE_FORMAT_VERSION); + setupParcel.writeStrongBinder(new Binder()); + setupParcel.setDataPosition(0); + ShadowBinder.setCallingUid(1 + myUid()); + ((BinderClientTransport) client).handleTransaction(SETUP_TRANSPORT, setupParcel); + } finally { + ShadowBinder.setCallingUid(myUid()); + setupParcel.recycle(); + } + + assertThat(((ConnectionClientTransport) client).getAttributes().get(REMOTE_UID)) + .isEqualTo(myUid()); + } + @Test @Ignore("See BinderTransportTest#socketStats.") @Override From cdd3202a1f04ad496259712f8004201b420cecc8 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 27 Aug 2025 15:22:36 -0700 Subject: [PATCH 360/591] interop-testing: Show full status for interop test failures It's pretty annoying to see a test failure with "expected: but was:" and not know the description or throwable cause of the status. Introduce a convenience to include the full status on unexpected Status.Code. There were two usages of assertWithMessage() that did give nice errors; those were converted to this new utility. It'd be even better to make a StatusSubject, but that'd take more time and this was easy and still an improvement. This was created because we're seeing servlet test failures with INTERNAL code, and we need to see the details. --- .../integration/AbstractInteropTest.java | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java index 88d570e7134..11455790497 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java @@ -483,7 +483,7 @@ public void clientCompressedUnary(boolean probe) throws Exception { blockingStub.unaryCall(expectCompressedRequest); fail("expected INVALID_ARGUMENT"); } catch (StatusRuntimeException e) { - assertEquals(Status.INVALID_ARGUMENT.getCode(), e.getStatus().getCode()); + assertCodeEquals(Status.Code.INVALID_ARGUMENT, e.getStatus()); } assertStatsTrace("grpc.testing.TestService/UnaryCall", Status.Code.INVALID_ARGUMENT); } @@ -652,7 +652,7 @@ public void clientCompressedStreaming(boolean probe) throws Exception { responseObserver.awaitCompletion(operationTimeoutMillis(), TimeUnit.MILLISECONDS); Throwable e = responseObserver.getError(); assertNotNull("expected INVALID_ARGUMENT", e); - assertEquals(Status.INVALID_ARGUMENT.getCode(), Status.fromThrowable(e).getCode()); + assertCodeEquals(Status.Code.INVALID_ARGUMENT, Status.fromThrowable(e)); } // Start a new stream @@ -801,8 +801,7 @@ public void cancelAfterBegin() throws Exception { requestObserver.onError(new RuntimeException()); responseObserver.awaitCompletion(); assertEquals(Arrays.asList(), responseObserver.getValues()); - assertEquals(Status.Code.CANCELLED, - Status.fromThrowable(responseObserver.getError()).getCode()); + assertCodeEquals(Status.Code.CANCELLED, Status.fromThrowable(responseObserver.getError())); if (metricsExpected()) { MetricsRecord clientStartRecord = clientStatsRecorder.pollRecord(5, TimeUnit.SECONDS); @@ -839,8 +838,7 @@ public void cancelAfterFirstResponse() throws Exception { requestObserver.onError(new RuntimeException()); responseObserver.awaitCompletion(operationTimeoutMillis(), TimeUnit.MILLISECONDS); assertEquals(1, responseObserver.getValues().size()); - assertEquals(Status.Code.CANCELLED, - Status.fromThrowable(responseObserver.getError()).getCode()); + assertCodeEquals(Status.Code.CANCELLED, Status.fromThrowable(responseObserver.getError())); assertStatsTrace("grpc.testing.TestService/FullDuplexCall", Status.Code.CANCELLED); } @@ -1107,7 +1105,7 @@ public void deadlineExceeded() throws Exception { stub.streamingOutputCall(request).next(); fail("Expected deadline to be exceeded"); } catch (StatusRuntimeException ex) { - assertEquals(Status.DEADLINE_EXCEEDED.getCode(), ex.getStatus().getCode()); + assertCodeEquals(Status.Code.DEADLINE_EXCEEDED, ex.getStatus()); String desc = ex.getStatus().getDescription(); assertTrue(desc, // There is a race between client and server-side deadline expiration. @@ -1153,8 +1151,7 @@ public void deadlineExceededServerStreaming() throws Exception { .withDeadlineAfter(30, TimeUnit.MILLISECONDS) .streamingOutputCall(request, recorder); recorder.awaitCompletion(); - assertEquals(Status.DEADLINE_EXCEEDED.getCode(), - Status.fromThrowable(recorder.getError()).getCode()); + assertCodeEquals(Status.Code.DEADLINE_EXCEEDED, Status.fromThrowable(recorder.getError())); if (metricsExpected()) { // Stream may not have been created when deadline is exceeded, thus we don't check tracer // stats. @@ -1179,7 +1176,7 @@ public void deadlineInPast() throws Exception { .emptyCall(Empty.getDefaultInstance()); fail("Should have thrown"); } catch (StatusRuntimeException ex) { - assertEquals(Status.Code.DEADLINE_EXCEEDED, ex.getStatus().getCode()); + assertCodeEquals(Status.Code.DEADLINE_EXCEEDED, ex.getStatus()); assertThat(ex.getStatus().getDescription()) .startsWith("ClientCall started after CallOptions deadline was exceeded"); } @@ -1212,7 +1209,7 @@ public void deadlineInPast() throws Exception { .emptyCall(Empty.getDefaultInstance()); fail("Should have thrown"); } catch (StatusRuntimeException ex) { - assertEquals(Status.Code.DEADLINE_EXCEEDED, ex.getStatus().getCode()); + assertCodeEquals(Status.Code.DEADLINE_EXCEEDED, ex.getStatus()); assertThat(ex.getStatus().getDescription()) .startsWith("ClientCall started after CallOptions deadline was exceeded"); } @@ -1278,8 +1275,7 @@ public void maxInboundSize_tooBig() { stub.streamingOutputCall(request).next(); fail(); } catch (StatusRuntimeException ex) { - Status s = ex.getStatus(); - assertWithMessage(s.toString()).that(s.getCode()).isEqualTo(Status.Code.RESOURCE_EXHAUSTED); + assertCodeEquals(Status.Code.RESOURCE_EXHAUSTED, ex.getStatus()); assertThat(Throwables.getStackTraceAsString(ex)).contains("exceeds maximum"); } } @@ -1334,8 +1330,7 @@ public void maxOutboundSize_tooBig() { stub.streamingOutputCall(request).next(); fail(); } catch (StatusRuntimeException ex) { - Status s = ex.getStatus(); - assertWithMessage(s.toString()).that(s.getCode()).isEqualTo(Status.Code.CANCELLED); + assertCodeEquals(Status.Code.CANCELLED, ex.getStatus()); assertThat(Throwables.getStackTraceAsString(ex)).contains("message too large"); } } @@ -1557,7 +1552,7 @@ public void statusCodeAndMessage() throws Exception { blockingStub.unaryCall(simpleRequest); fail(); } catch (StatusRuntimeException e) { - assertEquals(Status.UNKNOWN.getCode(), e.getStatus().getCode()); + assertCodeEquals(Status.Code.UNKNOWN, e.getStatus()); assertEquals(errorMessage, e.getStatus().getDescription()); } assertStatsTrace("grpc.testing.TestService/UnaryCall", Status.Code.UNKNOWN); @@ -1573,7 +1568,7 @@ public void statusCodeAndMessage() throws Exception { .isTrue(); assertThat(responseObserver.getError()).isNotNull(); Status status = Status.fromThrowable(responseObserver.getError()); - assertEquals(Status.UNKNOWN.getCode(), status.getCode()); + assertCodeEquals(Status.Code.UNKNOWN, status); assertEquals(errorMessage, status.getDescription()); assertStatsTrace("grpc.testing.TestService/FullDuplexCall", Status.Code.UNKNOWN); } @@ -1593,7 +1588,7 @@ public void specialStatusMessage() throws Exception { blockingStub.unaryCall(simpleRequest); fail(); } catch (StatusRuntimeException e) { - assertEquals(Status.UNKNOWN.getCode(), e.getStatus().getCode()); + assertCodeEquals(Status.Code.UNKNOWN, e.getStatus()); assertEquals(errorMessage, e.getStatus().getDescription()); } assertStatsTrace("grpc.testing.TestService/UnaryCall", Status.Code.UNKNOWN); @@ -1606,7 +1601,7 @@ public void unimplementedMethod() { blockingStub.unimplementedCall(Empty.getDefaultInstance()); fail(); } catch (StatusRuntimeException e) { - assertEquals(Status.UNIMPLEMENTED.getCode(), e.getStatus().getCode()); + assertCodeEquals(Status.Code.UNIMPLEMENTED, e.getStatus()); } assertClientStatsTrace("grpc.testing.TestService/UnimplementedCall", @@ -1622,7 +1617,7 @@ public void unimplementedService() { stub.unimplementedCall(Empty.getDefaultInstance()); fail(); } catch (StatusRuntimeException e) { - assertEquals(Status.UNIMPLEMENTED.getCode(), e.getStatus().getCode()); + assertCodeEquals(Status.Code.UNIMPLEMENTED, e.getStatus()); } assertStatsTrace("grpc.testing.UnimplementedService/UnimplementedCall", @@ -1652,8 +1647,8 @@ public void timeoutOnSleepingServer() throws Exception { assertTrue(responseObserver.awaitCompletion(operationTimeoutMillis(), TimeUnit.MILLISECONDS)); assertEquals(0, responseObserver.getValues().size()); - assertEquals(Status.DEADLINE_EXCEEDED.getCode(), - Status.fromThrowable(responseObserver.getError()).getCode()); + assertCodeEquals( + Status.Code.DEADLINE_EXCEEDED, Status.fromThrowable(responseObserver.getError())); if (metricsExpected()) { // CensusStreamTracerModule record final status in the interceptor, thus is guaranteed to be @@ -2035,6 +2030,10 @@ private void assertPayload(Payload expected, Payload actual) { } } + private static void assertCodeEquals(Status.Code expected, Status actual) { + assertWithMessage("Unexpected status: %s", actual).that(actual.getCode()).isEqualTo(expected); + } + /** * Captures the request attributes. Useful for testing ServerCalls. * {@link ServerCall#getAttributes()} From ad5c6d557511d27cd07f8d57fb93387323556d4e Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 28 Aug 2025 15:58:39 -0700 Subject: [PATCH 361/591] binder: Replace queryIntentServices() hack with the new SystemApis.createContextAsUser() (#12280) createContextAsUser() wrapper makes the call to PackageManager's resolveService() look the same in both the same-user and cross-user cases. This is how the Android team recommends accessing XXXAsUser() APIs in general. We can also remove all the apologies for using reflection since SystemApis already explains all that. --- .../grpc/binder/internal/ServiceBinding.java | 69 +++++-------------- .../binder/internal/ServiceBindingTest.java | 9 +++ 2 files changed, 28 insertions(+), 50 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java index 3885afb19a8..3351736108e 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -17,6 +17,7 @@ package io.grpc.binder.internal; import static com.google.common.base.Preconditions.checkState; +import static io.grpc.binder.internal.SystemApis.createContextAsUser; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; @@ -32,13 +33,10 @@ import androidx.annotation.AnyThread; import androidx.annotation.MainThread; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.VerifyException; import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Status; import io.grpc.StatusException; import io.grpc.binder.BinderChannelCredentials; -import java.lang.reflect.Method; -import java.util.List; import java.util.concurrent.Executor; import java.util.logging.Level; import java.util.logging.Logger; @@ -93,8 +91,6 @@ public String methodName() { private final Observer observer; private final Executor mainThreadExecutor; - private static volatile Method queryIntentServicesAsUserMethod; - @GuardedBy("this") private State state; @@ -194,8 +190,10 @@ private static Status bindInternal( break; case BIND_SERVICE_AS_USER: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // We don't need SystemApis because bindServiceAsUser() is simply public in R+. bindResult = context.bindServiceAsUser(bindIntent, conn, flags, targetUserHandle); } else { + // TODO(#12279): Use SystemApis to make this work pre-R. return Status.INTERNAL.withDescription("Cross user Channel requires Android R+"); } break; @@ -266,51 +264,9 @@ void unbindInternal(Status reason) { } } - // Sadly the PackageManager#resolveServiceAsUser() API we need isn't part of the SDK or even a - // @SystemApi as of this writing. Modern Android prevents even system apps from calling it, by any - // means (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces). - // So instead we call queryIntentServicesAsUser(), which does more than we need but *is* a - // @SystemApi in all the SDK versions where we support cross-user Channels. - @Nullable - private static ResolveInfo resolveServiceAsUser( - PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) { - List results = - queryIntentServicesAsUser(packageManager, intent, flags, targetUserHandle); - // The first query result is "what would be returned by resolveService", per the javadoc. - return (results != null && !results.isEmpty()) ? results.get(0) : null; - } - - // The cross-user Channel feature requires the client to be a system app so we assume @SystemApi - // queryIntentServicesAsUser() is visible to us at runtime. It would be visible at build time too, - // if our host system app were written to call it directly. We only have to use reflection here - // because grpc-java is a library built outside the Android source tree where the compiler can't - // see the "non-SDK" @SystemApis that we need. - @Nullable - @SuppressWarnings("unchecked") // Safe by PackageManager#queryIntentServicesAsUser spec in AOSP. - private static List queryIntentServicesAsUser( - PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) { - try { - if (queryIntentServicesAsUserMethod == null) { - synchronized (ServiceBinding.class) { - if (queryIntentServicesAsUserMethod == null) { - queryIntentServicesAsUserMethod = - PackageManager.class.getMethod( - "queryIntentServicesAsUser", Intent.class, int.class, UserHandle.class); - } - } - } - return (List) - queryIntentServicesAsUserMethod.invoke(packageManager, intent, flags, targetUserHandle); - } catch (ReflectiveOperationException e) { - throw new VerifyException(e); - } - } - @AnyThread @Override public ServiceInfo resolve() throws StatusException { - checkState(sourceContext != null); - PackageManager packageManager = sourceContext.getPackageManager(); int flags = 0; if (Build.VERSION.SDK_INT >= 29) { // Filter out non-'directBootAware' s when 'targetUserHandle' is locked. Here's why: @@ -320,9 +276,9 @@ public ServiceInfo resolve() throws StatusException { flags |= PackageManager.MATCH_DIRECT_BOOT_AUTO; } ResolveInfo resolveInfo = - targetUserHandle != null - ? resolveServiceAsUser(packageManager, bindIntent, flags, targetUserHandle) - : packageManager.resolveService(bindIntent, flags); + getContextForTargetUser("Cross-user pre-auth") + .getPackageManager() + .resolveService(bindIntent, flags); if (resolveInfo == null) { throw Status.UNIMPLEMENTED // Same status code as when bindService() returns false. .withDescription("resolveService(" + bindIntent + " / " + targetUserHandle + ") was null") @@ -331,6 +287,19 @@ public ServiceInfo resolve() throws StatusException { return resolveInfo.serviceInfo; } + private Context getContextForTargetUser(String purpose) throws StatusException { + checkState(sourceContext != null, "Already unbound!"); + try { + return targetUserHandle == null + ? sourceContext + : createContextAsUser(sourceContext, targetUserHandle, /* flags= */ 0); + } catch (ReflectiveOperationException e) { + throw Status.INTERNAL + .withDescription(purpose + " requires SDK_INT >= R and @SystemApi visibility") + .asException(); + } + } + @MainThread private void clearReferences() { sourceContext = null; diff --git a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java index 2079b0eed2c..0875881dcc5 100644 --- a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java @@ -304,6 +304,15 @@ public void testResolveWithTargetUserHandle() throws Exception { assertThat(resolvedServiceInfo.processName).isEqualTo(serviceInfo.processName); } + @Test + @Config(sdk = 29) + public void testResolveWithUnsupportedTargetUserHandle() throws Exception { + binding = newBuilder().setTargetUserHandle(generateUserHandle(/* userId= */ 0)).build(); + StatusException statusException = assertThrows(StatusException.class, binding::resolve); + assertThat(statusException.getStatus().getCode()).isEqualTo(Code.INTERNAL); + assertThat(statusException.getStatus().getDescription()).contains("SDK_INT >= R"); + } + @Test public void testResolveNonExistentServiceThrows() throws Exception { ComponentName doesNotExistService = new ComponentName("does.not.exist", "NoService"); From 7322c075ba048f06281149d40084b37b3323e341 Mon Sep 17 00:00:00 2001 From: Lam Gia Thuan <22922064+duckladydinh@users.noreply.github.com> Date: Thu, 28 Aug 2025 23:07:14 +0000 Subject: [PATCH 362/591] bazel: Use java/proto rules from rules_java/rules_proto instead of native rules It's discouraged in modern Bazel to use native rules and will make it difficult for modules dependent on grpc-java to fully migrate to new versions of Bazel (like grpc-kotlin). For example, try building this repo using: ```bash $ bazelisk build ... --incompatible_autoload_externally= ``` It will fail. **IMPORTANT**: Now you still see errors after this commit, but it's better because the errors come from `@protobuf`, not `@grpc-java`. --- BUILD.bazel | 1 + alts/BUILD.bazel | 1 + api/BUILD.bazel | 1 + auth/BUILD.bazel | 1 + census/BUILD.bazel | 1 + compiler/BUILD.bazel | 1 + context/BUILD.bazel | 1 + core/BUILD.bazel | 1 + googleapis/BUILD.bazel | 1 + grpclb/BUILD.bazel | 1 + inprocess/BUILD.bazel | 1 + java_grpc_library.bzl | 2 +- netty/BUILD.bazel | 1 + netty/shaded/BUILD.bazel | 1 + okhttp/BUILD.bazel | 1 + protobuf-lite/BUILD.bazel | 1 + protobuf/BUILD.bazel | 1 + rls/BUILD.bazel | 1 + s2a/BUILD.bazel | 1 + services/BUILD.bazel | 1 + stub/BUILD.bazel | 1 + testing-proto/BUILD.bazel | 2 ++ testing/BUILD.bazel | 1 + util/BUILD.bazel | 1 + xds/BUILD.bazel | 2 ++ 25 files changed, 27 insertions(+), 1 deletion(-) diff --git a/BUILD.bazel b/BUILD.bazel index 8350ed9aa39..fee2c0bd12d 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("@rules_java//java:defs.bzl", "java_library", "java_plugin", "java_proto_library") load("@rules_jvm_external//:defs.bzl", "artifact") load(":java_grpc_library.bzl", "java_grpc_library") diff --git a/alts/BUILD.bazel b/alts/BUILD.bazel index 3595064ffa4..2d9d3508461 100644 --- a/alts/BUILD.bazel +++ b/alts/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library", "java_proto_library") load("@rules_jvm_external//:defs.bzl", "artifact") load("@rules_proto//proto:defs.bzl", "proto_library") load("//:java_grpc_library.bzl", "java_grpc_library") diff --git a/api/BUILD.bazel b/api/BUILD.bazel index 965d14a9eb0..4c5e750b5e4 100644 --- a/api/BUILD.bazel +++ b/api/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/auth/BUILD.bazel b/auth/BUILD.bazel index a19562fa7f7..da44243e583 100644 --- a/auth/BUILD.bazel +++ b/auth/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/census/BUILD.bazel b/census/BUILD.bazel index 36be88fc3d8..f017eeaf8bd 100644 --- a/census/BUILD.bazel +++ b/census/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/compiler/BUILD.bazel b/compiler/BUILD.bazel index 6d885ef3f69..30ff3ce5dff 100644 --- a/compiler/BUILD.bazel +++ b/compiler/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_cc//cc:defs.bzl", "cc_binary") load("@rules_jvm_external//:defs.bzl", "artifact") load("//:java_grpc_library.bzl", "java_rpc_toolchain") diff --git a/context/BUILD.bazel b/context/BUILD.bazel index d0c4b04ce00..fad8e2ef881 100644 --- a/context/BUILD.bazel +++ b/context/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") java_library( name = "context", visibility = ["//visibility:public"], diff --git a/core/BUILD.bazel b/core/BUILD.bazel index 6dd5199b5c0..1a743ff9eda 100644 --- a/core/BUILD.bazel +++ b/core/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/googleapis/BUILD.bazel b/googleapis/BUILD.bazel index 9ce4179f7b7..42632e0f99d 100644 --- a/googleapis/BUILD.bazel +++ b/googleapis/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/grpclb/BUILD.bazel b/grpclb/BUILD.bazel index 902ae7f47d4..ca9975b7ce6 100644 --- a/grpclb/BUILD.bazel +++ b/grpclb/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") load("//:java_grpc_library.bzl", "java_grpc_library") diff --git a/inprocess/BUILD.bazel b/inprocess/BUILD.bazel index bef38612713..e9c5001c5ec 100644 --- a/inprocess/BUILD.bazel +++ b/inprocess/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/java_grpc_library.bzl b/java_grpc_library.bzl index 2caa161b268..aaebfe3f933 100644 --- a/java_grpc_library.bzl +++ b/java_grpc_library.bzl @@ -1,5 +1,5 @@ """Build rule for java_grpc_library.""" - +load("@rules_proto//proto:defs.bzl", "ProtoInfo") load("@rules_java//java:defs.bzl", "JavaInfo", "JavaPluginInfo", "java_common") _JavaRpcToolchainInfo = provider( diff --git a/netty/BUILD.bazel b/netty/BUILD.bazel index 52fdcb19fd8..8253d1f5bff 100644 --- a/netty/BUILD.bazel +++ b/netty/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/netty/shaded/BUILD.bazel b/netty/shaded/BUILD.bazel index 6248f406de9..0a93907bd2f 100644 --- a/netty/shaded/BUILD.bazel +++ b/netty/shaded/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") # Publicly exposed in //netty package. Purposefully does not export any symbols. diff --git a/okhttp/BUILD.bazel b/okhttp/BUILD.bazel index 80068c9bb5b..74a9f7a4300 100644 --- a/okhttp/BUILD.bazel +++ b/okhttp/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/protobuf-lite/BUILD.bazel b/protobuf-lite/BUILD.bazel index dad794e8b58..97a5e492d80 100644 --- a/protobuf-lite/BUILD.bazel +++ b/protobuf-lite/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/protobuf/BUILD.bazel b/protobuf/BUILD.bazel index 02a136554d2..a31f8b6f6f5 100644 --- a/protobuf/BUILD.bazel +++ b/protobuf/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/rls/BUILD.bazel b/rls/BUILD.bazel index bfa0c7eee97..70c17a9c8b6 100644 --- a/rls/BUILD.bazel +++ b/rls/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") load("//:java_grpc_library.bzl", "java_grpc_library") diff --git a/s2a/BUILD.bazel b/s2a/BUILD.bazel index 807103bde4e..943ae9ece46 100644 --- a/s2a/BUILD.bazel +++ b/s2a/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/services/BUILD.bazel b/services/BUILD.bazel index 9ac894ffaee..ba9d334a5c9 100644 --- a/services/BUILD.bazel +++ b/services/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") load("//:java_grpc_library.bzl", "java_grpc_library") diff --git a/stub/BUILD.bazel b/stub/BUILD.bazel index 692ebd6059f..f9188c27272 100644 --- a/stub/BUILD.bazel +++ b/stub/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/testing-proto/BUILD.bazel b/testing-proto/BUILD.bazel index 985dbd9777e..f02957c6fc3 100644 --- a/testing-proto/BUILD.bazel +++ b/testing-proto/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_proto_library") +load("@rules_proto//proto:defs.bzl", "proto_library") load("//:java_grpc_library.bzl", "java_grpc_library") proto_library( diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index 78f9b840754..d280ab97ee1 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/util/BUILD.bazel b/util/BUILD.bazel index 8fb00e21d56..32d5a367b95 100644 --- a/util/BUILD.bazel +++ b/util/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") java_library( diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index 203fa2c3d71..627bfdd62a5 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -1,3 +1,5 @@ +load("@rules_java//java:defs.bzl", "java_binary", "java_library", "java_proto_library", "java_test") +load("@rules_proto//proto:defs.bzl", "proto_library") load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar") load("@rules_jvm_external//:defs.bzl", "artifact") load("//:java_grpc_library.bzl", "INTERNAL_java_grpc_library_for_xds", "java_grpc_library", "java_rpc_toolchain") From 3d5eff99ec7061d56351e098fed1497ebef1992d Mon Sep 17 00:00:00 2001 From: Hongxu Xu Date: Thu, 28 Aug 2025 19:12:16 -0400 Subject: [PATCH 363/591] bazel: remove unnecessary build deps (#12310) --- grpclb/BUILD.bazel | 1 - s2a/BUILD.bazel | 1 - xds/BUILD.bazel | 1 - 3 files changed, 3 deletions(-) diff --git a/grpclb/BUILD.bazel b/grpclb/BUILD.bazel index ca9975b7ce6..4612968ebcd 100644 --- a/grpclb/BUILD.bazel +++ b/grpclb/BUILD.bazel @@ -17,7 +17,6 @@ java_library( "//context", "//core:internal", "//stub", - "//util", "@com_google_protobuf//:protobuf_java_util", "@io_grpc_grpc_proto//:grpclb_load_balancer_java_proto", artifact("com.google.code.findbugs:jsr305"), diff --git a/s2a/BUILD.bazel b/s2a/BUILD.bazel index 943ae9ece46..6f58670534b 100644 --- a/s2a/BUILD.bazel +++ b/s2a/BUILD.bazel @@ -57,7 +57,6 @@ java_library( ], deps = [ ":token_manager", - ":s2a_channel_pool", ":s2a_identity", "//api", "//core:internal", diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index 627bfdd62a5..66c790a654d 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -344,7 +344,6 @@ java_library( "//api", "//core:internal", "//stub", - "//testing", "//testing-proto:simpleservice_java_grpc", "//testing-proto:simpleservice_java_proto", "//util", From 4f6948f594a5cf5775ef3f2f8f236ac91e3e2ad0 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Fri, 29 Aug 2025 12:00:22 +0530 Subject: [PATCH 364/591] Remove org.apache.tomcat:annotations-api from README (#12320) With the default @generated=omit option used by grpc-compiler, it is no longer necessary to have the compile-only dependency on org.apache.tomcat:annotations-api. --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index d7e57a9f80c..d696164ee32 100644 --- a/README.md +++ b/README.md @@ -69,12 +69,6 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: grpc-stub 1.75.0 - - org.apache.tomcat - annotations-api - 6.0.53 - provided - ``` Or for Gradle with non-Android, add to your dependencies: @@ -82,7 +76,6 @@ Or for Gradle with non-Android, add to your dependencies: runtimeOnly 'io.grpc:grpc-netty-shaded:1.75.0' implementation 'io.grpc:grpc-protobuf:1.75.0' implementation 'io.grpc:grpc-stub:1.75.0' -compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and @@ -91,7 +84,6 @@ For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and implementation 'io.grpc:grpc-okhttp:1.75.0' implementation 'io.grpc:grpc-protobuf-lite:1.75.0' implementation 'io.grpc:grpc-stub:1.75.0' -compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For [Bazel](https://bazel.build), you can either From d9509b5bb3b9525669346124c7560f1fda5a3638 Mon Sep 17 00:00:00 2001 From: zrlw Date: Sat, 30 Aug 2025 05:01:11 +0800 Subject: [PATCH 365/591] Upgrade guava version to 33.4.8 (#12219) Guava seems to call a deprecated sun.misc.Unsafe::objectFieldOffset method which might be removed in a future JDK release. There are still a few things to do for -android versions, which are tracked in https://github.com/google/guava/issues/7742, see details at google/guava#7811 Fixes #12215 --- MODULE.bazel | 6 +++--- core/build.gradle | 2 +- cronet/build.gradle | 1 + examples/android/clientcache/settings.gradle | 16 ++++++++++++++++ examples/android/helloworld/settings.gradle | 16 ++++++++++++++++ examples/android/routeguide/settings.gradle | 16 ++++++++++++++++ examples/android/strictmode/settings.gradle | 16 ++++++++++++++++ examples/example-alts/settings.gradle | 14 ++++++++++++++ examples/example-debug/settings.gradle | 16 ++++++++++++++++ examples/example-dualstack/settings.gradle | 14 ++++++++++++++ examples/example-gauth/settings.gradle | 14 ++++++++++++++ .../settings.gradle | 16 ++++++++++++++++ .../example-gcp-observability/settings.gradle | 16 ++++++++++++++++ examples/example-hostname/settings.gradle | 16 ++++++++++++++++ examples/example-jwt-auth/settings.gradle | 14 ++++++++++++++ examples/example-oauth/settings.gradle | 14 ++++++++++++++ examples/example-opentelemetry/settings.gradle | 16 ++++++++++++++++ examples/example-orca/settings.gradle | 16 ++++++++++++++++ examples/example-reflection/settings.gradle | 16 ++++++++++++++++ examples/example-servlet/settings.gradle | 14 ++++++++++++++ examples/example-tls/settings.gradle | 14 ++++++++++++++ examples/example-xds/settings.gradle | 16 ++++++++++++++++ examples/settings.gradle | 14 ++++++++++++++ gradle/libs.versions.toml | 14 +++++--------- repositories.bzl | 6 +++--- s2a/build.gradle | 1 + settings.gradle | 13 +++++++++++++ 27 files changed, 331 insertions(+), 16 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 7d49c4e4b49..4465f46e6c5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -15,9 +15,9 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.auto.value:auto-value:1.11.0", "com.google.code.findbugs:jsr305:3.0.2", "com.google.code.gson:gson:2.11.0", - "com.google.errorprone:error_prone_annotations:2.30.0", + "com.google.errorprone:error_prone_annotations:2.36.0", "com.google.guava:failureaccess:1.0.1", - "com.google.guava:guava:33.3.1-android", + "com.google.guava:guava:33.4.8-android", "com.google.re2j:re2j:1.8", "com.google.s2a.proto.v2:s2a-proto:0.1.2", "com.google.truth:truth:1.4.2", @@ -41,7 +41,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", "junit:junit:4.13.2", - "org.checkerframework:checker-qual:3.12.0", + "org.checkerframework:checker-qual:3.49.5", "org.codehaus.mojo:animal-sniffer-annotations:1.24", ] # GRPC_DEPS_END diff --git a/core/build.gradle b/core/build.gradle index 2fac9ddba04..b320f326b41 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,6 +1,6 @@ buildscript { dependencies { - classpath 'com.google.guava:guava:30.0-android' + classpath 'com.google.guava:guava:33.4.8-android' } } diff --git a/cronet/build.gradle b/cronet/build.gradle index 0715b4129bf..d6d773a97e4 100644 --- a/cronet/build.gradle +++ b/cronet/build.gradle @@ -46,6 +46,7 @@ dependencies { libraries.cronet.api implementation project(':grpc-core') implementation libraries.guava + implementation 'org.checkerframework:checker-qual:3.49.5' testImplementation project(':grpc-testing') testImplementation libraries.cronet.embedded diff --git a/examples/android/clientcache/settings.gradle b/examples/android/clientcache/settings.gradle index e7b4def49cb..6208d70e838 100644 --- a/examples/android/clientcache/settings.gradle +++ b/examples/android/clientcache/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + include ':app' diff --git a/examples/android/helloworld/settings.gradle b/examples/android/helloworld/settings.gradle index e7b4def49cb..6208d70e838 100644 --- a/examples/android/helloworld/settings.gradle +++ b/examples/android/helloworld/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + include ':app' diff --git a/examples/android/routeguide/settings.gradle b/examples/android/routeguide/settings.gradle index e7b4def49cb..6208d70e838 100644 --- a/examples/android/routeguide/settings.gradle +++ b/examples/android/routeguide/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + include ':app' diff --git a/examples/android/strictmode/settings.gradle b/examples/android/strictmode/settings.gradle index e7b4def49cb..6208d70e838 100644 --- a/examples/android/strictmode/settings.gradle +++ b/examples/android/strictmode/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + include ':app' diff --git a/examples/example-alts/settings.gradle b/examples/example-alts/settings.gradle index c665ff96674..6bd0f0cdc2d 100644 --- a/examples/example-alts/settings.gradle +++ b/examples/example-alts/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-debug/settings.gradle b/examples/example-debug/settings.gradle index 3700c983b6c..48c08629ca9 100644 --- a/examples/example-debug/settings.gradle +++ b/examples/example-debug/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-debug' diff --git a/examples/example-dualstack/settings.gradle b/examples/example-dualstack/settings.gradle index 49a762696f7..160d5134334 100644 --- a/examples/example-dualstack/settings.gradle +++ b/examples/example-dualstack/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-gauth/settings.gradle b/examples/example-gauth/settings.gradle index c665ff96674..6bd0f0cdc2d 100644 --- a/examples/example-gauth/settings.gradle +++ b/examples/example-gauth/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-gcp-csm-observability/settings.gradle b/examples/example-gcp-csm-observability/settings.gradle index 6b7615117d6..44e6f340ede 100644 --- a/examples/example-gcp-csm-observability/settings.gradle +++ b/examples/example-gcp-csm-observability/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-gcp-csm-observability' diff --git a/examples/example-gcp-observability/settings.gradle b/examples/example-gcp-observability/settings.gradle index 1e4ba3812eb..39efc20a459 100644 --- a/examples/example-gcp-observability/settings.gradle +++ b/examples/example-gcp-observability/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-gcp-observability' diff --git a/examples/example-hostname/settings.gradle b/examples/example-hostname/settings.gradle index aa159eb0946..5bd641b3fc1 100644 --- a/examples/example-hostname/settings.gradle +++ b/examples/example-hostname/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'hostname' diff --git a/examples/example-jwt-auth/settings.gradle b/examples/example-jwt-auth/settings.gradle index c665ff96674..6bd0f0cdc2d 100644 --- a/examples/example-jwt-auth/settings.gradle +++ b/examples/example-jwt-auth/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-oauth/settings.gradle b/examples/example-oauth/settings.gradle index c665ff96674..6bd0f0cdc2d 100644 --- a/examples/example-oauth/settings.gradle +++ b/examples/example-oauth/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-opentelemetry/settings.gradle b/examples/example-opentelemetry/settings.gradle index ff7ea3fc2be..26e3bea044b 100644 --- a/examples/example-opentelemetry/settings.gradle +++ b/examples/example-opentelemetry/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-opentelemetry' diff --git a/examples/example-orca/settings.gradle b/examples/example-orca/settings.gradle index 3c62dc663ce..12536c0ca8d 100644 --- a/examples/example-orca/settings.gradle +++ b/examples/example-orca/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-orca' diff --git a/examples/example-reflection/settings.gradle b/examples/example-reflection/settings.gradle index dccb973085e..28e44b77905 100644 --- a/examples/example-reflection/settings.gradle +++ b/examples/example-reflection/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-reflection' diff --git a/examples/example-servlet/settings.gradle b/examples/example-servlet/settings.gradle index c665ff96674..6bd0f0cdc2d 100644 --- a/examples/example-servlet/settings.gradle +++ b/examples/example-servlet/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-tls/settings.gradle b/examples/example-tls/settings.gradle index c665ff96674..6bd0f0cdc2d 100644 --- a/examples/example-tls/settings.gradle +++ b/examples/example-tls/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-xds/settings.gradle b/examples/example-xds/settings.gradle index 878f1f23ae3..4197fa6760d 100644 --- a/examples/example-xds/settings.gradle +++ b/examples/example-xds/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-xds' diff --git a/examples/settings.gradle b/examples/settings.gradle index dadd24b1c0f..4d39e8b45ba 100644 --- a/examples/settings.gradle +++ b/examples/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a374ba5aa73..e66f253fc21 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,11 +31,7 @@ commons-math3 = "org.apache.commons:commons-math3:3.6.1" conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2" cronet-api = "org.chromium.net:cronet-api:119.6045.31" cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31" -# error-prone 2.31.0+ blocked on https://github.com/grpc/grpc-java/issues/10152 -# It breaks Bazel (ArrayIndexOutOfBoundsException in turbine) and Dexing ("D8: -# java.lang.NullPointerException"). We can trivially upgrade the Bazel CI to -# 6.3.0+ (https://github.com/bazelbuild/bazel/issues/18743). -errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.30.0" +errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.36.0" # error-prone 2.32.0+ require Java 17+ errorprone-core = "com.google.errorprone:error_prone_core:2.31.0" google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.59.2" @@ -47,13 +43,13 @@ google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.24.1 google-cloud-logging = "com.google.cloud:google-cloud-logging:3.23.1" # 2.12.1 requires error_prone_annotations:2.36.0 but we are stuck with 2.30.0 gson = "com.google.code.gson:gson:2.11.0" -# 33.4.0 requires com.google.errorprone:error_prone_annotations:2.36.0 but we are stuck with 2.30.0 (see above) -guava = "com.google.guava:guava:33.3.1-android" +# 33.4.8 requires com.google.errorprone:error_prone_annotations:2.36.0 +guava = "com.google.guava:guava:33.4.8-android" guava-betaChecker = "com.google.guava:guava-beta-checker:1.0" -guava-testlib = "com.google.guava:guava-testlib:33.3.1-android" +guava-testlib = "com.google.guava:guava-testlib:33.4.8-android" # JRE version is needed for projects where its a transitive dependency, f.e. gcp-observability. # May be different from the -android version. -guava-jre = "com.google.guava:guava:33.3.1-jre" +guava-jre = "com.google.guava:guava:33.4.8-jre" hdrhistogram = "org.hdrhistogram:HdrHistogram:2.2.2" # 6.0.0+ use java.lang.Deprecated forRemoval and since from Java 9 jakarta-servlet-api = "jakarta.servlet:jakarta.servlet-api:5.0.0" diff --git a/repositories.bzl b/repositories.bzl index 47609ae7671..4b9d0327b66 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -19,9 +19,9 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.auto.value:auto-value:1.11.0", "com.google.code.findbugs:jsr305:3.0.2", "com.google.code.gson:gson:2.11.0", - "com.google.errorprone:error_prone_annotations:2.30.0", + "com.google.errorprone:error_prone_annotations:2.36.0", "com.google.guava:failureaccess:1.0.1", - "com.google.guava:guava:33.3.1-android", + "com.google.guava:guava:33.4.8-android", "com.google.re2j:re2j:1.8", "com.google.s2a.proto.v2:s2a-proto:0.1.2", "com.google.truth:truth:1.4.2", @@ -45,7 +45,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", "junit:junit:4.13.2", - "org.checkerframework:checker-qual:3.12.0", + "org.checkerframework:checker-qual:3.49.5", "org.codehaus.mojo:animal-sniffer-annotations:1.24", ] # GRPC_DEPS_END diff --git a/s2a/build.gradle b/s2a/build.gradle index 1e48e2bb297..012411c19ba 100644 --- a/s2a/build.gradle +++ b/s2a/build.gradle @@ -12,6 +12,7 @@ description = "gRPC: S2A" dependencies { implementation libraries.s2a.proto + implementation 'org.checkerframework:checker-qual:3.49.5' api project(':grpc-api') implementation project(':grpc-stub'), diff --git a/settings.gradle b/settings.gradle index 22a49f0c3be..f4df1105090 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,17 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } plugins { // https://developer.android.com/build/releases/gradle-plugin // 8+ has many changes: https://github.com/grpc/grpc-java/issues/10152 From 9746bb4bc3dd4c71c4992c35b1e2b93db70f6fcd Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Mon, 1 Sep 2025 13:10:09 +0530 Subject: [PATCH 366/591] allow java21 in jre matrix (#12281) --- .github/workflows/testing.yml | 2 +- .../main/java/io/grpc/StatusException.java | 2 + .../java/io/grpc/StatusRuntimeException.java | 2 + .../grpc/internal/AbstractClientStream.java | 3 +- .../grpc/internal/AbstractServerStream.java | 3 +- .../java/io/grpc/internal/AbstractStream.java | 5 +- .../main/java/io/grpc/internal/JsonUtil.java | 6 +- .../AnonymousInProcessSocketAddress.java | 7 +++ .../netty/GrpcHttp2ConnectionHandler.java | 1 + .../java/io/grpc/servlet/GrpcServlet.java | 1 + .../io/grpc/util/MultiChildLoadBalancer.java | 2 + .../grpc/xds/GrpcXdsClientImplTestBase.java | 63 +++++++++++-------- 12 files changed, 63 insertions(+), 34 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 0f099cbcac7..4fe75b0be78 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - jre: [8, 11, 17] + jre: [8, 11, 17, 21] fail-fast: false # Should swap to true if we grow a large matrix steps: diff --git a/api/src/main/java/io/grpc/StatusException.java b/api/src/main/java/io/grpc/StatusException.java index f9416bf72e3..2a235c3aaaf 100644 --- a/api/src/main/java/io/grpc/StatusException.java +++ b/api/src/main/java/io/grpc/StatusException.java @@ -25,7 +25,9 @@ */ public class StatusException extends Exception { private static final long serialVersionUID = -660954903976144640L; + @SuppressWarnings("serial") // https://github.com/grpc/grpc-java/issues/1913 private final Status status; + @SuppressWarnings("serial") private final Metadata trailers; /** diff --git a/api/src/main/java/io/grpc/StatusRuntimeException.java b/api/src/main/java/io/grpc/StatusRuntimeException.java index dd22d6b2486..ebcc2f0d671 100644 --- a/api/src/main/java/io/grpc/StatusRuntimeException.java +++ b/api/src/main/java/io/grpc/StatusRuntimeException.java @@ -26,7 +26,9 @@ public class StatusRuntimeException extends RuntimeException { private static final long serialVersionUID = 1950934672280720624L; + @SuppressWarnings("serial") // https://github.com/grpc/grpc-java/issues/1913 private final Status status; + @SuppressWarnings("serial") private final Metadata trailers; /** diff --git a/core/src/main/java/io/grpc/internal/AbstractClientStream.java b/core/src/main/java/io/grpc/internal/AbstractClientStream.java index 9718f8c5171..14fd5888147 100644 --- a/core/src/main/java/io/grpc/internal/AbstractClientStream.java +++ b/core/src/main/java/io/grpc/internal/AbstractClientStream.java @@ -101,6 +101,7 @@ void writeFrame( */ private volatile boolean cancelled; + @SuppressWarnings("this-escape") protected AbstractClientStream( WritableBufferAllocator bufferAllocator, StatsTraceContext statsTraceCtx, @@ -113,7 +114,7 @@ protected AbstractClientStream( this.shouldBeCountedForInUse = GrpcUtil.shouldBeCountedForInUse(callOptions); this.useGet = useGet; if (!useGet) { - framer = new MessageFramer(this, bufferAllocator, statsTraceCtx); + this.framer = new MessageFramer(this, bufferAllocator, statsTraceCtx); this.headers = headers; } else { framer = new GetFramer(headers, statsTraceCtx); diff --git a/core/src/main/java/io/grpc/internal/AbstractServerStream.java b/core/src/main/java/io/grpc/internal/AbstractServerStream.java index a535330f4b1..c468cba978a 100644 --- a/core/src/main/java/io/grpc/internal/AbstractServerStream.java +++ b/core/src/main/java/io/grpc/internal/AbstractServerStream.java @@ -75,10 +75,11 @@ protected interface Sink { private boolean outboundClosed; private boolean headersSent; + @SuppressWarnings("this-escape") protected AbstractServerStream( WritableBufferAllocator bufferAllocator, StatsTraceContext statsTraceCtx) { this.statsTraceCtx = Preconditions.checkNotNull(statsTraceCtx, "statsTraceCtx"); - framer = new MessageFramer(this, bufferAllocator, statsTraceCtx); + this.framer = new MessageFramer(this, bufferAllocator, statsTraceCtx); } @Override diff --git a/core/src/main/java/io/grpc/internal/AbstractStream.java b/core/src/main/java/io/grpc/internal/AbstractStream.java index 46cdab7ef28..9f5fb035dab 100644 --- a/core/src/main/java/io/grpc/internal/AbstractStream.java +++ b/core/src/main/java/io/grpc/internal/AbstractStream.java @@ -163,20 +163,21 @@ public abstract static class TransportState @GuardedBy("onReadyLock") private int onReadyThreshold; + @SuppressWarnings("this-escape") protected TransportState( int maxMessageSize, StatsTraceContext statsTraceCtx, TransportTracer transportTracer) { this.statsTraceCtx = checkNotNull(statsTraceCtx, "statsTraceCtx"); this.transportTracer = checkNotNull(transportTracer, "transportTracer"); - rawDeframer = new MessageDeframer( + this.rawDeframer = new MessageDeframer( this, Codec.Identity.NONE, maxMessageSize, statsTraceCtx, transportTracer); // TODO(#7168): use MigratingThreadDeframer when enabling retry doesn't break. - deframer = rawDeframer; + deframer = this.rawDeframer; onReadyThreshold = DEFAULT_ONREADY_THRESHOLD; } diff --git a/core/src/main/java/io/grpc/internal/JsonUtil.java b/core/src/main/java/io/grpc/internal/JsonUtil.java index 44cb22abda5..a0d5eef8660 100644 --- a/core/src/main/java/io/grpc/internal/JsonUtil.java +++ b/core/src/main/java/io/grpc/internal/JsonUtil.java @@ -356,7 +356,7 @@ private static int parseNanos(String value) throws ParseException { return result; } - private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1); + private static final int NANOS_PER_SECOND = 1_000_000_000; /** * Copy of {@link com.google.protobuf.util.Durations#normalizedDuration}. @@ -368,11 +368,11 @@ private static long normalizedDuration(long seconds, int nanos) { nanos %= NANOS_PER_SECOND; } if (seconds > 0 && nanos < 0) { - nanos += NANOS_PER_SECOND; // no overflow since nanos is negative (and we're adding) + nanos += NANOS_PER_SECOND; // no overflow— nanos is negative (and we're adding) seconds--; // no overflow since seconds is positive (and we're decrementing) } if (seconds < 0 && nanos > 0) { - nanos -= NANOS_PER_SECOND; // no overflow since nanos is positive (and we're subtracting) + nanos -= NANOS_PER_SECOND; // no overflow— nanos is positive (and we're subtracting) seconds++; // no overflow since seconds is negative (and we're incrementing) } if (!durationIsValid(seconds, nanos)) { diff --git a/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java b/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java index 089a9f12b02..c458857d70b 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java +++ b/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java @@ -21,6 +21,8 @@ import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.ExperimentalApi; import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectOutputStream; import java.net.SocketAddress; import javax.annotation.Nullable; @@ -34,8 +36,13 @@ public final class AnonymousInProcessSocketAddress extends SocketAddress { @Nullable @GuardedBy("this") + @SuppressWarnings("serial") private InProcessServer server; + private void writeObject(ObjectOutputStream out) throws IOException { + throw new NotSerializableException("AnonymousInProcessSocketAddress is not serializable"); + } + /** Creates a new AnonymousInProcessSocketAddress. */ public AnonymousInProcessSocketAddress() { } diff --git a/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java b/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java index 3b8c595a12e..a463cf01d95 100644 --- a/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java +++ b/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java @@ -68,6 +68,7 @@ public abstract class GrpcHttp2ConnectionHandler extends Http2ConnectionHandler usingPre4_1_111_Netty = identifiedOldVersion; } + @SuppressWarnings("this-escape") protected GrpcHttp2ConnectionHandler( ChannelPromise channelUnused, Http2ConnectionDecoder decoder, diff --git a/servlet/src/main/java/io/grpc/servlet/GrpcServlet.java b/servlet/src/main/java/io/grpc/servlet/GrpcServlet.java index f68ed083506..8c1eb858ad1 100644 --- a/servlet/src/main/java/io/grpc/servlet/GrpcServlet.java +++ b/servlet/src/main/java/io/grpc/servlet/GrpcServlet.java @@ -37,6 +37,7 @@ public class GrpcServlet extends HttpServlet { private static final long serialVersionUID = 1L; + @SuppressWarnings("serial") private final ServletAdapter servletAdapter; GrpcServlet(ServletAdapter servletAdapter) { diff --git a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java index 2a93ef964f7..acc186e3be6 100644 --- a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java @@ -268,6 +268,8 @@ public class ChildLbState { private ConnectivityState currentState; private SubchannelPicker currentPicker = new FixedResultPicker(PickResult.withNoResult()); + @SuppressWarnings("this-escape") + // TODO(okshiva): Fix 'this-escape' from the constructor before making the API public. public ChildLbState(Object key, LoadBalancer.Factory policyFactory) { this.key = key; this.lb = policyFactory.newLoadBalancer(createChildHelper()); diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 19266b0d289..9ff19c6d1b0 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -220,7 +220,7 @@ public boolean shouldAccept(Runnable command) { protected final Queue loadReportCalls = new ArrayDeque<>(); protected final AtomicBoolean adsEnded = new AtomicBoolean(true); protected final AtomicBoolean lrsEnded = new AtomicBoolean(true); - private final MessageFactory mf = createMessageFactory(); + protected MessageFactory mf; private static final long TIME_INCREMENT = TimeUnit.SECONDS.toNanos(1); /** Fake time provider increments time TIME_INCREMENT each call. */ @@ -234,37 +234,22 @@ public long currentTimeNanos() { private static final int VHOST_SIZE = 2; // LDS test resources. - private final Any testListenerVhosts = Any.pack(mf.buildListenerWithApiListener(LDS_RESOURCE, - mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); - private final Any testListenerRds = - Any.pack(mf.buildListenerWithApiListenerForRds(LDS_RESOURCE, RDS_RESOURCE)); + private Any testListenerVhosts; + private Any testListenerRds; // RDS test resources. - private final Any testRouteConfig = - Any.pack(mf.buildRouteConfiguration(RDS_RESOURCE, mf.buildOpaqueVirtualHosts(VHOST_SIZE))); + private Any testRouteConfig; // CDS test resources. - private final Any testClusterRoundRobin = - Any.pack(mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, - null, false, null, "envoy.transport_sockets.tls", null, null - )); + private Any testClusterRoundRobin; // EDS test resources. - private final Message lbEndpointHealthy = - mf.buildLocalityLbEndpoints("region1", "zone1", "subzone1", - mf.buildLbEndpoint("192.168.0.1", 8080, "healthy", 2, "endpoint-host-name"), 1, 0); + private Message lbEndpointHealthy; // Locality with 0 endpoints - private final Message lbEndpointEmpty = - mf.buildLocalityLbEndpoints("region3", "zone3", "subzone3", - ImmutableList.of(), 2, 1); + private Message lbEndpointEmpty; // Locality with 0-weight endpoint - private final Message lbEndpointZeroWeight = - mf.buildLocalityLbEndpoints("region4", "zone4", "subzone4", - mf.buildLbEndpoint("192.168.142.5", 80, "unknown", 5, "endpoint-host-name"), 0, 2); - private final Any testClusterLoadAssignment = Any.pack(mf.buildClusterLoadAssignment(EDS_RESOURCE, - ImmutableList.of(lbEndpointHealthy, lbEndpointEmpty, lbEndpointZeroWeight), - ImmutableList.of(mf.buildDropOverload("lb", 200), mf.buildDropOverload("throttle", 1000)))); - + private Message lbEndpointZeroWeight; + private Any testClusterLoadAssignment; @Captor private ArgumentCaptor ldsUpdateCaptor; @Captor @@ -304,8 +289,8 @@ public long currentTimeNanos() { private boolean originalEnableLeastRequest; private Server xdsServer; private final String serverName = InProcessServerBuilder.generateName(); - private final BindableService adsService = createAdsService(); - private final BindableService lrsService = createLrsService(); + private BindableService adsService; + private BindableService lrsService; private XdsTransportFactory xdsTransportFactory = new XdsTransportFactory() { @Override @@ -333,6 +318,32 @@ public XdsTransport create(ServerInfo serverInfo) { @Before public void setUp() throws IOException { + mf = createMessageFactory(); + testListenerVhosts = Any.pack(mf.buildListenerWithApiListener(LDS_RESOURCE, + mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); + testListenerRds = + Any.pack(mf.buildListenerWithApiListenerForRds(LDS_RESOURCE, RDS_RESOURCE)); + testRouteConfig = + Any.pack(mf.buildRouteConfiguration(RDS_RESOURCE, mf.buildOpaqueVirtualHosts(VHOST_SIZE))); + testClusterRoundRobin = + Any.pack(mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, + null, false, null, "envoy.transport_sockets.tls", null, null + )); + lbEndpointHealthy = + mf.buildLocalityLbEndpoints("region1", "zone1", "subzone1", + mf.buildLbEndpoint("192.168.0.1", 8080, "healthy", 2, "endpoint-host-name"), 1, 0); + lbEndpointEmpty = + mf.buildLocalityLbEndpoints("region3", "zone3", "subzone3", + ImmutableList.of(), 2, 1); + lbEndpointZeroWeight = + mf.buildLocalityLbEndpoints("region4", "zone4", "subzone4", + mf.buildLbEndpoint("192.168.142.5", 80, "unknown", 5, "endpoint-host-name"), 0, 2); + testClusterLoadAssignment = Any.pack(mf.buildClusterLoadAssignment(EDS_RESOURCE, + ImmutableList.of(lbEndpointHealthy, lbEndpointEmpty, lbEndpointZeroWeight), + ImmutableList.of(mf.buildDropOverload("lb", 200), mf.buildDropOverload("throttle", 1000)))); + adsService = createAdsService(); + lrsService = createLrsService(); + when(backoffPolicyProvider.get()).thenReturn(backoffPolicy1, backoffPolicy2); when(backoffPolicy1.nextBackoffNanos()).thenReturn(10L, 100L); when(backoffPolicy2.nextBackoffNanos()).thenReturn(20L, 200L); From b3390227ae0c4e1c01b89d601f8233692f33324f Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 2 Sep 2025 15:12:17 -0700 Subject: [PATCH 367/591] Remove org.apache.tomcat:annotations-api dep f8700a1 stopped using the dependency in our generated code and removed the dependency the Bazel build. 4f6948f removed mention of the dependency in our README. This deletes it from our Gradle build and the examples. --- alts/build.gradle | 1 - android-interop-testing/build.gradle | 2 -- authz/build.gradle | 1 - benchmarks/build.gradle | 1 - compiler/build.gradle | 6 ++---- examples/android/clientcache/app/build.gradle | 1 - examples/android/helloworld/app/build.gradle | 1 - examples/android/routeguide/app/build.gradle | 1 - examples/android/strictmode/app/build.gradle | 1 - examples/build.gradle | 1 - examples/example-alts/build.gradle | 1 - examples/example-debug/build.gradle | 1 - examples/example-debug/pom.xml | 6 ------ examples/example-dualstack/build.gradle | 1 - examples/example-dualstack/pom.xml | 6 ------ examples/example-gauth/build.gradle | 1 - examples/example-gauth/pom.xml | 6 ------ examples/example-gcp-csm-observability/build.gradle | 1 - examples/example-gcp-observability/build.gradle | 1 - examples/example-hostname/build.gradle | 1 - examples/example-hostname/pom.xml | 6 ------ examples/example-jwt-auth/build.gradle | 2 -- examples/example-jwt-auth/pom.xml | 6 ------ examples/example-oauth/build.gradle | 2 -- examples/example-oauth/pom.xml | 6 ------ examples/example-opentelemetry/build.gradle | 1 - examples/example-orca/build.gradle | 2 -- examples/example-reflection/build.gradle | 2 -- examples/example-servlet/build.gradle | 3 +-- examples/example-tls/build.gradle | 1 - examples/example-tls/pom.xml | 6 ------ examples/example-xds/build.gradle | 1 - examples/pom.xml | 6 ------ gradle/libs.versions.toml | 3 --- grpclb/build.gradle | 1 - interop-testing/build.gradle | 1 - istio-interop-testing/build.gradle | 2 -- rls/build.gradle | 1 - s2a/build.gradle | 1 - services/build.gradle | 2 -- servlet/build.gradle | 3 +-- servlet/jakarta/build.gradle | 3 +-- testing-proto/build.gradle | 2 -- xds/build.gradle | 1 - 44 files changed, 5 insertions(+), 100 deletions(-) diff --git a/alts/build.gradle b/alts/build.gradle index 3e472d9cea6..fe2e27784fc 100644 --- a/alts/build.gradle +++ b/alts/build.gradle @@ -22,7 +22,6 @@ dependencies { libraries.guava.jre, // JRE required by protobuf-java-util from grpclb libraries.google.auth.oauth2Http def nettyDependency = implementation project(':grpc-netty') - compileOnly libraries.javax.annotation shadow configurations.implementation.getDependencies().minus(nettyDependency) shadow project(path: ':grpc-netty-shaded', configuration: 'shadow') diff --git a/android-interop-testing/build.gradle b/android-interop-testing/build.gradle index 72b6ac0a302..17551465f05 100644 --- a/android-interop-testing/build.gradle +++ b/android-interop-testing/build.gradle @@ -83,8 +83,6 @@ dependencies { exclude group: 'com.google.guava' } - compileOnly libraries.javax.annotation - androidTestImplementation 'androidx.test.ext:junit:1.1.3', 'androidx.test:runner:1.4.0' } diff --git a/authz/build.gradle b/authz/build.gradle index b72088bfbaa..4b02b01aa29 100644 --- a/authz/build.gradle +++ b/authz/build.gradle @@ -15,7 +15,6 @@ dependencies { libraries.guava.jre // JRE required by transitive protobuf-java-util annotationProcessor libraries.auto.value - compileOnly libraries.javax.annotation testImplementation project(':grpc-testing'), project(':grpc-testing-proto'), diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index bf043106050..88b26397e78 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -38,7 +38,6 @@ dependencies { classifier = "linux-x86_64" } } - compileOnly libraries.javax.annotation testImplementation libraries.junit, libraries.mockito.core diff --git a/compiler/build.gradle b/compiler/build.gradle index 6d832ecd56b..dbecb889a43 100644 --- a/compiler/build.gradle +++ b/compiler/build.gradle @@ -147,11 +147,9 @@ sourceSets { dependencies { testImplementation project(':grpc-protobuf'), - project(':grpc-stub'), - libraries.javax.annotation + project(':grpc-stub') testLiteImplementation project(':grpc-protobuf-lite'), - project(':grpc-stub'), - libraries.javax.annotation + project(':grpc-stub') } tasks.named("compileTestJava").configure { diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index a9716ea1f62..8048eba73ab 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -57,7 +57,6 @@ dependencies { implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'org.apache.tomcat:annotations-api:6.0.53' testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.5' diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index 2fd2d2fa950..69519c8e4ab 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -55,5 +55,4 @@ dependencies { implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index 371e277bdc6..a09de35e994 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -55,5 +55,4 @@ dependencies { implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index 02327041d34..c0447a42af1 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -56,5 +56,4 @@ dependencies { implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/build.gradle b/examples/build.gradle index 507b87df4db..ddd8e7b3a65 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -29,7 +29,6 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-services:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" // examples/advanced need this for JsonFormat implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}" diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index 33ea02a5875..9f86adb0aeb 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -27,7 +27,6 @@ def protocVersion = '3.25.8' dependencies { // grpc-alts transitively depends on grpc-netty-shaded, grpc-protobuf, and grpc-stub implementation "io.grpc:grpc-alts:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" } protobuf { diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index ed01bbb2636..3dc873e179b 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -30,7 +30,6 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-services:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" testImplementation 'junit:junit:4.13.2' diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index 90ce766d8a9..c4fce2d793b 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -44,12 +44,6 @@ io.grpc grpc-stub - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-netty-shaded diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index fa9db23f987..5d8c30b2127 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -31,7 +31,6 @@ dependencies { implementation "io.grpc:grpc-netty:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-services:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" } protobuf { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index b70a3eca3d8..b65beb84103 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -48,12 +48,6 @@ io.grpc grpc-netty - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-netty-shaded diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 52d945196fa..befc2f7c0c7 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -30,7 +30,6 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-auth:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" implementation "com.google.auth:google-auth-library-oauth2-http:1.23.0" implementation "com.google.api.grpc:grpc-google-cloud-pubsub-v1:0.1.24" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index 68a98c526a5..98bbebd114a 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -49,12 +49,6 @@ io.grpc grpc-auth - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-testing diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index 816ef9e6742..abb2dd8a220 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -35,7 +35,6 @@ dependencies { implementation "io.opentelemetry:opentelemetry-sdk:${openTelemetryVersion}" implementation "io.opentelemetry:opentelemetry-sdk-metrics:${openTelemetryVersion}" implementation "io.opentelemetry:opentelemetry-exporter-prometheus:${openTelemetryPrometheusVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-xds:${grpcVersion}" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" } diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 432dceb6730..08e00294452 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -29,7 +29,6 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-gcp-observability:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" } diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 7345d873e4f..94a85e8f8ab 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -28,7 +28,6 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-services:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" testImplementation 'junit:junit:4.13.2' diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index 00209657e1d..e93b36e39d1 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -44,12 +44,6 @@ io.grpc grpc-stub - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-netty-shaded diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index b14040ad58f..28df68e4fc7 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -31,8 +31,6 @@ dependencies { implementation "io.jsonwebtoken:jjwt:0.9.1" implementation "javax.xml.bind:jaxb-api:2.3.1" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" - runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" testImplementation "io.grpc:grpc-testing:${grpcVersion}" diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index 8dce2a71032..4834cfa09ef 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -57,12 +57,6 @@ jaxb-api 2.3.1 - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-testing diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 521d8b082ce..05a450dcc8d 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -31,8 +31,6 @@ dependencies { implementation "io.grpc:grpc-auth:${grpcVersion}" implementation "com.google.auth:google-auth-library-oauth2-http:1.23.0" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" - runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" testImplementation "io.grpc:grpc-testing:${grpcVersion}" diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index 2907d062053..a2f61e38467 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -62,12 +62,6 @@ google-auth-library-oauth2-http 1.23.0 - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-testing diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index 482f5766bee..edcca46480e 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -34,7 +34,6 @@ dependencies { implementation "io.opentelemetry:opentelemetry-sdk-metrics:${openTelemetryVersion}" implementation "io.opentelemetry:opentelemetry-exporter-logging:${openTelemetryVersion}" implementation "io.opentelemetry:opentelemetry-exporter-prometheus:${openTelemetryPrometheusVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" } diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index 2716adf7de1..f21c89ee0a4 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -24,8 +24,6 @@ dependencies { implementation "io.grpc:grpc-services:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-xds:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" - } protobuf { diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index 2b48cb30b5f..a5bda680ef4 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -24,8 +24,6 @@ dependencies { implementation "io.grpc:grpc-services:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-netty-shaded:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" - } protobuf { diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index bbdb65349ca..d0f475f1833 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -23,8 +23,7 @@ dependencies { "io.grpc:grpc-servlet:${grpcVersion}", "io.grpc:grpc-stub:${grpcVersion}" - compileOnly "javax.servlet:javax.servlet-api:4.0.1", - "org.apache.tomcat:annotations-api:6.0.53" + compileOnly "javax.servlet:javax.servlet-api:4.0.1" } protobuf { diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index aeb769a479b..0b3914febf3 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -27,7 +27,6 @@ def protocVersion = '3.25.8' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" } diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index 575400c3608..3932f4c32f4 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -40,12 +40,6 @@ io.grpc grpc-stub - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-netty-shaded diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index 6c78db3513c..3914aa3fea0 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -29,7 +29,6 @@ dependencies { implementation "io.grpc:grpc-services:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-xds:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" } diff --git a/examples/pom.xml b/examples/pom.xml index f81346c913c..4da1eb14f90 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -60,12 +60,6 @@ j2objc-annotations 3.0.0 - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-testing diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e66f253fc21..cb5dce02843 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,9 +53,6 @@ guava-jre = "com.google.guava:guava:33.4.8-jre" hdrhistogram = "org.hdrhistogram:HdrHistogram:2.2.2" # 6.0.0+ use java.lang.Deprecated forRemoval and since from Java 9 jakarta-servlet-api = "jakarta.servlet:jakarta.servlet-api:5.0.0" -# Using javax.annotation is fine as it is part of the JDK, we don't want to depend on J2EE -# where it is relocated to as org.apache.tomcat:tomcat-annotations-api. See issue #9179. -javax-annotation = "org.apache.tomcat:annotations-api:6.0.53" javax-servlet-api = "javax.servlet:javax.servlet-api:4.0.1" # 12.0.0+ require Java 17+ jetty-client = "org.eclipse.jetty:jetty-client:11.0.24" diff --git a/grpclb/build.gradle b/grpclb/build.gradle index 3f67181372b..f543e0d71fc 100644 --- a/grpclb/build.gradle +++ b/grpclb/build.gradle @@ -23,7 +23,6 @@ dependencies { libraries.protobuf.java, libraries.protobuf.java.util runtimeOnly libraries.errorprone.annotations - compileOnly libraries.javax.annotation testImplementation libraries.truth, project(':grpc-inprocess'), testFixtures(project(':grpc-core')) diff --git a/interop-testing/build.gradle b/interop-testing/build.gradle index 97e7c69533a..5160759460c 100644 --- a/interop-testing/build.gradle +++ b/interop-testing/build.gradle @@ -31,7 +31,6 @@ dependencies { project(':grpc-stub'), project(':grpc-protobuf'), libraries.junit - compileOnly libraries.javax.annotation // TODO(sergiitk): replace with com.google.cloud:google-cloud-logging // Used instead of google-cloud-logging because it's failing // due to a circular dependency on grpc. diff --git a/istio-interop-testing/build.gradle b/istio-interop-testing/build.gradle index 4550f0ea202..083d8fcb9bf 100644 --- a/istio-interop-testing/build.gradle +++ b/istio-interop-testing/build.gradle @@ -18,8 +18,6 @@ dependencies { project(':grpc-testing'), project(':grpc-xds') - compileOnly libraries.javax.annotation - runtimeOnly libraries.netty.tcnative, libraries.netty.tcnative.classes testImplementation testFixtures(project(':grpc-api')), diff --git a/rls/build.gradle b/rls/build.gradle index 1193ab3d4bc..10b1d5fc371 100644 --- a/rls/build.gradle +++ b/rls/build.gradle @@ -22,7 +22,6 @@ dependencies { libraries.auto.value.annotations, libraries.guava annotationProcessor libraries.auto.value - compileOnly libraries.javax.annotation testImplementation libraries.truth, project(':grpc-grpclb'), project(':grpc-inprocess'), diff --git a/s2a/build.gradle b/s2a/build.gradle index 012411c19ba..c46993ec9c8 100644 --- a/s2a/build.gradle +++ b/s2a/build.gradle @@ -21,7 +21,6 @@ dependencies { libraries.protobuf.java, libraries.guava.jre // JRE required by protobuf-java-util from grpclb def nettyDependency = implementation project(':grpc-netty') - compileOnly libraries.javax.annotation shadow configurations.implementation.getDependencies().minus(nettyDependency) shadow project(path: ':grpc-netty-shaded', configuration: 'shadow') diff --git a/services/build.gradle b/services/build.gradle index 758f2a5c899..c30e1ba53bd 100644 --- a/services/build.gradle +++ b/services/build.gradle @@ -32,13 +32,11 @@ dependencies { runtimeOnly libraries.errorprone.annotations, libraries.gson // to fix checkUpperBoundDeps error here - compileOnly libraries.javax.annotation testImplementation project(':grpc-testing'), project(':grpc-inprocess'), libraries.netty.transport.epoll, // for DomainSocketAddress testFixtures(project(':grpc-core')), testFixtures(project(':grpc-api')) - testCompileOnly libraries.javax.annotation signature (libraries.signature.java) { artifact { extension = "signature" diff --git a/servlet/build.gradle b/servlet/build.gradle index 7f9cd04a57c..1367a72ab44 100644 --- a/servlet/build.gradle +++ b/servlet/build.gradle @@ -34,8 +34,7 @@ tasks.named("jar").configure { dependencies { api project(':grpc-api') - compileOnly libraries.javax.servlet.api, - libraries.javax.annotation // java 9, 10 needs it + compileOnly libraries.javax.servlet.api implementation project(':grpc-core'), libraries.guava diff --git a/servlet/jakarta/build.gradle b/servlet/jakarta/build.gradle index 456b2b75e3e..bcd904ccaee 100644 --- a/servlet/jakarta/build.gradle +++ b/servlet/jakarta/build.gradle @@ -85,8 +85,7 @@ tasks.named("jar").configure { dependencies { api project(':grpc-api') - compileOnly libraries.jakarta.servlet.api, - libraries.javax.annotation + compileOnly libraries.jakarta.servlet.api implementation project(':grpc-util'), project(':grpc-core'), diff --git a/testing-proto/build.gradle b/testing-proto/build.gradle index a34392b26d2..ee602bc5135 100644 --- a/testing-proto/build.gradle +++ b/testing-proto/build.gradle @@ -17,9 +17,7 @@ tasks.named("jar").configure { dependencies { api project(':grpc-protobuf'), project(':grpc-stub') - compileOnly libraries.javax.annotation testImplementation libraries.truth - testRuntimeOnly libraries.javax.annotation signature (libraries.signature.java) { artifact { extension = "signature" diff --git a/xds/build.gradle b/xds/build.gradle index 72dea373097..8394fe12f6b 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -41,7 +41,6 @@ configurations { } dependencies { - thirdpartyCompileOnly libraries.javax.annotation thirdpartyImplementation project(':grpc-protobuf'), project(':grpc-stub') compileOnly sourceSets.thirdparty.output From 01f2862b90631c8114174b0735b24bbb11cbb060 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 28 Aug 2025 16:26:23 -0700 Subject: [PATCH 368/591] xds: Pretty-print Resource in logs Noticed at b/431017968#comment31 --- xds/src/main/java/io/grpc/xds/MessagePrinter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/MessagePrinter.java b/xds/src/main/java/io/grpc/xds/MessagePrinter.java index db15e961204..d6fdaa81dd7 100644 --- a/xds/src/main/java/io/grpc/xds/MessagePrinter.java +++ b/xds/src/main/java/io/grpc/xds/MessagePrinter.java @@ -37,6 +37,7 @@ import io.envoyproxy.envoy.extensions.load_balancing_policies.wrr_locality.v3.WrrLocality; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext; +import io.envoyproxy.envoy.service.discovery.v3.Resource; import io.grpc.xds.client.MessagePrettyPrinter; /** @@ -55,6 +56,7 @@ private static class LazyHolder { private static JsonFormat.Printer newPrinter() { TypeRegistry.Builder registry = TypeRegistry.newBuilder() + .add(Resource.getDescriptor()) .add(Listener.getDescriptor()) .add(HttpConnectionManager.getDescriptor()) .add(HTTPFault.getDescriptor()) From 5a543726fd47e42f7ac959eb55d77c5bf173238a Mon Sep 17 00:00:00 2001 From: Sangamesh Date: Thu, 4 Sep 2025 11:25:04 +0530 Subject: [PATCH 369/591] android-interop-testing : Fix lint warnings in android-interop and binder modules (#12272) Fixes #6868 --- .../src/main/AndroidManifest.xml | 7 ++++--- .../android/integrationtest/TesterActivity.java | 2 +- .../src/main/res/layout/activity_tester.xml | 1 + .../src/main/res/values/strings.xml | 1 + binder/src/main/AndroidManifest.xml | 13 +++++++++++-- lint.xml | 5 +++++ 6 files changed, 23 insertions(+), 6 deletions(-) diff --git a/android-interop-testing/src/main/AndroidManifest.xml b/android-interop-testing/src/main/AndroidManifest.xml index 35f3ee33a2b..da7ccef5b1d 100644 --- a/android-interop-testing/src/main/AndroidManifest.xml +++ b/android-interop-testing/src/main/AndroidManifest.xml @@ -5,7 +5,9 @@ - + + android:exported="true"> diff --git a/android-interop-testing/src/main/java/io/grpc/android/integrationtest/TesterActivity.java b/android-interop-testing/src/main/java/io/grpc/android/integrationtest/TesterActivity.java index fb5b35c42d5..17c7e24cbfa 100644 --- a/android-interop-testing/src/main/java/io/grpc/android/integrationtest/TesterActivity.java +++ b/android-interop-testing/src/main/java/io/grpc/android/integrationtest/TesterActivity.java @@ -121,7 +121,7 @@ private void startTest(String testCase) { ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow( hostEdit.getWindowToken(), 0); enableButtons(false); - resultText.setText("Testing..."); + resultText.setText(R.string.testing_message); String host = hostEdit.getText().toString(); String portStr = portEdit.getText().toString(); diff --git a/android-interop-testing/src/main/res/layout/activity_tester.xml b/android-interop-testing/src/main/res/layout/activity_tester.xml index e25bd1bb6f6..042da6437c0 100644 --- a/android-interop-testing/src/main/res/layout/activity_tester.xml +++ b/android-interop-testing/src/main/res/layout/activity_tester.xml @@ -16,6 +16,7 @@ android:layout_weight="2" android:layout_width="0dp" android:layout_height="wrap_content" + android:inputType="text" android:hint="Enter Host" /> gRPC Integration Test + Testing… diff --git a/binder/src/main/AndroidManifest.xml b/binder/src/main/AndroidManifest.xml index a30cbbdd6fa..239c3b39b38 100644 --- a/binder/src/main/AndroidManifest.xml +++ b/binder/src/main/AndroidManifest.xml @@ -1,2 +1,11 @@ - - + + + + + + + + + + + \ No newline at end of file diff --git a/lint.xml b/lint.xml index 93e2f603108..5b35a8d151b 100644 --- a/lint.xml +++ b/lint.xml @@ -5,4 +5,9 @@ Remove after AGP upgrade. --> + + + From d4e1b69e781d4132f971c97a1f0893d54cf761b7 Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Mon, 8 Sep 2025 21:55:40 +0000 Subject: [PATCH 370/591] otel: subchannel metrics A94 (#12202) Implements [A94](https://github.com/grpc/proposal/pull/485/files) except for the exact reason for disconnect_error --- .../java/io/grpc/EquivalentAddressGroup.java | 5 + .../LongUpDownCounterMetricInstrument.java | 32 +++ .../io/grpc/MetricInstrumentRegistry.java | 41 ++++ api/src/main/java/io/grpc/MetricRecorder.java | 25 ++- api/src/main/java/io/grpc/MetricSink.java | 18 +- .../io/grpc/internal/InternalSubchannel.java | 55 ++++- .../io/grpc/internal/ManagedChannelImpl.java | 9 +- .../io/grpc/internal/MetricRecorderImpl.java | 29 ++- .../internal/PickFirstLeafLoadBalancer.java | 2 +- .../io/grpc/internal/SubchannelMetrics.java | 189 ++++++++++++++++++ .../grpc/internal/InternalSubchannelTest.java | 154 +++++++++++++- .../grpc/internal/MetricRecorderImplTest.java | 33 ++- .../OpenTelemetryMetricSink.java | 23 +++ .../internal/OpenTelemetryConstants.java | 6 + .../OpenTelemetryMetricSinkTest.java | 68 ++++++- .../io/grpc/xds/ClusterImplLoadBalancer.java | 2 +- .../grpc/xds/ClusterResolverLoadBalancer.java | 4 +- .../io/grpc/xds/WrrLocalityLoadBalancer.java | 2 +- .../main/java/io/grpc/xds/XdsAttributes.java | 7 - .../grpc/xds/ClusterImplLoadBalancerTest.java | 2 +- .../grpc/xds/WrrLocalityLoadBalancerTest.java | 2 +- 21 files changed, 678 insertions(+), 30 deletions(-) create mode 100644 api/src/main/java/io/grpc/LongUpDownCounterMetricInstrument.java create mode 100644 core/src/main/java/io/grpc/internal/SubchannelMetrics.java diff --git a/api/src/main/java/io/grpc/EquivalentAddressGroup.java b/api/src/main/java/io/grpc/EquivalentAddressGroup.java index 4b3db006684..bf8a864902c 100644 --- a/api/src/main/java/io/grpc/EquivalentAddressGroup.java +++ b/api/src/main/java/io/grpc/EquivalentAddressGroup.java @@ -50,6 +50,11 @@ public final class EquivalentAddressGroup { @ExperimentalApi("https://github.com/grpc/grpc-java/issues/6138") public static final Attributes.Key ATTR_AUTHORITY_OVERRIDE = Attributes.Key.create("io.grpc.EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE"); + /** + * The name of the locality that this EquivalentAddressGroup is in. + */ + public static final Attributes.Key ATTR_LOCALITY_NAME = + Attributes.Key.create("io.grpc.EquivalentAddressGroup.LOCALITY"); private final List addrs; private final Attributes attrs; diff --git a/api/src/main/java/io/grpc/LongUpDownCounterMetricInstrument.java b/api/src/main/java/io/grpc/LongUpDownCounterMetricInstrument.java new file mode 100644 index 00000000000..07e099cde5d --- /dev/null +++ b/api/src/main/java/io/grpc/LongUpDownCounterMetricInstrument.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import java.util.List; + +/** + * Represents a long-valued up down counter metric instrument. + */ +@Internal +public final class LongUpDownCounterMetricInstrument extends PartialMetricInstrument { + public LongUpDownCounterMetricInstrument(int index, String name, String description, String unit, + List requiredLabelKeys, + List optionalLabelKeys, + boolean enableByDefault) { + super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault); + } +} \ No newline at end of file diff --git a/api/src/main/java/io/grpc/MetricInstrumentRegistry.java b/api/src/main/java/io/grpc/MetricInstrumentRegistry.java index 1b33ed17a71..ce0f8f1b5cb 100644 --- a/api/src/main/java/io/grpc/MetricInstrumentRegistry.java +++ b/api/src/main/java/io/grpc/MetricInstrumentRegistry.java @@ -144,6 +144,47 @@ public LongCounterMetricInstrument registerLongCounter(String name, } } + /** + * Registers a new Long Up Down Counter metric instrument. + * + * @param name the name of the metric + * @param description a description of the metric + * @param unit the unit of measurement for the metric + * @param requiredLabelKeys a list of required label keys + * @param optionalLabelKeys a list of optional label keys + * @param enableByDefault whether the metric should be enabled by default + * @return the newly created LongUpDownCounterMetricInstrument + * @throws IllegalStateException if a metric with the same name already exists + */ + public LongUpDownCounterMetricInstrument registerLongUpDownCounter(String name, + String description, + String unit, + List requiredLabelKeys, + List optionalLabelKeys, + boolean enableByDefault) { + checkArgument(!Strings.isNullOrEmpty(name), "missing metric name"); + checkNotNull(description, "description"); + checkNotNull(unit, "unit"); + checkNotNull(requiredLabelKeys, "requiredLabelKeys"); + checkNotNull(optionalLabelKeys, "optionalLabelKeys"); + synchronized (lock) { + if (registeredMetricNames.contains(name)) { + throw new IllegalStateException("Metric with name " + name + " already exists"); + } + int index = nextAvailableMetricIndex; + if (index + 1 == metricInstruments.length) { + resizeMetricInstruments(); + } + LongUpDownCounterMetricInstrument instrument = new LongUpDownCounterMetricInstrument( + index, name, description, unit, requiredLabelKeys, optionalLabelKeys, + enableByDefault); + metricInstruments[index] = instrument; + registeredMetricNames.add(name); + nextAvailableMetricIndex += 1; + return instrument; + } + } + /** * Registers a new Double Histogram metric instrument. * diff --git a/api/src/main/java/io/grpc/MetricRecorder.java b/api/src/main/java/io/grpc/MetricRecorder.java index d418dcbf590..897c28011cd 100644 --- a/api/src/main/java/io/grpc/MetricRecorder.java +++ b/api/src/main/java/io/grpc/MetricRecorder.java @@ -50,7 +50,7 @@ default void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, do * Adds a value for a long valued counter metric instrument. * * @param metricInstrument The counter metric instrument to add the value against. - * @param value The value to add. + * @param value The value to add. MUST be non-negative. * @param requiredLabelValues A list of required label values for the metric. * @param optionalLabelValues A list of additional, optional label values for the metric. */ @@ -66,6 +66,29 @@ default void addLongCounter(LongCounterMetricInstrument metricInstrument, long v metricInstrument.getOptionalLabelKeys().size()); } + /** + * Adds a value for a long valued up down counter metric instrument. + * + * @param metricInstrument The counter metric instrument to add the value against. + * @param value The value to add. May be positive, negative or zero. + * @param requiredLabelValues A list of required label values for the metric. + * @param optionalLabelValues A list of additional, optional label values for the metric. + */ + default void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, + long value, + List requiredLabelValues, + List optionalLabelValues) { + checkArgument(requiredLabelValues != null + && requiredLabelValues.size() == metricInstrument.getRequiredLabelKeys().size(), + "Incorrect number of required labels provided. Expected: %s", + metricInstrument.getRequiredLabelKeys().size()); + checkArgument(optionalLabelValues != null + && optionalLabelValues.size() == metricInstrument.getOptionalLabelKeys().size(), + "Incorrect number of optional labels provided. Expected: %s", + metricInstrument.getOptionalLabelKeys().size()); + } + + /** * Records a value for a double-precision histogram metric instrument. * diff --git a/api/src/main/java/io/grpc/MetricSink.java b/api/src/main/java/io/grpc/MetricSink.java index 0f56b1acb73..ce5d3822520 100644 --- a/api/src/main/java/io/grpc/MetricSink.java +++ b/api/src/main/java/io/grpc/MetricSink.java @@ -65,12 +65,26 @@ default void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, do * Adds a value for a long valued counter metric associated with specified metric instrument. * * @param metricInstrument The counter metric instrument identifies metric measure to add. - * @param value The value to record. + * @param value The value to record. MUST be non-negative. * @param requiredLabelValues A list of required label values for the metric. * @param optionalLabelValues A list of additional, optional label values for the metric. */ default void addLongCounter(LongCounterMetricInstrument metricInstrument, long value, - List requiredLabelValues, List optionalLabelValues) { + List requiredLabelValues, List optionalLabelValues) { + } + + /** + * Adds a value for a long valued up down counter metric associated with specified metric + * instrument. + * + * @param metricInstrument The counter metric instrument identifies metric measure to add. + * @param value The value to record. May be positive, negative or zero. + * @param requiredLabelValues A list of required label values for the metric. + * @param optionalLabelValues A list of additional, optional label values for the metric. + */ + default void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, long value, + List requiredLabelValues, + List optionalLabelValues) { } /** diff --git a/core/src/main/java/io/grpc/internal/InternalSubchannel.java b/core/src/main/java/io/grpc/internal/InternalSubchannel.java index a27e46eaf60..649843c5c03 100644 --- a/core/src/main/java/io/grpc/internal/InternalSubchannel.java +++ b/core/src/main/java/io/grpc/internal/InternalSubchannel.java @@ -48,6 +48,9 @@ import io.grpc.LoadBalancer; import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import io.grpc.MetricRecorder; +import io.grpc.NameResolver; +import io.grpc.SecurityLevel; import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; @@ -160,6 +163,8 @@ protected void handleNotInUse() { private Status shutdownReason; private volatile Attributes connectedAddressAttributes; + private final SubchannelMetrics subchannelMetrics; + private final String target; InternalSubchannel(LoadBalancer.CreateSubchannelArgs args, String authority, String userAgent, BackoffPolicy.Provider backoffPolicyProvider, @@ -168,7 +173,9 @@ protected void handleNotInUse() { Supplier stopwatchSupplier, SynchronizationContext syncContext, Callback callback, InternalChannelz channelz, CallTracer callsTracer, ChannelTracer channelTracer, InternalLogId logId, - ChannelLogger channelLogger, List transportFilters) { + ChannelLogger channelLogger, List transportFilters, + String target, + MetricRecorder metricRecorder) { List addressGroups = args.getAddresses(); Preconditions.checkNotNull(addressGroups, "addressGroups"); Preconditions.checkArgument(!addressGroups.isEmpty(), "addressGroups is empty"); @@ -192,6 +199,8 @@ protected void handleNotInUse() { this.channelLogger = Preconditions.checkNotNull(channelLogger, "channelLogger"); this.transportFilters = transportFilters; this.reconnectDisabled = args.getOption(LoadBalancer.DISABLE_SUBCHANNEL_RECONNECT_KEY); + this.target = target; + this.subchannelMetrics = new SubchannelMetrics(metricRecorder); } ChannelLogger getChannelLogger() { @@ -593,6 +602,13 @@ public void run() { pendingTransport = null; connectedAddressAttributes = addressIndex.getCurrentEagAttributes(); gotoNonErrorState(READY); + subchannelMetrics.recordConnectionAttemptSucceeded(/* target= */ target, + /* backendService= */ getAttributeOrDefault( + addressIndex.getCurrentEagAttributes(), NameResolver.ATTR_BACKEND_SERVICE), + /* locality= */ getAttributeOrDefault(addressIndex.getCurrentEagAttributes(), + EquivalentAddressGroup.ATTR_LOCALITY_NAME), + /* securityLevel= */ extractSecurityLevel(addressIndex.getCurrentEagAttributes() + .get(GrpcAttributes.ATTR_SECURITY_LEVEL))); } } }); @@ -618,11 +634,25 @@ public void run() { activeTransport = null; addressIndex.reset(); gotoNonErrorState(IDLE); + subchannelMetrics.recordDisconnection(/* target= */ target, + /* backendService= */ getAttributeOrDefault(addressIndex.getCurrentEagAttributes(), + NameResolver.ATTR_BACKEND_SERVICE), + /* locality= */ getAttributeOrDefault(addressIndex.getCurrentEagAttributes(), + EquivalentAddressGroup.ATTR_LOCALITY_NAME), + /* disconnectError= */ SubchannelMetrics.DisconnectError.UNKNOWN + .getErrorString(null), + /* securityLevel= */ extractSecurityLevel(addressIndex.getCurrentEagAttributes() + .get(GrpcAttributes.ATTR_SECURITY_LEVEL))); } else if (pendingTransport == transport) { + subchannelMetrics.recordConnectionAttemptFailed(/* target= */ target, + /* backendService= */getAttributeOrDefault(addressIndex.getCurrentEagAttributes(), + NameResolver.ATTR_BACKEND_SERVICE), + /* locality= */ getAttributeOrDefault(addressIndex.getCurrentEagAttributes(), + EquivalentAddressGroup.ATTR_LOCALITY_NAME)); Preconditions.checkState(state.getState() == CONNECTING, "Expected state is CONNECTING, actual state is %s", state.getState()); addressIndex.increment(); - // Continue reconnect if there are still addresses to try. + // Continue to reconnect if there are still addresses to try. if (!addressIndex.isValid()) { pendingTransport = null; addressIndex.reset(); @@ -658,6 +688,27 @@ public void run() { } }); } + + private String extractSecurityLevel(SecurityLevel securityLevel) { + if (securityLevel == null) { + return "none"; + } + switch (securityLevel) { + case NONE: + return "none"; + case INTEGRITY: + return "integrity_only"; + case PRIVACY_AND_INTEGRITY: + return "privacy_and_integrity"; + default: + throw new IllegalArgumentException("Unknown SecurityLevel: " + securityLevel); + } + } + + private String getAttributeOrDefault(Attributes attributes, Attributes.Key key) { + String value = attributes.get(key); + return value == null ? "" : value; + } } // All methods are called in syncContext diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 16b8adbd347..78c5181502f 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -415,7 +415,7 @@ void exitIdleMode() { LbHelperImpl lbHelper = new LbHelperImpl(); lbHelper.lb = loadBalancerFactory.newLoadBalancer(lbHelper); // Delay setting lbHelper until fully initialized, since loadBalancerFactory is user code and - // may throw. We don't want to confuse our state, even if we will enter panic mode. + // may throw. We don't want to confuse our state, even if we enter panic mode. this.lbHelper = lbHelper; channelStateManager.gotoState(CONNECTING); @@ -1464,7 +1464,9 @@ void onStateChange(InternalSubchannel is, ConnectivityStateInfo newState) { subchannelTracer, subchannelLogId, subchannelLogger, - transportFilters); + transportFilters, + target, + lbHelper.getMetricRecorder()); oobChannelTracer.reportEvent(new ChannelTrace.Event.Builder() .setDescription("Child Subchannel created") .setSeverity(ChannelTrace.Event.Severity.CT_INFO) @@ -1895,7 +1897,8 @@ void onNotInUse(InternalSubchannel is) { subchannelTracer, subchannelLogId, subchannelLogger, - transportFilters); + transportFilters, target, + lbHelper.getMetricRecorder()); channelTracer.reportEvent(new ChannelTrace.Event.Builder() .setDescription("Child Subchannel started") diff --git a/core/src/main/java/io/grpc/internal/MetricRecorderImpl.java b/core/src/main/java/io/grpc/internal/MetricRecorderImpl.java index 452b1c5df07..ded9d5ce589 100644 --- a/core/src/main/java/io/grpc/internal/MetricRecorderImpl.java +++ b/core/src/main/java/io/grpc/internal/MetricRecorderImpl.java @@ -26,6 +26,7 @@ import io.grpc.LongCounterMetricInstrument; import io.grpc.LongGaugeMetricInstrument; import io.grpc.LongHistogramMetricInstrument; +import io.grpc.LongUpDownCounterMetricInstrument; import io.grpc.MetricInstrument; import io.grpc.MetricInstrumentRegistry; import io.grpc.MetricRecorder; @@ -82,7 +83,7 @@ public void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, dou * Records a long counter value. * * @param metricInstrument the {@link LongCounterMetricInstrument} to record. - * @param value the value to record. + * @param value the value to record. Must be non-negative. * @param requiredLabelValues the required label values for the metric. * @param optionalLabelValues the optional label values for the metric. */ @@ -103,6 +104,32 @@ public void addLongCounter(LongCounterMetricInstrument metricInstrument, long va } } + /** + * Adds a long up down counter value. + * + * @param metricInstrument the {@link io.grpc.LongUpDownCounterMetricInstrument} to record. + * @param value the value to record. May be positive, negative or zero. + * @param requiredLabelValues the required label values for the metric. + * @param optionalLabelValues the optional label values for the metric. + */ + @Override + public void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, long value, + List requiredLabelValues, + List optionalLabelValues) { + MetricRecorder.super.addLongUpDownCounter(metricInstrument, value, requiredLabelValues, + optionalLabelValues); + for (MetricSink sink : metricSinks) { + int measuresSize = sink.getMeasuresSize(); + if (measuresSize <= metricInstrument.getIndex()) { + // Measures may need updating in two cases: + // 1. When the sink is initially created with an empty list of measures. + // 2. When new metric instruments are registered, requiring the sink to accommodate them. + sink.updateMeasures(registry.getMetricInstruments()); + } + sink.addLongUpDownCounter(metricInstrument, value, requiredLabelValues, optionalLabelValues); + } + } + /** * Records a double histogram value. * diff --git a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java index bbc144ea775..ebe329ca591 100644 --- a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java +++ b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java @@ -92,7 +92,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { return Status.FAILED_PRECONDITION.withDescription("Already shut down"); } - // Cache whether or not this is a petiole policy, which is based off of an address attribute + // Check whether this is a petiole policy, which is based off of an address attribute Boolean isPetiolePolicy = resolvedAddresses.getAttributes().get(IS_PETIOLE_POLICY); this.notAPetiolePolicy = isPetiolePolicy == null || !isPetiolePolicy; diff --git a/core/src/main/java/io/grpc/internal/SubchannelMetrics.java b/core/src/main/java/io/grpc/internal/SubchannelMetrics.java new file mode 100644 index 00000000000..8921f13ebe6 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/SubchannelMetrics.java @@ -0,0 +1,189 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import io.grpc.LongCounterMetricInstrument; +import io.grpc.LongUpDownCounterMetricInstrument; +import io.grpc.MetricInstrumentRegistry; +import io.grpc.MetricRecorder; +import javax.annotation.Nullable; + +final class SubchannelMetrics { + + private static final LongCounterMetricInstrument disconnections; + private static final LongCounterMetricInstrument connectionAttemptsSucceeded; + private static final LongCounterMetricInstrument connectionAttemptsFailed; + private static final LongUpDownCounterMetricInstrument openConnections; + private final MetricRecorder metricRecorder; + + public SubchannelMetrics(MetricRecorder metricRecorder) { + this.metricRecorder = metricRecorder; + } + + static { + MetricInstrumentRegistry metricInstrumentRegistry + = MetricInstrumentRegistry.getDefaultRegistry(); + disconnections = metricInstrumentRegistry.registerLongCounter( + "grpc.subchannel.disconnections", + "EXPERIMENTAL. Number of times the selected subchannel becomes disconnected", + "{disconnection}", + Lists.newArrayList("grpc.target"), + Lists.newArrayList("grpc.lb.backend_service", "grpc.lb.locality", "grpc.disconnect_error"), + false + ); + + connectionAttemptsSucceeded = metricInstrumentRegistry.registerLongCounter( + "grpc.subchannel.connection_attempts_succeeded", + "EXPERIMENTAL. Number of successful connection attempts", + "{attempt}", + Lists.newArrayList("grpc.target"), + Lists.newArrayList("grpc.lb.backend_service", "grpc.lb.locality"), + false + ); + + connectionAttemptsFailed = metricInstrumentRegistry.registerLongCounter( + "grpc.subchannel.connection_attempts_failed", + "EXPERIMENTAL. Number of failed connection attempts", + "{attempt}", + Lists.newArrayList("grpc.target"), + Lists.newArrayList("grpc.lb.backend_service", "grpc.lb.locality"), + false + ); + + openConnections = metricInstrumentRegistry.registerLongUpDownCounter( + "grpc.subchannel.open_connections", + "EXPERIMENTAL. Number of open connections.", + "{connection}", + Lists.newArrayList("grpc.target"), + Lists.newArrayList("grpc.security_level", "grpc.lb.backend_service", "grpc.lb.locality"), + false + ); + } + + public void recordConnectionAttemptSucceeded(String target, String backendService, + String locality, String securityLevel) { + metricRecorder + .addLongCounter(connectionAttemptsSucceeded, 1, + ImmutableList.of(target), + ImmutableList.of(backendService, locality)); + metricRecorder + .addLongUpDownCounter(openConnections, 1, + ImmutableList.of(target), + ImmutableList.of(securityLevel, backendService, locality)); + } + + public void recordConnectionAttemptFailed(String target, String backendService, String locality) { + metricRecorder + .addLongCounter(connectionAttemptsFailed, 1, + ImmutableList.of(target), + ImmutableList.of(backendService, locality)); + } + + public void recordDisconnection(String target, String backendService, String locality, + String disconnectError, String securityLevel) { + metricRecorder + .addLongCounter(disconnections, 1, + ImmutableList.of(target), + ImmutableList.of(backendService, locality, disconnectError)); + metricRecorder + .addLongUpDownCounter(openConnections, -1, + ImmutableList.of(target), + ImmutableList.of(securityLevel, backendService, locality)); + } + + /** + * Represents the reason for a subchannel failure. + */ + public enum DisconnectError { + + /** + * Represents an HTTP/2 GOAWAY frame. The specific error code + * (e.g., "NO_ERROR", "PROTOCOL_ERROR") should be handled separately + * as it is a dynamic part of the error. + * See RFC 9113 for error codes: https://www.rfc-editor.org/rfc/rfc9113.html#name-error-codes + */ + GOAWAY("goaway"), + + /** + * The subchannel was shut down for various reasons like parent channel shutdown, + * idleness, or load balancing policy changes. + */ + SUBCHANNEL_SHUTDOWN("subchannel shutdown"), + + /** + * Connection was reset (e.g., ECONNRESET, WSAECONNERESET). + */ + CONNECTION_RESET("connection reset"), + + /** + * Connection timed out (e.g., ETIMEDOUT, WSAETIMEDOUT), including closures + * from gRPC keepalives. + */ + CONNECTION_TIMED_OUT("connection timed out"), + + /** + * Connection was aborted (e.g., ECONNABORTED, WSAECONNABORTED). + */ + CONNECTION_ABORTED("connection aborted"), + + /** + * Any socket error not covered by other specific disconnect errors. + */ + SOCKET_ERROR("socket error"), + + /** + * A catch-all for any other unclassified reason. + */ + UNKNOWN("unknown"); + + private final String errorTag; + + /** + * Private constructor to associate a description with each enum constant. + * + * @param errorTag The detailed explanation of the error. + */ + DisconnectError(String errorTag) { + this.errorTag = errorTag; + } + + /** + * Gets the error string suitable for use as a metric tag. + * + *

    If the reason is {@code GOAWAY}, this method requires the specific + * HTTP/2 error code to create the complete tag (e.g., "goaway PROTOCOL_ERROR"). + * For all other reasons, the parameter is ignored.

    + * + * @param goawayErrorCode The specific HTTP/2 error code. This is only + * used if the reason is GOAWAY and should not be null in that case. + * @return The formatted error string. + */ + public String getErrorString(@Nullable String goawayErrorCode) { + if (this == GOAWAY) { + if (goawayErrorCode == null || goawayErrorCode.isEmpty()) { + // Return the base tag if the code is missing, or consider throwing an exception + // throw new IllegalArgumentException("goawayErrorCode is required for GOAWAY reason."); + return this.errorTag; + } + return this.errorTag + " " + goawayErrorCode; + } + return this.errorTag; + } + } +} diff --git a/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java b/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java index bed722f5f3a..4ac5fbac362 100644 --- a/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java +++ b/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java @@ -29,10 +29,13 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -48,6 +51,10 @@ import io.grpc.InternalLogId; import io.grpc.InternalWithLogId; import io.grpc.LoadBalancer; +import io.grpc.MetricInstrument; +import io.grpc.MetricRecorder; +import io.grpc.NameResolver; +import io.grpc.SecurityLevel; import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.internal.InternalSubchannel.CallTracingTransport; @@ -68,6 +75,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -81,6 +89,9 @@ public class InternalSubchannelTest { public final MockitoRule mocks = MockitoJUnit.rule(); private static final String AUTHORITY = "fakeauthority"; + private static final String BACKEND_SERVICE = "ice-cream-factory-service"; + private static final String LOCALITY = "mars-olympus-mons-datacenter"; + private static final SecurityLevel SECURITY_LEVEL = SecurityLevel.PRIVACY_AND_INTEGRITY; private static final String USER_AGENT = "mosaic"; private static final ConnectivityStateInfo UNAVAILABLE_STATE = ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE); @@ -108,6 +119,10 @@ public void uncaughtException(Thread t, Throwable e) { @Mock private BackoffPolicy.Provider mockBackoffPolicyProvider; @Mock private ClientTransportFactory mockTransportFactory; + @Mock private BackoffPolicy mockBackoffPolicy; + private MetricRecorder mockMetricRecorder = mock(MetricRecorder.class, + delegatesTo(new MetricRecorderImpl())); + private final LinkedList callbackInvokes = new LinkedList<>(); private final InternalSubchannel.Callback mockInternalSubchannelCallback = new InternalSubchannel.Callback() { @@ -1446,7 +1461,136 @@ private void createInternalSubchannel(boolean reconnectDisabled, subchannelTracer, logId, new ChannelLoggerImpl(subchannelTracer, fakeClock.getTimeProvider()), - Collections.emptyList()); + Collections.emptyList(), + "", + new MetricRecorder() { + } + ); + } + + @Test + public void subchannelStateChanges_triggersAttemptFailedMetric() { + // 1. Setup: Standard subchannel initialization + when(mockBackoffPolicyProvider.get()).thenReturn(mockBackoffPolicy); + SocketAddress addr = mock(SocketAddress.class); + Attributes eagAttributes = Attributes.newBuilder() + .set(NameResolver.ATTR_BACKEND_SERVICE, BACKEND_SERVICE) + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, LOCALITY) + .set(GrpcAttributes.ATTR_SECURITY_LEVEL, SECURITY_LEVEL) + .build(); + List addressGroups = + Arrays.asList(new EquivalentAddressGroup(Arrays.asList(addr), eagAttributes)); + InternalLogId logId = InternalLogId.allocate("Subchannel", /*details=*/ AUTHORITY); + ChannelTracer subchannelTracer = new ChannelTracer(logId, 10, + fakeClock.getTimeProvider().currentTimeNanos(), "Subchannel"); + LoadBalancer.CreateSubchannelArgs createSubchannelArgs = + LoadBalancer.CreateSubchannelArgs.newBuilder().setAddresses(addressGroups).build(); + internalSubchannel = new InternalSubchannel( + createSubchannelArgs, AUTHORITY, USER_AGENT, mockBackoffPolicyProvider, + mockTransportFactory, fakeClock.getScheduledExecutorService(), + fakeClock.getStopwatchSupplier(), syncContext, mockInternalSubchannelCallback, channelz, + CallTracer.getDefaultFactory().create(), subchannelTracer, logId, + new ChannelLoggerImpl(subchannelTracer, fakeClock.getTimeProvider()), + Collections.emptyList(), AUTHORITY, mockMetricRecorder + ); + + // --- Action: Simulate the "connecting to failed" transition --- + // a. Initiate the connection attempt. The subchannel is now CONNECTING. + internalSubchannel.obtainActiveTransport(); + MockClientTransportInfo transportInfo = transports.poll(); + assertNotNull("A connection attempt should have been made", transportInfo); + + // b. Fail the transport before it can signal `transportReady()`. + transportInfo.listener.transportShutdown( + Status.INTERNAL.withDescription("Simulated connect failure")); + fakeClock.runDueTasks(); // Process the failure event + + // --- Verification --- + // a. Verify that the "connection_attempts_failed" metric was recorded exactly once. + verify(mockMetricRecorder).addLongCounter( + eqMetricInstrumentName("grpc.subchannel.connection_attempts_failed"), + eq(1L), + eq(Arrays.asList(AUTHORITY)), + eq(Arrays.asList(BACKEND_SERVICE, LOCALITY)) + ); + + // b. Verify no other metrics were recorded. This confirms it wasn't incorrectly + // logged as a success, disconnection, or open connection. + verifyNoMoreInteractions(mockMetricRecorder); + } + + @Test + public void subchannelStateChanges_triggersSuccessAndDisconnectMetrics() { + // 1. Mock the backoff policy (needed for subchannel creation) + when(mockBackoffPolicyProvider.get()).thenReturn(mockBackoffPolicy); + + // 2. Setup Subchannel with attributes + SocketAddress addr = mock(SocketAddress.class); + Attributes eagAttributes = Attributes.newBuilder() + .set(NameResolver.ATTR_BACKEND_SERVICE, BACKEND_SERVICE) + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, LOCALITY) + .set(GrpcAttributes.ATTR_SECURITY_LEVEL, SECURITY_LEVEL) + .build(); + List addressGroups = + Arrays.asList(new EquivalentAddressGroup(Arrays.asList(addr), eagAttributes)); + createInternalSubchannel(new EquivalentAddressGroup(addr)); + InternalLogId logId = InternalLogId.allocate("Subchannel", /*details=*/ AUTHORITY); + ChannelTracer subchannelTracer = new ChannelTracer(logId, 10, + fakeClock.getTimeProvider().currentTimeNanos(), "Subchannel"); + LoadBalancer.CreateSubchannelArgs createSubchannelArgs = + LoadBalancer.CreateSubchannelArgs.newBuilder().setAddresses(addressGroups).build(); + internalSubchannel = new InternalSubchannel( + createSubchannelArgs, AUTHORITY, USER_AGENT, mockBackoffPolicyProvider, + mockTransportFactory, fakeClock.getScheduledExecutorService(), + fakeClock.getStopwatchSupplier(), syncContext, mockInternalSubchannelCallback, channelz, + CallTracer.getDefaultFactory().create(), subchannelTracer, logId, + new ChannelLoggerImpl(subchannelTracer, fakeClock.getTimeProvider()), + Collections.emptyList(), AUTHORITY, mockMetricRecorder + ); + + // --- Action: Successful connection --- + internalSubchannel.obtainActiveTransport(); + MockClientTransportInfo transportInfo = transports.poll(); + assertNotNull(transportInfo); + transportInfo.listener.transportReady(); + fakeClock.runDueTasks(); // Process the successful connection + + // --- Action: Transport is shut down --- + transportInfo.listener.transportShutdown(Status.UNAVAILABLE.withDescription("unknown")); + fakeClock.runDueTasks(); // Process the shutdown + + // --- Verification --- + InOrder inOrder = inOrder(mockMetricRecorder); + + // Verify successful connection metrics + inOrder.verify(mockMetricRecorder).addLongCounter( + eqMetricInstrumentName("grpc.subchannel.connection_attempts_succeeded"), + eq(1L), + eq(Arrays.asList(AUTHORITY)), + eq(Arrays.asList(BACKEND_SERVICE, LOCALITY)) + ); + inOrder.verify(mockMetricRecorder).addLongUpDownCounter( + eqMetricInstrumentName("grpc.subchannel.open_connections"), + eq(1L), + eq(Arrays.asList(AUTHORITY)), + eq(Arrays.asList("privacy_and_integrity", BACKEND_SERVICE, LOCALITY)) + ); + + // Verify disconnection metrics + inOrder.verify(mockMetricRecorder).addLongCounter( + eqMetricInstrumentName("grpc.subchannel.disconnections"), + eq(1L), + eq(Arrays.asList(AUTHORITY)), + eq(Arrays.asList(BACKEND_SERVICE, LOCALITY, "unknown")) + ); + inOrder.verify(mockMetricRecorder).addLongUpDownCounter( + eqMetricInstrumentName("grpc.subchannel.open_connections"), + eq(-1L), + eq(Arrays.asList(AUTHORITY)), + eq(Arrays.asList("privacy_and_integrity", BACKEND_SERVICE, LOCALITY)) + ); + + inOrder.verifyNoMoreInteractions(); } private void assertNoCallbackInvoke() { @@ -1459,5 +1603,13 @@ private void assertExactCallbackInvokes(String ... expectedInvokes) { callbackInvokes.clear(); } + static class MetricRecorderImpl implements MetricRecorder { + } + + @SuppressWarnings("TypeParameterUnusedInFormals") + private T eqMetricInstrumentName(String name) { + return argThat(instrument -> instrument.getName().equals(name)); + } + private static class FakeSocketAddress extends SocketAddress {} } diff --git a/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java b/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java index 08f34a267f9..33bf9bb41e2 100644 --- a/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java +++ b/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java @@ -32,6 +32,7 @@ import io.grpc.LongCounterMetricInstrument; import io.grpc.LongGaugeMetricInstrument; import io.grpc.LongHistogramMetricInstrument; +import io.grpc.LongUpDownCounterMetricInstrument; import io.grpc.MetricInstrumentRegistry; import io.grpc.MetricInstrumentRegistryAccessor; import io.grpc.MetricRecorder; @@ -79,6 +80,9 @@ public class MetricRecorderImplTest { private final LongGaugeMetricInstrument longGaugeInstrument = registry.registerLongGauge("gauge0", DESCRIPTION, UNIT, REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, ENABLED); + private final LongUpDownCounterMetricInstrument longUpDownCounterInstrument = + registry.registerLongUpDownCounter("upDownCounter0", DESCRIPTION, UNIT, + REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, ENABLED); private MetricRecorder recorder; @Before @@ -88,7 +92,7 @@ public void setUp() { @Test public void addCounter() { - when(mockSink.getMeasuresSize()).thenReturn(4); + when(mockSink.getMeasuresSize()).thenReturn(6); recorder.addDoubleCounter(doubleCounterInstrument, 1.0, REQUIRED_LABEL_VALUES, OPTIONAL_LABEL_VALUES); @@ -100,6 +104,12 @@ public void addCounter() { verify(mockSink, times(2)).addLongCounter(eq(longCounterInstrument), eq(1L), eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES)); + recorder.addLongUpDownCounter(longUpDownCounterInstrument, -10, REQUIRED_LABEL_VALUES, + OPTIONAL_LABEL_VALUES); + verify(mockSink, times(2)) + .addLongUpDownCounter(eq(longUpDownCounterInstrument), eq(-10L), + eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES)); + verify(mockSink, never()).updateMeasures(registry.getMetricInstruments()); } @@ -190,6 +200,13 @@ public void newRegisteredMetricUpdateMeasures() { verify(mockSink, times(2)) .registerBatchCallback(any(Runnable.class), eq(longGaugeInstrument)); registration.close(); + + // Long UpDown Counter + recorder.addLongUpDownCounter(longUpDownCounterInstrument, -10, REQUIRED_LABEL_VALUES, + OPTIONAL_LABEL_VALUES); + verify(mockSink, times(12)).updateMeasures(anyList()); + verify(mockSink, times(2)).addLongUpDownCounter(eq(longUpDownCounterInstrument), eq(-10L), + eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES)); } @Test(expected = IllegalArgumentException.class) @@ -208,6 +225,13 @@ public void addLongCounterMismatchedRequiredLabelValues() { OPTIONAL_LABEL_VALUES); } + @Test(expected = IllegalArgumentException.class) + public void addLongUpDownCounterMismatchedRequiredLabelValues() { + when(mockSink.getMeasuresSize()).thenReturn(6); + recorder.addLongUpDownCounter(longUpDownCounterInstrument, 1, ImmutableList.of(), + OPTIONAL_LABEL_VALUES); + } + @Test(expected = IllegalArgumentException.class) public void recordDoubleHistogramMismatchedRequiredLabelValues() { when(mockSink.getMeasuresSize()).thenReturn(4); @@ -260,6 +284,13 @@ public void addLongCounterMismatchedOptionalLabelValues() { ImmutableList.of()); } + @Test(expected = IllegalArgumentException.class) + public void addLongUpDownCounterMismatchedOptionalLabelValues() { + when(mockSink.getMeasuresSize()).thenReturn(6); + recorder.addLongUpDownCounter(longUpDownCounterInstrument, 1, REQUIRED_LABEL_VALUES, + ImmutableList.of()); + } + @Test(expected = IllegalArgumentException.class) public void recordDoubleHistogramMismatchedOptionalLabelValues() { when(mockSink.getMeasuresSize()).thenReturn(4); diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricSink.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricSink.java index 8f612804436..fd8af7f998f 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricSink.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricSink.java @@ -27,6 +27,7 @@ import io.grpc.LongCounterMetricInstrument; import io.grpc.LongGaugeMetricInstrument; import io.grpc.LongHistogramMetricInstrument; +import io.grpc.LongUpDownCounterMetricInstrument; import io.grpc.MetricInstrument; import io.grpc.MetricSink; import io.opentelemetry.api.common.Attributes; @@ -36,6 +37,7 @@ import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongUpDownCounter; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.ObservableLongMeasurement; import io.opentelemetry.api.metrics.ObservableMeasurement; @@ -117,6 +119,22 @@ public void addLongCounter(LongCounterMetricInstrument metricInstrument, long va counter.add(value, attributes); } + @Override + public void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, long value, + List requiredLabelValues, + List optionalLabelValues) { + MeasuresData instrumentData = measures.get(metricInstrument.getIndex()); + if (instrumentData == null) { + // Disabled metric + return; + } + Attributes attributes = createAttributes(metricInstrument.getRequiredLabelKeys(), + metricInstrument.getOptionalLabelKeys(), requiredLabelValues, optionalLabelValues, + instrumentData.getOptionalLabelsBitSet()); + LongUpDownCounter counter = (LongUpDownCounter) instrumentData.getMeasure(); + counter.add(value, attributes); + } + @Override public void recordDoubleHistogram(DoubleHistogramMetricInstrument metricInstrument, double value, List requiredLabelValues, List optionalLabelValues) { @@ -256,6 +274,11 @@ public void updateMeasures(List instruments) { .setDescription(description) .ofLongs() .buildObserver(); + } else if (instrument instanceof LongUpDownCounterMetricInstrument) { + openTelemetryMeasure = openTelemetryMeter.upDownCounterBuilder(name) + .setUnit(unit) + .setDescription(description) + .build(); } else { logger.log(Level.FINE, "Unsupported metric instrument type : {0}", instrument); openTelemetryMeasure = null; diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java index 5214804d369..ef21903c8e7 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java @@ -36,6 +36,12 @@ public final class OpenTelemetryConstants { public static final AttributeKey BACKEND_SERVICE_KEY = AttributeKey.stringKey("grpc.lb.backend_service"); + public static final AttributeKey DISCONNECT_ERROR_KEY = + AttributeKey.stringKey("grpc.disconnect_error"); + + public static final AttributeKey SECURITY_LEVEL_KEY = + AttributeKey.stringKey("grpc.security_level"); + public static final List LATENCY_BUCKETS = ImmutableList.of( 0d, 0.00001d, 0.00005d, 0.0001d, 0.0003d, 0.0006d, 0.0008d, 0.001d, 0.002d, diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricSinkTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricSinkTest.java index c538da55dcb..cced4de3cb4 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricSinkTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricSinkTest.java @@ -24,6 +24,7 @@ import io.grpc.LongCounterMetricInstrument; import io.grpc.LongGaugeMetricInstrument; import io.grpc.LongHistogramMetricInstrument; +import io.grpc.LongUpDownCounterMetricInstrument; import io.grpc.MetricInstrument; import io.grpc.MetricSink; import io.grpc.opentelemetry.internal.OpenTelemetryConstants; @@ -144,16 +145,25 @@ public void addCounter_enabledMetric() { "Number of client calls started", "count", Collections.emptyList(), Collections.emptyList(), true); + LongUpDownCounterMetricInstrument longUpDownCounterInstrument = + new LongUpDownCounterMetricInstrument(2, "active_carrier_pigeons", + "Active Carrier Pigeons", "pigeons", + Collections.emptyList(), + Collections.emptyList(), true); + // Create sink sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, false, Collections.emptyList()); // Invoke updateMeasures - sink.updateMeasures(Arrays.asList(longCounterInstrument, doubleCounterInstrument)); + sink.updateMeasures(Arrays.asList(longCounterInstrument, doubleCounterInstrument, + longUpDownCounterInstrument)); sink.addLongCounter(longCounterInstrument, 123L, Collections.emptyList(), Collections.emptyList()); sink.addDoubleCounter(doubleCounterInstrument, 12.0, Collections.emptyList(), Collections.emptyList()); + sink.addLongUpDownCounter(longUpDownCounterInstrument, -3L, Collections.emptyList(), + Collections.emptyList()); assertThat(openTelemetryTesting.getMetrics()) .satisfiesExactlyInAnyOrder( @@ -184,7 +194,21 @@ public void addCounter_enabledMetric() { .hasPointsSatisfying( point -> point - .hasValue(12.0D)))); + .hasValue(12.0D))), + metric -> + assertThat(metric) + .hasInstrumentationScope(InstrumentationScopeInfo.create( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .hasName("active_carrier_pigeons") + .hasDescription("Active Carrier Pigeons") + .hasUnit("pigeons") + .hasLongSumSatisfying( + longSum -> + longSum + .hasPointsSatisfying( + point -> + point + .hasValue(-3L)))); } @Test @@ -192,18 +216,27 @@ public void addCounter_disabledMetric() { // set up sink with disabled metric Map enabledMetrics = new HashMap<>(); enabledMetrics.put("client_latency", false); + enabledMetrics.put("active_carrier_pigeons", false); LongCounterMetricInstrument instrument = new LongCounterMetricInstrument(0, "client_latency", "Client latency", "s", Collections.emptyList(), Collections.emptyList(), true); + LongUpDownCounterMetricInstrument longUpDownCounterInstrument = + new LongUpDownCounterMetricInstrument(1, "active_carrier_pigeons", + "Active Carrier Pigeons", "pigeons", + Collections.emptyList(), + Collections.emptyList(), false); + // Create sink sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, true, Collections.emptyList()); // Invoke updateMeasures - sink.updateMeasures(Arrays.asList(instrument)); + sink.updateMeasures(Arrays.asList(instrument, longUpDownCounterInstrument)); sink.addLongCounter(instrument, 123L, Collections.emptyList(), Collections.emptyList()); + sink.addLongUpDownCounter(longUpDownCounterInstrument, -13L, Collections.emptyList(), + Collections.emptyList()); assertThat(openTelemetryTesting.getMetrics()).isEmpty(); } @@ -377,6 +410,7 @@ public void registerBatchCallback_bothEnabledAndDisabled() { public void recordLabels() { Map enabledMetrics = new HashMap<>(); enabledMetrics.put("client_latency", true); + enabledMetrics.put("ghosts_in_the_wire", true); List optionalLabels = Arrays.asList("optional_label_key_2"); @@ -384,16 +418,24 @@ public void recordLabels() { new LongCounterMetricInstrument(0, "client_latency", "Client latency", "s", ImmutableList.of("required_label_key_1", "required_label_key_2"), ImmutableList.of("optional_label_key_1", "optional_label_key_2"), false); + LongUpDownCounterMetricInstrument longUpDownCounterInstrument = + new LongUpDownCounterMetricInstrument(1, "ghosts_in_the_wire", + "Number of Ghosts Haunting the Wire", "{ghosts}", + ImmutableList.of("required_label_key_1", "required_label_key_2"), + ImmutableList.of("optional_label_key_1", "optional_label_key_2"), false); // Create sink sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, false, optionalLabels); // Invoke updateMeasures - sink.updateMeasures(Arrays.asList(longCounterInstrument)); + sink.updateMeasures(Arrays.asList(longCounterInstrument, longUpDownCounterInstrument)); sink.addLongCounter(longCounterInstrument, 123L, ImmutableList.of("required_label_value_1", "required_label_value_2"), ImmutableList.of("optional_label_value_1", "optional_label_value_2")); + sink.addLongUpDownCounter(longUpDownCounterInstrument, -400L, + ImmutableList.of("required_label_value_1", "required_label_value_2"), + ImmutableList.of("optional_label_value_1", "optional_label_value_2")); io.opentelemetry.api.common.Attributes expectedAtrributes = io.opentelemetry.api.common.Attributes.of( @@ -417,6 +459,22 @@ public void recordLabels() { point -> point .hasAttributes(expectedAtrributes) - .hasValue(123L)))); + .hasValue(123L))), + metric -> + assertThat(metric) + .hasInstrumentationScope(InstrumentationScopeInfo.create( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .hasName("ghosts_in_the_wire") + .hasDescription("Number of Ghosts Haunting the Wire") + .hasUnit("{ghosts}") + .hasLongSumSatisfying( + longSum -> + longSum + .hasPointsSatisfying( + point -> + point + .hasAttributes(expectedAtrributes) + .hasValue(-400L)))); + } } diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index 034cdee0815..fba66e2e8d7 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -305,7 +305,7 @@ private List withAdditionalAttributes( private ClusterLocality createClusterLocalityFromAttributes(Attributes addressAttributes) { Locality locality = addressAttributes.get(XdsAttributes.ATTR_LOCALITY); - String localityName = addressAttributes.get(XdsAttributes.ATTR_LOCALITY_NAME); + String localityName = addressAttributes.get(EquivalentAddressGroup.ATTR_LOCALITY_NAME); // Endpoint addresses resolved by ClusterResolverLoadBalancer should always contain // attributes with its locality, including endpoints in LOGICAL_DNS clusters. diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index e333c46750c..f57cada52e9 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -408,7 +408,7 @@ public void run() { Attributes attr = endpoint.eag().getAttributes().toBuilder() .set(XdsAttributes.ATTR_LOCALITY, locality) - .set(XdsAttributes.ATTR_LOCALITY_NAME, localityName) + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, localityName) .set(XdsAttributes.ATTR_LOCALITY_WEIGHT, localityLbInfo.localityWeight()) .set(XdsAttributes.ATTR_SERVER_WEIGHT, weight) @@ -659,7 +659,7 @@ public Status onResult2(final ResolutionResult resolutionResult) { String localityName = localityName(LOGICAL_DNS_CLUSTER_LOCALITY); Attributes attr = eag.getAttributes().toBuilder() .set(XdsAttributes.ATTR_LOCALITY, LOGICAL_DNS_CLUSTER_LOCALITY) - .set(XdsAttributes.ATTR_LOCALITY_NAME, localityName) + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, localityName) .set(XdsAttributes.ATTR_ADDRESS_NAME, dnsHostName) .build(); eag = new EquivalentAddressGroup(eag.getAddresses(), attr); diff --git a/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java index ab1abb1da15..1a12412f923 100644 --- a/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java @@ -74,7 +74,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { Map localityWeights = new HashMap<>(); for (EquivalentAddressGroup eag : resolvedAddresses.getAddresses()) { Attributes eagAttrs = eag.getAttributes(); - String locality = eagAttrs.get(XdsAttributes.ATTR_LOCALITY_NAME); + String locality = eagAttrs.get(EquivalentAddressGroup.ATTR_LOCALITY_NAME); Integer localityWeight = eagAttrs.get(XdsAttributes.ATTR_LOCALITY_WEIGHT); if (locality == null) { diff --git a/xds/src/main/java/io/grpc/xds/XdsAttributes.java b/xds/src/main/java/io/grpc/xds/XdsAttributes.java index 4a64fdb1453..2e165201e5f 100644 --- a/xds/src/main/java/io/grpc/xds/XdsAttributes.java +++ b/xds/src/main/java/io/grpc/xds/XdsAttributes.java @@ -81,13 +81,6 @@ final class XdsAttributes { static final Attributes.Key ATTR_LOCALITY = Attributes.Key.create("io.grpc.xds.XdsAttributes.locality"); - /** - * The name of the locality that this EquivalentAddressGroup is in. - */ - @EquivalentAddressGroup.Attr - static final Attributes.Key ATTR_LOCALITY_NAME = - Attributes.Key.create("io.grpc.xds.XdsAttributes.localityName"); - /** * Endpoint weight for load balancing purposes. */ diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index 7df0630b779..c5e3f80f170 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -1017,7 +1017,7 @@ public String toString() { Attributes.Builder attributes = Attributes.newBuilder() .set(XdsAttributes.ATTR_LOCALITY, locality) // Unique but arbitrary string - .set(XdsAttributes.ATTR_LOCALITY_NAME, locality.toString()); + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, locality.toString()); if (authorityHostname != null) { attributes.set(XdsAttributes.ATTR_ADDRESS_NAME, authorityHostname); } diff --git a/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java index b6a5d8dbf73..584c32738c5 100644 --- a/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java @@ -254,7 +254,7 @@ public String toString() { } Attributes.Builder attrBuilder = Attributes.newBuilder() - .set(XdsAttributes.ATTR_LOCALITY_NAME, locality); + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, locality); if (localityWeight != null) { attrBuilder.set(XdsAttributes.ATTR_LOCALITY_WEIGHT, localityWeight); } From b2a95cae725169870e3fc0869439b488b15a6190 Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Wed, 10 Sep 2025 20:09:33 +0800 Subject: [PATCH 371/591] netty, okhttp: Add allow header for response code 405 (#12334) Fixes #12329 --- .../io/grpc/netty/NettyServerHandler.java | 13 ++++++++++++- .../io/grpc/netty/NettyServerHandlerTest.java | 4 +++- .../io/grpc/okhttp/OkHttpServerTransport.java | 14 +++++++++++++- .../okhttp/OkHttpServerTransportTest.java | 19 +++++++++++++++---- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java index 48f1aae91a1..69128e0e7a6 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java @@ -60,6 +60,7 @@ import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http2.DecoratingHttp2ConnectionEncoder; import io.netty.handler.codec.http2.DecoratingHttp2FrameWriter; import io.netty.handler.codec.http2.DefaultHttp2Connection; @@ -70,6 +71,7 @@ import io.netty.handler.codec.http2.DefaultHttp2Headers; import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController; import io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController; +import io.netty.handler.codec.http2.EmptyHttp2Headers; import io.netty.handler.codec.http2.Http2Connection; import io.netty.handler.codec.http2.Http2ConnectionAdapter; import io.netty.handler.codec.http2.Http2ConnectionDecoder; @@ -480,8 +482,10 @@ private void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers } if (!HTTP_METHOD.contentEquals(headers.method())) { + Http2Headers extraHeaders = new DefaultHttp2Headers(); + extraHeaders.add(HttpHeaderNames.ALLOW, HTTP_METHOD); respondWithHttpError(ctx, streamId, 405, Status.Code.INTERNAL, - String.format("Method '%s' is not supported", headers.method())); + String.format("Method '%s' is not supported", headers.method()), extraHeaders); return; } @@ -869,6 +873,12 @@ public boolean visit(Http2Stream stream) throws Http2Exception { private void respondWithHttpError( ChannelHandlerContext ctx, int streamId, int code, Status.Code statusCode, String msg) { + respondWithHttpError(ctx, streamId, code, statusCode, msg, EmptyHttp2Headers.INSTANCE); + } + + private void respondWithHttpError( + ChannelHandlerContext ctx, int streamId, int code, Status.Code statusCode, String msg, + Http2Headers extraHeaders) { Metadata metadata = new Metadata(); metadata.put(InternalStatus.CODE_KEY, statusCode.toStatus()); metadata.put(InternalStatus.MESSAGE_KEY, msg); @@ -880,6 +890,7 @@ private void respondWithHttpError( for (int i = 0; i < serialized.length; i += 2) { headers.add(new AsciiString(serialized[i], false), new AsciiString(serialized[i + 1], false)); } + headers.add(extraHeaders); encoder().writeHeaders(ctx, streamId, headers, 0, false, ctx.newPromise()); ByteBuf msgBuf = ByteBufUtil.writeUtf8(ctx.alloc(), msg); encoder().writeData(ctx, streamId, msgBuf, 0, true, ctx.newPromise()); diff --git a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java index 28217937adc..0d5a9bab176 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java @@ -78,6 +78,7 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http2.DefaultHttp2Headers; import io.netty.handler.codec.http2.Http2CodecUtil; import io.netty.handler.codec.http2.Http2Error; @@ -542,7 +543,8 @@ public void headersWithInvalidMethodShouldFail() throws Exception { .set(InternalStatus.CODE_KEY.name(), String.valueOf(Code.INTERNAL.value())) .set(InternalStatus.MESSAGE_KEY.name(), "Method 'FAKE' is not supported") .status("" + 405) - .set(CONTENT_TYPE_HEADER, "text/plain; charset=utf-8"); + .set(CONTENT_TYPE_HEADER, "text/plain; charset=utf-8") + .set(HttpHeaderNames.ALLOW, HTTP_METHOD); verifyWrite() .writeHeaders( diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java index cc52bee85eb..b744bca3116 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java @@ -20,6 +20,7 @@ import static io.grpc.okhttp.OkHttpServerBuilder.MAX_CONNECTION_IDLE_NANOS_DISABLED; import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.concurrent.GuardedBy; @@ -52,6 +53,7 @@ import java.io.IOException; import java.net.Socket; import java.net.SocketException; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -91,6 +93,7 @@ final class OkHttpServerTransport implements ServerTransport, private static final ByteString TE_TRAILERS = ByteString.encodeUtf8("trailers"); private static final ByteString CONTENT_TYPE = ByteString.encodeUtf8("content-type"); private static final ByteString CONTENT_LENGTH = ByteString.encodeUtf8("content-length"); + private static final ByteString ALLOW = ByteString.encodeUtf8("allow"); private final Config config; private final Variant variant = new Http2(); @@ -772,8 +775,9 @@ public void headers(boolean outFinished, } if (!POST_METHOD.equals(httpMethod)) { + List
    extraHeaders = Lists.newArrayList(new Header(ALLOW, POST_METHOD)); respondWithHttpError(streamId, inFinished, 405, Status.Code.INTERNAL, - "HTTP Method is not supported: " + asciiString(httpMethod)); + "HTTP Method is not supported: " + asciiString(httpMethod), extraHeaders); return; } @@ -1066,11 +1070,19 @@ private void streamError(int streamId, ErrorCode errorCode, String reason) { private void respondWithHttpError( int streamId, boolean inFinished, int httpCode, Status.Code statusCode, String msg) { + respondWithHttpError(streamId, inFinished, httpCode, statusCode, msg, + Collections.emptyList()); + } + + private void respondWithHttpError( + int streamId, boolean inFinished, int httpCode, Status.Code statusCode, String msg, + List
    extraHeaders) { Metadata metadata = new Metadata(); metadata.put(InternalStatus.CODE_KEY, statusCode.toStatus()); metadata.put(InternalStatus.MESSAGE_KEY, msg); List
    headers = Headers.createHttpResponseHeaders(httpCode, "text/plain; charset=utf-8", metadata); + headers.addAll(extraHeaders); Buffer data = new Buffer().writeUtf8(msg); synchronized (lock) { diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java index d64d314d7d8..4d2744dc9c7 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; +import com.google.common.collect.Lists; import com.google.common.io.ByteStreams; import io.grpc.Attributes; import io.grpc.InternalChannelz.SocketStats; @@ -62,6 +63,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Deque; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -919,8 +921,9 @@ public void httpGet_failsWith405() throws Exception { CONTENT_TYPE_HEADER, TE_HEADER)); clientFrameWriter.flush(); - - verifyHttpError(1, 405, Status.Code.INTERNAL, "HTTP Method is not supported: GET"); + List
    extraHeaders = Lists.newArrayList(new Header("allow", "POST")); + verifyHttpError(1, 405, Status.Code.INTERNAL, "HTTP Method is not supported: GET", + extraHeaders); shutdownAndTerminate(/*lastStreamId=*/ 1); } @@ -976,7 +979,8 @@ public void httpErrorsAdhereToFlowControl() throws Exception { new Header(":status", "405"), new Header("content-type", "text/plain; charset=utf-8"), new Header("grpc-status", "" + Status.Code.INTERNAL.value()), - new Header("grpc-message", errorDescription)); + new Header("grpc-message", errorDescription), + new Header("allow", "POST")); assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); verify(clientFramesRead) .headers(false, false, 1, -1, responseHeaders, HeadersMode.HTTP_20_HEADERS); @@ -1398,11 +1402,18 @@ private void pingPong() throws IOException { private void verifyHttpError( int streamId, int httpCode, Status.Code grpcCode, String errorDescription) throws Exception { - List
    responseHeaders = Arrays.asList( + verifyHttpError(streamId, httpCode, grpcCode, errorDescription, Collections.emptyList()); + } + + private void verifyHttpError( + int streamId, int httpCode, Status.Code grpcCode, String errorDescription, + List
    extraHeaders) throws Exception { + List
    responseHeaders = Lists.newArrayList( new Header(":status", "" + httpCode), new Header("content-type", "text/plain; charset=utf-8"), new Header("grpc-status", "" + grpcCode.value()), new Header("grpc-message", errorDescription)); + responseHeaders.addAll(extraHeaders); assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); verify(clientFramesRead) .headers(false, false, streamId, -1, responseHeaders, HeadersMode.HTTP_20_HEADERS); From 7d6ea288a2286af8cac7617810e758c3c9c03f5a Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 11 Sep 2025 08:13:56 -0700 Subject: [PATCH 372/591] Add a basic SocketStats with just the local and remote addresses. (#12349) This will make channelz more useful. --- .../grpc/binder/internal/BinderTransport.java | 12 ++++++++- .../RobolectricBinderTransportTest.java | 25 ++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 0fe131a0728..8fde69d8530 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -31,7 +31,9 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; +import io.grpc.Grpc; import io.grpc.Internal; +import io.grpc.InternalChannelz; import io.grpc.InternalChannelz.SocketStats; import io.grpc.InternalLogId; import io.grpc.Status; @@ -205,7 +207,15 @@ public final ScheduledExecutorService getScheduledExecutorService() { // Override in child class. public final ListenableFuture getStats() { - return immediateFuture(null); + Attributes attributes = getAttributes(); + return immediateFuture( + new InternalChannelz.SocketStats( + /* data= */ null, // TODO: Keep track of these stats with TransportTracer or similar. + /* local= */ attributes.get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR), + /* remote= */ attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR), + // TODO: SocketOptions are meaningless for binder but we're still forced to provide one. + new InternalChannelz.SocketOptions.Builder().build(), + /* security= */ null)); } // Override in child class. diff --git a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java index 8f1209f389b..b1822f2f85e 100644 --- a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java @@ -39,6 +39,7 @@ import androidx.test.core.content.pm.PackageInfoBuilder; import com.google.common.collect.ImmutableList; import io.grpc.Attributes; +import io.grpc.InternalChannelz.SocketStats; import io.grpc.ServerStreamTracer; import io.grpc.Status; import io.grpc.binder.AndroidComponentAddress; @@ -52,6 +53,7 @@ import io.grpc.internal.GrpcUtil; import io.grpc.internal.InternalServer; import io.grpc.internal.ManagedClientTransport; +import io.grpc.internal.MockServerTransportListener; import io.grpc.internal.ObjectPool; import io.grpc.internal.SharedResourcePool; import java.util.List; @@ -301,13 +303,30 @@ public void clientIgnoresDuplicateSetupTransaction() throws Exception { } assertThat(((ConnectionClientTransport) client).getAttributes().get(REMOTE_UID)) - .isEqualTo(myUid()); + .isEqualTo(myUid()); } @Test - @Ignore("See BinderTransportTest#socketStats.") @Override - public void socketStats() {} + // We don't quite pass the official/abstract version of this test yet because + // today's binder client and server transports have different ideas of each others' address. + // TODO(#12347): Remove this @Override once this difference is resolved. + public void socketStats() throws Exception { + server.start(serverListener); + ManagedClientTransport client = newClientTransport(server); + startTransport(client, mockClientTransportListener); + + SocketStats clientSocketStats = client.getStats().get(); + assertThat(clientSocketStats.local).isInstanceOf(AndroidComponentAddress.class); + assertThat(((AndroidComponentAddress) clientSocketStats.remote).getPackage()) + .isEqualTo(((AndroidComponentAddress) server.getListenSocketAddress()).getPackage()); + + MockServerTransportListener serverTransportListener = + serverListener.takeListenerOrFail(TIMEOUT_MS, MILLISECONDS); + SocketStats serverSocketStats = serverTransportListener.transport.getStats().get(); + assertThat(serverSocketStats.local).isEqualTo(server.getListenSocketAddress()); + assertThat(serverSocketStats.remote).isEqualTo(new BoundClientAddress(myUid())); + } @Test @Ignore("See BinderTransportTest#flowControlPushBack") From e91378cf96ac8dc031ae013eb169022f50bf4229 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Fri, 12 Sep 2025 16:23:54 -0700 Subject: [PATCH 373/591] binder: Only accept post-setup transactions from the server UID we actually authorize. (#12359) --- .../internal/BinderClientTransport.java | 1 + .../grpc/binder/internal/BinderTransport.java | 17 +++++ .../binder/internal/LeakSafeOneWayBinder.java | 16 ++++- .../binder/internal/TransactionUtils.java | 24 +++++++ .../RobolectricBinderTransportTest.java | 47 +++++++++++++ .../binder/internal/TransactionUtilsTest.java | 70 +++++++++++++++++++ 6 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 binder/src/test/java/io/grpc/binder/internal/TransactionUtilsTest.java diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 82c9e17b871..144ad56eec3 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -339,6 +339,7 @@ protected void handleSetupTransport(Parcel parcel) { shutdownInternal( Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); } else { + restrictIncomingBinderToCallsFrom(remoteUid); attributes = setSecurityAttrs(attributes, remoteUid); authResultFuture = checkServerAuthorizationAsync(remoteUid); Futures.addCallback( diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 8fde69d8530..6c89c56ffd4 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.util.concurrent.Futures.immediateFuture; +import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; import android.os.DeadObjectException; import android.os.IBinder; @@ -39,6 +40,7 @@ import io.grpc.Status; import io.grpc.StatusException; import io.grpc.binder.InboundParcelablePolicy; +import io.grpc.binder.internal.LeakSafeOneWayBinder.TransactionHandler; import io.grpc.internal.ObjectPool; import java.util.ArrayList; import java.util.Iterator; @@ -155,6 +157,7 @@ protected enum TransportState { private final ObjectPool executorServicePool; private final ScheduledExecutorService scheduledExecutorService; private final InternalLogId logId; + @GuardedBy("this") private final LeakSafeOneWayBinder incomingBinder; protected final ConcurrentHashMap> ongoingCalls; @@ -476,6 +479,15 @@ private boolean handleTransactionInternal(int code, Parcel parcel) { } } + @BinderThread + @GuardedBy("this") + protected void restrictIncomingBinderToCallsFrom(int allowedCallingUid) { + TransactionHandler currentHandler = incomingBinder.getHandler(); + if (currentHandler != null) { + incomingBinder.setHandler(newCallerFilteringHandler(allowedCallingUid, currentHandler)); + } + } + @Nullable @GuardedBy("this") protected Inbound createInbound(int callId) { @@ -561,6 +573,11 @@ Map> getOngoingCalls() { return ongoingCalls; } + @VisibleForTesting + LeakSafeOneWayBinder getIncomingBinderForTesting() { + return this.incomingBinder; + } + private static Status statusFromRemoteException(RemoteException e) { if (e instanceof DeadObjectException || e instanceof TransactionTooLargeException) { // These are to be expected from time to time and can simply be retried. diff --git a/binder/src/main/java/io/grpc/binder/internal/LeakSafeOneWayBinder.java b/binder/src/main/java/io/grpc/binder/internal/LeakSafeOneWayBinder.java index e7837b520f8..c36bc7d5bd3 100644 --- a/binder/src/main/java/io/grpc/binder/internal/LeakSafeOneWayBinder.java +++ b/binder/src/main/java/io/grpc/binder/internal/LeakSafeOneWayBinder.java @@ -73,7 +73,21 @@ public void detach() { setHandler(null); } - /** Replaces the current {@link TransactionHandler} with `handler`. */ + /** Returns the current {@link TransactionHandler} or null if already detached. */ + public @Nullable TransactionHandler getHandler() { + return handler; + } + + /** + * Replaces the current {@link TransactionHandler} with `handler`. + * + *

    {@link TransactionHandler} mutations race against incoming transactions except in the + * special case where the caller is already handling an incoming transaction on this same {@link + * LeakSafeOneWayBinder} instance. In that case, mutations are safe and the provided 'handler' is + * guaranteed to be used for the very next transaction. This follows from the one-at-a-time + * property of one-way Binder transactions as explained by {@link + * TransactionHandler#handleTransaction}. + */ public void setHandler(@Nullable TransactionHandler handler) { this.handler = handler; } diff --git a/binder/src/main/java/io/grpc/binder/internal/TransactionUtils.java b/binder/src/main/java/io/grpc/binder/internal/TransactionUtils.java index c962554d125..2777a78d4ac 100644 --- a/binder/src/main/java/io/grpc/binder/internal/TransactionUtils.java +++ b/binder/src/main/java/io/grpc/binder/internal/TransactionUtils.java @@ -16,9 +16,13 @@ package io.grpc.binder.internal; +import android.os.Binder; import android.os.Parcel; import io.grpc.MethodDescriptor.MethodType; import io.grpc.Status; +import java.util.logging.Level; +import java.util.logging.Logger; +import io.grpc.binder.internal.LeakSafeOneWayBinder.TransactionHandler; import javax.annotation.Nullable; /** Constants and helpers for managing inbound / outbound transactions. */ @@ -99,4 +103,24 @@ static void fillInFlags(Parcel parcel, int flags) { parcel.writeInt(flags); parcel.setDataPosition(pos); } + + /** + * Decorates the given {@link TransactionHandler} with a wrapper that only forwards transactions + * from the given `allowedCallingUid`. + */ + static TransactionHandler newCallerFilteringHandler( + int allowedCallingUid, TransactionHandler wrapped) { + final Logger logger = Logger.getLogger(TransactionUtils.class.getName()); + return new TransactionHandler() { + @Override + public boolean handleTransaction(int code, Parcel data) { + int callingUid = Binder.getCallingUid(); + if (callingUid != allowedCallingUid) { + logger.log(Level.WARNING, "dropped txn from " + callingUid + " !=" + allowedCallingUid); + return false; + } + return wrapped.handleTransaction(code, data); + } + }; + } } diff --git a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java index b1822f2f85e..d3d73f0e9eb 100644 --- a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java @@ -16,12 +16,17 @@ package io.grpc.binder.internal; +import static android.os.IBinder.FLAG_ONEWAY; import static android.os.Process.myUid; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static io.grpc.binder.internal.BinderTransport.REMOTE_UID; import static io.grpc.binder.internal.BinderTransport.SETUP_TRANSPORT; +import static io.grpc.binder.internal.BinderTransport.SHUTDOWN_TRANSPORT; import static io.grpc.binder.internal.BinderTransport.WIRE_FORMAT_VERSION; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; @@ -48,6 +53,7 @@ import io.grpc.binder.SecurityPolicies; import io.grpc.binder.internal.SettableAsyncSecurityPolicy.AuthRequest; import io.grpc.internal.AbstractTransportTest; +import io.grpc.internal.ClientTransport; import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; import io.grpc.internal.ConnectionClientTransport; import io.grpc.internal.GrpcUtil; @@ -64,6 +70,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -102,6 +110,9 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest @Mock AsyncSecurityPolicy mockClientSecurityPolicy; + @Captor + ArgumentCaptor statusCaptor; + ApplicationInfo serverAppInfo; PackageInfo serverPkgInfo; ServiceInfo serviceInfo; @@ -306,6 +317,42 @@ public void clientIgnoresDuplicateSetupTransaction() throws Exception { .isEqualTo(myUid()); } + @Test + public void clientIgnoresTransactionFromNonServerUids() throws Exception { + server.start(serverListener); + client = newClientTransport(server); + startTransport(client, mockClientTransportListener); + + int serverUid = ((ConnectionClientTransport) client).getAttributes().get(REMOTE_UID); + int someOtherUid = 1 + serverUid; + sendShutdownTransportTransactionAsUid(client, someOtherUid); + + // Demonstrate that the transport is still working and that shutdown transaction was ignored. + ClientTransport.PingCallback mockPingCallback = mock(ClientTransport.PingCallback.class); + client.ping(mockPingCallback, directExecutor()); + verify(mockPingCallback, timeout(TIMEOUT_MS)).onSuccess(anyLong()); + + // Try again as the expected uid to demonstrate that this wasn't ignored for some other reason. + sendShutdownTransportTransactionAsUid(client, serverUid); + + verify(mockClientTransportListener, timeout(TIMEOUT_MS)) + .transportShutdown(statusCaptor.capture()); + assertThat(statusCaptor.getValue().getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(statusCaptor.getValue().getDescription()).contains("shutdown"); + } + + static void sendShutdownTransportTransactionAsUid(ClientTransport client, int sendingUid) { + int originalUid = Binder.getCallingUid(); + try { + ShadowBinder.setCallingUid(sendingUid); + ((BinderClientTransport) client) + .getIncomingBinderForTesting() + .onTransact(SHUTDOWN_TRANSPORT, null, null, FLAG_ONEWAY); + } finally { + ShadowBinder.setCallingUid(originalUid); + } + } + @Test @Override // We don't quite pass the official/abstract version of this test yet because diff --git a/binder/src/test/java/io/grpc/binder/internal/TransactionUtilsTest.java b/binder/src/test/java/io/grpc/binder/internal/TransactionUtilsTest.java new file mode 100644 index 00000000000..44a3ce3ef26 --- /dev/null +++ b/binder/src/test/java/io/grpc/binder/internal/TransactionUtilsTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.binder.internal; + +import static com.google.common.truth.Truth.assertThat; +import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Binder; +import android.os.Parcel; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowBinder; + +@RunWith(RobolectricTestRunner.class) +public final class TransactionUtilsTest { + + @Rule public MockitoRule mocks = MockitoJUnit.rule(); + + @Mock LeakSafeOneWayBinder.TransactionHandler mockHandler; + + @Test + public void shouldIgnoreTransactionFromWrongUid() { + Parcel p = Parcel.obtain(); + int originalUid = Binder.getCallingUid(); + try { + when(mockHandler.handleTransaction(eq(1234), same(p))).thenReturn(true); + LeakSafeOneWayBinder.TransactionHandler uid100OnlyHandler = + newCallerFilteringHandler(1000, mockHandler); + + ShadowBinder.setCallingUid(9999); + boolean result = uid100OnlyHandler.handleTransaction(1234, p); + assertThat(result).isFalse(); + verify(mockHandler, never()).handleTransaction(anyInt(), any()); + + ShadowBinder.setCallingUid(1000); + result = uid100OnlyHandler.handleTransaction(1234, p); + assertThat(result).isTrue(); + verify(mockHandler).handleTransaction(1234, p); + } finally { + ShadowBinder.setCallingUid(originalUid); + p.recycle(); + } + } +} From 866d226c1492db7142a07ec693376b223b95edac Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Sat, 13 Sep 2025 11:54:48 +0530 Subject: [PATCH 374/591] compiler: Mangle generated method names that conflict with java.lang.Object methods (#12332) Generated gRPC method names in the BlockingV2Stub can conflict with final methods on `java.lang.Object` (e.g., `toString()`, `hashCode()`) for client-streaming and bidi-streaming RPCs. This occurs because they are generated with no arguments, leading to a compilation error. One such case in #12331. This change introduces a dedicated list of no-argument method names from java.lang.Object and applies name-mangling (appending an underscore) only when generating these specific methods in the v2 blocking stub. This resolves the compilation failure while ensuring that the behavior for all other stubs remains unchanged. Fixes: #12331 --- .../src/java_plugin/cpp/java_generator.cpp | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/compiler/src/java_plugin/cpp/java_generator.cpp b/compiler/src/java_plugin/cpp/java_generator.cpp index 4cee7999402..659d7ccca47 100644 --- a/compiler/src/java_plugin/cpp/java_generator.cpp +++ b/compiler/src/java_plugin/cpp/java_generator.cpp @@ -143,11 +143,24 @@ static std::set java_keywords = { "false", }; +// Methods on java.lang.Object that take no arguments. +static std::set java_object_methods = { + "clone", + "finalize", + "getClass", + "hashCode", + "notify", + "notifyAll", + "toString", + "wait", +}; + // Adjust a method name prefix identifier to follow the JavaBean spec: // - decapitalize the first letter // - remove embedded underscores & capitalize the following letter -// Finally, if the result is a reserved java keyword, append an underscore. -static std::string MixedLower(std::string word) { +// Finally, if the result is a reserved java keyword or an Object method, +// append an underscore. +static std::string MixedLower(std::string word, bool mangle_object_methods = false) { std::string w; w += tolower(word[0]); bool after_underscore = false; @@ -159,7 +172,9 @@ static std::string MixedLower(std::string word) { after_underscore = false; } } - if (java_keywords.find(w) != java_keywords.end()) { + if (java_keywords.find(w) != java_keywords.end() || + (mangle_object_methods && + java_object_methods.find(w) != java_object_methods.end())) { return w + "_"; } return w; @@ -180,8 +195,9 @@ static std::string ToAllUpperCase(std::string word) { return w; } -static inline std::string LowerMethodName(const MethodDescriptor* method) { - return MixedLower(std::string(method->name())); +static inline std::string LowerMethodName(const MethodDescriptor* method, + bool mangle_object_methods = false) { + return MixedLower(std::string(method->name()), mangle_object_methods); } static inline std::string MethodPropertiesFieldName(const MethodDescriptor* method) { @@ -676,10 +692,12 @@ static void PrintStub( const MethodDescriptor* method = service->method(i); (*vars)["input_type"] = MessageFullJavaName(method->input_type()); (*vars)["output_type"] = MessageFullJavaName(method->output_type()); - (*vars)["lower_method_name"] = LowerMethodName(method); - (*vars)["method_method_name"] = MethodPropertiesGetterName(method); bool client_streaming = method->client_streaming(); bool server_streaming = method->server_streaming(); + bool mangle_object_methods = (call_type == BLOCKING_V2_CALL && client_streaming) + || (call_type == BLOCKING_CALL && client_streaming && server_streaming); + (*vars)["lower_method_name"] = LowerMethodName(method, mangle_object_methods); + (*vars)["method_method_name"] = MethodPropertiesGetterName(method); if (call_type == BLOCKING_CALL && client_streaming) { // Blocking client interface with client streaming is not available From fe54dd618d3bd9d5c86236b12f682f87aae5caa4 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Sat, 13 Sep 2025 13:37:32 +0530 Subject: [PATCH 375/591] Add support for macOS aarch64 with universal binary (#12319) --- buildscripts/kokoro/unix.sh | 5 +++++ buildscripts/make_dependencies.sh | 8 +++++++- compiler/build.gradle | 6 ++++++ compiler/check-artifact.sh | 20 ++++++++++---------- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/buildscripts/kokoro/unix.sh b/buildscripts/kokoro/unix.sh index e65825cac01..455d9c07199 100755 --- a/buildscripts/kokoro/unix.sh +++ b/buildscripts/kokoro/unix.sh @@ -39,6 +39,11 @@ ARCH="$ARCH" buildscripts/make_dependencies.sh # Set properties via flags, do not pollute gradle.properties GRADLE_FLAGS="${GRADLE_FLAGS:-}" GRADLE_FLAGS+=" -PtargetArch=$ARCH" + +# For universal binaries on macOS, signal Gradle to use universal flags. +if [[ "$(uname -s)" == "Darwin" ]]; then + GRADLE_FLAGS+=" -PbuildUniversal=true" +fi GRADLE_FLAGS+=" -Pcheckstyle.ignoreFailures=false" GRADLE_FLAGS+=" -PfailOnWarnings=true" GRADLE_FLAGS+=" -PerrorProne=true" diff --git a/buildscripts/make_dependencies.sh b/buildscripts/make_dependencies.sh index e5d2450afe7..73cb54b7b68 100755 --- a/buildscripts/make_dependencies.sh +++ b/buildscripts/make_dependencies.sh @@ -41,7 +41,13 @@ else mkdir "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" pushd "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" # install here so we don't need sudo - if [[ "$ARCH" == x86* ]]; then + if [[ "$(uname -s)" == "Darwin" ]]; then + cmake .. \ + -DCMAKE_CXX_STANDARD=14 -Dprotobuf_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" -DABSL_INTERNAL_AT_LEAST_CXX17=0 \ + -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \ + -B. || exit 1 + elif [[ "$ARCH" == x86* ]]; then CFLAGS=-m${ARCH#*_} CXXFLAGS=-m${ARCH#*_} cmake .. \ -DCMAKE_CXX_STANDARD=14 -Dprotobuf_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=OFF \ -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" -DABSL_INTERNAL_AT_LEAST_CXX17=0 \ diff --git a/compiler/build.gradle b/compiler/build.gradle index dbecb889a43..c5acccebae7 100644 --- a/compiler/build.gradle +++ b/compiler/build.gradle @@ -103,6 +103,12 @@ model { cppCompiler.args "--std=c++14" addEnvArgs("CXXFLAGS", cppCompiler.args) addEnvArgs("CPPFLAGS", cppCompiler.args) + if (project.hasProperty('buildUniversal') && + project.getProperty('buildUniversal').toBoolean() && + osdetector.os == "osx") { + cppCompiler.args "-arch", "arm64", "-arch", "x86_64" + linker.args "-arch", "arm64", "-arch", "x86_64" + } if (osdetector.os == "osx") { cppCompiler.args "-mmacosx-version-min=10.7", "-stdlib=libc++" linker.args "-framework", "CoreFoundation" diff --git a/compiler/check-artifact.sh b/compiler/check-artifact.sh index 12d7709a2a8..83b41f50282 100755 --- a/compiler/check-artifact.sh +++ b/compiler/check-artifact.sh @@ -86,17 +86,17 @@ checkArch () fi fi elif [[ "$OS" == osx ]]; then - format="$(file -b "$1" | grep -o "[^ ]*$")" - echo Format=$format - if [[ "$ARCH" == x86_32 ]]; then - assertEq "$format" "i386" $LINENO - elif [[ "$ARCH" == x86_64 ]]; then - assertEq "$format" "x86_64" $LINENO - elif [[ "$ARCH" == aarch_64 ]]; then - assertEq "$format" "arm64" $LINENO - else - fail "Unsupported arch: $ARCH" + # For macOS, we now build a universal binary. We check that both + # required architectures are present. + format="$(lipo -archs "$1")" + echo "Architectures found: $format" + if ! echo "$format" | grep -q "x86_64"; then + fail "Universal binary is missing x86_64 architecture." + fi + if ! echo "$format" | grep -q "arm64"; then + fail "Universal binary is missing arm64 architecture." fi + echo "Universal binary check successful." else fail "Unsupported system: $OS" fi From 53d81116c53e4a0102e5ac874291732802d34895 Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Sun, 14 Sep 2025 21:09:17 -0700 Subject: [PATCH 376/591] stub: fix typo in method name (#12356) --- stub/src/main/java/io/grpc/stub/ClientCalls.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stub/src/main/java/io/grpc/stub/ClientCalls.java b/stub/src/main/java/io/grpc/stub/ClientCalls.java index e5a94f4d864..8cd31ea9cca 100644 --- a/stub/src/main/java/io/grpc/stub/ClientCalls.java +++ b/stub/src/main/java/io/grpc/stub/ClientCalls.java @@ -931,7 +931,7 @@ public void waitAndDrainWithTimeout(boolean waitForever, long end, } while ((runnable = poll()) != null); // Wake everything up now that we've done something and they can check in their outer loop // if they can continue or need to wait again. - signallAll(); + signalAll(); } } @@ -949,11 +949,11 @@ public void drain() throws InterruptedException { } if (didWork) { - signallAll(); + signalAll(); } } - private void signallAll() { + private void signalAll() { waiterLock.lock(); try { waiterCondition.signalAll(); From c1f3287601696ddd160241132ebb126658b24e59 Mon Sep 17 00:00:00 2001 From: Sangamesh Date: Mon, 15 Sep 2025 18:04:09 +0530 Subject: [PATCH 377/591] util: Fix misleading exception in AdvancedTlsX509TrustManager when cert file is missing (#12353) --- .../io/grpc/util/AdvancedTlsX509TrustManager.java | 4 ++++ .../io/grpc/util/AdvancedTlsX509TrustManagerTest.java | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java b/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java index b4b9b25d1de..0739fa3d453 100644 --- a/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java +++ b/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java @@ -339,6 +339,10 @@ public void run() { private long readAndUpdate(File trustCertFile, long oldTime) throws IOException, GeneralSecurityException { long newTime = checkNotNull(trustCertFile, "trustCertFile").lastModified(); + if (newTime == 0) { + throw new IOException( + "Certificate file not found or not readable: " + trustCertFile.getAbsolutePath()); + } if (newTime == oldTime) { return oldTime; } diff --git a/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java b/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java index 228dbf5ea5b..b9803b03570 100644 --- a/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java +++ b/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java @@ -142,6 +142,17 @@ record -> record.getMessage().contains("Default value of ")); } } + @Test + public void missingFile_throwsFileNotFoundException() throws Exception { + AdvancedTlsX509TrustManager trustManager = AdvancedTlsX509TrustManager.newBuilder().build(); + File nonExistentFile = new File("missing_cert.pem"); + Exception thrown = + assertThrows(Exception.class, () -> trustManager.updateTrustCredentials(nonExistentFile)); + assertNotNull(thrown); + assertEquals(thrown.getMessage(), + "Certificate file not found or not readable: " + nonExistentFile.getAbsolutePath()); + } + @Test public void clientTrustedWithSocketTest() throws Exception { AdvancedTlsX509TrustManager trustManager = AdvancedTlsX509TrustManager.newBuilder() From 6fae71b740feb6ea6c54436b3105d8f0ac393499 Mon Sep 17 00:00:00 2001 From: Umair Khan Date: Mon, 15 Sep 2025 05:47:47 -0700 Subject: [PATCH 378/591] netty: disable Huffman coding in server response headers (#12357) Follow up to PR #10563 Previously, we disabled Huffman encoding on the `NettyClientHandler` to improve header encoding performance. This change also ensures consistency in the header encoding strategy across both client and server components. --- .../io/grpc/netty/NettyServerHandler.java | 6 +- .../grpc/netty/NettyClientTransportTest.java | 71 +++++++++++++++++-- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java index 69128e0e7a6..036fde55e2c 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java @@ -69,6 +69,7 @@ import io.netty.handler.codec.http2.DefaultHttp2FrameReader; import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder; import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController; import io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController; import io.netty.handler.codec.http2.EmptyHttp2Headers; @@ -85,6 +86,7 @@ import io.netty.handler.codec.http2.Http2FrameWriter; import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2HeadersDecoder; +import io.netty.handler.codec.http2.Http2HeadersEncoder; import io.netty.handler.codec.http2.Http2InboundFrameLogger; import io.netty.handler.codec.http2.Http2LifecycleManager; import io.netty.handler.codec.http2.Http2OutboundFrameLogger; @@ -179,8 +181,10 @@ static NettyServerHandler newHandler( Http2HeadersDecoder headersDecoder = new GrpcHttp2ServerHeadersDecoder(maxHeaderListSize); Http2FrameReader frameReader = new Http2InboundFrameLogger( new DefaultHttp2FrameReader(headersDecoder), frameLogger); + Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder( + Http2HeadersEncoder.NEVER_SENSITIVE, false, 16, Integer.MAX_VALUE); Http2FrameWriter frameWriter = - new Http2OutboundFrameLogger(new DefaultHttp2FrameWriter(), frameLogger); + new Http2OutboundFrameLogger(new DefaultHttp2FrameWriter(encoder), frameLogger); return newHandler( channelUnused, frameReader, diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index 65683dd8396..55abe29e93a 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -151,6 +151,9 @@ public class NettyClientTransportTest { private static final SslContext SSL_CONTEXT = createSslContext(); + @SuppressWarnings("InlineMeInliner") // Requires Java 11 + private static final String LONG_STRING_OF_A = Strings.repeat("a", 128); + @Mock private ManagedClientTransport.Listener clientTransportListener; @@ -624,9 +627,6 @@ public void maxHeaderListSizeShouldBeEnforcedOnClient() throws Exception { @Test public void huffmanCodingShouldNotBePerformed() throws Exception { - @SuppressWarnings("InlineMeInliner") // Requires Java 11 - String longStringOfA = Strings.repeat("a", 128); - negotiator = ProtocolNegotiators.serverPlaintext(); startServer(); @@ -637,7 +637,7 @@ public void huffmanCodingShouldNotBePerformed() throws Exception { Metadata headers = new Metadata(); headers.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), - longStringOfA); + LONG_STRING_OF_A); callMeMaybe(transport.start(clientTransportListener)); verify(clientTransportListener, timeout(5000)).transportReady(); @@ -649,7 +649,7 @@ public void huffmanCodingShouldNotBePerformed() throws Exception { public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (msg instanceof ByteBuf) { - if (((ByteBuf) msg).toString(StandardCharsets.UTF_8).contains(longStringOfA)) { + if (((ByteBuf) msg).toString(StandardCharsets.UTF_8).contains(LONG_STRING_OF_A)) { foundExpectedHeaderBytes.set(true); } } @@ -664,6 +664,47 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) } } + @Test + public void huffmanCodingShouldNotBePerformedOnServer() throws Exception { + negotiator = ProtocolNegotiators.serverPlaintext(); + + Metadata responseHeaders = new Metadata(); + responseHeaders.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), + LONG_STRING_OF_A); + + startServer(new EchoServerListener(responseHeaders)); + + NettyClientTransport transport = newTransport(ProtocolNegotiators.plaintext(), + DEFAULT_MAX_MESSAGE_SIZE, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, null, false, + TimeUnit.SECONDS.toNanos(10L), TimeUnit.SECONDS.toNanos(1L), + new ReflectiveChannelFactory<>(NioSocketChannel.class), group); + + callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); + + AtomicBoolean foundExpectedHeaderBytes = new AtomicBoolean(false); + + // Add a handler to the client pipeline to inspect server's response + transport.channel().pipeline().addFirst(new ChannelDuplexHandler() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof ByteBuf) { + String data = ((ByteBuf) msg).toString(StandardCharsets.UTF_8); + if (data.contains(LONG_STRING_OF_A)) { + foundExpectedHeaderBytes.set(true); + } + } + super.channelRead(ctx, msg); + } + }); + + new Rpc(transport).halfClose().waitForResponse(); + + if (!foundExpectedHeaderBytes.get()) { + fail("expected to find UTF-8 encoded 'a's in the response header sent by the server"); + } + } + @Test public void maxHeaderListSizeShouldBeEnforcedOnServer() throws Exception { startServer(100, 1); @@ -1115,7 +1156,16 @@ private void startServer() throws IOException { startServer(100, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE); } + private void startServer(ServerListener serverListener) throws IOException { + startServer(100, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, serverListener); + } + private void startServer(int maxStreamsPerConnection, int maxHeaderListSize) throws IOException { + startServer(maxStreamsPerConnection, maxHeaderListSize, serverListener); + } + + private void startServer(int maxStreamsPerConnection, int maxHeaderListSize, + ServerListener serverListener) throws IOException { server = new NettyServer( TestUtils.testServerAddresses(new InetSocketAddress(0)), @@ -1283,6 +1333,15 @@ private final class EchoServerListener implements ServerListener { final List transports = new ArrayList<>(); final List streamListeners = Collections.synchronizedList(new ArrayList()); + Metadata responseHeaders; + + public EchoServerListener() { + this(new Metadata()); + } + + public EchoServerListener(Metadata responseHeaders) { + this.responseHeaders = responseHeaders; + } @Override public ServerTransportListener transportCreated(final ServerTransport transport) { @@ -1292,7 +1351,7 @@ public ServerTransportListener transportCreated(final ServerTransport transport) public void streamCreated(ServerStream stream, String method, Metadata headers) { EchoServerStreamListener listener = new EchoServerStreamListener(stream, headers); stream.setListener(listener); - stream.writeHeaders(new Metadata(), true); + stream.writeHeaders(responseHeaders, true); stream.request(1); streamListeners.add(listener); } From 6f21bc2e6ca7224243f8f1ea1a28eb72eba3dd9a Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Tue, 16 Sep 2025 05:47:18 +0200 Subject: [PATCH 379/591] servlet: configurable methodNameResolver (#12333) Introduces configuring a method name resolver in `ServletServerBuilder` for customizing the servlet context root path for request paths. --- .../io/grpc/servlet/JettyTransportTest.java | 1 + .../java/io/grpc/servlet/ServletAdapter.java | 8 +++++++- .../io/grpc/servlet/ServletServerBuilder.java | 19 ++++++++++++++++++- .../io/grpc/servlet/TomcatTransportTest.java | 4 +++- .../grpc/servlet/UndertowTransportTest.java | 4 +++- 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/servlet/src/jettyTest/java/io/grpc/servlet/JettyTransportTest.java b/servlet/src/jettyTest/java/io/grpc/servlet/JettyTransportTest.java index e9cb391ea08..c896c7a23ea 100644 --- a/servlet/src/jettyTest/java/io/grpc/servlet/JettyTransportTest.java +++ b/servlet/src/jettyTest/java/io/grpc/servlet/JettyTransportTest.java @@ -69,6 +69,7 @@ public void start(ServerListener listener) throws IOException { listener.transportCreated(new ServletServerBuilder.ServerTransportImpl(scheduler)); ServletAdapter adapter = new ServletAdapter(serverTransportListener, streamTracerFactories, + ServletAdapter.DEFAULT_METHOD_NAME_RESOLVER, Integer.MAX_VALUE); GrpcServlet grpcServlet = new GrpcServlet(adapter); diff --git a/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java b/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java index 4bfe8949776..e84b9341fd9 100644 --- a/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java +++ b/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java @@ -45,6 +45,7 @@ import java.util.Enumeration; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.logging.Logger; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; @@ -72,18 +73,23 @@ public final class ServletAdapter { static final Logger logger = Logger.getLogger(ServletAdapter.class.getName()); + static final Function DEFAULT_METHOD_NAME_RESOLVER = + req -> req.getRequestURI().substring(1); // remove the leading "/" private final ServerTransportListener transportListener; private final List streamTracerFactories; + private final Function methodNameResolver; private final int maxInboundMessageSize; private final Attributes attributes; ServletAdapter( ServerTransportListener transportListener, List streamTracerFactories, + Function methodNameResolver, int maxInboundMessageSize) { this.transportListener = transportListener; this.streamTracerFactories = streamTracerFactories; + this.methodNameResolver = methodNameResolver; this.maxInboundMessageSize = maxInboundMessageSize; attributes = transportListener.transportReady(Attributes.EMPTY); } @@ -119,7 +125,7 @@ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOEx AsyncContext asyncCtx = req.startAsync(req, resp); - String method = req.getRequestURI().substring(1); // remove the leading "/" + String method = methodNameResolver.apply(req); Metadata headers = getHeaders(req); if (logger.isLoggable(FINEST)) { diff --git a/servlet/src/main/java/io/grpc/servlet/ServletServerBuilder.java b/servlet/src/main/java/io/grpc/servlet/ServletServerBuilder.java index 72c4383d273..aee25de01ad 100644 --- a/servlet/src/main/java/io/grpc/servlet/ServletServerBuilder.java +++ b/servlet/src/main/java/io/grpc/servlet/ServletServerBuilder.java @@ -49,8 +49,10 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; +import javax.servlet.http.HttpServletRequest; /** * Builder to build a gRPC server that can run as a servlet. This is for advanced custom settings. @@ -64,6 +66,8 @@ @NotThreadSafe public final class ServletServerBuilder extends ForwardingServerBuilder { List streamTracerFactories; + private Function methodNameResolver = + ServletAdapter.DEFAULT_METHOD_NAME_RESOLVER; int maxInboundMessageSize = DEFAULT_MAX_MESSAGE_SIZE; private final ServerImplBuilder serverImplBuilder; @@ -98,7 +102,8 @@ public Server build() { * Creates a {@link ServletAdapter}. */ public ServletAdapter buildServletAdapter() { - return new ServletAdapter(buildAndStart(), streamTracerFactories, maxInboundMessageSize); + return new ServletAdapter(buildAndStart(), streamTracerFactories, methodNameResolver, + maxInboundMessageSize); } /** @@ -176,6 +181,18 @@ public ServletServerBuilder useTransportSecurity(File certChain, File privateKey throw new UnsupportedOperationException("TLS should be configured by the servlet container"); } + /** + * Specifies how to determine gRPC method name from servlet request. + * + *

    The default strategy is using {@link HttpServletRequest#getRequestURI()} without the leading + * slash.

    + */ + public ServletServerBuilder methodNameResolver( + Function methodResolver) { + this.methodNameResolver = checkNotNull(methodResolver); + return this; + } + @Override public ServletServerBuilder maxInboundMessageSize(int bytes) { checkArgument(bytes >= 0, "bytes must be >= 0"); diff --git a/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java b/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java index 262036883a9..2171c6eb2df 100644 --- a/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java +++ b/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java @@ -81,7 +81,9 @@ public void start(ServerListener listener) throws IOException { ServerTransportListener serverTransportListener = listener.transportCreated(new ServerTransportImpl(scheduler)); ServletAdapter adapter = - new ServletAdapter(serverTransportListener, streamTracerFactories, Integer.MAX_VALUE); + new ServletAdapter(serverTransportListener, streamTracerFactories, + ServletAdapter.DEFAULT_METHOD_NAME_RESOLVER, + Integer.MAX_VALUE); GrpcServlet grpcServlet = new GrpcServlet(adapter); tomcatServer = new Tomcat(); diff --git a/servlet/src/undertowTest/java/io/grpc/servlet/UndertowTransportTest.java b/servlet/src/undertowTest/java/io/grpc/servlet/UndertowTransportTest.java index e14c11985de..ef897c87d70 100644 --- a/servlet/src/undertowTest/java/io/grpc/servlet/UndertowTransportTest.java +++ b/servlet/src/undertowTest/java/io/grpc/servlet/UndertowTransportTest.java @@ -100,7 +100,9 @@ public void start(ServerListener listener) throws IOException { ServerTransportListener serverTransportListener = listener.transportCreated(new ServerTransportImpl(scheduler)); ServletAdapter adapter = - new ServletAdapter(serverTransportListener, streamTracerFactories, Integer.MAX_VALUE); + new ServletAdapter(serverTransportListener, streamTracerFactories, + ServletAdapter.DEFAULT_METHOD_NAME_RESOLVER, + Integer.MAX_VALUE); GrpcServlet grpcServlet = new GrpcServlet(adapter); InstanceFactory instanceFactory = () -> new ImmediateInstanceHandle<>(grpcServlet); From d163c063b93e20e1ba530bb7e502debc350cfc8e Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Tue, 16 Sep 2025 06:50:11 +0200 Subject: [PATCH 380/591] servlet: add extra 5s to AsyncContext timeout (#12321) Currently there is a race between 2 tasks scheduled to handle request timeout * servlet AsyncContext timeout * gRPC Context which can cause instability. This change makes the gRPC Context timeout happen first. --- .../main/java/io/grpc/servlet/ServletAdapter.java | 14 ++++++++++---- .../io/grpc/servlet/ServletServerBuilderTest.java | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java b/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java index e84b9341fd9..668e82425cb 100644 --- a/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java +++ b/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java @@ -22,6 +22,7 @@ import static java.util.logging.Level.FINE; import static java.util.logging.Level.FINEST; +import com.google.common.annotations.VisibleForTesting; import com.google.common.io.BaseEncoding; import io.grpc.Attributes; import io.grpc.ExperimentalApi; @@ -134,10 +135,9 @@ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOEx } Long timeoutNanos = headers.get(TIMEOUT_KEY); - if (timeoutNanos == null) { - timeoutNanos = 0L; - } - asyncCtx.setTimeout(TimeUnit.NANOSECONDS.toMillis(timeoutNanos)); + asyncCtx.setTimeout(timeoutNanos != null + ? TimeUnit.NANOSECONDS.toMillis(timeoutNanos) + ASYNC_TIMEOUT_SAFETY_MARGIN + : 0); StatsTraceContext statsTraceCtx = StatsTraceContext.newServerContext(streamTracerFactories, method, headers); @@ -164,6 +164,12 @@ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOEx asyncCtx.addListener(new GrpcAsyncListener(stream, logId)); } + /** + * Deadlines are managed via Context, servlet async timeout is not supposed to happen. + */ + @VisibleForTesting + static final long ASYNC_TIMEOUT_SAFETY_MARGIN = 5_000; + // This method must use Enumeration and its members, since that is the only way to read headers // from the servlet api. @SuppressWarnings("JdkObsolete") diff --git a/servlet/src/test/java/io/grpc/servlet/ServletServerBuilderTest.java b/servlet/src/test/java/io/grpc/servlet/ServletServerBuilderTest.java index d571cfd45d5..7a8c5b91f25 100644 --- a/servlet/src/test/java/io/grpc/servlet/ServletServerBuilderTest.java +++ b/servlet/src/test/java/io/grpc/servlet/ServletServerBuilderTest.java @@ -80,7 +80,7 @@ public void scheduledExecutorService() throws Exception { ServletAdapter servletAdapter = serverBuilder.buildServletAdapter(); servletAdapter.doPost(request, response); - verify(asyncContext).setTimeout(1); + verify(asyncContext).setTimeout(1 + ServletAdapter.ASYNC_TIMEOUT_SAFETY_MARGIN); // The following just verifies that scheduler is populated to the transport. // It doesn't matter what tasks (such as handshake timeout and request deadline) are actually From ead6a54602d44f1cbecb4152fb9503a0104fd58b Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 16 Sep 2025 15:00:43 +0530 Subject: [PATCH 381/591] binder: Suppress GuardedBy("this") warning for test access (#12362) Co-authored-by: MV Shiva --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 1 + 1 file changed, 1 insertion(+) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 6c89c56ffd4..a7ca5b25b24 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -574,6 +574,7 @@ Map> getOngoingCalls() { } @VisibleForTesting + @SuppressWarnings("GuardedBy") LeakSafeOneWayBinder getIncomingBinderForTesting() { return this.incomingBinder; } From 4b5ce99b02a61e9257d7d92a872d279de23223ab Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 17 Sep 2025 09:37:08 -0700 Subject: [PATCH 382/591] binder: Fix synchronization instead of suppressing GuardedBy There's no reason why we shouldn't just have proper synchronization here, even if only used in tests. We shouldn't get into the habit of suppressing them. --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index a7ca5b25b24..904b7e83001 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -574,8 +574,7 @@ Map> getOngoingCalls() { } @VisibleForTesting - @SuppressWarnings("GuardedBy") - LeakSafeOneWayBinder getIncomingBinderForTesting() { + synchronized LeakSafeOneWayBinder getIncomingBinderForTesting() { return this.incomingBinder; } From 0c179e3f9e8c4306578f00ecdab2b37480ea780e Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 18 Sep 2025 11:08:43 -0700 Subject: [PATCH 383/591] xds: Convert ClusterResolverLb to XdsDepManager No longer need to hard-code pick_first because of gRFC A61. https://github.com/grpc/proposal/pull/477 --- .../grpc/xds/ClusterResolverLoadBalancer.java | 834 +++------- .../ClusterResolverLoadBalancerProvider.java | 16 +- .../io/grpc/xds/EnvoyServerProtoData.java | 2 +- .../io/grpc/xds/XdsDependencyManager.java | 10 +- .../java/io/grpc/xds/XdsNameResolver.java | 16 +- .../xds/ClusterResolverLoadBalancerTest.java | 1442 ++++++++--------- .../java/io/grpc/xds/XdsNameResolverTest.java | 13 + 7 files changed, 896 insertions(+), 1437 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index f57cada52e9..06fafbb6cf1 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -17,12 +17,9 @@ package io.grpc.xds; import static com.google.common.base.Preconditions.checkNotNull; -import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; import static io.grpc.xds.XdsLbPolicies.PRIORITY_POLICY_NAME; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; -import com.google.protobuf.Struct; import io.grpc.Attributes; import io.grpc.EquivalentAddressGroup; import io.grpc.HttpConnectProxiedSocketAddress; @@ -30,16 +27,8 @@ import io.grpc.LoadBalancer; import io.grpc.LoadBalancerProvider; import io.grpc.LoadBalancerRegistry; -import io.grpc.NameResolver; -import io.grpc.NameResolver.ResolutionResult; import io.grpc.Status; import io.grpc.StatusOr; -import io.grpc.SynchronizationContext; -import io.grpc.SynchronizationContext.ScheduledHandle; -import io.grpc.internal.BackoffPolicy; -import io.grpc.internal.ExponentialBackoffPolicy; -import io.grpc.internal.ObjectPool; -import io.grpc.util.ForwardingLoadBalancerHelper; import io.grpc.util.GracefulSwitchLoadBalancer; import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig; import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; @@ -51,94 +40,46 @@ import io.grpc.xds.EnvoyServerProtoData.FailurePercentageEjection; import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection; -import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; +import io.grpc.xds.XdsConfig.XdsClusterConfig; import io.grpc.xds.XdsEndpointResource.EdsUpdate; -import io.grpc.xds.client.Bootstrapper.ServerInfo; import io.grpc.xds.client.Locality; -import io.grpc.xds.client.XdsClient; -import io.grpc.xds.client.XdsClient.ResourceWatcher; import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger.XdsLogLevel; import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.TreeMap; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; /** * Load balancer for cluster_resolver_experimental LB policy. This LB policy is the child LB policy * of the cds_experimental LB policy and the parent LB policy of the priority_experimental LB - * policy in the xDS load balancing hierarchy. This policy resolves endpoints of non-aggregate + * policy in the xDS load balancing hierarchy. This policy converts endpoints of non-aggregate * clusters (e.g., EDS or Logical DNS) and groups endpoints in priorities and localities to be * used in the downstream LB policies for fine-grained load balancing purposes. */ final class ClusterResolverLoadBalancer extends LoadBalancer { - // DNS-resolved endpoints do not have the definition of the locality it belongs to, just hardcode - // to an empty locality. - private static final Locality LOGICAL_DNS_CLUSTER_LOCALITY = Locality.create("", "", ""); private final XdsLogger logger; - private final SynchronizationContext syncContext; - private final ScheduledExecutorService timeService; private final LoadBalancerRegistry lbRegistry; - private final BackoffPolicy.Provider backoffPolicyProvider; - private final GracefulSwitchLoadBalancer delegate; - private ObjectPool xdsClientPool; - private XdsClient xdsClient; - private ClusterResolverConfig config; - - ClusterResolverLoadBalancer(Helper helper) { - this(helper, LoadBalancerRegistry.getDefaultRegistry(), - new ExponentialBackoffPolicy.Provider()); - } + private final LoadBalancer delegate; + private ClusterState clusterState; - @VisibleForTesting - ClusterResolverLoadBalancer(Helper helper, LoadBalancerRegistry lbRegistry, - BackoffPolicy.Provider backoffPolicyProvider) { + ClusterResolverLoadBalancer(Helper helper, LoadBalancerRegistry lbRegistry) { + this.delegate = lbRegistry.getProvider(PRIORITY_POLICY_NAME).newLoadBalancer(helper); this.lbRegistry = checkNotNull(lbRegistry, "lbRegistry"); - this.backoffPolicyProvider = checkNotNull(backoffPolicyProvider, "backoffPolicyProvider"); - this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); - this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); - delegate = new GracefulSwitchLoadBalancer(helper); logger = XdsLogger.withLogId( InternalLogId.allocate("cluster-resolver-lb", helper.getAuthority())); logger.log(XdsLogLevel.INFO, "Created"); } - @Override - public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses); - if (xdsClientPool == null) { - xdsClientPool = resolvedAddresses.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL); - xdsClient = xdsClientPool.getObject(); - } - ClusterResolverConfig config = - (ClusterResolverConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); - if (!Objects.equals(this.config, config)) { - logger.log(XdsLogLevel.DEBUG, "Config: {0}", config); - this.config = config; - Object gracefulConfig = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - new ClusterResolverLbStateFactory(), config); - delegate.handleResolvedAddresses( - resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(gracefulConfig).build()); - } - return Status.OK; - } - @Override public void handleNameResolutionError(Status error) { logger.log(XdsLogLevel.WARNING, "Received name resolution error: {0}", error); @@ -149,575 +90,213 @@ public void handleNameResolutionError(Status error) { public void shutdown() { logger.log(XdsLogLevel.INFO, "Shutdown"); delegate.shutdown(); - if (xdsClientPool != null) { - xdsClientPool.returnObject(xdsClient); - } - } - - private final class ClusterResolverLbStateFactory extends LoadBalancer.Factory { - @Override - public LoadBalancer newLoadBalancer(Helper helper) { - return new ClusterResolverLbState(helper); - } } - /** - * The state of a cluster_resolver LB working session. A new instance is created whenever - * the cluster_resolver LB receives a new config. The old instance is replaced when the - * new one is ready to handle new RPCs. - */ - private final class ClusterResolverLbState extends LoadBalancer { - private final Helper helper; - private ClusterState clusterState; - private String cluster; - private Object endpointLbConfig; - private ResolvedAddresses resolvedAddresses; - private LoadBalancer childLb; - - ClusterResolverLbState(Helper helper) { - this.helper = new RefreshableHelper(checkNotNull(helper, "helper")); - logger.log(XdsLogLevel.DEBUG, "New ClusterResolverLbState"); - } - - @Override - public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - this.resolvedAddresses = resolvedAddresses; - ClusterResolverConfig config = - (ClusterResolverConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); - endpointLbConfig = config.lbConfig; - DiscoveryMechanism instance = config.discoveryMechanism; - cluster = instance.cluster; - if (instance.type == DiscoveryMechanism.Type.EDS) { - clusterState = new EdsClusterState(instance.cluster, instance.edsServiceName, - instance.lrsServerInfo, instance.maxConcurrentRequests, instance.tlsContext, - instance.filterMetadata, instance.outlierDetection); - } else { // logical DNS - clusterState = new LogicalDnsClusterState(instance.cluster, instance.dnsHostName, - instance.lrsServerInfo, instance.maxConcurrentRequests, instance.tlsContext, - instance.filterMetadata); - } - clusterState.start(); - return Status.OK; - } + @Override + public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses); + ClusterResolverConfig config = + (ClusterResolverConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); + XdsConfig xdsConfig = resolvedAddresses.getAttributes().get(XdsAttributes.XDS_CONFIG); - @Override - public void handleNameResolutionError(Status error) { - if (childLb != null) { - childLb.handleNameResolutionError(error); - } else { - helper.updateBalancingState( - TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error))); - } + DiscoveryMechanism instance = config.discoveryMechanism; + String cluster = instance.cluster; + if (clusterState == null) { + clusterState = new ClusterState(); } - @Override - public void shutdown() { - clusterState.shutdown(); - if (childLb != null) { - childLb.shutdown(); - } + StatusOr edsUpdate = getEdsUpdate(xdsConfig, cluster); + StatusOr statusOrResult = + clusterState.edsUpdateToResult(config, instance, edsUpdate); + if (!statusOrResult.hasValue()) { + Status status = Status.UNAVAILABLE + .withDescription(statusOrResult.getStatus().getDescription()) + .withCause(statusOrResult.getStatus().getCause()); + delegate.handleNameResolutionError(status); + return status; } - - private void handleEndpointResourceUpdate() { - List addresses = new ArrayList<>(); - Map priorityChildConfigs = new HashMap<>(); - List priorities = new ArrayList<>(); // totally ordered priority list - - Status endpointNotFound = Status.OK; - // Propagate endpoints to the child LB policy only after all clusters have been resolved. - if (!clusterState.resolved && clusterState.status.isOk()) { - return; - } - if (clusterState.result != null) { - addresses.addAll(clusterState.result.addresses); - priorityChildConfigs.putAll(clusterState.result.priorityChildConfigs); - priorities.addAll(clusterState.result.priorities); - } else { - endpointNotFound = clusterState.status; - } - if (addresses.isEmpty()) { - if (endpointNotFound.isOk()) { - endpointNotFound = Status.UNAVAILABLE.withDescription( - "No usable endpoint from cluster: " + cluster); - } else { - endpointNotFound = - Status.UNAVAILABLE.withCause(endpointNotFound.getCause()) - .withDescription(endpointNotFound.getDescription()); - } - helper.updateBalancingState( - TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(endpointNotFound))); - if (childLb != null) { - childLb.shutdown(); - childLb = null; - } - return; - } - PriorityLbConfig childConfig = - new PriorityLbConfig(Collections.unmodifiableMap(priorityChildConfigs), - Collections.unmodifiableList(priorities)); - if (childLb == null) { - childLb = lbRegistry.getProvider(PRIORITY_POLICY_NAME).newLoadBalancer(helper); - } - childLb.handleResolvedAddresses( - resolvedAddresses.toBuilder() - .setLoadBalancingPolicyConfig(childConfig) - .setAddresses(Collections.unmodifiableList(addresses)) - .build()); + ClusterResolutionResult result = statusOrResult.getValue(); + List addresses = result.addresses; + if (addresses.isEmpty()) { + Status status = Status.UNAVAILABLE + .withDescription("No usable endpoint from cluster: " + cluster); + delegate.handleNameResolutionError(status); + return status; } + PriorityLbConfig childConfig = + new PriorityLbConfig( + Collections.unmodifiableMap(result.priorityChildConfigs), + Collections.unmodifiableList(result.priorities)); + return delegate.acceptResolvedAddresses( + resolvedAddresses.toBuilder() + .setLoadBalancingPolicyConfig(childConfig) + .setAddresses(Collections.unmodifiableList(addresses)) + .build()); + } - private void handleEndpointResolutionError() { - if (!clusterState.status.isOk()) { - if (childLb != null) { - childLb.handleNameResolutionError(clusterState.status); - } else { - helper.updateBalancingState( - TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(clusterState.status))); - } - } + private static StatusOr getEdsUpdate(XdsConfig xdsConfig, String cluster) { + StatusOr clusterConfig = xdsConfig.getClusters().get(cluster); + if (clusterConfig == null) { + return StatusOr.fromStatus(Status.INTERNAL + .withDescription("BUG: cluster resolver could not find cluster in xdsConfig")); } - - /** - * Wires re-resolution requests from downstream LB policies with DNS resolver. - */ - private final class RefreshableHelper extends ForwardingLoadBalancerHelper { - private final Helper delegate; - - private RefreshableHelper(Helper delegate) { - this.delegate = checkNotNull(delegate, "delegate"); - } - - @Override - public void refreshNameResolution() { - if (clusterState instanceof LogicalDnsClusterState) { - ((LogicalDnsClusterState) clusterState).refresh(); - } - } - - @Override - protected Helper delegate() { - return delegate; - } + if (!clusterConfig.hasValue()) { + return StatusOr.fromStatus(clusterConfig.getStatus()); } - - /** - * Resolution state of an underlying cluster. - */ - private abstract class ClusterState { - // Name of the cluster to be resolved. - protected final String name; - @Nullable - protected final ServerInfo lrsServerInfo; - @Nullable - protected final Long maxConcurrentRequests; - @Nullable - protected final UpstreamTlsContext tlsContext; - protected final Map filterMetadata; - @Nullable - protected final OutlierDetection outlierDetection; - // Resolution status, may contain most recent error encountered. - protected Status status = Status.OK; - // True if has received resolution result. - protected boolean resolved; - // Most recently resolved addresses and config, or null if resource not exists. - @Nullable - protected ClusterResolutionResult result; - - protected boolean shutdown; - - private ClusterState(String name, @Nullable ServerInfo lrsServerInfo, - @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext tlsContext, - Map filterMetadata, @Nullable OutlierDetection outlierDetection) { - this.name = name; - this.lrsServerInfo = lrsServerInfo; - this.maxConcurrentRequests = maxConcurrentRequests; - this.tlsContext = tlsContext; - this.filterMetadata = ImmutableMap.copyOf(filterMetadata); - this.outlierDetection = outlierDetection; - } - - abstract void start(); - - void shutdown() { - shutdown = true; - } + if (!(clusterConfig.getValue().getChildren() instanceof XdsClusterConfig.EndpointConfig)) { + return StatusOr.fromStatus(Status.INTERNAL + .withDescription("BUG: cluster resolver cluster with children of unknown type")); } + XdsClusterConfig.EndpointConfig endpointConfig = + (XdsClusterConfig.EndpointConfig) clusterConfig.getValue().getChildren(); + return endpointConfig.getEndpoint(); + } - private final class EdsClusterState extends ClusterState implements ResourceWatcher { - @Nullable - private final String edsServiceName; - private Map localityPriorityNames = Collections.emptyMap(); - int priorityNameGenId = 1; - - private EdsClusterState(String name, @Nullable String edsServiceName, - @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, - @Nullable UpstreamTlsContext tlsContext, Map filterMetadata, - @Nullable OutlierDetection outlierDetection) { - super(name, lrsServerInfo, maxConcurrentRequests, tlsContext, filterMetadata, - outlierDetection); - this.edsServiceName = edsServiceName; - } - - @Override - void start() { - String resourceName = edsServiceName != null ? edsServiceName : name; - logger.log(XdsLogLevel.INFO, "Start watching EDS resource {0}", resourceName); - xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), - resourceName, this, syncContext); - } - - @Override - protected void shutdown() { - super.shutdown(); - String resourceName = edsServiceName != null ? edsServiceName : name; - logger.log(XdsLogLevel.INFO, "Stop watching EDS resource {0}", resourceName); - xdsClient.cancelXdsResourceWatch(XdsEndpointResource.getInstance(), resourceName, this); - } - - @Override - public void onChanged(final EdsUpdate update) { - class EndpointsUpdated implements Runnable { - @Override - public void run() { - if (shutdown) { - return; - } - logger.log(XdsLogLevel.DEBUG, "Received endpoint update {0}", update); - if (logger.isLoggable(XdsLogLevel.INFO)) { - logger.log(XdsLogLevel.INFO, "Cluster {0}: {1} localities, {2} drop categories", - update.clusterName, update.localityLbEndpointsMap.size(), - update.dropPolicies.size()); + private final class ClusterState { + private Map localityPriorityNames = Collections.emptyMap(); + int priorityNameGenId = 1; + + StatusOr edsUpdateToResult( + ClusterResolverConfig config, DiscoveryMechanism discovery, StatusOr updateOr) { + if (!updateOr.hasValue()) { + return StatusOr.fromStatus(updateOr.getStatus()); + } + EdsUpdate update = updateOr.getValue(); + logger.log(XdsLogLevel.DEBUG, "Received endpoint update {0}", update); + if (logger.isLoggable(XdsLogLevel.INFO)) { + logger.log(XdsLogLevel.INFO, "Cluster {0}: {1} localities, {2} drop categories", + discovery.cluster, update.localityLbEndpointsMap.size(), + update.dropPolicies.size()); + } + Map localityLbEndpoints = + update.localityLbEndpointsMap; + List dropOverloads = update.dropPolicies; + List addresses = new ArrayList<>(); + Map> prioritizedLocalityWeights = new HashMap<>(); + List sortedPriorityNames = + generatePriorityNames(discovery.cluster, localityLbEndpoints); + for (Locality locality : localityLbEndpoints.keySet()) { + LocalityLbEndpoints localityLbInfo = localityLbEndpoints.get(locality); + String priorityName = localityPriorityNames.get(locality); + boolean discard = true; + for (LbEndpoint endpoint : localityLbInfo.endpoints()) { + if (endpoint.isHealthy()) { + discard = false; + long weight = localityLbInfo.localityWeight(); + if (endpoint.loadBalancingWeight() != 0) { + weight *= endpoint.loadBalancingWeight(); } - Map localityLbEndpoints = - update.localityLbEndpointsMap; - List dropOverloads = update.dropPolicies; - List addresses = new ArrayList<>(); - Map> prioritizedLocalityWeights = new HashMap<>(); - List sortedPriorityNames = generatePriorityNames(name, localityLbEndpoints); - for (Locality locality : localityLbEndpoints.keySet()) { - LocalityLbEndpoints localityLbInfo = localityLbEndpoints.get(locality); - String priorityName = localityPriorityNames.get(locality); - boolean discard = true; - for (LbEndpoint endpoint : localityLbInfo.endpoints()) { - if (endpoint.isHealthy()) { - discard = false; - long weight = localityLbInfo.localityWeight(); - if (endpoint.loadBalancingWeight() != 0) { - weight *= endpoint.loadBalancingWeight(); - } - String localityName = localityName(locality); - Attributes attr = - endpoint.eag().getAttributes().toBuilder() - .set(XdsAttributes.ATTR_LOCALITY, locality) - .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, localityName) - .set(XdsAttributes.ATTR_LOCALITY_WEIGHT, - localityLbInfo.localityWeight()) - .set(XdsAttributes.ATTR_SERVER_WEIGHT, weight) - .set(XdsAttributes.ATTR_ADDRESS_NAME, endpoint.hostname()) - .build(); - - EquivalentAddressGroup eag; - if (config.isHttp11ProxyAvailable()) { - List rewrittenAddresses = new ArrayList<>(); - for (SocketAddress addr : endpoint.eag().getAddresses()) { - rewrittenAddresses.add(rewriteAddress( - addr, endpoint.endpointMetadata(), localityLbInfo.localityMetadata())); - } - eag = new EquivalentAddressGroup(rewrittenAddresses, attr); - } else { - eag = new EquivalentAddressGroup(endpoint.eag().getAddresses(), attr); - } - eag = AddressFilter.setPathFilter(eag, Arrays.asList(priorityName, localityName)); - addresses.add(eag); - } - } - if (discard) { - logger.log(XdsLogLevel.INFO, - "Discard locality {0} with 0 healthy endpoints", locality); - continue; + String localityName = localityName(locality); + Attributes attr = + endpoint.eag().getAttributes().toBuilder() + .set(XdsAttributes.ATTR_LOCALITY, locality) + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, localityName) + .set(XdsAttributes.ATTR_LOCALITY_WEIGHT, + localityLbInfo.localityWeight()) + .set(XdsAttributes.ATTR_SERVER_WEIGHT, weight) + .set(XdsAttributes.ATTR_ADDRESS_NAME, endpoint.hostname()) + .build(); + EquivalentAddressGroup eag; + if (config.isHttp11ProxyAvailable()) { + List rewrittenAddresses = new ArrayList<>(); + for (SocketAddress addr : endpoint.eag().getAddresses()) { + rewrittenAddresses.add(rewriteAddress( + addr, endpoint.endpointMetadata(), localityLbInfo.localityMetadata())); } - if (!prioritizedLocalityWeights.containsKey(priorityName)) { - prioritizedLocalityWeights.put(priorityName, new HashMap()); - } - prioritizedLocalityWeights.get(priorityName).put( - locality, localityLbInfo.localityWeight()); - } - if (prioritizedLocalityWeights.isEmpty()) { - // Will still update the result, as if the cluster resource is revoked. - logger.log(XdsLogLevel.INFO, - "Cluster {0} has no usable priority/locality/endpoint", update.clusterName); + eag = new EquivalentAddressGroup(rewrittenAddresses, attr); + } else { + eag = new EquivalentAddressGroup(endpoint.eag().getAddresses(), attr); } - sortedPriorityNames.retainAll(prioritizedLocalityWeights.keySet()); - Map priorityChildConfigs = - generateEdsBasedPriorityChildConfigs( - name, edsServiceName, lrsServerInfo, maxConcurrentRequests, tlsContext, - filterMetadata, outlierDetection, endpointLbConfig, lbRegistry, - prioritizedLocalityWeights, dropOverloads); - status = Status.OK; - resolved = true; - result = new ClusterResolutionResult(addresses, priorityChildConfigs, - sortedPriorityNames); - handleEndpointResourceUpdate(); - } - } - - new EndpointsUpdated().run(); - } - - private SocketAddress rewriteAddress(SocketAddress addr, - ImmutableMap endpointMetadata, - ImmutableMap localityMetadata) { - if (!(addr instanceof InetSocketAddress)) { - return addr; - } - - SocketAddress proxyAddress; - try { - proxyAddress = (SocketAddress) endpointMetadata.get( - "envoy.http11_proxy_transport_socket.proxy_address"); - if (proxyAddress == null) { - proxyAddress = (SocketAddress) localityMetadata.get( - "envoy.http11_proxy_transport_socket.proxy_address"); + eag = AddressFilter.setPathFilter(eag, Arrays.asList(priorityName, localityName)); + addresses.add(eag); } - } catch (ClassCastException e) { - return addr; } - - if (proxyAddress == null) { - return addr; + if (discard) { + logger.log(XdsLogLevel.INFO, + "Discard locality {0} with 0 healthy endpoints", locality); + continue; } - - return HttpConnectProxiedSocketAddress.newBuilder() - .setTargetAddress((InetSocketAddress) addr) - .setProxyAddress(proxyAddress) - .build(); - } - - private List generatePriorityNames(String name, - Map localityLbEndpoints) { - TreeMap> todo = new TreeMap<>(); - for (Locality locality : localityLbEndpoints.keySet()) { - int priority = localityLbEndpoints.get(locality).priority(); - if (!todo.containsKey(priority)) { - todo.put(priority, new ArrayList<>()); - } - todo.get(priority).add(locality); + if (!prioritizedLocalityWeights.containsKey(priorityName)) { + prioritizedLocalityWeights.put(priorityName, new HashMap()); } - Map newNames = new HashMap<>(); - Set usedNames = new HashSet<>(); - List ret = new ArrayList<>(); - for (Integer priority: todo.keySet()) { - String foundName = ""; - for (Locality locality : todo.get(priority)) { - if (localityPriorityNames.containsKey(locality) - && usedNames.add(localityPriorityNames.get(locality))) { - foundName = localityPriorityNames.get(locality); - break; - } - } - if ("".equals(foundName)) { - foundName = String.format(Locale.US, "%s[child%d]", name, priorityNameGenId++); - } - for (Locality locality : todo.get(priority)) { - newNames.put(locality, foundName); - } - ret.add(foundName); - } - localityPriorityNames = newNames; - return ret; - } - - @Override - public void onResourceDoesNotExist(final String resourceName) { - if (shutdown) { - return; - } - logger.log(XdsLogLevel.INFO, "Resource {0} unavailable", resourceName); - status = Status.OK; - resolved = true; - result = null; // resource revoked - handleEndpointResourceUpdate(); - } - - @Override - public void onError(final Status error) { - if (shutdown) { - return; - } - String resourceName = edsServiceName != null ? edsServiceName : name; - status = Status.UNAVAILABLE - .withDescription(String.format("Unable to load EDS %s. xDS server returned: %s: %s", - resourceName, error.getCode(), error.getDescription())) - .withCause(error.getCause()); - logger.log(XdsLogLevel.WARNING, "Received EDS error: {0}", error); - handleEndpointResolutionError(); - } + prioritizedLocalityWeights.get(priorityName).put( + locality, localityLbInfo.localityWeight()); + } + if (prioritizedLocalityWeights.isEmpty()) { + // Will still update the result, as if the cluster resource is revoked. + logger.log(XdsLogLevel.INFO, + "Cluster {0} has no usable priority/locality/endpoint", discovery.cluster); + } + sortedPriorityNames.retainAll(prioritizedLocalityWeights.keySet()); + Map priorityChildConfigs = + generatePriorityChildConfigs( + discovery, config.lbConfig, lbRegistry, + prioritizedLocalityWeights, dropOverloads); + return StatusOr.fromValue(new ClusterResolutionResult(addresses, priorityChildConfigs, + sortedPriorityNames)); } - private final class LogicalDnsClusterState extends ClusterState { - private final String dnsHostName; - private final NameResolver.Factory nameResolverFactory; - private final NameResolver.Args nameResolverArgs; - private NameResolver resolver; - @Nullable - private BackoffPolicy backoffPolicy; - @Nullable - private ScheduledHandle scheduledRefresh; - - private LogicalDnsClusterState(String name, String dnsHostName, - @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, - @Nullable UpstreamTlsContext tlsContext, Map filterMetadata) { - super(name, lrsServerInfo, maxConcurrentRequests, tlsContext, filterMetadata, null); - this.dnsHostName = checkNotNull(dnsHostName, "dnsHostName"); - nameResolverFactory = - checkNotNull(helper.getNameResolverRegistry().asFactory(), "nameResolverFactory"); - nameResolverArgs = checkNotNull(helper.getNameResolverArgs(), "nameResolverArgs"); + private SocketAddress rewriteAddress(SocketAddress addr, + ImmutableMap endpointMetadata, + ImmutableMap localityMetadata) { + if (!(addr instanceof InetSocketAddress)) { + return addr; } - @Override - void start() { - URI uri; - try { - uri = new URI("dns", "", "/" + dnsHostName, null); - } catch (URISyntaxException e) { - status = Status.INTERNAL.withDescription( - "Bug, invalid URI creation: " + dnsHostName).withCause(e); - handleEndpointResolutionError(); - return; - } - resolver = nameResolverFactory.newNameResolver(uri, nameResolverArgs); - if (resolver == null) { - status = Status.INTERNAL.withDescription("Xds cluster resolver lb for logical DNS " - + "cluster [" + name + "] cannot find DNS resolver with uri:" + uri); - handleEndpointResolutionError(); - return; - } - resolver.start(new NameResolverListener(dnsHostName)); - } - - void refresh() { - if (resolver == null) { - return; - } - cancelBackoff(); - resolver.refresh(); - } - - @Override - void shutdown() { - super.shutdown(); - if (resolver != null) { - resolver.shutdown(); - } - cancelBackoff(); - } - - private void cancelBackoff() { - if (scheduledRefresh != null) { - scheduledRefresh.cancel(); - scheduledRefresh = null; - backoffPolicy = null; + SocketAddress proxyAddress; + try { + proxyAddress = (SocketAddress) endpointMetadata.get( + "envoy.http11_proxy_transport_socket.proxy_address"); + if (proxyAddress == null) { + proxyAddress = (SocketAddress) localityMetadata.get( + "envoy.http11_proxy_transport_socket.proxy_address"); } + } catch (ClassCastException e) { + return addr; } - private class DelayedNameResolverRefresh implements Runnable { - @Override - public void run() { - scheduledRefresh = null; - if (!shutdown) { - resolver.refresh(); - } - } + if (proxyAddress == null) { + return addr; } - private class NameResolverListener extends NameResolver.Listener2 { - private final String dnsHostName; - - NameResolverListener(String dnsHostName) { - this.dnsHostName = dnsHostName; - } + return HttpConnectProxiedSocketAddress.newBuilder() + .setTargetAddress((InetSocketAddress) addr) + .setProxyAddress(proxyAddress) + .build(); + } - @Override - public void onResult(final ResolutionResult resolutionResult) { - syncContext.execute(() -> onResult2(resolutionResult)); + private List generatePriorityNames(String name, + Map localityLbEndpoints) { + TreeMap> todo = new TreeMap<>(); + for (Locality locality : localityLbEndpoints.keySet()) { + int priority = localityLbEndpoints.get(locality).priority(); + if (!todo.containsKey(priority)) { + todo.put(priority, new ArrayList<>()); } - - @Override - public Status onResult2(final ResolutionResult resolutionResult) { - if (shutdown) { - return Status.OK; - } - // Arbitrary priority notation for all DNS-resolved endpoints. - String priorityName = priorityName(name, 0); // value doesn't matter - List addresses = new ArrayList<>(); - StatusOr> addressesOrError = - resolutionResult.getAddressesOrError(); - if (addressesOrError.hasValue()) { - backoffPolicy = null; // reset backoff sequence if succeeded - for (EquivalentAddressGroup eag : addressesOrError.getValue()) { - // No weight attribute is attached, all endpoint-level LB policy should be able - // to handle such it. - String localityName = localityName(LOGICAL_DNS_CLUSTER_LOCALITY); - Attributes attr = eag.getAttributes().toBuilder() - .set(XdsAttributes.ATTR_LOCALITY, LOGICAL_DNS_CLUSTER_LOCALITY) - .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, localityName) - .set(XdsAttributes.ATTR_ADDRESS_NAME, dnsHostName) - .build(); - eag = new EquivalentAddressGroup(eag.getAddresses(), attr); - eag = AddressFilter.setPathFilter(eag, Arrays.asList(priorityName, localityName)); - addresses.add(eag); - } - PriorityChildConfig priorityChildConfig = generateDnsBasedPriorityChildConfig( - name, lrsServerInfo, maxConcurrentRequests, tlsContext, filterMetadata, - lbRegistry, Collections.emptyList()); - status = Status.OK; - resolved = true; - result = new ClusterResolutionResult(addresses, priorityName, priorityChildConfig); - handleEndpointResourceUpdate(); - return Status.OK; - } else { - handleErrorInSyncContext(addressesOrError.getStatus()); - return addressesOrError.getStatus(); + todo.get(priority).add(locality); + } + Map newNames = new HashMap<>(); + Set usedNames = new HashSet<>(); + List ret = new ArrayList<>(); + for (Integer priority: todo.keySet()) { + String foundName = ""; + for (Locality locality : todo.get(priority)) { + if (localityPriorityNames.containsKey(locality) + && usedNames.add(localityPriorityNames.get(locality))) { + foundName = localityPriorityNames.get(locality); + break; } } - - @Override - public void onError(final Status error) { - syncContext.execute(() -> handleErrorInSyncContext(error)); + if ("".equals(foundName)) { + foundName = priorityName(name, priorityNameGenId++); } - - private void handleErrorInSyncContext(final Status error) { - if (shutdown) { - return; - } - status = error; - // NameResolver.Listener API cannot distinguish between address-not-found and - // transient errors. If the error occurs in the first resolution, treat it as - // address not found. Otherwise, either there is previously resolved addresses - // previously encountered error, propagate the error to downstream/upstream and - // let downstream/upstream handle it. - if (!resolved) { - resolved = true; - handleEndpointResourceUpdate(); - } else { - handleEndpointResolutionError(); - } - if (scheduledRefresh != null && scheduledRefresh.isPending()) { - return; - } - if (backoffPolicy == null) { - backoffPolicy = backoffPolicyProvider.get(); - } - long delayNanos = backoffPolicy.nextBackoffNanos(); - logger.log(XdsLogLevel.DEBUG, - "Logical DNS resolver for cluster {0} encountered name resolution " - + "error: {1}, scheduling DNS resolution backoff for {2} ns", - name, error, delayNanos); - scheduledRefresh = - syncContext.schedule( - new DelayedNameResolverRefresh(), delayNanos, TimeUnit.NANOSECONDS, - timeService); + for (Locality locality : todo.get(priority)) { + newNames.put(locality, foundName); } + ret.add(foundName); } + localityPriorityNames = newNames; + return ret; } } @@ -729,12 +308,6 @@ private static class ClusterResolutionResult { // List of priority names ordered in descending priorities. private final List priorities; - ClusterResolutionResult(List addresses, String priority, - PriorityChildConfig config) { - this(addresses, Collections.singletonMap(priority, config), - Collections.singletonList(priority)); - } - ClusterResolutionResult(List addresses, Map configs, List priorities) { this.addresses = addresses; @@ -744,46 +317,24 @@ private static class ClusterResolutionResult { } /** - * Generates the config to be used in the priority LB policy for the single priority of - * logical DNS cluster. - * - *

    priority LB -> cluster_impl LB (single hardcoded priority) -> pick_first - */ - private static PriorityChildConfig generateDnsBasedPriorityChildConfig( - String cluster, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, - @Nullable UpstreamTlsContext tlsContext, Map filterMetadata, - LoadBalancerRegistry lbRegistry, List dropOverloads) { - // Override endpoint-level LB policy with pick_first for logical DNS cluster. - Object endpointLbConfig = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - lbRegistry.getProvider("pick_first"), null); - ClusterImplConfig clusterImplConfig = - new ClusterImplConfig(cluster, null, lrsServerInfo, maxConcurrentRequests, - dropOverloads, endpointLbConfig, tlsContext, filterMetadata); - LoadBalancerProvider clusterImplLbProvider = - lbRegistry.getProvider(XdsLbPolicies.CLUSTER_IMPL_POLICY_NAME); - Object clusterImplPolicy = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - clusterImplLbProvider, clusterImplConfig); - return new PriorityChildConfig(clusterImplPolicy, false /* ignoreReresolution*/); - } - - /** - * Generates configs to be used in the priority LB policy for priorities in an EDS cluster. + * Generates configs to be used in the priority LB policy for priorities in a cluster. * *

    priority LB -> cluster_impl LB (one per priority) -> (weighted_target LB * -> round_robin / least_request_experimental (one per locality)) / ring_hash_experimental */ - private static Map generateEdsBasedPriorityChildConfigs( - String cluster, @Nullable String edsServiceName, @Nullable ServerInfo lrsServerInfo, - @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext tlsContext, - Map filterMetadata, - @Nullable OutlierDetection outlierDetection, Object endpointLbConfig, - LoadBalancerRegistry lbRegistry, Map> prioritizedLocalityWeights, List dropOverloads) { + private static Map generatePriorityChildConfigs( + DiscoveryMechanism discovery, + Object endpointLbConfig, + LoadBalancerRegistry lbRegistry, + Map> prioritizedLocalityWeights, + List dropOverloads) { Map configs = new HashMap<>(); for (String priority : prioritizedLocalityWeights.keySet()) { ClusterImplConfig clusterImplConfig = - new ClusterImplConfig(cluster, edsServiceName, lrsServerInfo, maxConcurrentRequests, - dropOverloads, endpointLbConfig, tlsContext, filterMetadata); + new ClusterImplConfig( + discovery.cluster, discovery.edsServiceName, discovery.lrsServerInfo, + discovery.maxConcurrentRequests, dropOverloads, endpointLbConfig, + discovery.tlsContext, discovery.filterMetadata); LoadBalancerProvider clusterImplLbProvider = lbRegistry.getProvider(XdsLbPolicies.CLUSTER_IMPL_POLICY_NAME); Object priorityChildPolicy = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( @@ -791,16 +342,17 @@ private static Map generateEdsBasedPriorityChildCon // If outlier detection has been configured we wrap the child policy in the outlier detection // load balancer. - if (outlierDetection != null) { + if (discovery.outlierDetection != null) { LoadBalancerProvider outlierDetectionProvider = lbRegistry.getProvider( "outlier_detection_experimental"); priorityChildPolicy = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( outlierDetectionProvider, - buildOutlierDetectionLbConfig(outlierDetection, priorityChildPolicy)); + buildOutlierDetectionLbConfig(discovery.outlierDetection, priorityChildPolicy)); } + boolean isEds = discovery.type == DiscoveryMechanism.Type.EDS; PriorityChildConfig priorityChildConfig = - new PriorityChildConfig(priorityChildPolicy, true /* ignoreReresolution */); + new PriorityChildConfig(priorityChildPolicy, isEds /* ignoreReresolution */); configs.put(priority, priorityChildConfig); } return configs; diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java index 48101cd9c54..8cff272fcba 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java @@ -25,6 +25,7 @@ import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancerProvider; +import io.grpc.LoadBalancerRegistry; import io.grpc.NameResolver.ConfigOrError; import io.grpc.Status; import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; @@ -41,6 +42,15 @@ */ @Internal public final class ClusterResolverLoadBalancerProvider extends LoadBalancerProvider { + private final LoadBalancerRegistry lbRegistry; + + public ClusterResolverLoadBalancerProvider() { + this.lbRegistry = null; + } + + ClusterResolverLoadBalancerProvider(LoadBalancerRegistry lbRegistry) { + this.lbRegistry = checkNotNull(lbRegistry, "lbRegistry"); + } @Override public boolean isAvailable() { @@ -65,7 +75,11 @@ public ConfigOrError parseLoadBalancingPolicyConfig(Map rawLoadBalanc @Override public LoadBalancer newLoadBalancer(Helper helper) { - return new ClusterResolverLoadBalancer(helper); + LoadBalancerRegistry lbRegistry = this.lbRegistry; + if (lbRegistry == null) { + lbRegistry = LoadBalancerRegistry.getDefaultRegistry(); + } + return new ClusterResolverLoadBalancer(helper, lbRegistry); } static final class ClusterResolverConfig { diff --git a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java index fd2a1d2a069..9c2ee641423 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java @@ -328,7 +328,7 @@ static OutlierDetection fromEnvoyOutlierDetection( Integer minimumHosts = envoyOutlierDetection.hasSuccessRateMinimumHosts() ? envoyOutlierDetection.getSuccessRateMinimumHosts().getValue() : null; Integer requestVolume = envoyOutlierDetection.hasSuccessRateRequestVolume() - ? envoyOutlierDetection.getSuccessRateMinimumHosts().getValue() : null; + ? envoyOutlierDetection.getSuccessRateRequestVolume().getValue() : null; successRateEjection = SuccessRateEjection.create(stdevFactor, enforcementPercentage, minimumHosts, requestVolume); diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index 5ed3821b7e4..21b0ad7dc66 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -41,6 +41,7 @@ import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsClient.ResourceWatcher; import io.grpc.xds.client.XdsResourceType; +import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -83,7 +84,7 @@ private enum TrackedWatcherTypeEnum { private static final int MAX_CLUSTER_RECURSION_DEPTH = 16; // Specified by gRFC A37 - static boolean enableLogicalDns = false; + static boolean enableLogicalDns = true; private final String listenerName; private final XdsClient xdsClient; @@ -394,10 +395,13 @@ private static StatusOr dnsToEdsUpdate( return StatusOr.fromStatus(dnsData.getStatus()); } - List endpoints = new ArrayList<>(); + List addresses = new ArrayList<>(); for (EquivalentAddressGroup eag : dnsData.getValue()) { - endpoints.add(Endpoints.LbEndpoint.create(eag, 1, true, dnsHostName, ImmutableMap.of())); + addresses.addAll(eag.getAddresses()); } + EquivalentAddressGroup eag = new EquivalentAddressGroup(addresses); + List endpoints = ImmutableList.of( + Endpoints.LbEndpoint.create(eag, 1, true, dnsHostName, ImmutableMap.of())); LocalityLbEndpoints lbEndpoints = LocalityLbEndpoints.create(endpoints, 1, 0, ImmutableMap.of()); return StatusOr.fromValue(new XdsEndpointResource.EdsUpdate( diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 58d1ff769fe..55059dc4a5a 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -234,6 +234,13 @@ public void start(Listener2 listener) { resolveState.start(); } + @Override + public void refresh() { + if (resolveState != null) { + resolveState.refresh(); + } + } + private static String expandPercentS(String template, String replacement) { return template.replace("%s", replacement); } @@ -323,7 +330,10 @@ private void updateResolutionResult(XdsConfig xdsConfig) { .setAttributes(attrs) .setServiceConfig(parsedServiceConfig) .build(); - listener.onResult2(result); + if (!listener.onResult2(result).isOk()) { + // TODO: check if this is right + resolveState.xdsDependencyManager.requestReresolution(); + } } /** @@ -662,6 +672,10 @@ void start() { xdsDependencyManager.start(this); } + void refresh() { + xdsDependencyManager.requestReresolution(); + } + private void shutdown() { if (stopped) { return; diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index 982677d24da..be68018792b 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -21,18 +21,41 @@ import static io.grpc.xds.XdsLbPolicies.PRIORITY_POLICY_NAME; import static io.grpc.xds.XdsLbPolicies.WEIGHTED_TARGET_POLICY_NAME; import static io.grpc.xds.XdsLbPolicies.WRR_LOCALITY_POLICY_NAME; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_EDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_LDS; +import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_RDS; import static java.util.stream.Collectors.toList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.testing.EqualsTester; +import com.google.protobuf.Any; +import com.google.protobuf.Duration; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import io.envoyproxy.envoy.config.cluster.v3.Cluster; +import io.envoyproxy.envoy.config.cluster.v3.OutlierDetection; +import io.envoyproxy.envoy.config.core.v3.Address; +import io.envoyproxy.envoy.config.core.v3.AggregatedConfigSource; +import io.envoyproxy.envoy.config.core.v3.ConfigSource; +import io.envoyproxy.envoy.config.core.v3.HealthStatus; +import io.envoyproxy.envoy.config.core.v3.Locality; +import io.envoyproxy.envoy.config.core.v3.Metadata; +import io.envoyproxy.envoy.config.core.v3.SocketAddress; +import io.envoyproxy.envoy.config.core.v3.TransportSocket; +import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; +import io.envoyproxy.envoy.config.endpoint.v3.Endpoint; +import io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint; +import io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints; +import io.envoyproxy.envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport; import io.grpc.Attributes; import io.grpc.ChannelLogger; import io.grpc.ConnectivityState; @@ -53,25 +76,23 @@ import io.grpc.NameResolverProvider; import io.grpc.NameResolverRegistry; import io.grpc.Status; -import io.grpc.Status.Code; import io.grpc.StatusOr; import io.grpc.SynchronizationContext; -import io.grpc.internal.BackoffPolicy; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.internal.FakeClock; -import io.grpc.internal.FakeClock.ScheduledTask; import io.grpc.internal.GrpcUtil; -import io.grpc.internal.ObjectPool; +import io.grpc.testing.GrpcCleanupRule; import io.grpc.util.GracefulSwitchLoadBalancer; import io.grpc.util.GracefulSwitchLoadBalancerAccessor; +import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig; import io.grpc.util.OutlierDetectionLoadBalancerProvider; +import io.grpc.xds.CdsLoadBalancerProvider.CdsConfig; import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; import io.grpc.xds.Endpoints.DropOverload; -import io.grpc.xds.Endpoints.LbEndpoint; -import io.grpc.xds.Endpoints.LocalityLbEndpoints; import io.grpc.xds.EnvoyServerProtoData.FailurePercentageEjection; -import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; @@ -79,24 +100,16 @@ import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; import io.grpc.xds.WrrLocalityLoadBalancer.WrrLocalityConfig; -import io.grpc.xds.XdsEndpointResource.EdsUpdate; import io.grpc.xds.client.Bootstrapper.ServerInfo; -import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsClient; -import io.grpc.xds.client.XdsResourceType; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import java.net.InetSocketAddress; -import java.net.SocketAddress; import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; +import java.util.Iterator; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import org.junit.After; @@ -107,9 +120,7 @@ import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; import org.mockito.Captor; -import org.mockito.InOrder; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -117,39 +128,43 @@ @RunWith(JUnit4.class) public class ClusterResolverLoadBalancerTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + @Rule + public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule(); - private static final String AUTHORITY = "api.google.com"; - private static final String CLUSTER1 = "cluster-foo.googleapis.com"; - private static final String CLUSTER2 = "cluster-bar.googleapis.com"; - private static final String CLUSTER_DNS = "cluster-dns.googleapis.com"; - private static final String EDS_SERVICE_NAME1 = "backend-service-foo.googleapis.com"; - private static final String EDS_SERVICE_NAME2 = "backend-service-bar.googleapis.com"; + private static final String SERVER_NAME = "example.com"; + private static final String CLUSTER = "cluster-foo.googleapis.com"; + private static final String EDS_SERVICE_NAME = "backend-service-foo.googleapis.com"; private static final String DNS_HOST_NAME = "dns-service.googleapis.com"; - private static final ServerInfo LRS_SERVER_INFO = - ServerInfo.create("lrs.googleapis.com", InsecureChannelCredentials.create()); - private final Locality locality1 = - Locality.create("test-region-1", "test-zone-1", "test-subzone-1"); - private final Locality locality2 = - Locality.create("test-region-2", "test-zone-2", "test-subzone-2"); - private final Locality locality3 = - Locality.create("test-region-3", "test-zone-3", "test-subzone-3"); - private final UpstreamTlsContext tlsContext = - CommonTlsContextTestsUtil.buildUpstreamTlsContext("google_cloud_private_spiffe", true); - private final OutlierDetection outlierDetection = OutlierDetection.create( - 100L, 100L, 100L, 100, SuccessRateEjection.create(100, 100, 100, 100), - FailurePercentageEjection.create(100, 100, 100, 100)); - private final DiscoveryMechanism edsDiscoveryMechanism1 = - DiscoveryMechanism.forEds(CLUSTER1, EDS_SERVICE_NAME1, LRS_SERVER_INFO, 100L, tlsContext, - Collections.emptyMap(), null); - private final DiscoveryMechanism edsDiscoveryMechanism2 = - DiscoveryMechanism.forEds(CLUSTER2, EDS_SERVICE_NAME2, LRS_SERVER_INFO, 200L, tlsContext, - Collections.emptyMap(), null); - private final DiscoveryMechanism edsDiscoveryMechanismWithOutlierDetection = - DiscoveryMechanism.forEds(CLUSTER1, EDS_SERVICE_NAME1, LRS_SERVER_INFO, 100L, tlsContext, - Collections.emptyMap(), outlierDetection); - private final DiscoveryMechanism logicalDnsDiscoveryMechanism = - DiscoveryMechanism.forLogicalDns(CLUSTER_DNS, DNS_HOST_NAME, LRS_SERVER_INFO, 300L, null, - Collections.emptyMap()); + private static final Cluster EDS_CLUSTER = Cluster.newBuilder() + .setName(CLUSTER) + .setType(Cluster.DiscoveryType.EDS) + .setEdsClusterConfig(Cluster.EdsClusterConfig.newBuilder() + .setServiceName(EDS_SERVICE_NAME) + .setEdsConfig(ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.newBuilder()))) + .build(); + private static final Cluster LOGICAL_DNS_CLUSTER = Cluster.newBuilder() + .setName(CLUSTER) + .setType(Cluster.DiscoveryType.LOGICAL_DNS) + .setLoadAssignment(ClusterLoadAssignment.newBuilder() + .addEndpoints(LocalityLbEndpoints.newBuilder() + .addLbEndpoints(newSocketLbEndpoint(DNS_HOST_NAME, 9000)))) + .build(); + private static final Locality LOCALITY1 = Locality.newBuilder() + .setRegion("test-region-1") + .setZone("test-zone-1") + .setSubZone("test-subzone-1") + .build(); + private static final Locality LOCALITY2 = Locality.newBuilder() + .setRegion("test-region-2") + .setZone("test-zone-2") + .setSubZone("test-subzone-2") + .build(); + private static final Locality LOCALITY3 = Locality.newBuilder() + .setRegion("test-region-3") + .setZone("test-zone-3") + .setSubZone("test-subzone-3") + .build(); private final SynchronizationContext syncContext = new SynchronizationContext( new Thread.UncaughtExceptionHandler() { @@ -161,50 +176,32 @@ public void uncaughtException(Thread t, Throwable e) { private final FakeClock fakeClock = new FakeClock(); private final LoadBalancerRegistry lbRegistry = new LoadBalancerRegistry(); private final NameResolverRegistry nsRegistry = new NameResolverRegistry(); - private final Object roundRobin = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - new FakeLoadBalancerProvider("wrr_locality_experimental"), new WrrLocalityConfig( - GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - new FakeLoadBalancerProvider("round_robin"), null))); - private final Object ringHash = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - new FakeLoadBalancerProvider("ring_hash_experimental"), new RingHashConfig(10L, 100L, "")); - private final Object leastRequest = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - new FakeLoadBalancerProvider("wrr_locality_experimental"), new WrrLocalityConfig( - GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - new FakeLoadBalancerProvider("least_request_experimental"), - new LeastRequestConfig(3)))); private final List childBalancers = new ArrayList<>(); private final List resolvers = new ArrayList<>(); - private final FakeXdsClient xdsClient = new FakeXdsClient(); - private final ObjectPool xdsClientPool = new ObjectPool() { - @Override - public XdsClient getObject() { - xdsClientRefs++; - return xdsClient; - } - - @Override - public XdsClient returnObject(Object object) { - xdsClientRefs--; - return null; - } - }; - + private final XdsTestControlPlaneService controlPlaneService = new XdsTestControlPlaneService(); + private final XdsClient xdsClient = XdsTestUtils.createXdsClient( + Arrays.asList("control-plane.example.com"), + serverInfo -> new GrpcXdsTransportFactory.GrpcXdsTransport( + InProcessChannelBuilder + .forName(serverInfo.target()) + .directExecutor() + .build()), + fakeClock); + + + private XdsDependencyManager xdsDepManager; @Mock private Helper helper; - @Mock - private BackoffPolicy.Provider backoffPolicyProvider; - @Mock - private BackoffPolicy backoffPolicy1; - @Mock - private BackoffPolicy backoffPolicy2; @Captor private ArgumentCaptor pickerCaptor; - private int xdsClientRefs; - private ClusterResolverLoadBalancer loadBalancer; - private NameResolverProvider fakeNameResolverProvider; + private CdsLoadBalancer2 loadBalancer; + private boolean originalIsEnabledXdsHttpConnect; @Before - public void setUp() throws URISyntaxException { + public void setUp() throws Exception { + lbRegistry.register(new ClusterResolverLoadBalancerProvider(lbRegistry)); + lbRegistry.register(new RingHashLoadBalancerProvider()); + lbRegistry.register(new WrrLocalityLoadBalancerProvider()); lbRegistry.register(new FakeLoadBalancerProvider(PRIORITY_POLICY_NAME)); lbRegistry.register(new FakeLoadBalancerProvider(CLUSTER_IMPL_POLICY_NAME)); lbRegistry.register(new FakeLoadBalancerProvider(WEIGHTED_TARGET_POLICY_NAME)); @@ -217,81 +214,112 @@ public void setUp() throws URISyntaxException { .setSynchronizationContext(syncContext) .setServiceConfigParser(mock(ServiceConfigParser.class)) .setChannelLogger(mock(ChannelLogger.class)) + .setScheduledExecutorService(fakeClock.getScheduledExecutorService()) + .setNameResolverRegistry(nsRegistry) .build(); - fakeNameResolverProvider = new FakeNameResolverProvider(false); - nsRegistry.register(fakeNameResolverProvider); - when(helper.getNameResolverRegistry()).thenReturn(nsRegistry); - when(helper.getNameResolverArgs()).thenReturn(args); - when(helper.getSynchronizationContext()).thenReturn(syncContext); - when(helper.getScheduledExecutorService()).thenReturn(fakeClock.getScheduledExecutorService()); - when(helper.getAuthority()).thenReturn(AUTHORITY); - when(backoffPolicyProvider.get()).thenReturn(backoffPolicy1, backoffPolicy2); - when(backoffPolicy1.nextBackoffNanos()) - .thenReturn(TimeUnit.SECONDS.toNanos(1L), TimeUnit.SECONDS.toNanos(10L)); - when(backoffPolicy2.nextBackoffNanos()) - .thenReturn(TimeUnit.SECONDS.toNanos(5L), TimeUnit.SECONDS.toNanos(50L)); - loadBalancer = new ClusterResolverLoadBalancer(helper, lbRegistry, backoffPolicyProvider); + + xdsDepManager = new XdsDependencyManager( + xdsClient, + syncContext, + SERVER_NAME, + SERVER_NAME, + args); + + cleanupRule.register(InProcessServerBuilder + .forName("control-plane.example.com") + .addService(controlPlaneService) + .directExecutor() + .build() + .start()); + + controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, ImmutableMap.of( + SERVER_NAME, ControlPlaneRule.buildClientListener(SERVER_NAME, "my-route"))); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_RDS, ImmutableMap.of( + "my-route", XdsTestUtils.buildRouteConfiguration(SERVER_NAME, "my-route", CLUSTER))); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + CLUSTER, EDS_CLUSTER)); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, ControlPlaneRule.buildClusterLoadAssignment( + "127.0.0.1", "", 8080, EDS_SERVICE_NAME))); + + nsRegistry.register(new FakeNameResolverProvider()); + when(helper.getAuthority()).thenReturn("api.google.com"); + doAnswer((inv) -> { + xdsDepManager.requestReresolution(); + return null; + }).when(helper).refreshNameResolution(); + loadBalancer = new CdsLoadBalancer2(helper, lbRegistry); + + originalIsEnabledXdsHttpConnect = XdsClusterResource.isEnabledXdsHttpConnect; } @After - public void tearDown() { + public void tearDown() throws Exception { + XdsClusterResource.isEnabledXdsHttpConnect = originalIsEnabledXdsHttpConnect; loadBalancer.shutdown(); + if (xdsDepManager != null) { + xdsDepManager.shutdown(); + } + assertThat(xdsClient.getSubscribedResourcesMetadataSnapshot().get()).isEmpty(); + xdsClient.shutdown(); + assertThat(childBalancers).isEmpty(); assertThat(resolvers).isEmpty(); - assertThat(xdsClient.watchers).isEmpty(); - assertThat(xdsClientRefs).isEqualTo(0); assertThat(fakeClock.getPendingTasks()).isEmpty(); } @Test - public void edsClustersWithRingHashEndpointLbPolicy() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism1, ringHash, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - assertThat(childBalancers).isEmpty(); - + public void edsClustersWithRingHashEndpointLbPolicy() throws Exception { + Cluster cluster = EDS_CLUSTER.toBuilder() + .setLbPolicy(Cluster.LbPolicy.RING_HASH) + .setRingHashLbConfig(Cluster.RingHashLbConfig.newBuilder() + .setMinimumRingSize(UInt64Value.of(10)) + .setMaximumRingSize(UInt64Value.of(100)) + .build()) + .build(); // One priority with two localities of different weights. - EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); - EquivalentAddressGroup endpoint3 = makeAddress("endpoint-addr-3"); - LocalityLbEndpoints localityLbEndpoints1 = - LocalityLbEndpoints.create( - Arrays.asList( - LbEndpoint.create(endpoint1, 0 /* loadBalancingWeight */, - true, "hostname1", ImmutableMap.of()), - LbEndpoint.create(endpoint2, 0 /* loadBalancingWeight */, - true, "hostname2", ImmutableMap.of())), - 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - LocalityLbEndpoints localityLbEndpoints2 = - LocalityLbEndpoints.create( - Collections.singletonList( - LbEndpoint.create( - endpoint3, 60 /* loadBalancingWeight */, true, - "hostname3", ImmutableMap.of())), - 50 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, - ImmutableMap.of(locality1, localityLbEndpoints1, locality2, localityLbEndpoints2)); + ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(10)) + .setLocality(LOCALITY1) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.1", 8080)) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.2", 8080))) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(50)) + .setLocality(LOCALITY2) + .addLbEndpoints(newSocketLbEndpoint("127.0.1.1", 8080) + .setLoadBalancingWeight(UInt32Value.of(60)))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + CLUSTER, cluster)); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, clusterLoadAssignment)); + startXdsDepManager(); + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.addresses).hasSize(3); EquivalentAddressGroup addr1 = childBalancer.addresses.get(0); EquivalentAddressGroup addr2 = childBalancer.addresses.get(1); EquivalentAddressGroup addr3 = childBalancer.addresses.get(2); - // Endpoints in locality1 have no endpoint-level weight specified, so all endpoints within - // locality1 are equally weighted. - assertThat(addr1.getAddresses()).isEqualTo(endpoint1.getAddresses()); + // Endpoints in LOCALITY1 have no endpoint-level weight specified, so all endpoints within + // LOCALITY1 are equally weighted. + assertThat(addr1.getAddresses()) + .isEqualTo(Arrays.asList(newInetSocketAddress("127.0.0.1", 8080))); assertThat(addr1.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT)) .isEqualTo(10); - assertThat(addr2.getAddresses()).isEqualTo(endpoint2.getAddresses()); + assertThat(addr2.getAddresses()) + .isEqualTo(Arrays.asList(newInetSocketAddress("127.0.0.2", 8080))); assertThat(addr2.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT)) .isEqualTo(10); - assertThat(addr3.getAddresses()).isEqualTo(endpoint3.getAddresses()); + assertThat(addr3.getAddresses()) + .isEqualTo(Arrays.asList(newInetSocketAddress("127.0.1.1", 8080))); assertThat(addr3.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT)) .isEqualTo(50 * 60); assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); PriorityLbConfig priorityLbConfig = (PriorityLbConfig) childBalancer.config; - assertThat(priorityLbConfig.priorities).containsExactly(CLUSTER1 + "[child1]"); + assertThat(priorityLbConfig.priorities).containsExactly(CLUSTER + "[child1]"); PriorityChildConfig priorityChildConfig = Iterables.getOnlyElement(priorityLbConfig.childConfigs.values()); assertThat(priorityChildConfig.ignoreReresolution).isTrue(); @@ -300,8 +328,8 @@ public void edsClustersWithRingHashEndpointLbPolicy() { .isEqualTo(CLUSTER_IMPL_POLICY_NAME); ClusterImplConfig clusterImplConfig = (ClusterImplConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig.childConfig); - assertClusterImplConfig(clusterImplConfig, CLUSTER1, EDS_SERVICE_NAME1, LRS_SERVER_INFO, 100L, - tlsContext, Collections.emptyList(), "ring_hash_experimental"); + assertClusterImplConfig(clusterImplConfig, CLUSTER, EDS_SERVICE_NAME, null, null, + null, Collections.emptyList(), "ring_hash_experimental"); RingHashConfig ringHashConfig = (RingHashConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(clusterImplConfig.childConfig); assertThat(ringHashConfig.minRingSize).isEqualTo(10L); @@ -310,31 +338,33 @@ public void edsClustersWithRingHashEndpointLbPolicy() { @Test public void edsClustersWithLeastRequestEndpointLbPolicy() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism1, leastRequest, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - assertThat(childBalancers).isEmpty(); - + Cluster cluster = EDS_CLUSTER.toBuilder() + .setLbPolicy(Cluster.LbPolicy.LEAST_REQUEST) + .build(); // Simple case with one priority and one locality - EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); - LocalityLbEndpoints localityLbEndpoints = - LocalityLbEndpoints.create( - Arrays.asList( - LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, - "hostname1", ImmutableMap.of())), - 100 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, - ImmutableMap.of(locality1, localityLbEndpoints)); + ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(100)) + .setLocality(LOCALITY1) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.1", 8080))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + CLUSTER, cluster)); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, clusterLoadAssignment)); + startXdsDepManager(); + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.addresses).hasSize(1); EquivalentAddressGroup addr = childBalancer.addresses.get(0); - assertThat(addr.getAddresses()).isEqualTo(endpoint.getAddresses()); + assertThat(addr.getAddresses()) + .isEqualTo(Arrays.asList(newInetSocketAddress("127.0.0.1", 8080))); assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); PriorityLbConfig priorityLbConfig = (PriorityLbConfig) childBalancer.config; - assertThat(priorityLbConfig.priorities).containsExactly(CLUSTER1 + "[child1]"); + assertThat(priorityLbConfig.priorities).containsExactly(CLUSTER + "[child1]"); PriorityChildConfig priorityChildConfig = Iterables.getOnlyElement(priorityLbConfig.childConfigs.values()); assertThat(GracefulSwitchLoadBalancerAccessor.getChildProvider(priorityChildConfig.childConfig) @@ -342,8 +372,8 @@ public void edsClustersWithLeastRequestEndpointLbPolicy() { .isEqualTo(CLUSTER_IMPL_POLICY_NAME); ClusterImplConfig clusterImplConfig = (ClusterImplConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig.childConfig); - assertClusterImplConfig(clusterImplConfig, CLUSTER1, EDS_SERVICE_NAME1, LRS_SERVER_INFO, 100L, - tlsContext, Collections.emptyList(), WRR_LOCALITY_POLICY_NAME); + assertClusterImplConfig(clusterImplConfig, CLUSTER, EDS_SERVICE_NAME, null, null, + null, Collections.emptyList(), WRR_LOCALITY_POLICY_NAME); WrrLocalityConfig wrrLocalityConfig = (WrrLocalityConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(clusterImplConfig.childConfig); LoadBalancerProvider childProvider = @@ -357,23 +387,22 @@ public void edsClustersWithLeastRequestEndpointLbPolicy() { @Test public void edsClustersEndpointHostname_addedToAddressAttribute() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanismWithOutlierDetection, leastRequest, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - assertThat(childBalancers).isEmpty(); - // Simple case with one priority and one locality - EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); - LocalityLbEndpoints localityLbEndpoints = - LocalityLbEndpoints.create( - Arrays.asList( - LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, - "hostname1", ImmutableMap.of())), - 100 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, - ImmutableMap.of(locality1, localityLbEndpoints)); + ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(100)) + .setLocality(LOCALITY1) + .addLbEndpoints(LbEndpoint.newBuilder() + .setEndpoint(Endpoint.newBuilder() + .setHostname("hostname1") + .setAddress(newAddress("127.0.0.1", 8000))))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, clusterLoadAssignment)); + startXdsDepManager(); + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); @@ -384,85 +413,87 @@ public void edsClustersEndpointHostname_addedToAddressAttribute() { @Test public void endpointAddressRewritten_whenProxyMetadataIsInEndpointMetadata() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanismWithOutlierDetection, leastRequest, true); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - assertThat(childBalancers).isEmpty(); - - EquivalentAddressGroup endpoint = - new EquivalentAddressGroup(InetSocketAddress.createUnresolved("127.0.0.1", 8080)); - - // Proxy address in endpointMetadata (use FakeSocketAddress directly) - SocketAddress proxyAddress = new FakeSocketAddress("127.0.0.2"); - ImmutableMap endpointMetadata = - ImmutableMap.of("envoy.http11_proxy_transport_socket.proxy_address", proxyAddress); - - // No proxy in locality metadata - ImmutableMap localityMetadata = ImmutableMap.of(); - - LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( - Arrays.asList( - LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, - "hostname1", endpointMetadata)), - 100 /* localityWeight */, 1 /* priority */, localityMetadata); - - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, - ImmutableMap.of(locality1, localityLbEndpoints)); + XdsClusterResource.isEnabledXdsHttpConnect = true; + Cluster cluster = EDS_CLUSTER.toBuilder() + .setTransportSocket(TransportSocket.newBuilder() + .setName( + "type.googleapis.com/" + Http11ProxyUpstreamTransport.getDescriptor().getFullName()) + .setTypedConfig(Any.pack(Http11ProxyUpstreamTransport.getDefaultInstance()))) + .build(); + // Proxy address in endpointMetadata, and no proxy in locality metadata + ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(100)) + .setLocality(LOCALITY1) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.1", 8080) + .setMetadata(Metadata.newBuilder() + .putTypedFilterMetadata( + "envoy.http11_proxy_transport_socket.proxy_address", + Any.pack(newAddress("127.0.0.2", 8081).build())))) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.3", 8082))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + CLUSTER, cluster)); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, clusterLoadAssignment)); + startXdsDepManager(); + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); // Get the rewritten address - SocketAddress rewrittenAddress = + java.net.SocketAddress rewrittenAddress = childBalancer.addresses.get(0).getAddresses().get(0); assertThat(rewrittenAddress).isInstanceOf(HttpConnectProxiedSocketAddress.class); HttpConnectProxiedSocketAddress proxiedSocket = (HttpConnectProxiedSocketAddress) rewrittenAddress; // Assert that the target address is the original address - assertThat(proxiedSocket.getTargetAddress()) - .isEqualTo(endpoint.getAddresses().get(0)); + assertThat(proxiedSocket.getTargetAddress()).isEqualTo(newInetSocketAddress("127.0.0.1", 8080)); // Assert that the proxy address is correctly set - assertThat(proxiedSocket.getProxyAddress()).isEqualTo(proxyAddress); + assertThat(proxiedSocket.getProxyAddress()).isEqualTo(newInetSocketAddress("127.0.0.2", 8081)); + + // Check the non-rewritten address + java.net.SocketAddress normalAddress = childBalancer.addresses.get(1).getAddresses().get(0); + assertThat(normalAddress).isEqualTo(newInetSocketAddress("127.0.0.3", 8082)); } @Test public void endpointAddressRewritten_whenProxyMetadataIsInLocalityMetadata() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanismWithOutlierDetection, leastRequest, true); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - assertThat(childBalancers).isEmpty(); - - EquivalentAddressGroup endpoint = - new EquivalentAddressGroup(InetSocketAddress.createUnresolved("127.0.0.2", 8080)); - - // No proxy in endpointMetadata - ImmutableMap endpointMetadata = ImmutableMap.of(); - - // Proxy address is now in localityMetadata - SocketAddress proxyAddress = new FakeSocketAddress("proxy-addr"); - ImmutableMap localityMetadata = - ImmutableMap.of("envoy.http11_proxy_transport_socket.proxy_address", proxyAddress); - - LocalityLbEndpoints localityLbEndpoints = LocalityLbEndpoints.create( - Arrays.asList( - LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true, - "hostname2", endpointMetadata)), - 100 /* localityWeight */, 1 /* priority */, localityMetadata); - - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, - ImmutableMap.of(locality1, localityLbEndpoints)); + XdsClusterResource.isEnabledXdsHttpConnect = true; + Cluster cluster = EDS_CLUSTER.toBuilder() + .setTransportSocket(TransportSocket.newBuilder() + .setName( + "type.googleapis.com/" + Http11ProxyUpstreamTransport.getDescriptor().getFullName()) + .setTypedConfig(Any.pack(Http11ProxyUpstreamTransport.getDefaultInstance()))) + .build(); + // No proxy address in endpointMetadata, and proxy in locality metadata + ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(100)) + .setLocality(LOCALITY1) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.1", 8080)) + .setMetadata(Metadata.newBuilder() + .putTypedFilterMetadata( + "envoy.http11_proxy_transport_socket.proxy_address", + Any.pack(newAddress("127.0.0.2", 8081).build())))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + CLUSTER, cluster)); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, clusterLoadAssignment)); + startXdsDepManager(); + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); // Get the rewritten address - SocketAddress rewrittenAddress = childBalancer.addresses.get(0).getAddresses().get(0); + java.net.SocketAddress rewrittenAddress = childBalancer.addresses.get(0).getAddresses().get(0); // Assert that the address was rewritten assertThat(rewrittenAddress).isInstanceOf(HttpConnectProxiedSocketAddress.class); @@ -470,47 +501,37 @@ public void endpointAddressRewritten_whenProxyMetadataIsInLocalityMetadata() { (HttpConnectProxiedSocketAddress) rewrittenAddress; // Assert that the target address is the original address - assertThat(proxiedSocket.getTargetAddress()).isEqualTo(endpoint.getAddresses().get(0)); + assertThat(proxiedSocket.getTargetAddress()).isEqualTo(newInetSocketAddress("127.0.0.1", 8080)); // Assert that the proxy address is correctly set from locality metadata - assertThat(proxiedSocket.getProxyAddress()).isEqualTo(proxyAddress); + assertThat(proxiedSocket.getProxyAddress()).isEqualTo(newInetSocketAddress("127.0.0.2", 8081)); } @Test public void onlyEdsClusters_receivedEndpoints() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism2, roundRobin, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME2); - assertThat(childBalancers).isEmpty(); - // CLUSTER1 has priority 1 (priority3), which has locality 2, which has endpoint3. - // CLUSTER2 has priority 1 (priority1) and 2 (priority2); priority1 has locality1, - // which has endpoint1 and endpoint2; priority2 has locality3, which has endpoint4. - EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); - EquivalentAddressGroup endpoint4 = makeAddress("endpoint-addr-4"); - LocalityLbEndpoints localityLbEndpoints1 = - LocalityLbEndpoints.create( - Arrays.asList( - LbEndpoint.create(endpoint1, 100, - true, "hostname1", ImmutableMap.of()), - LbEndpoint.create(endpoint2, 100, - true, "hostname1", ImmutableMap.of())), - 70 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - LocalityLbEndpoints localityLbEndpoints3 = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint4, 100, true, - "hostname3", ImmutableMap.of())), - 20 /* localityWeight */, 2 /* priority */, ImmutableMap.of()); - String priority1 = CLUSTER2 + "[child1]"; - String priority2 = CLUSTER2 + "[child2]"; - - // CLUSTER2: locality1 with priority 1 and locality3 with priority 2. - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME2, - ImmutableMap.of(locality1, localityLbEndpoints1, locality3, localityLbEndpoints3)); - - // Endpoints of all clusters have been resolved. + // Has two localities with different priorities + ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(70)) + .setPriority(0) + .setLocality(LOCALITY1) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.1", 8080)) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.2", 8080))) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(30)) + .setPriority(1) + .setLocality(LOCALITY2) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.3", 8080))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, clusterLoadAssignment)); + startXdsDepManager(); + + String priority1 = CLUSTER + "[child1]"; + String priority2 = CLUSTER + "[child2]"; + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); @@ -525,8 +546,8 @@ public void onlyEdsClusters_receivedEndpoints() { .isEqualTo(CLUSTER_IMPL_POLICY_NAME); ClusterImplConfig clusterImplConfig1 = (ClusterImplConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig1.childConfig); - assertClusterImplConfig(clusterImplConfig1, CLUSTER2, EDS_SERVICE_NAME2, LRS_SERVER_INFO, 200L, - tlsContext, Collections.emptyList(), WRR_LOCALITY_POLICY_NAME); + assertClusterImplConfig(clusterImplConfig1, CLUSTER, EDS_SERVICE_NAME, null, null, + null, Collections.emptyList(), WRR_LOCALITY_POLICY_NAME); WrrLocalityConfig wrrLocalityConfig1 = (WrrLocalityConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(clusterImplConfig1.childConfig); LoadBalancerProvider childProvider1 = @@ -540,8 +561,8 @@ public void onlyEdsClusters_receivedEndpoints() { .isEqualTo(CLUSTER_IMPL_POLICY_NAME); ClusterImplConfig clusterImplConfig2 = (ClusterImplConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig2.childConfig); - assertClusterImplConfig(clusterImplConfig2, CLUSTER2, EDS_SERVICE_NAME2, LRS_SERVER_INFO, 200L, - tlsContext, Collections.emptyList(), WRR_LOCALITY_POLICY_NAME); + assertClusterImplConfig(clusterImplConfig2, CLUSTER, EDS_SERVICE_NAME, null, null, + null, Collections.emptyList(), WRR_LOCALITY_POLICY_NAME); WrrLocalityConfig wrrLocalityConfig2 = (WrrLocalityConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(clusterImplConfig1.childConfig); LoadBalancerProvider childProvider2 = @@ -554,36 +575,41 @@ public void onlyEdsClusters_receivedEndpoints() { GracefulSwitchLoadBalancerAccessor.getChildProvider(wrrLocalityConfig3.childConfig); assertThat(childProvider3.getPolicyName()).isEqualTo("round_robin"); + io.grpc.xds.client.Locality locality1 = io.grpc.xds.client.Locality.create( + LOCALITY1.getRegion(), LOCALITY1.getZone(), LOCALITY1.getSubZone()); + io.grpc.xds.client.Locality locality2 = io.grpc.xds.client.Locality.create( + LOCALITY2.getRegion(), LOCALITY2.getZone(), LOCALITY2.getSubZone()); for (EquivalentAddressGroup eag : childBalancer.addresses) { - if (eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY) == locality1) { + io.grpc.xds.client.Locality locality = eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY); + if (locality.equals(locality1)) { assertThat(eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY_WEIGHT)) .isEqualTo(70); - } - if (eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY) == locality2) { - assertThat(eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY_WEIGHT)) - .isEqualTo(10); - } - if (eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY) == locality3) { + } else if (locality.equals(locality2)) { assertThat(eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY_WEIGHT)) - .isEqualTo(20); + .isEqualTo(30); + } else { + throw new AssertionError("Unexpected locality region: " + locality.region()); } } } @SuppressWarnings("unchecked") - private void verifyEdsPriorityNames(List want, - Map... updates) { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism2, roundRobin, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME2); - assertThat(childBalancers).isEmpty(); - - for (Map update: updates) { - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME2, - update); + private void verifyEdsPriorityNames(List want, List... updates) { + Iterator edsUpdates = Arrays.asList(updates).stream() + .map(update -> ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addAllEndpoints(update) + .build()) + .iterator(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, edsUpdates.next())); + startXdsDepManager(); + + while (edsUpdates.hasNext()) { + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, edsUpdates.next())); } + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); @@ -594,151 +620,198 @@ private void verifyEdsPriorityNames(List want, @Test @SuppressWarnings("unchecked") public void edsUpdatePriorityName_twoPriorities() { - verifyEdsPriorityNames(Arrays.asList(CLUSTER2 + "[child1]", CLUSTER2 + "[child2]"), - ImmutableMap.of(locality1, createEndpoints(1), - locality2, createEndpoints(2) - )); + verifyEdsPriorityNames(Arrays.asList(CLUSTER + "[child1]", CLUSTER + "[child2]"), + Arrays.asList(createEndpoints(LOCALITY1, 0), createEndpoints(LOCALITY2, 1))); } @Test @SuppressWarnings("unchecked") public void edsUpdatePriorityName_addOnePriority() { - verifyEdsPriorityNames(Arrays.asList(CLUSTER2 + "[child2]"), - ImmutableMap.of(locality1, createEndpoints(1)), - ImmutableMap.of(locality2, createEndpoints(1) - )); + verifyEdsPriorityNames(Arrays.asList(CLUSTER + "[child2]"), + Arrays.asList(createEndpoints(LOCALITY1, 0)), + Arrays.asList(createEndpoints(LOCALITY2, 0))); } @Test @SuppressWarnings("unchecked") public void edsUpdatePriorityName_swapTwoPriorities() { - verifyEdsPriorityNames(Arrays.asList(CLUSTER2 + "[child2]", CLUSTER2 + "[child1]", - CLUSTER2 + "[child3]"), - ImmutableMap.of(locality1, createEndpoints(1), - locality2, createEndpoints(2), - locality3, createEndpoints(3) - ), - ImmutableMap.of(locality1, createEndpoints(2), - locality2, createEndpoints(1), - locality3, createEndpoints(3)) - ); + verifyEdsPriorityNames(Arrays.asList(CLUSTER + "[child2]", CLUSTER + "[child1]", + CLUSTER + "[child3]"), + Arrays.asList( + createEndpoints(LOCALITY1, 0), + createEndpoints(LOCALITY2, 1), + createEndpoints(LOCALITY3, 2)), + Arrays.asList( + createEndpoints(LOCALITY1, 1), + createEndpoints(LOCALITY2, 0), + createEndpoints(LOCALITY3, 2))); } @Test @SuppressWarnings("unchecked") public void edsUpdatePriorityName_mergeTwoPriorities() { - verifyEdsPriorityNames(Arrays.asList(CLUSTER2 + "[child3]", CLUSTER2 + "[child1]"), - ImmutableMap.of(locality1, createEndpoints(1), - locality3, createEndpoints(3), - locality2, createEndpoints(2)), - ImmutableMap.of(locality1, createEndpoints(2), - locality3, createEndpoints(1), - locality2, createEndpoints(1) - )); + verifyEdsPriorityNames(Arrays.asList(CLUSTER + "[child3]", CLUSTER + "[child1]"), + Arrays.asList( + createEndpoints(LOCALITY1, 0), + createEndpoints(LOCALITY3, 2), + createEndpoints(LOCALITY2, 1)), + Arrays.asList( + createEndpoints(LOCALITY1, 1), + createEndpoints(LOCALITY3, 0), + createEndpoints(LOCALITY2, 0))); } - private LocalityLbEndpoints createEndpoints(int priority) { - return LocalityLbEndpoints.create( - Arrays.asList( - LbEndpoint.create(makeAddress("endpoint-addr-1"), 100, - true, "hostname1", ImmutableMap.of()), - LbEndpoint.create(makeAddress("endpoint-addr-2"), 100, - true, "hostname2", ImmutableMap.of())), - 70 /* localityWeight */, priority /* priority */, ImmutableMap.of()); + private LocalityLbEndpoints createEndpoints(Locality locality, int priority) { + return LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(70)) + .setLocality(locality) + .setPriority(priority) + .addLbEndpoints(newSocketLbEndpoint("127.0." + priority + ".1", 8080)) + .build(); } @Test public void onlyEdsClusters_resourceNeverExist_returnErrorPicker() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism1, roundRobin, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - assertThat(childBalancers).isEmpty(); - reset(helper); - xdsClient.deliverResourceNotFound(EDS_SERVICE_NAME1); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of()); + startXdsDepManager(); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); assertPicker( pickerCaptor.getValue(), Status.UNAVAILABLE.withDescription( - "No usable endpoint from cluster: " + CLUSTER1), + "CDS resource " + CLUSTER + " does not exist nodeID: node-id"), null); } @Test - public void edsCluster_resourcesRevoked_shutDownChildLbPolicy() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism1, roundRobin, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - assertThat(childBalancers).isEmpty(); - reset(helper); - EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - LocalityLbEndpoints localityLbEndpoints1 = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint1, 100, true, - "hostname1", ImmutableMap.of())), - 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints1)); + public void cdsMissing_handledDirectly() { + ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(100)) + .setLocality(LOCALITY1) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.1", 8000))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of()); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, clusterLoadAssignment)); + + startXdsDepManager(); + assertThat(childBalancers).hasSize(0); // no child LB policy created + verify(helper).updateBalancingState( + eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); + Status expectedError = Status.UNAVAILABLE.withDescription( + "CDS resource " + CLUSTER + " does not exist nodeID: node-id"); + assertPicker(pickerCaptor.getValue(), expectedError, null); + } + + @Test + public void cdsRevoked_handledDirectly() { + ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(100)) + .setLocality(LOCALITY1) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.1", 8000))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, clusterLoadAssignment)); + + startXdsDepManager(); assertThat(childBalancers).hasSize(1); // child LB policy created FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(((PriorityLbConfig) childBalancer.config).priorities).hasSize(1); - assertAddressesEqual(Arrays.asList(endpoint1), childBalancer.addresses); + assertThat(childBalancer.addresses).hasSize(1); + assertAddressesEqual( + Arrays.asList(newInetSocketAddressEag("127.0.0.1", 8000)), + childBalancer.addresses); - xdsClient.deliverResourceNotFound(EDS_SERVICE_NAME1); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of()); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); Status expectedError = Status.UNAVAILABLE.withDescription( - "No usable endpoint from cluster: " + CLUSTER1); + "CDS resource " + CLUSTER + " does not exist nodeID: node-id"); assertPicker(pickerCaptor.getValue(), expectedError, null); + assertThat(childBalancer.shutdown).isTrue(); + } + + @Test + public void edsMissing_handledByChildPolicy() { + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of()); + + startXdsDepManager(); + assertThat(childBalancers).hasSize(1); // child LB policy created + FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); + assertThat(childBalancer.upstreamError).isNotNull(); + assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(childBalancer.upstreamError.getDescription()) + .isEqualTo("EDS resource " + EDS_SERVICE_NAME + " does not exist nodeID: node-id"); + assertThat(childBalancer.shutdown).isFalse(); + } + + @Test + public void logicalDnsLookupFailed_handledByChildPolicy() { + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + CLUSTER, LOGICAL_DNS_CLUSTER)); + startXdsDepManager(new CdsConfig(CLUSTER), /* forwardTime= */ false); + FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME + ":9000"); + assertThat(childBalancers).isEmpty(); + resolver.deliverError(Status.UNAVAILABLE.withDescription("OH NO! Who would have guessed?")); + + assertThat(childBalancers).hasSize(1); // child LB policy created + FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); + assertThat(childBalancer.upstreamError).isNotNull(); + assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(childBalancer.upstreamError.getDescription()) + .isEqualTo("OH NO! Who would have guessed?"); + assertThat(childBalancer.shutdown).isFalse(); } @Test public void handleEdsResource_ignoreUnhealthyEndpoints() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism1, roundRobin, false); - deliverLbConfig(config); - EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); - LocalityLbEndpoints localityLbEndpoints = - LocalityLbEndpoints.create( - Arrays.asList( - LbEndpoint.create(endpoint1, 100, false /* isHealthy */, - "hostname1", ImmutableMap.of()), - LbEndpoint.create(endpoint2, 100, true /* isHealthy */, - "hostname2", ImmutableMap.of())), - 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); + ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(100)) + .setLocality(LOCALITY1) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.1", 8000) + .setHealthStatus(HealthStatus.UNHEALTHY)) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.2", 8000))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, clusterLoadAssignment)); + startXdsDepManager(); + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(childBalancer.addresses).hasSize(1); - assertAddressesEqual(Collections.singletonList(endpoint2), childBalancer.addresses); + assertAddressesEqual( + Arrays.asList(new EquivalentAddressGroup(newInetSocketAddress("127.0.0.2", 8000))), + childBalancer.addresses); } @Test public void handleEdsResource_ignoreLocalitiesWithNoHealthyEndpoints() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism1, roundRobin, false); - deliverLbConfig(config); - EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); - LocalityLbEndpoints localityLbEndpoints1 = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint1, 100, false /* isHealthy */, - "hostname1", ImmutableMap.of())), - 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - LocalityLbEndpoints localityLbEndpoints2 = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint2, 100, true /* isHealthy */, - "hostname2", ImmutableMap.of())), - 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, - ImmutableMap.of(locality1, localityLbEndpoints1, locality2, localityLbEndpoints2)); + ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(100)) + .setLocality(LOCALITY1) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.1", 8000) + .setHealthStatus(HealthStatus.UNHEALTHY))) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(100)) + .setLocality(LOCALITY2) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.2", 8000))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, clusterLoadAssignment)); + startXdsDepManager(); + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); + io.grpc.xds.client.Locality locality2 = io.grpc.xds.client.Locality.create( + LOCALITY2.getRegion(), LOCALITY2.getZone(), LOCALITY2.getSubZone()); for (EquivalentAddressGroup eag : childBalancer.addresses) { assertThat(eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY)).isEqualTo(locality2); } @@ -746,76 +819,65 @@ public void handleEdsResource_ignoreLocalitiesWithNoHealthyEndpoints() { @Test public void handleEdsResource_ignorePrioritiesWithNoHealthyEndpoints() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism1, roundRobin, false); - deliverLbConfig(config); - EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); - LocalityLbEndpoints localityLbEndpoints1 = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint1, 100, false /* isHealthy */, - "hostname1", ImmutableMap.of())), - 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - LocalityLbEndpoints localityLbEndpoints2 = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint2, 200, true /* isHealthy */, - "hostname2", ImmutableMap.of())), - 10 /* localityWeight */, 2 /* priority */, ImmutableMap.of()); - String priority2 = CLUSTER1 + "[child2]"; - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, - ImmutableMap.of(locality1, localityLbEndpoints1, locality2, localityLbEndpoints2)); + ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(100)) + .setLocality(LOCALITY1) + .setPriority(0) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.1", 8000) + .setHealthStatus(HealthStatus.UNHEALTHY))) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(100)) + .setLocality(LOCALITY2) + .setPriority(1) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.2", 8000))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, clusterLoadAssignment)); + startXdsDepManager(); + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); + String priority2 = CLUSTER + "[child2]"; FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(((PriorityLbConfig) childBalancer.config).priorities).containsExactly(priority2); } @Test public void handleEdsResource_noHealthyEndpoint() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism1, roundRobin, false); - deliverLbConfig(config); - EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); - LocalityLbEndpoints localityLbEndpoints = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint, 100, false /* isHealthy */, - "hostname1", ImmutableMap.of())), - 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - xdsClient.deliverClusterLoadAssignment(EDS_SERVICE_NAME1, - Collections.singletonMap(locality1, localityLbEndpoints)); // single endpoint, unhealthy + ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() + .setClusterName(EDS_SERVICE_NAME) + .addEndpoints(LocalityLbEndpoints.newBuilder() + .setLoadBalancingWeight(UInt32Value.of(100)) + .setLocality(LOCALITY1) + .addLbEndpoints(newSocketLbEndpoint("127.0.0.1", 8000) + .setHealthStatus(HealthStatus.UNHEALTHY))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of( + EDS_SERVICE_NAME, clusterLoadAssignment)); + startXdsDepManager(); - assertThat(childBalancers).isEmpty(); - verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - assertPicker( - pickerCaptor.getValue(), - Status.UNAVAILABLE.withDescription( - "No usable endpoint from cluster: " + CLUSTER1), - null); + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); + assertThat(childBalancers).hasSize(1); + FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); + assertThat(childBalancer.upstreamError).isNotNull(); + assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(childBalancer.upstreamError.getDescription()) + .isEqualTo("No usable endpoint from cluster: " + CLUSTER); } @Test public void onlyLogicalDnsCluster_endpointsResolved() { - do_onlyLogicalDnsCluster_endpointsResolved(); - } - - @Test - public void oldListenerCallback_onlyLogicalDnsCluster_endpointsResolved() { - nsRegistry.deregister(fakeNameResolverProvider); - nsRegistry.register(new FakeNameResolverProvider(true)); - do_onlyLogicalDnsCluster_endpointsResolved(); - } - - void do_onlyLogicalDnsCluster_endpointsResolved() { - ClusterResolverConfig config = new ClusterResolverConfig( - logicalDnsDiscoveryMechanism, roundRobin, false); - deliverLbConfig(config); - FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + CLUSTER, LOGICAL_DNS_CLUSTER)); + startXdsDepManager(new CdsConfig(CLUSTER), /* forwardTime= */ false); + FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME + ":9000"); assertThat(childBalancers).isEmpty(); - EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); - resolver.deliverEndpointAddresses(Arrays.asList(endpoint1, endpoint2)); + resolver.deliverEndpointAddresses(Arrays.asList( + newInetSocketAddressEag("127.0.2.1", 9000), newInetSocketAddressEag("127.0.2.2", 9000))); + fakeClock.forwardTime(10, TimeUnit.MINUTES); + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); @@ -828,263 +890,132 @@ void do_onlyLogicalDnsCluster_endpointsResolved() { .isEqualTo(CLUSTER_IMPL_POLICY_NAME); ClusterImplConfig clusterImplConfig = (ClusterImplConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig.childConfig); - assertClusterImplConfig(clusterImplConfig, CLUSTER_DNS, null, LRS_SERVER_INFO, 300L, null, - Collections.emptyList(), "pick_first"); - assertAddressesEqual(Arrays.asList(endpoint1, endpoint2), childBalancer.addresses); + assertClusterImplConfig(clusterImplConfig, CLUSTER, null, null, null, null, + Collections.emptyList(), "wrr_locality_experimental"); + assertAddressesEqual( + Arrays.asList(new EquivalentAddressGroup(Arrays.asList( + newInetSocketAddress("127.0.2.1", 9000), newInetSocketAddress("127.0.2.2", 9000)))), + childBalancer.addresses); assertThat(childBalancer.addresses.get(0).getAttributes() - .get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME); - assertThat(childBalancer.addresses.get(1).getAttributes() - .get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME); + .get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME + ":9000"); } @Test public void onlyLogicalDnsCluster_handleRefreshNameResolution() { - ClusterResolverConfig config = new ClusterResolverConfig( - logicalDnsDiscoveryMechanism, roundRobin, false); - deliverLbConfig(config); - FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + CLUSTER, LOGICAL_DNS_CLUSTER)); + startXdsDepManager(new CdsConfig(CLUSTER), /* forwardTime= */ false); + FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME + ":9000"); assertThat(childBalancers).isEmpty(); - EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); - resolver.deliverEndpointAddresses(Arrays.asList(endpoint1, endpoint2)); - assertThat(resolver.refreshCount).isEqualTo(0); - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - childBalancer.helper.refreshNameResolution(); - assertThat(resolver.refreshCount).isEqualTo(1); - } - - @Test - public void resolutionError_backoffAndRefresh() { - do_onlyLogicalDnsCluster_resolutionError_backoffAndRefresh(); - } - - @Test - public void oldListenerCallback_resolutionError_backoffAndRefresh() { - nsRegistry.deregister(fakeNameResolverProvider); - nsRegistry.register(new FakeNameResolverProvider(true)); - do_onlyLogicalDnsCluster_resolutionError_backoffAndRefresh(); - } + resolver.deliverEndpointAddresses(Arrays.asList(newInetSocketAddressEag("127.0.2.1", 9000))); + fakeClock.forwardTime(10, TimeUnit.MINUTES); - void do_onlyLogicalDnsCluster_resolutionError_backoffAndRefresh() { - InOrder inOrder = Mockito.inOrder(helper, backoffPolicyProvider, - backoffPolicy1, backoffPolicy2); - ClusterResolverConfig config = new ClusterResolverConfig( - logicalDnsDiscoveryMechanism, roundRobin, false); - deliverLbConfig(config); - FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); - assertThat(childBalancers).isEmpty(); - Status error = Status.UNAVAILABLE.withDescription("cannot reach DNS server"); - resolver.deliverError(error); - inOrder.verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - assertPicker(pickerCaptor.getValue(), error, null); + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(resolver.refreshCount).isEqualTo(0); - inOrder.verify(backoffPolicyProvider).get(); - inOrder.verify(backoffPolicy1).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks()).hasSize(1); - assertThat(Iterables.getOnlyElement(fakeClock.getPendingTasks()).getDelay(TimeUnit.SECONDS)) - .isEqualTo(1L); - fakeClock.forwardTime(1L, TimeUnit.SECONDS); - assertThat(resolver.refreshCount).isEqualTo(1); - - error = Status.UNKNOWN.withDescription("I am lost"); - resolver.deliverError(error); - inOrder.verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - inOrder.verify(backoffPolicy1).nextBackoffNanos(); - assertPicker(pickerCaptor.getValue(), error, null); - assertThat(fakeClock.getPendingTasks()).hasSize(1); - assertThat(Iterables.getOnlyElement(fakeClock.getPendingTasks()).getDelay(TimeUnit.SECONDS)) - .isEqualTo(10L); - fakeClock.forwardTime(10L, TimeUnit.SECONDS); - assertThat(resolver.refreshCount).isEqualTo(2); - - // Succeed. - EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); - resolver.deliverEndpointAddresses(Arrays.asList(endpoint1, endpoint2)); - assertThat(childBalancers).hasSize(1); - assertAddressesEqual(Arrays.asList(endpoint1, endpoint2), - Iterables.getOnlyElement(childBalancers).addresses); - - assertThat(fakeClock.getPendingTasks()).isEmpty(); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void onlyLogicalDnsCluster_refreshNameResolutionRaceWithResolutionError() { - InOrder inOrder = Mockito.inOrder(backoffPolicyProvider, backoffPolicy1, backoffPolicy2); - ClusterResolverConfig config = new ClusterResolverConfig( - logicalDnsDiscoveryMechanism, roundRobin, false); - deliverLbConfig(config); - FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); - assertThat(childBalancers).isEmpty(); - EquivalentAddressGroup endpoint = makeAddress("endpoint-addr"); - resolver.deliverEndpointAddresses(Collections.singletonList(endpoint)); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertAddressesEqual(Collections.singletonList(endpoint), childBalancer.addresses); - assertThat(resolver.refreshCount).isEqualTo(0); - childBalancer.helper.refreshNameResolution(); assertThat(resolver.refreshCount).isEqualTo(1); - resolver.deliverError(Status.UNAVAILABLE.withDescription("I am lost")); - inOrder.verify(backoffPolicyProvider).get(); - inOrder.verify(backoffPolicy1).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks()).hasSize(1); - ScheduledTask task = Iterables.getOnlyElement(fakeClock.getPendingTasks()); - assertThat(task.getDelay(TimeUnit.SECONDS)).isEqualTo(1L); - - fakeClock.forwardTime( 100L, TimeUnit.MILLISECONDS); - childBalancer.helper.refreshNameResolution(); - assertThat(resolver.refreshCount).isEqualTo(2); - assertThat(task.isCancelled()).isTrue(); - assertThat(fakeClock.getPendingTasks()).isEmpty(); - resolver.deliverError(Status.UNAVAILABLE.withDescription("I am still lost")); - inOrder.verify(backoffPolicyProvider).get(); // active refresh resets backoff sequence - inOrder.verify(backoffPolicy2).nextBackoffNanos(); - task = Iterables.getOnlyElement(fakeClock.getPendingTasks()); - assertThat(task.getDelay(TimeUnit.SECONDS)).isEqualTo(5L); - - fakeClock.forwardTime(5L, TimeUnit.SECONDS); - assertThat(resolver.refreshCount).isEqualTo(3); - inOrder.verifyNoMoreInteractions(); } @Test - public void resolutionErrorAfterChildLbCreated_propagateError() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism1, roundRobin, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - assertThat(childBalancers).isEmpty(); - reset(helper); - EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); - LocalityLbEndpoints localityLbEndpoints = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint, 100, true, - "hostname1", ImmutableMap.of())), - 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); - - assertThat(childBalancers).hasSize(1); - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); // child LB created - assertThat(childBalancer.upstreamError).isNull(); // should not propagate error to child LB - assertAddressesEqual(Collections.singletonList(endpoint), childBalancer.addresses); - - xdsClient.deliverError(Status.RESOURCE_EXHAUSTED.withDescription("out of memory")); - assertThat(childBalancer.upstreamError).isNotNull(); // last cluster's (DNS) error propagated - assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Code.UNAVAILABLE); - assertThat(childBalancer.upstreamError.getDescription()) - .isEqualTo("Unable to load EDS backend-service-foo.googleapis.com. xDS server returned: " - + "RESOURCE_EXHAUSTED: out of memory"); - assertThat(childBalancer.shutdown).isFalse(); - verify(helper, never()).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), any(SubchannelPicker.class)); - } - - @Test - public void resolutionErrorBeforeChildLbCreated_returnErrorPicker() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism1, roundRobin, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - assertThat(childBalancers).isEmpty(); - reset(helper); - xdsClient.deliverError(Status.RESOURCE_EXHAUSTED.withDescription("OOM")); - assertThat(childBalancers).isEmpty(); - verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - PickResult result = pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)); - Status actualStatus = result.getStatus(); - assertThat(actualStatus.getCode()).isEqualTo(Status.Code.UNAVAILABLE); - assertThat(actualStatus.getDescription()).contains("RESOURCE_EXHAUSTED: OOM"); - } - - @Test - public void handleNameResolutionErrorFromUpstream_eds_beforeChildLbCreated_returnErrorPicker() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism1, roundRobin, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - assertThat(childBalancers).isEmpty(); - reset(helper); - Status upstreamError = Status.UNAVAILABLE.withDescription("unreachable"); - loadBalancer.handleNameResolutionError(upstreamError); - verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - assertPicker(pickerCaptor.getValue(), upstreamError, null); - } - - @Test - public void handleNameResolutionErrorFromUpstream_lDns_beforeChildLbCreated_returnErrorPicker() { - ClusterResolverConfig config = new ClusterResolverConfig( - logicalDnsDiscoveryMechanism, roundRobin, false); - deliverLbConfig(config); - assertResolverCreated("/" + DNS_HOST_NAME); - assertThat(childBalancers).isEmpty(); - reset(helper); - Status upstreamError = Status.UNAVAILABLE.withDescription("unreachable"); - loadBalancer.handleNameResolutionError(upstreamError); - verify(helper).updateBalancingState( - eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - assertPicker(pickerCaptor.getValue(), upstreamError, null); - } + public void outlierDetection_disabledConfig() { + Cluster cluster = EDS_CLUSTER.toBuilder() + .setOutlierDetection(OutlierDetection.newBuilder() + .setEnforcingSuccessRate(UInt32Value.of(0)) + .setEnforcingFailurePercentage(UInt32Value.of(0))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + CLUSTER, cluster)); + startXdsDepManager(); - @Test - public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_eds_fallThrough() { - ClusterResolverConfig config = new ClusterResolverConfig( - edsDiscoveryMechanism1, roundRobin, false); - deliverLbConfig(config); - assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); - assertThat(childBalancers).isEmpty(); - reset(helper); - EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr-1"); - LocalityLbEndpoints localityLbEndpoints = - LocalityLbEndpoints.create( - Collections.singletonList(LbEndpoint.create(endpoint1, 100, true, - "hostname1", ImmutableMap.of())), - 10 /* localityWeight */, 1 /* priority */, ImmutableMap.of()); - xdsClient.deliverClusterLoadAssignment( - EDS_SERVICE_NAME1, Collections.singletonMap(locality1, localityLbEndpoints)); + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(((PriorityLbConfig) childBalancer.config).priorities) - .containsExactly(CLUSTER1 + "[child1]"); - assertAddressesEqual(Arrays.asList(endpoint1), childBalancer.addresses); - - loadBalancer.handleNameResolutionError(Status.UNAVAILABLE.withDescription("unreachable")); - assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Code.UNAVAILABLE); - assertThat(childBalancer.upstreamError.getDescription()).isEqualTo("unreachable"); - verify(helper, never()).updateBalancingState( - any(ConnectivityState.class), any(SubchannelPicker.class)); + assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); + PriorityLbConfig priorityLbConfig = (PriorityLbConfig) childBalancer.config; + PriorityChildConfig priorityChildConfig = + Iterables.getOnlyElement(priorityLbConfig.childConfigs.values()); + OutlierDetectionLoadBalancerConfig outlier = (OutlierDetectionLoadBalancerConfig) + GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig.childConfig); + assertThat(outlier.successRateEjection).isNull(); + assertThat(outlier.failurePercentageEjection).isNull(); } @Test - public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_logicalDns_fallThrough() { - ClusterResolverConfig config = new ClusterResolverConfig( - logicalDnsDiscoveryMechanism, roundRobin, false); - deliverLbConfig(config); - FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME); - assertThat(childBalancers).isEmpty(); - reset(helper); - EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr-2"); - resolver.deliverEndpointAddresses(Collections.singletonList(endpoint2)); + public void outlierDetection_fullConfig() { + Cluster cluster = EDS_CLUSTER.toBuilder() + .setLbPolicy(Cluster.LbPolicy.ROUND_ROBIN) + .setOutlierDetection(OutlierDetection.newBuilder() + .setInterval(Duration.newBuilder().setNanos(101)) + .setBaseEjectionTime(Duration.newBuilder().setNanos(102)) + .setMaxEjectionTime(Duration.newBuilder().setNanos(103)) + .setMaxEjectionPercent(UInt32Value.of(80)) + .setSuccessRateStdevFactor(UInt32Value.of(105)) + .setEnforcingSuccessRate(UInt32Value.of(81)) + .setSuccessRateMinimumHosts(UInt32Value.of(107)) + .setSuccessRateRequestVolume(UInt32Value.of(108)) + .setFailurePercentageThreshold(UInt32Value.of(82)) + .setEnforcingFailurePercentage(UInt32Value.of(83)) + .setFailurePercentageMinimumHosts(UInt32Value.of(111)) + .setFailurePercentageRequestVolume(UInt32Value.of(112))) + .build(); + controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( + CLUSTER, cluster)); + startXdsDepManager(); + + verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(((PriorityLbConfig) childBalancer.config).priorities) - .containsExactly(CLUSTER_DNS + "[child0]"); - assertAddressesEqual(Arrays.asList(endpoint2), childBalancer.addresses); - - loadBalancer.handleNameResolutionError(Status.UNAVAILABLE.withDescription("unreachable")); - assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Code.UNAVAILABLE); - assertThat(childBalancer.upstreamError.getDescription()).isEqualTo("unreachable"); - verify(helper, never()).updateBalancingState( - any(ConnectivityState.class), any(SubchannelPicker.class)); + assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); + PriorityLbConfig priorityLbConfig = (PriorityLbConfig) childBalancer.config; + PriorityChildConfig priorityChildConfig = + Iterables.getOnlyElement(priorityLbConfig.childConfigs.values()); + OutlierDetectionLoadBalancerConfig outlier = (OutlierDetectionLoadBalancerConfig) + GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig.childConfig); + assertThat(outlier.intervalNanos).isEqualTo(101); + assertThat(outlier.baseEjectionTimeNanos).isEqualTo(102); + assertThat(outlier.maxEjectionTimeNanos).isEqualTo(103); + assertThat(outlier.maxEjectionPercent).isEqualTo(80); + assertThat(outlier.successRateEjection.stdevFactor).isEqualTo(105); + assertThat(outlier.successRateEjection.enforcementPercentage).isEqualTo(81); + assertThat(outlier.successRateEjection.minimumHosts).isEqualTo(107); + assertThat(outlier.successRateEjection.requestVolume).isEqualTo(108); + assertThat(outlier.failurePercentageEjection.threshold).isEqualTo(82); + assertThat(outlier.failurePercentageEjection.enforcementPercentage).isEqualTo(83); + assertThat(outlier.failurePercentageEjection.minimumHosts).isEqualTo(111); + assertThat(outlier.failurePercentageEjection.requestVolume).isEqualTo(112); + assertClusterImplConfig( + (ClusterImplConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(outlier.childConfig), + CLUSTER, EDS_SERVICE_NAME, null, null, null, Collections.emptyList(), + "wrr_locality_experimental"); } @Test public void config_equalsTester() { + ServerInfo lrsServerInfo = + ServerInfo.create("lrs.googleapis.com", InsecureChannelCredentials.create()); + UpstreamTlsContext tlsContext = + CommonTlsContextTestsUtil.buildUpstreamTlsContext("google_cloud_private_spiffe", true); + DiscoveryMechanism edsDiscoveryMechanism1 = + DiscoveryMechanism.forEds(CLUSTER, EDS_SERVICE_NAME, lrsServerInfo, 100L, tlsContext, + Collections.emptyMap(), null); + io.grpc.xds.EnvoyServerProtoData.OutlierDetection outlierDetection = + io.grpc.xds.EnvoyServerProtoData.OutlierDetection.create( + 100L, 100L, 100L, 100, SuccessRateEjection.create(100, 100, 100, 100), + FailurePercentageEjection.create(100, 100, 100, 100)); + DiscoveryMechanism edsDiscoveryMechanismWithOutlierDetection = + DiscoveryMechanism.forEds(CLUSTER, EDS_SERVICE_NAME, lrsServerInfo, 100L, tlsContext, + Collections.emptyMap(), outlierDetection); + Object roundRobin = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + new FakeLoadBalancerProvider("wrr_locality_experimental"), new WrrLocalityConfig( + GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + new FakeLoadBalancerProvider("round_robin"), null))); + Object leastRequest = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + new FakeLoadBalancerProvider("wrr_locality_experimental"), new WrrLocalityConfig( + GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + new FakeLoadBalancerProvider("least_request_experimental"), + new LeastRequestConfig(3)))); + new EqualsTester() .addEqualityGroup( new ClusterResolverConfig( @@ -1102,17 +1033,36 @@ public void config_equalsTester() { .testEquals(); } - private void deliverLbConfig(ClusterResolverConfig config) { - loadBalancer.acceptResolvedAddresses( - ResolvedAddresses.newBuilder() - .setAddresses(Collections.emptyList()) - .setAttributes( - // Other attributes not used by cluster_resolver LB are omitted. - Attributes.newBuilder() - .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) - .build()) - .setLoadBalancingPolicyConfig(config) - .build()); + private void startXdsDepManager() { + startXdsDepManager(new CdsConfig(CLUSTER)); + } + + private void startXdsDepManager(final CdsConfig cdsConfig) { + startXdsDepManager(cdsConfig, true); + } + + private void startXdsDepManager(final CdsConfig cdsConfig, boolean forwardTime) { + xdsDepManager.start( + xdsConfig -> { + if (!xdsConfig.hasValue()) { + throw new AssertionError("" + xdsConfig.getStatus()); + } + if (loadBalancer == null) { + return; + } + loadBalancer.acceptResolvedAddresses(ResolvedAddresses.newBuilder() + .setAddresses(Collections.emptyList()) + .setAttributes(Attributes.newBuilder() + .set(XdsAttributes.XDS_CONFIG, xdsConfig.getValue()) + .set(XdsAttributes.XDS_CLUSTER_SUBSCRIPT_REGISTRY, xdsDepManager) + .build()) + .setLoadBalancingPolicyConfig(cdsConfig) + .build()); + }); + if (forwardTime) { + // trigger does not exist timer, so broken config is more obvious + fakeClock.forwardTime(10, TimeUnit.MINUTES); + } } private FakeNameResolver assertResolverCreated(String uriPath) { @@ -1151,112 +1101,41 @@ private static void assertClusterImplConfig(ClusterImplConfig config, String clu /** Asserts two list of EAGs contains same addresses, regardless of attributes. */ private static void assertAddressesEqual( List expected, List actual) { - List> expectedAddresses + List> expectedAddresses = expected.stream().map(EquivalentAddressGroup::getAddresses).collect(toList()); - List> actualAddresses + List> actualAddresses = actual.stream().map(EquivalentAddressGroup::getAddresses).collect(toList()); assertThat(actualAddresses).isEqualTo(expectedAddresses); } - private static EquivalentAddressGroup makeAddress(final String name) { - return new EquivalentAddressGroup(new FakeSocketAddress(name)); + @SuppressWarnings("AddressSelection") + private static InetSocketAddress newInetSocketAddress(String ip, int port) { + return new InetSocketAddress(ip, port); } - static class FakeSocketAddress extends SocketAddress { - private final String name; - - private FakeSocketAddress(String name) { - this.name = name; - } - - @Override - public int hashCode() { - return Objects.hash(name); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof FakeSocketAddress)) { - return false; - } - FakeSocketAddress that = (FakeSocketAddress) o; - return Objects.equals(name, that.name); - } - - @Override - public String toString() { - return name; - } + private static EquivalentAddressGroup newInetSocketAddressEag(String ip, int port) { + return new EquivalentAddressGroup(newInetSocketAddress(ip, port)); } - private static final class FakeXdsClient extends XdsClient { - - private final Map> watchers = new HashMap<>(); - - @Override - @SuppressWarnings("unchecked") - public void watchXdsResource(XdsResourceType type, - String resourceName, - ResourceWatcher watcher, - Executor syncContext) { - assertThat(type.typeName()).isEqualTo("EDS"); - assertThat(watchers).doesNotContainKey(resourceName); - watchers.put(resourceName, (ResourceWatcher) watcher); - } - - @Override - @SuppressWarnings("unchecked") - public void cancelXdsResourceWatch(XdsResourceType type, - String resourceName, - ResourceWatcher watcher) { - assertThat(type.typeName()).isEqualTo("EDS"); - assertThat(watchers).containsKey(resourceName); - watchers.remove(resourceName); - } - - void deliverClusterLoadAssignment( - String resource, Map localityLbEndpointsMap) { - deliverClusterLoadAssignment( - resource, Collections.emptyList(), localityLbEndpointsMap); - } - - void deliverClusterLoadAssignment(String resource, List dropOverloads, - Map localityLbEndpointsMap) { - if (watchers.containsKey(resource)) { - watchers.get(resource).onChanged( - new XdsEndpointResource.EdsUpdate(resource, localityLbEndpointsMap, dropOverloads)); - } - } - - void deliverResourceNotFound(String resource) { - if (watchers.containsKey(resource)) { - watchers.get(resource).onResourceDoesNotExist(resource); - } - } + private static LbEndpoint.Builder newSocketLbEndpoint(String ip, int port) { + return LbEndpoint.newBuilder() + .setEndpoint(Endpoint.newBuilder() + .setAddress(newAddress(ip, port))) + .setHealthStatus(HealthStatus.HEALTHY); + } - void deliverError(Status error) { - for (ResourceWatcher watcher : watchers.values()) { - watcher.onError(error); - } - } + private static Address.Builder newAddress(String ip, int port) { + return Address.newBuilder() + .setSocketAddress(SocketAddress.newBuilder() + .setAddress(ip) + .setPortValue(port)); } private class FakeNameResolverProvider extends NameResolverProvider { - private final boolean useOldListenerCallback; - - private FakeNameResolverProvider(boolean useOldListenerCallback) { - this.useOldListenerCallback = useOldListenerCallback; - } - @Override public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) { assertThat(targetUri.getScheme()).isEqualTo("dns"); - FakeNameResolver resolver = useOldListenerCallback - ? new FakeNameResolverUsingOldListenerCallback(targetUri) - : new FakeNameResolver(targetUri); + FakeNameResolver resolver = new FakeNameResolver(targetUri); resolvers.add(resolver); return resolver; } @@ -1321,23 +1200,6 @@ protected void deliverError(Status error) { } } - private class FakeNameResolverUsingOldListenerCallback extends FakeNameResolver { - private FakeNameResolverUsingOldListenerCallback(URI targetUri) { - super(targetUri); - } - - @Override - protected void deliverEndpointAddresses(List addresses) { - listener.onResult(ResolutionResult.newBuilder() - .setAddressesOrError(StatusOr.fromValue(addresses)).build()); - } - - @Override - protected void deliverError(Status error) { - listener.onError(error); - } - } - private final class FakeLoadBalancerProvider extends LoadBalancerProvider { private final String policyName; diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index b50bdfce01f..e80fcd008bd 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -205,6 +205,8 @@ public ConfigOrError parseServiceConfig(Map rawServiceConfig) { @Before public void setUp() { + lenient().doReturn(Status.OK).when(mockListener).onResult2(any()); + try { targetUri = new URI(AUTHORITY); } catch (URISyntaxException e) { @@ -435,6 +437,7 @@ public void resolving_ldsResourceUpdateRdsName() { (Map) resolutionResultCaptor.getValue().getServiceConfig().getConfig()); reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(ResolutionResult.class); String alternativeRdsResource = "route-configuration-alter.googleapis.com"; @@ -492,11 +495,13 @@ public void resolving_ldsResourceRevokedAndAddedBack() { (Map) resolutionResultCaptor.getValue().getServiceConfig().getConfig()); reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); xdsClient.deliverLdsResourceNotFound(); // revoke LDS resource assertThat(xdsClient.rdsWatchers.keySet()).isEmpty(); // stop subscribing to stale RDS resource assertEmptyResolutionResult(expectedLdsResourceName); reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); xdsClient.deliverLdsUpdateForRdsName(RDS_RESOURCE_NAME); // No name resolution result until new RDS resource update is received. Do not use stale config verifyNoInteractions(mockListener); @@ -533,11 +538,13 @@ public void resolving_rdsResourceRevokedAndAddedBack() { (Map) resolutionResultCaptor.getValue().getServiceConfig().getConfig()); reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); xdsClient.deliverRdsResourceNotFound(RDS_RESOURCE_NAME); // revoke RDS resource assertEmptyResolutionResult(RDS_RESOURCE_NAME); // Simulate management server adds back the previously used RDS resource. reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); createAndDeliverClusterUpdates(xdsClient, cluster1); verify(mockListener).onResult2(resolutionResultCaptor.capture()); @@ -941,6 +948,7 @@ public void resolved_rpcHashingByChannelId() { // A different resolver/Channel. resolver.shutdown(); reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); when(mockRandom.nextLong()).thenReturn(123L); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, @@ -1044,6 +1052,7 @@ public void resolved_resourceUpdateAfterCallStarted() { TestCall firstCall = testCall; reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( Arrays.asList( @@ -1086,6 +1095,7 @@ public void resolved_resourceUpdateAfterCallStarted() { public void resolved_resourceUpdatedBeforeCallStarted() { InternalConfigSelector configSelector = resolveToClusters(); reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( Arrays.asList( @@ -1122,6 +1132,7 @@ public void resolved_raceBetweenCallAndRepeatedResourceUpdate() { assertCallSelectClusterResult(call1, configSelector, cluster1, 15.0); reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( Arrays.asList( @@ -1546,6 +1557,7 @@ public void filterState_shutdown_onLdsNotFound() { // LDS 2: resource not found. reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); xdsClient.deliverLdsResourceNotFound(); assertEmptyResolutionResult(expectedLdsResourceName); // Verify shutdown. @@ -1603,6 +1615,7 @@ public void filterState_shutdown_onRdsNotFound() { // RDS 2: RDS_RESOURCE_NAME not found. reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); xdsClient.deliverRdsResourceNotFound(RDS_RESOURCE_NAME); assertEmptyResolutionResult(RDS_RESOURCE_NAME); assertThat(lds1Filter1.isShutdown()).isTrue(); From 8f0db07d5d1a0f7b234db5740f7419e996c84425 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Thu, 18 Sep 2025 12:43:40 -0700 Subject: [PATCH 384/591] api: Avoid allocating empty array in LoadBalancer (#12337) Use a singleton empty Object array to initialize the customOptions default in LoadBalancer.Builder. --- api/src/main/java/io/grpc/LoadBalancer.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/io/grpc/LoadBalancer.java b/api/src/main/java/io/grpc/LoadBalancer.java index d2fd8409e01..adc43b19841 100644 --- a/api/src/main/java/io/grpc/LoadBalancer.java +++ b/api/src/main/java/io/grpc/LoadBalancer.java @@ -855,9 +855,11 @@ public String toString() { @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1771") public static final class Builder { + private static final Object[][] EMPTY_CUSTOM_OPTIONS = new Object[0][2]; + private List addrs; private Attributes attrs = Attributes.EMPTY; - private Object[][] customOptions = new Object[0][2]; + private Object[][] customOptions = EMPTY_CUSTOM_OPTIONS; Builder() { } From 1a7042ac98a8fda6d4332563891131f75b1edb36 Mon Sep 17 00:00:00 2001 From: dmytroreutov <56252415+dmytroreutov@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:51:25 +0300 Subject: [PATCH 385/591] android: fix network change handling on API levels < 24 Fixes https://github.com/grpc/grpc-java/issues/12313 In case if VPN toggled on<->off `wasConnected` returns true and `delegate.enterIdle()` is skipped. --- .../main/java/io/grpc/android/AndroidChannelBuilder.java | 6 ++---- .../java/io/grpc/android/AndroidChannelBuilderTest.java | 6 ------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java index 54b38bc3bd3..3a750e02795 100644 --- a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java +++ b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java @@ -323,7 +323,6 @@ public void onBlockedStatusChanged(Network network, boolean blocked) { /** Respond to network changes. Only used on API levels < 24. */ private class NetworkReceiver extends BroadcastReceiver { - private boolean isConnected = false; @SuppressWarnings("deprecation") @Override @@ -331,9 +330,8 @@ public void onReceive(Context context, Intent intent) { ConnectivityManager conn = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); android.net.NetworkInfo networkInfo = conn.getActiveNetworkInfo(); - boolean wasConnected = isConnected; - isConnected = networkInfo != null && networkInfo.isConnected(); - if (isConnected && !wasConnected) { + + if (networkInfo != null && networkInfo.isConnected()) { delegate.enterIdle(); } } diff --git a/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java b/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java index 83367d93b32..c0884e4d7cf 100644 --- a/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java +++ b/android/src/test/java/io/grpc/android/AndroidChannelBuilderTest.java @@ -152,12 +152,6 @@ public void networkChanges_api23() { .sendBroadcast(new Intent(ConnectivityManager.CONNECTIVITY_ACTION)); assertThat(delegateChannel.enterIdleCount).isEqualTo(1); - // The broadcast receiver may fire when the active network status has not actually changed - ApplicationProvider - .getApplicationContext() - .sendBroadcast(new Intent(ConnectivityManager.CONNECTIVITY_ACTION)); - assertThat(delegateChannel.enterIdleCount).isEqualTo(1); - // Drop the connection shadowOf(connectivityManager).setActiveNetworkInfo(null); ApplicationProvider From afe72220d0aaa205b9d7cc8a291c2707510854f1 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 18 Sep 2025 22:55:53 -0700 Subject: [PATCH 386/591] SECURITY.md: Mention gcompat for Alpine (#12365) In the olden days Alpine didn't work at all. Then it worked. And then sometime in 2021 it started failing (#8751) because of missing __strndup. The most recent deep investigation showed it is missing __strdup these days https://github.com/grpc/grpc-java/issues/11660#issuecomment-2469284111 . I'm not expecting too many people to find this themselves, but we can link to it when they experience problems. --- SECURITY.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index cd02fbe3e18..1555882c3c6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -330,14 +330,10 @@ is an option](#tls-with-conscrypt). Otherwise you need to [build your own 32-bit version of `netty-tcnative`](https://netty.io/wiki/forked-tomcat-native.html#wiki-h2-6). -If on Alpine Linux and you see "Error loading shared library libcrypt.so.1: No -such file or directory". Run `apk update && apk add libc6-compat` to install the -necessary dependency. - -If on Alpine Linux, try to use `grpc-netty-shaded` instead of `grpc-netty` or -(if you need `grpc-netty`) `netty-tcnative-boringssl-static` instead of -`netty-tcnative`. If those are not an option, you may consider using -[netty-tcnative-alpine](https://github.com/pires/netty-tcnative-alpine). +If on Alpine Linux, depending on your specific JDK you may see a crash in +netty_tcnative. This is generally caused by a missing symbol. Run `apk install +gcompat` and use the environment variable `LD_PRELOAD=/lib/libgcompat.so.0` when +executing Java. If on Fedora 30 or later and you see "libcrypt.so.1: cannot open shared object file: No such file or directory". Run `dnf -y install libxcrypt-compat` to From 4995700069ad31d11d846900cc62df35001653fb Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 18 Sep 2025 11:17:51 -0700 Subject: [PATCH 387/591] xds: Remove verify TODO for onResult2 error status This had been accidentally left in 0c179e3f9. Requesting a refresh is pretty close to RetryingNameResolver's behavior of exponential backoff. While not identical, it is the closest we can get easily. --- xds/src/main/java/io/grpc/xds/XdsNameResolver.java | 1 - 1 file changed, 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 55059dc4a5a..c3bc7c2e326 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -331,7 +331,6 @@ private void updateResolutionResult(XdsConfig xdsConfig) { .setServiceConfig(parsedServiceConfig) .build(); if (!listener.onResult2(result).isOk()) { - // TODO: check if this is right resolveState.xdsDependencyManager.requestReresolution(); } } From 040665f2438ec04c1d5144a99c84cf7aa4ade3c1 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 18 Sep 2025 13:09:03 -0700 Subject: [PATCH 388/591] examples: Explain Bazel BCR releases and git_override option --- examples/MODULE.bazel | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index 8bd50810c46..18d74d86973 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -4,7 +4,16 @@ bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1" bazel_dep(name = "rules_jvm_external", version = "6.0") bazel_dep(name = "rules_proto", version = "5.3.0-21.7") -# Do not use this override in your own MODULE.bazel. Use a version from BCR +# Do not use this override in your own MODULE.bazel. It is unnecessary when +# using a version from BCR. Be aware the gRPC Java team does not update the +# BCR for new releases, so you may need to create a PR for the BCR to add the +# version. To not use the BCR, you could use: +# +# git_override( +# module_name = "grpc-java", +# remote = "https://github.com/grpc/grpc-java.git", +# tag = "v", +# ) local_path_override( module_name = "grpc-java", path = "..", From 1937ef8debf453ea80f89f45edccf07b0db52c5a Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Fri, 19 Sep 2025 16:52:04 -0400 Subject: [PATCH 389/591] Start 1.77.0 development cycle (#12369) --- MODULE.bazel | 2 +- build.gradle | 2 +- .../src/test/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/test/golden/TestService.java.txt | 2 +- core/src/main/java/io/grpc/internal/GrpcUtil.java | 2 +- examples/MODULE.bazel | 2 +- examples/android/clientcache/app/build.gradle | 10 +++++----- examples/android/helloworld/app/build.gradle | 8 ++++---- examples/android/routeguide/app/build.gradle | 8 ++++---- examples/android/strictmode/app/build.gradle | 8 ++++---- examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 4 ++-- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 4 ++-- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 4 ++-- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 4 ++-- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 4 ++-- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- 34 files changed, 55 insertions(+), 55 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 4465f46e6c5..83ff9eaa539 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "grpc-java", compatibility_level = 0, repo_name = "io_grpc_grpc_java", - version = "1.76.0-SNAPSHOT", # CURRENT_GRPC_VERSION + version = "1.77.0-SNAPSHOT", # CURRENT_GRPC_VERSION ) # GRPC_DEPS_START diff --git a/build.gradle b/build.gradle index 366edf1b9e5..f80b27a4476 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.76.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.77.0-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index 22d3491be95..f42922c76b2 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.76.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.77.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index 22978149759..2e8d702a877 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.76.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.77.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index 2ad4a01d9e6..a8199ae5bef 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -219,7 +219,7 @@ public byte[] parseAsciiString(byte[] serialized) { public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); - public static final String IMPLEMENTATION_VERSION = "1.76.0-SNAPSHOT"; // CURRENT_GRPC_VERSION + public static final String IMPLEMENTATION_VERSION = "1.77.0-SNAPSHOT"; // CURRENT_GRPC_VERSION /** * The default timeout in nanos for a keepalive ping request. diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index 18d74d86973..7f639476b84 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,4 +1,4 @@ -bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.76.0-SNAPSHOT") # CURRENT_GRPC_VERSION +bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.77.0-SNAPSHOT") # CURRENT_GRPC_VERSION bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") bazel_dep(name = "rules_jvm_external", version = "6.0") diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index 8048eba73ab..d9b3f50a941 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -34,7 +34,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -54,11 +54,11 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.5' - testImplementation 'io.grpc:grpc-testing:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + testImplementation 'io.grpc:grpc-testing:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index 69519c8e4ab..81ea9235155 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,7 +52,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index a09de35e994..a0aef26cb61 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,7 +52,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index c0447a42af1..e1e6b8badc4 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -33,7 +33,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -53,7 +53,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/build.gradle b/examples/build.gradle index ddd8e7b3a65..f0965338b8f 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index 9f86adb0aeb..970e287f609 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index 3dc873e179b..b9baf5f3817 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' dependencies { diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index c4fce2d793b..4bb8b33f9ab 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -6,13 +6,13 @@ jar - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT example-debug https://github.com/grpc/grpc-java UTF-8 - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index 5d8c30b2127..8c32559a6dc 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' dependencies { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index b65beb84103..3561254ed0c 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -6,13 +6,13 @@ jar - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT example-dualstack https://github.com/grpc/grpc-java UTF-8 - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index befc2f7c0c7..3c3978603c7 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index 98bbebd114a..70d7ea56a5a 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,13 +6,13 @@ jar - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index abb2dd8a220..a1e767d02df 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' def openTelemetryVersion = '1.52.0' def openTelemetryPrometheusVersion = '1.52.0-alpha' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 08e00294452..605c93ea75f 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 94a85e8f8ab..19fe0816580 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' dependencies { diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index e93b36e39d1..3d9225d72da 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,13 +6,13 @@ jar - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index 28df68e4fc7..e1509f08b79 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index 4834cfa09ef..2a097386637 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,13 +7,13 @@ jar - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT 3.25.8 3.25.8 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 05a450dcc8d..38f105ec37f 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index a2f61e38467..30c0608c24a 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -7,13 +7,13 @@ jar - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT example-oauth https://github.com/grpc/grpc-java UTF-8 - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT 3.25.8 3.25.8 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index edcca46480e..46591b42c80 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' def openTelemetryVersion = '1.52.0' def openTelemetryPrometheusVersion = '1.52.0-alpha' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index f21c89ee0a4..3e3d141b5a1 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index a5bda680ef4..9c49388a946 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index d0f475f1833..ee891209000 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -15,7 +15,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index 0b3914febf3..e55a7ce7f33 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index 3932f4c32f4..202ad17d896 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,13 +6,13 @@ jar - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT example-tls https://github.com/grpc/grpc-java UTF-8 - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index 3914aa3fea0..bd040511b3e 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/pom.xml b/examples/pom.xml index 4da1eb14f90..2a6817a56a0 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,13 +6,13 @@ jar - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT examples https://github.com/grpc/grpc-java UTF-8 - 1.76.0-SNAPSHOT + 1.77.0-SNAPSHOT 3.25.8 3.25.8 From 9cddcb4a8ef55c455dd1e2f611d73e3b2400cb1b Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Mon, 22 Sep 2025 02:51:20 -0700 Subject: [PATCH 390/591] stub: remove interrupt checking from ThreadSafeThreadlessExecutor.drain (#12358) The only caller of `drain()` just reset the interrupt flag. So, we can avoid the whole exception dance. --- .../main/java/io/grpc/stub/BlockingClientCall.java | 14 +++----------- stub/src/main/java/io/grpc/stub/ClientCalls.java | 7 ++----- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/stub/src/main/java/io/grpc/stub/BlockingClientCall.java b/stub/src/main/java/io/grpc/stub/BlockingClientCall.java index 2c896c7208a..27bd42e53bb 100644 --- a/stub/src/main/java/io/grpc/stub/BlockingClientCall.java +++ b/stub/src/main/java/io/grpc/stub/BlockingClientCall.java @@ -273,7 +273,7 @@ public void halfClose() { */ @VisibleForTesting Status getClosedStatus() { - drainQuietly(); + executor.drain(); CloseState state = closeState.get(); return (state == null) ? null : state.status; } @@ -295,7 +295,7 @@ boolean isEitherReadOrWriteReady() { */ @VisibleForTesting boolean isReadReady() { - drainQuietly(); + executor.drain(); return !buffer.isEmpty(); } @@ -308,7 +308,7 @@ boolean isReadReady() { */ @VisibleForTesting boolean isWriteReady() { - drainQuietly(); + executor.drain(); return isWriteLegal() && call.isReady(); } @@ -325,14 +325,6 @@ ClientCall.Listener getListener() { return new QueuingListener(); } - private void drainQuietly() { - try { - executor.drain(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - private final class QueuingListener extends ClientCall.Listener { @Override public void onMessage(RespT value) { diff --git a/stub/src/main/java/io/grpc/stub/ClientCalls.java b/stub/src/main/java/io/grpc/stub/ClientCalls.java index 8cd31ea9cca..ff2804a0a1f 100644 --- a/stub/src/main/java/io/grpc/stub/ClientCalls.java +++ b/stub/src/main/java/io/grpc/stub/ClientCalls.java @@ -935,11 +935,8 @@ public void waitAndDrainWithTimeout(boolean waitForever, long end, } } - /** - * Executes all queued Runnables and if there were any wakes up any waiting threads. - */ - public void drain() throws InterruptedException { - throwIfInterrupted(); + /** Executes all queued Runnables and if there were any wakes up any waiting threads. */ + void drain() { Runnable runnable; boolean didWork = false; From d380191bed0dfccdb6f7432270979e42399cc4b7 Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Tue, 23 Sep 2025 20:08:38 -0400 Subject: [PATCH 391/591] Implement otel retry metrics (#12064) implements [A96](https://github.com/grpc/proposal/pull/488/files) --- .../main/java/io/grpc/ClientStreamTracer.java | 32 ++- .../java/io/grpc/internal/ClientCallImpl.java | 3 +- .../main/java/io/grpc/internal/GrpcUtil.java | 4 +- .../io/grpc/internal/ManagedChannelImpl.java | 7 +- .../java/io/grpc/internal/OobChannel.java | 3 +- .../io/grpc/internal/RetriableStream.java | 18 +- .../io/grpc/internal/SubchannelChannel.java | 3 +- .../io/grpc/internal/RetriableStreamTest.java | 3 +- .../grpc/opentelemetry/GrpcOpenTelemetry.java | 63 +++- .../OpenTelemetryMetricsModule.java | 76 ++++- .../OpenTelemetryMetricsResource.java | 19 ++ .../internal/OpenTelemetryConstants.java | 7 + .../OpenTelemetryMetricsModuleTest.java | 270 +++++++++++++++++- 13 files changed, 469 insertions(+), 39 deletions(-) diff --git a/api/src/main/java/io/grpc/ClientStreamTracer.java b/api/src/main/java/io/grpc/ClientStreamTracer.java index 2f366b7404c..42e1fdfebea 100644 --- a/api/src/main/java/io/grpc/ClientStreamTracer.java +++ b/api/src/main/java/io/grpc/ClientStreamTracer.java @@ -132,12 +132,15 @@ public static final class StreamInfo { private final CallOptions callOptions; private final int previousAttempts; private final boolean isTransparentRetry; + private final boolean isHedging; StreamInfo( - CallOptions callOptions, int previousAttempts, boolean isTransparentRetry) { + CallOptions callOptions, int previousAttempts, boolean isTransparentRetry, + boolean isHedging) { this.callOptions = checkNotNull(callOptions, "callOptions"); this.previousAttempts = previousAttempts; this.isTransparentRetry = isTransparentRetry; + this.isHedging = isHedging; } /** @@ -165,6 +168,15 @@ public boolean isTransparentRetry() { return isTransparentRetry; } + /** + * Whether the stream is hedging. + * + * @since 1.74.0 + */ + public boolean isHedging() { + return isHedging; + } + /** * Converts this StreamInfo into a new Builder. * @@ -174,7 +186,9 @@ public Builder toBuilder() { return new Builder() .setCallOptions(callOptions) .setPreviousAttempts(previousAttempts) - .setIsTransparentRetry(isTransparentRetry); + .setIsTransparentRetry(isTransparentRetry) + .setIsHedging(isHedging); + } /** @@ -192,6 +206,7 @@ public String toString() { .add("callOptions", callOptions) .add("previousAttempts", previousAttempts) .add("isTransparentRetry", isTransparentRetry) + .add("isHedging", isHedging) .toString(); } @@ -204,6 +219,7 @@ public static final class Builder { private CallOptions callOptions = CallOptions.DEFAULT; private int previousAttempts; private boolean isTransparentRetry; + private boolean isHedging; Builder() { } @@ -236,11 +252,21 @@ public Builder setIsTransparentRetry(boolean isTransparentRetry) { return this; } + /** + * Sets whether the stream is hedging. + * + * @since 1.74.0 + */ + public Builder setIsHedging(boolean isHedging) { + this.isHedging = isHedging; + return this; + } + /** * Builds a new StreamInfo. */ public StreamInfo build() { - return new StreamInfo(callOptions, previousAttempts, isTransparentRetry); + return new StreamInfo(callOptions, previousAttempts, isTransparentRetry, isHedging); } } } diff --git a/core/src/main/java/io/grpc/internal/ClientCallImpl.java b/core/src/main/java/io/grpc/internal/ClientCallImpl.java index b9e8dd79f09..4b24b1eae3d 100644 --- a/core/src/main/java/io/grpc/internal/ClientCallImpl.java +++ b/core/src/main/java/io/grpc/internal/ClientCallImpl.java @@ -250,7 +250,8 @@ public void runInContext() { stream = clientStreamProvider.newStream(method, callOptions, headers, context); } else { ClientStreamTracer[] tracers = - GrpcUtil.getClientStreamTracers(callOptions, headers, 0, false); + GrpcUtil.getClientStreamTracers(callOptions, headers, 0, + false, false); String deadlineName = contextIsDeadlineSource ? "Context" : "CallOptions"; Long nameResolutionDelay = callOptions.getOption(NAME_RESOLUTION_DELAYED); String description = String.format( diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index a8199ae5bef..a512fff6af2 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -759,13 +759,15 @@ public ListenableFuture getStats() { /** Gets stream tracers based on CallOptions. */ public static ClientStreamTracer[] getClientStreamTracers( - CallOptions callOptions, Metadata headers, int previousAttempts, boolean isTransparentRetry) { + CallOptions callOptions, Metadata headers, int previousAttempts, boolean isTransparentRetry, + boolean isHedging) { List factories = callOptions.getStreamTracerFactories(); ClientStreamTracer[] tracers = new ClientStreamTracer[factories.size() + 1]; StreamInfo streamInfo = StreamInfo.newBuilder() .setCallOptions(callOptions) .setPreviousAttempts(previousAttempts) .setIsTransparentRetry(isTransparentRetry) + .setIsHedging(isHedging) .build(); for (int i = 0; i < factories.size(); i++) { tracers[i] = factories.get(i).newClientStreamTracer(streamInfo, headers); diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 78c5181502f..849e4b8e45c 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -479,7 +479,8 @@ public ClientStream newStream( // the delayed transport or a real transport will go in-use and cancel the idle timer. if (!retryEnabled) { ClientStreamTracer[] tracers = GrpcUtil.getClientStreamTracers( - callOptions, headers, 0, /* isTransparentRetry= */ false); + callOptions, headers, 0, /* isTransparentRetry= */ false, + /* isHedging= */false); Context origContext = context.attach(); try { return delayedTransport.newStream(method, headers, callOptions, tracers); @@ -519,10 +520,10 @@ void postCommit() { @Override ClientStream newSubstream( Metadata newHeaders, ClientStreamTracer.Factory factory, int previousAttempts, - boolean isTransparentRetry) { + boolean isTransparentRetry, boolean isHedgedStream) { CallOptions newOptions = callOptions.withStreamTracerFactory(factory); ClientStreamTracer[] tracers = GrpcUtil.getClientStreamTracers( - newOptions, newHeaders, previousAttempts, isTransparentRetry); + newOptions, newHeaders, previousAttempts, isTransparentRetry, isHedgedStream); Context origContext = context.attach(); try { return delayedTransport.newStream(method, newHeaders, newOptions, tracers); diff --git a/core/src/main/java/io/grpc/internal/OobChannel.java b/core/src/main/java/io/grpc/internal/OobChannel.java index 01ef457460f..30c9f55e796 100644 --- a/core/src/main/java/io/grpc/internal/OobChannel.java +++ b/core/src/main/java/io/grpc/internal/OobChannel.java @@ -88,7 +88,8 @@ final class OobChannel extends ManagedChannel implements InternalInstrumented method, CallOptions callOptions, Metadata headers, Context context) { ClientStreamTracer[] tracers = GrpcUtil.getClientStreamTracers( - callOptions, headers, 0, /* isTransparentRetry= */ false); + callOptions, headers, 0, /* isTransparentRetry= */ false, + /* isHedging= */ false); Context origContext = context.attach(); // delayed transport's newStream() always acquires a lock, but concurrent performance doesn't // matter here because OOB communication should be sparse, and it's not on application RPC's diff --git a/core/src/main/java/io/grpc/internal/RetriableStream.java b/core/src/main/java/io/grpc/internal/RetriableStream.java index 6d33c2eb3e7..9fe8ab4ffff 100644 --- a/core/src/main/java/io/grpc/internal/RetriableStream.java +++ b/core/src/main/java/io/grpc/internal/RetriableStream.java @@ -245,7 +245,8 @@ private void commitAndRun(Substream winningSubstream) { // returns null means we should not create new sub streams, e.g. cancelled or // other close condition is met for retriableStream. @Nullable - private Substream createSubstream(int previousAttemptCount, boolean isTransparentRetry) { + private Substream createSubstream(int previousAttemptCount, boolean isTransparentRetry, + boolean isHedgedStream) { int inFlight; do { inFlight = inFlightSubStreams.get(); @@ -266,7 +267,8 @@ public ClientStreamTracer newClientStreamTracer( Metadata newHeaders = updateHeaders(headers, previousAttemptCount); // NOTICE: This set _must_ be done before stream.start() and it actually is. - sub.stream = newSubstream(newHeaders, tracerFactory, previousAttemptCount, isTransparentRetry); + sub.stream = newSubstream(newHeaders, tracerFactory, previousAttemptCount, isTransparentRetry, + isHedgedStream); return sub; } @@ -276,7 +278,7 @@ public ClientStreamTracer newClientStreamTracer( */ abstract ClientStream newSubstream( Metadata headers, ClientStreamTracer.Factory tracerFactory, int previousAttempts, - boolean isTransparentRetry); + boolean isTransparentRetry, boolean isHedgedStream); /** Adds grpc-previous-rpc-attempts in the headers of a retry/hedging RPC. */ @VisibleForTesting @@ -398,7 +400,7 @@ public final void start(ClientStreamListener listener) { state.buffer.add(new StartEntry()); } - Substream substream = createSubstream(0, false); + Substream substream = createSubstream(0, false, false); if (substream == null) { return; } @@ -471,7 +473,7 @@ public void run() { // If this run is not cancelled, the value of state.hedgingAttemptCount won't change // until state.addActiveHedge() is called subsequently, even the state could possibly // change. - Substream newSubstream = createSubstream(state.hedgingAttemptCount, false); + Substream newSubstream = createSubstream(state.hedgingAttemptCount, false, true); if (newSubstream == null) { return; } @@ -949,7 +951,8 @@ public void run() { || (rpcProgress == RpcProgress.REFUSED && noMoreTransparentRetry.compareAndSet(false, true))) { // transparent retry - final Substream newSubstream = createSubstream(substream.previousAttemptCount, true); + final Substream newSubstream = createSubstream(substream.previousAttemptCount, + true, false); if (newSubstream == null) { return; } @@ -1001,7 +1004,8 @@ public void run() { RetryPlan retryPlan = makeRetryDecision(status, trailers); if (retryPlan.shouldRetry) { // retry - Substream newSubstream = createSubstream(substream.previousAttemptCount + 1, false); + Substream newSubstream = createSubstream(substream.previousAttemptCount + 1, + false, false); if (newSubstream == null) { return; } diff --git a/core/src/main/java/io/grpc/internal/SubchannelChannel.java b/core/src/main/java/io/grpc/internal/SubchannelChannel.java index 773dcb99dd7..ced4272afe3 100644 --- a/core/src/main/java/io/grpc/internal/SubchannelChannel.java +++ b/core/src/main/java/io/grpc/internal/SubchannelChannel.java @@ -59,7 +59,8 @@ public ClientStream newStream(MethodDescriptor method, transport = notReadyTransport; } ClientStreamTracer[] tracers = GrpcUtil.getClientStreamTracers( - callOptions, headers, 0, /* isTransparentRetry= */ false); + callOptions, headers, 0, /* isTransparentRetry= */ false, + /* isHedging= */ false); Context origContext = context.attach(); try { return transport.newStream(method, headers, callOptions, tracers); diff --git a/core/src/test/java/io/grpc/internal/RetriableStreamTest.java b/core/src/test/java/io/grpc/internal/RetriableStreamTest.java index 9b1ec343bb7..afbdaa395b0 100644 --- a/core/src/test/java/io/grpc/internal/RetriableStreamTest.java +++ b/core/src/test/java/io/grpc/internal/RetriableStreamTest.java @@ -186,7 +186,8 @@ ClientStream newSubstream( Metadata metadata, ClientStreamTracer.Factory tracerFactory, int previousAttempts, - boolean isTransparentRetry) { + boolean isTransparentRetry, + boolean isHedgedStream) { bufferSizeTracer = tracerFactory.newClientStreamTracer(STREAM_INFO, metadata); int actualPreviousRpcAttemptsInHeader = metadata.get(GRPC_PREVIOUS_RPC_ATTEMPTS) == null diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java index 6b257a18a6c..e0af0f80ed3 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java @@ -18,8 +18,11 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.internal.GrpcUtil.IMPLEMENTATION_VERSION; +import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.HEDGE_BUCKETS; import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.LATENCY_BUCKETS; +import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.RETRY_BUCKETS; import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.SIZE_BUCKETS; +import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.TRANSPARENT_RETRY_BUCKETS; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; @@ -64,8 +67,8 @@ public Stopwatch get() { }; @VisibleForTesting - static boolean ENABLE_OTEL_TRACING = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_ENABLE_OTEL_TRACING", - false); + static boolean ENABLE_OTEL_TRACING = + GrpcUtil.getFlag("GRPC_EXPERIMENTAL_ENABLE_OTEL_TRACING", false); private final OpenTelemetry openTelemetrySdk; private final MeterProvider meterProvider; @@ -241,6 +244,54 @@ static OpenTelemetryMetricsResource createMetricInstruments(Meter meter, .build()); } + if (isMetricEnabled("grpc.client.call.retries", enableMetrics, disableDefault)) { + builder.clientCallRetriesCounter( + meter.histogramBuilder( + "grpc.client.call.retries") + .setUnit("{retry}") + .setDescription("Number of retries during the client call. " + + "If there were no retries, 0 is not reported.") + .ofLongs() + .setExplicitBucketBoundariesAdvice(RETRY_BUCKETS) + .build()); + } + + if (isMetricEnabled("grpc.client.call.transparent_retries", enableMetrics, + disableDefault)) { + builder.clientCallTransparentRetriesCounter( + meter.histogramBuilder( + "grpc.client.call.transparent_retries") + .setUnit("{transparent_retry}") + .setDescription("Number of transparent retries during the client call. " + + "If there were no transparent retries, 0 is not reported.") + .ofLongs() + .setExplicitBucketBoundariesAdvice(TRANSPARENT_RETRY_BUCKETS) + .build()); + } + + if (isMetricEnabled("grpc.client.call.hedges", enableMetrics, disableDefault)) { + builder.clientCallHedgesCounter( + meter.histogramBuilder( + "grpc.client.call.hedges") + .setUnit("{hedge}") + .setDescription("Number of hedges during the client call. " + + "If there were no hedges, 0 is not reported.") + .ofLongs() + .setExplicitBucketBoundariesAdvice(HEDGE_BUCKETS) + .build()); + } + + if (isMetricEnabled("grpc.client.call.retry_delay", enableMetrics, disableDefault)) { + builder.clientCallRetryDelayCounter( + meter.histogramBuilder( + "grpc.client.call.retry_delay") + .setUnit("s") + .setDescription("Total time of delay while there is no active attempt during the " + + "client call") + .setExplicitBucketBoundariesAdvice(LATENCY_BUCKETS) + .build()); + } + if (isMetricEnabled("grpc.server.call.started", enableMetrics, disableDefault)) { builder.serverCallCountCounter( meter.counterBuilder("grpc.server.call.started") @@ -259,8 +310,8 @@ static OpenTelemetryMetricsResource createMetricInstruments(Meter meter, .build()); } - if (isMetricEnabled("grpc.server.call.sent_total_compressed_message_size", enableMetrics, - disableDefault)) { + if (isMetricEnabled("grpc.server.call.sent_total_compressed_message_size", + enableMetrics, disableDefault)) { builder.serverTotalSentCompressedMessageSizeCounter( meter.histogramBuilder( "grpc.server.call.sent_total_compressed_message_size") @@ -271,8 +322,8 @@ static OpenTelemetryMetricsResource createMetricInstruments(Meter meter, .build()); } - if (isMetricEnabled("grpc.server.call.rcvd_total_compressed_message_size", enableMetrics, - disableDefault)) { + if (isMetricEnabled("grpc.server.call.rcvd_total_compressed_message_size", + enableMetrics, disableDefault)) { builder.serverTotalReceivedCompressedMessageSizeCounter( meter.histogramBuilder( "grpc.server.call.rcvd_total_compressed_message_size") diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java index 82ad41e7b20..1d77f9ee3e4 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java @@ -285,16 +285,19 @@ void recordFinishedAttempt() { static final class CallAttemptsTracerFactory extends ClientStreamTracer.Factory { private final OpenTelemetryMetricsModule module; private final String target; - private final Stopwatch attemptStopwatch; + private final Stopwatch attemptDelayStopwatch; private final Stopwatch callStopWatch; @GuardedBy("lock") private boolean callEnded; private final String fullMethodName; private final List callPlugins; private Status status; + private long retryDelayNanos; private long callLatencyNanos; private final Object lock = new Object(); private final AtomicLong attemptsPerCall = new AtomicLong(); + private final AtomicLong hedgedAttemptsPerCall = new AtomicLong(); + private final AtomicLong transparentRetriesPerCall = new AtomicLong(); @GuardedBy("lock") private int activeStreams; @GuardedBy("lock") @@ -309,7 +312,7 @@ static final class CallAttemptsTracerFactory extends ClientStreamTracer.Factory this.target = checkNotNull(target, "target"); this.fullMethodName = checkNotNull(fullMethodName, "fullMethodName"); this.callPlugins = checkNotNull(callPlugins, "callPlugins"); - this.attemptStopwatch = module.stopwatchSupplier.get(); + this.attemptDelayStopwatch = module.stopwatchSupplier.get(); this.callStopWatch = module.stopwatchSupplier.get().start(); io.opentelemetry.api.common.Attributes attribute = io.opentelemetry.api.common.Attributes.of( @@ -329,8 +332,9 @@ public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata metada // This can be the case when the call is cancelled but a retry attempt is created. return new ClientStreamTracer() {}; } - if (++activeStreams == 1 && attemptStopwatch.isRunning()) { - attemptStopwatch.stop(); + if (++activeStreams == 1 && attemptDelayStopwatch.isRunning()) { + attemptDelayStopwatch.stop(); + retryDelayNanos = attemptDelayStopwatch.elapsed(TimeUnit.NANOSECONDS); } } // Skip recording for the first time, since it is already recorded in @@ -344,7 +348,11 @@ public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata metada module.resource.clientAttemptCountCounter().add(1, attribute); } } - if (!info.isTransparentRetry()) { + if (info.isTransparentRetry()) { + transparentRetriesPerCall.incrementAndGet(); + } else if (info.isHedging()) { + hedgedAttemptsPerCall.incrementAndGet(); + } else { attemptsPerCall.incrementAndGet(); } return newClientTracer(info); @@ -367,7 +375,7 @@ void attemptEnded() { boolean shouldRecordFinishedCall = false; synchronized (lock) { if (--activeStreams == 0) { - attemptStopwatch.start(); + attemptDelayStopwatch.start(); if (callEnded && !finishedCallToBeRecorded) { shouldRecordFinishedCall = true; finishedCallToBeRecorded = true; @@ -402,19 +410,61 @@ void callEnded(Status status) { void recordFinishedCall() { if (attemptsPerCall.get() == 0) { ClientTracer tracer = newClientTracer(null); - tracer.attemptNanos = attemptStopwatch.elapsed(TimeUnit.NANOSECONDS); + tracer.attemptNanos = attemptDelayStopwatch.elapsed(TimeUnit.NANOSECONDS); tracer.statusCode = status.getCode(); tracer.recordFinishedAttempt(); } callLatencyNanos = callStopWatch.elapsed(TimeUnit.NANOSECONDS); - io.opentelemetry.api.common.Attributes attribute = - io.opentelemetry.api.common.Attributes.of(METHOD_KEY, fullMethodName, - TARGET_KEY, target, - STATUS_KEY, status.getCode().toString()); + // Base attributes + io.opentelemetry.api.common.Attributes baseAttributes = + io.opentelemetry.api.common.Attributes.of( + METHOD_KEY, fullMethodName, + TARGET_KEY, target + ); + + // Duration if (module.resource.clientCallDurationCounter() != null) { - module.resource.clientCallDurationCounter() - .record(callLatencyNanos * SECONDS_PER_NANO, attribute); + module.resource.clientCallDurationCounter().record( + callLatencyNanos * SECONDS_PER_NANO, + baseAttributes.toBuilder() + .put(STATUS_KEY, status.getCode().toString()) + .build() + ); + } + + // Retry counts + if (module.resource.clientCallRetriesCounter() != null) { + long retriesPerCall = Math.max(attemptsPerCall.get() - 1, 0); + if (retriesPerCall > 0) { + module.resource.clientCallRetriesCounter().record(retriesPerCall, baseAttributes); + } + } + + // Hedge counts + if (module.resource.clientCallHedgesCounter() != null) { + long hedges = hedgedAttemptsPerCall.get(); + if (hedges > 0) { + module.resource.clientCallHedgesCounter() + .record(hedges, baseAttributes); + } + } + + // Transparent Retry counts + if (module.resource.clientCallTransparentRetriesCounter() != null) { + long transparentRetries = transparentRetriesPerCall.get(); + if (transparentRetries > 0) { + module.resource.clientCallTransparentRetriesCounter().record( + transparentRetries, baseAttributes); + } + } + + // Retry delay + if (module.resource.clientCallRetryDelayCounter() != null) { + module.resource.clientCallRetryDelayCounter().record( + retryDelayNanos * SECONDS_PER_NANO, + baseAttributes + ); } } } diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsResource.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsResource.java index e519b7e1eb6..d32ae1e67f5 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsResource.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsResource.java @@ -41,6 +41,17 @@ abstract class OpenTelemetryMetricsResource { @Nullable abstract LongHistogram clientTotalReceivedCompressedMessageSizeCounter(); + @Nullable + abstract LongHistogram clientCallRetriesCounter(); + + @Nullable + abstract LongHistogram clientCallTransparentRetriesCounter(); + + @Nullable + abstract LongHistogram clientCallHedgesCounter(); + + @Nullable + abstract DoubleHistogram clientCallRetryDelayCounter(); /* Server Metrics */ @Nullable @@ -73,6 +84,14 @@ abstract static class Builder { abstract Builder clientTotalReceivedCompressedMessageSizeCounter( LongHistogram counter); + abstract Builder clientCallRetriesCounter(LongHistogram counter); + + abstract Builder clientCallTransparentRetriesCounter(LongHistogram counter); + + abstract Builder clientCallHedgesCounter(LongHistogram counter); + + abstract Builder clientCallRetryDelayCounter(DoubleHistogram counter); + abstract Builder serverCallCountCounter(LongCounter counter); abstract Builder serverCallDurationCounter(DoubleHistogram counter); diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java index ef21903c8e7..ff2b88acbfd 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java @@ -55,6 +55,13 @@ public final class OpenTelemetryConstants { 0L, 1024L, 2048L, 4096L, 16384L, 65536L, 262144L, 1048576L, 4194304L, 16777216L, 67108864L, 268435456L, 1073741824L, 4294967296L); + public static final List RETRY_BUCKETS = ImmutableList.of(1L, 2L, 3L, 4L, 5L); + + public static final List TRANSPARENT_RETRY_BUCKETS = + ImmutableList.of(1L, 2L, 3L, 4L, 5L, 10L); + + public static final List HEDGE_BUCKETS = ImmutableList.of(1L, 2L, 3L, 4L, 5L); + private OpenTelemetryConstants() { } } diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java index 77f0268ec2f..6d1234497d6 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java @@ -54,11 +54,14 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; @@ -94,6 +97,11 @@ public class OpenTelemetryMetricsModuleTest { private static final String CLIENT_ATTEMPT_RECV_TOTAL_COMPRESSED_MESSAGE_SIZE = "grpc.client.attempt.rcvd_total_compressed_message_size"; private static final String CLIENT_CALL_DURATION = "grpc.client.call.duration"; + private static final String CLIENT_CALL_RETRIES = "grpc.client.call.retries"; + private static final String CLIENT_CALL_TRANSPARENT_RETRIES = + "grpc.client.call.transparent_retries"; + private static final String CLIENT_CALL_HEDGES = "grpc.client.call.hedges"; + private static final String CLIENT_CALL_RETRY_DELAY = "grpc.client.call.retry_delay"; private static final String SERVER_CALL_COUNT = "grpc.server.call.started"; private static final String SERVER_CALL_DURATION = "grpc.server.call.duration"; private static final String SERVER_CALL_SENT_TOTAL_COMPRESSED_MESSAGE_SIZE @@ -194,7 +202,7 @@ public ServerCall.Listener startCall( }).build()); final AtomicReference capturedCallOptions = new AtomicReference<>(); - ClientInterceptor callOptionsCatureInterceptor = new ClientInterceptor() { + ClientInterceptor callOptionsCaptureInterceptor = new ClientInterceptor() { @Override public ClientCall interceptCall( MethodDescriptor method, CallOptions callOptions, Channel next) { @@ -204,7 +212,7 @@ public ClientCall interceptCall( }; Channel interceptedChannel = ClientInterceptors.intercept( - grpcServerRule.getChannel(), callOptionsCatureInterceptor, + grpcServerRule.getChannel(), callOptionsCaptureInterceptor, module.getClientInterceptor("target:///")); ClientCall call; call = interceptedChannel.newCall(method, CALL_OPTIONS); @@ -378,6 +386,88 @@ public void clientBasicMetrics() { .hasBucketCounts(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)))); + + assertThat(openTelemetryTesting.getMetrics()) + .extracting("name") + .doesNotContain( + CLIENT_CALL_RETRIES, + CLIENT_CALL_TRANSPARENT_RETRIES, + CLIENT_CALL_HEDGES, + CLIENT_CALL_RETRY_DELAY); + } + + @Test + public void clientBasicMetrics_withRetryMetricsEnabled_shouldRecordZeroOrBeAbsent() { + // Explicitly enable the retry metrics + Map enabledMetrics = ImmutableMap.of( + CLIENT_CALL_RETRIES, true, + CLIENT_CALL_TRANSPARENT_RETRIES, true, + CLIENT_CALL_HEDGES, true, + CLIENT_CALL_RETRY_DELAY, true + ); + + String target = "target:///"; + OpenTelemetryMetricsResource resource = GrpcOpenTelemetry.createMetricInstruments(testMeter, + enabledMetrics, disableDefaultMetrics); + OpenTelemetryMetricsModule module = newOpenTelemetryMetricsModule(resource); + OpenTelemetryMetricsModule.CallAttemptsTracerFactory callAttemptsTracerFactory = + new CallAttemptsTracerFactory(module, target, method.getFullMethodName(), emptyList()); + ClientStreamTracer tracer = + callAttemptsTracerFactory.newClientStreamTracer(STREAM_INFO, new Metadata()); + + fakeClock.forwardTime(30, TimeUnit.MILLISECONDS); + tracer.outboundHeaders(); + fakeClock.forwardTime(100, TimeUnit.MILLISECONDS); + tracer.outboundMessage(0); + tracer.streamClosed(Status.OK); + callAttemptsTracerFactory.callEnded(Status.OK); + + io.opentelemetry.api.common.Attributes finalAttributes + = io.opentelemetry.api.common.Attributes.of( + TARGET_KEY, target, + METHOD_KEY, method.getFullMethodName()); + + assertThat(openTelemetryTesting.getMetrics()) + .satisfiesExactlyInAnyOrder( + metric -> assertThat(metric).hasName(CLIENT_ATTEMPT_COUNT_INSTRUMENT_NAME), + metric -> assertThat(metric).hasName(CLIENT_ATTEMPT_DURATION_INSTRUMENT_NAME), + metric -> assertThat(metric).hasName(CLIENT_ATTEMPT_SENT_TOTAL_COMPRESSED_MESSAGE_SIZE), + metric -> assertThat(metric).hasName(CLIENT_ATTEMPT_RECV_TOTAL_COMPRESSED_MESSAGE_SIZE), + metric -> assertThat(metric).hasName(CLIENT_CALL_DURATION), + metric -> assertThat(metric) + .hasName(CLIENT_CALL_RETRY_DELAY) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0) + .hasCount(1) + .hasAttributes(finalAttributes))) + + ); + + List optionalMetricNames = Arrays.asList( + CLIENT_CALL_RETRIES, + CLIENT_CALL_TRANSPARENT_RETRIES, + CLIENT_CALL_HEDGES); + + for (String metricName : optionalMetricNames) { + Optional metric = openTelemetryTesting.getMetrics().stream() + .filter(m -> m.getName().equals(metricName)) + .findFirst(); + if (metric.isPresent()) { + assertThat(metric.get()) + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(0) + .hasCount(1) + .hasAttributes(finalAttributes))); + } + } } // This test is only unit-testing the metrics recording logic. The retry behavior is faked. @@ -831,6 +921,182 @@ public void recordAttemptMetrics() { .hasBucketBoundaries(sizeBuckets)))); } + @Test + public void recordAttemptMetrics_withRetryMetricsEnabled() { + Map enabledMetrics = ImmutableMap.of( + CLIENT_CALL_RETRIES, true, + CLIENT_CALL_TRANSPARENT_RETRIES, true, + CLIENT_CALL_HEDGES, true, + CLIENT_CALL_RETRY_DELAY, true + ); + + String target = "dns:///example.com"; + OpenTelemetryMetricsResource resource = GrpcOpenTelemetry.createMetricInstruments(testMeter, + enabledMetrics, disableDefaultMetrics); + OpenTelemetryMetricsModule module = newOpenTelemetryMetricsModule(resource); + OpenTelemetryMetricsModule.CallAttemptsTracerFactory callAttemptsTracerFactory = + new OpenTelemetryMetricsModule.CallAttemptsTracerFactory(module, target, + method.getFullMethodName(), emptyList()); + + ClientStreamTracer tracer = + callAttemptsTracerFactory.newClientStreamTracer(STREAM_INFO, new Metadata()); + fakeClock.forwardTime(154, TimeUnit.MILLISECONDS); + tracer.streamClosed(Status.UNAVAILABLE); + + fakeClock.forwardTime(1000, TimeUnit.MILLISECONDS); + tracer = callAttemptsTracerFactory.newClientStreamTracer(STREAM_INFO, new Metadata()); + fakeClock.forwardTime(100, TimeUnit.MILLISECONDS); + tracer.streamClosed(Status.NOT_FOUND); + + fakeClock.forwardTime(10, TimeUnit.MILLISECONDS); + tracer = callAttemptsTracerFactory.newClientStreamTracer( + STREAM_INFO.toBuilder().setIsTransparentRetry(true).build(), new Metadata()); + fakeClock.forwardTime(32, MILLISECONDS); + tracer.streamClosed(Status.UNAVAILABLE); + + fakeClock.forwardTime(10, MILLISECONDS); + tracer = callAttemptsTracerFactory.newClientStreamTracer( + STREAM_INFO.toBuilder().setIsTransparentRetry(true).build(), new Metadata()); + tracer.inboundWireSize(33); + fakeClock.forwardTime(24, MILLISECONDS); + tracer.streamClosed(Status.OK); // RPC succeeded + + // --- The overall call ends --- + callAttemptsTracerFactory.callEnded(Status.OK); + + // Define attributes for assertions + io.opentelemetry.api.common.Attributes finalAttributes + = io.opentelemetry.api.common.Attributes.of( + TARGET_KEY, target, + METHOD_KEY, method.getFullMethodName()); + + // FINAL ASSERTION BLOCK + assertThat(openTelemetryTesting.getMetrics()) + .satisfiesExactlyInAnyOrder( + // Default metrics + metric -> assertThat(metric).hasName(CLIENT_ATTEMPT_COUNT_INSTRUMENT_NAME), + metric -> assertThat(metric).hasName(CLIENT_ATTEMPT_DURATION_INSTRUMENT_NAME), + metric -> assertThat(metric).hasName(CLIENT_ATTEMPT_SENT_TOTAL_COMPRESSED_MESSAGE_SIZE), + metric -> assertThat(metric).hasName(CLIENT_ATTEMPT_RECV_TOTAL_COMPRESSED_MESSAGE_SIZE), + metric -> assertThat(metric).hasName(CLIENT_CALL_DURATION), + + // --- Assertions for the retry metrics --- + metric -> assertThat(metric) + .hasName(CLIENT_CALL_RETRIES) + .hasUnit("{retry}") + .hasHistogramSatisfying(histogram -> histogram.hasPointsSatisfying( + point -> point + .hasCount(1) + .hasSum(1) // We faked one standard retry + .hasAttributes(finalAttributes))), + metric -> assertThat(metric) + .hasName(CLIENT_CALL_TRANSPARENT_RETRIES) + .hasUnit("{transparent_retry}") + .hasHistogramSatisfying(histogram -> histogram.hasPointsSatisfying( + point -> point + .hasCount(1) + .hasSum(2) // We faked two transparent retries + .hasAttributes(finalAttributes))), + metric -> assertThat(metric) + .hasName(CLIENT_CALL_RETRY_DELAY) + .hasUnit("s") + .hasHistogramSatisfying(histogram -> histogram.hasPointsSatisfying( + point -> point + .hasCount(1) + .hasSum(1.02) // 1000ms + 10ms + 10ms + .hasAttributes(finalAttributes))) + ); + } + + @Test + public void recordAttemptMetrics_withHedgedCalls() { + // Enable the retry metrics, including hedges + Map enabledMetrics = ImmutableMap.of( + CLIENT_CALL_RETRIES, true, + CLIENT_CALL_TRANSPARENT_RETRIES, true, + CLIENT_CALL_HEDGES, true, + CLIENT_CALL_RETRY_DELAY, true + ); + + String target = "dns:///example.com"; + OpenTelemetryMetricsResource resource = GrpcOpenTelemetry.createMetricInstruments(testMeter, + enabledMetrics, disableDefaultMetrics); + OpenTelemetryMetricsModule module = newOpenTelemetryMetricsModule(resource); + OpenTelemetryMetricsModule.CallAttemptsTracerFactory callAttemptsTracerFactory = + new OpenTelemetryMetricsModule.CallAttemptsTracerFactory(module, target, + method.getFullMethodName(), emptyList()); + + // Create a StreamInfo specifically for hedged attempts + final ClientStreamTracer.StreamInfo hedgedStreamInfo = + STREAM_INFO.toBuilder().setIsHedging(true).build(); + + // --- First attempt starts --- + ClientStreamTracer tracer = + callAttemptsTracerFactory.newClientStreamTracer(STREAM_INFO, new Metadata()); + + // --- Faking a hedged attempt --- + fakeClock.forwardTime(10, TimeUnit.MILLISECONDS); // Hedging delay + ClientStreamTracer hedgeTracer1 = + callAttemptsTracerFactory.newClientStreamTracer(hedgedStreamInfo, new Metadata()); + + // --- Faking a second hedged attempt --- + fakeClock.forwardTime(20, TimeUnit.MILLISECONDS); // Another hedging delay + ClientStreamTracer hedgeTracer2 = + callAttemptsTracerFactory.newClientStreamTracer(hedgedStreamInfo, new Metadata()); + + // --- Let the attempts resolve --- + fakeClock.forwardTime(50, TimeUnit.MILLISECONDS); + // Initial attempt is cancelled because a hedge will succeed + tracer.streamClosed(Status.CANCELLED); + hedgeTracer1.streamClosed(Status.UNAVAILABLE); // First hedge fails + + fakeClock.forwardTime(30, TimeUnit.MILLISECONDS); + hedgeTracer2.streamClosed(Status.OK); // Second hedge succeeds + + // --- The overall call ends --- + callAttemptsTracerFactory.callEnded(Status.OK); + + // Define attributes for assertions + io.opentelemetry.api.common.Attributes finalAttributes + = io.opentelemetry.api.common.Attributes.of( + TARGET_KEY, target, + METHOD_KEY, method.getFullMethodName()); + + // FINAL ASSERTION BLOCK + // We expect 7 metrics: 5 default + hedges + retry_delay. + // Retries and transparent_retries are 0 and will not be reported. + assertThat(openTelemetryTesting.getMetrics()) + .satisfiesExactlyInAnyOrder( + // Default metrics + metric -> assertThat(metric).hasName(CLIENT_ATTEMPT_COUNT_INSTRUMENT_NAME), + metric -> assertThat(metric).hasName(CLIENT_ATTEMPT_DURATION_INSTRUMENT_NAME), + metric -> assertThat(metric).hasName(CLIENT_ATTEMPT_SENT_TOTAL_COMPRESSED_MESSAGE_SIZE), + metric -> assertThat(metric).hasName(CLIENT_ATTEMPT_RECV_TOTAL_COMPRESSED_MESSAGE_SIZE), + metric -> assertThat(metric).hasName(CLIENT_CALL_DURATION), + + // --- Assertions for the NEW metrics --- + metric -> assertThat(metric) + .hasName(CLIENT_CALL_HEDGES) + .hasUnit("{hedge}") + .hasHistogramSatisfying(histogram -> histogram.hasPointsSatisfying( + point -> point + .hasCount(1) + .hasSum(2) + .hasAttributes(finalAttributes))), + metric -> assertThat(metric) + .hasName(CLIENT_CALL_RETRY_DELAY) + .hasUnit("s") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasCount(1) + .hasSum(0) + .hasAttributes(finalAttributes))) + ); + } + @Test public void clientStreamNeverCreatedStillRecordMetrics() { String target = "dns:///foo.example.com"; From 55aefd5b8e76a15861c5988d1d8ced0a58160303 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Thu, 25 Sep 2025 11:03:54 +0530 Subject: [PATCH 392/591] compiler: Protobuf upgrade to 26.1 (#12330) --- buildscripts/kokoro/windows32.bat | 2 +- buildscripts/kokoro/windows64.bat | 2 +- buildscripts/make_dependencies.bat | 6 +++--- buildscripts/make_dependencies.sh | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/buildscripts/kokoro/windows32.bat b/buildscripts/kokoro/windows32.bat index f87899d0ab8..2b925c3095c 100644 --- a/buildscripts/kokoro/windows32.bat +++ b/buildscripts/kokoro/windows32.bat @@ -25,7 +25,7 @@ cd "%WORKSPACE%" SET TARGET_ARCH=x86_32 SET FAIL_ON_WARNINGS=true -SET PROTOBUF_VER=22.5 +SET PROTOBUF_VER=26.1 SET PKG_CONFIG_PATH=%ESCWORKSPACE%\\grpc-java-helper32\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\lib\\pkgconfig SET VC_PROTOBUF_LIBS=/LIBPATH:%ESCWORKSPACE%\\grpc-java-helper32\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\lib SET VC_PROTOBUF_INCLUDE=%ESCWORKSPACE%\\grpc-java-helper32\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\include diff --git a/buildscripts/kokoro/windows64.bat b/buildscripts/kokoro/windows64.bat index 0a99f47dd3d..0bed4612bcf 100644 --- a/buildscripts/kokoro/windows64.bat +++ b/buildscripts/kokoro/windows64.bat @@ -24,7 +24,7 @@ cd "%WORKSPACE%" SET TARGET_ARCH=x86_64 SET FAIL_ON_WARNINGS=true -SET PROTOBUF_VER=22.5 +SET PROTOBUF_VER=26.1 SET PKG_CONFIG_PATH=%ESCWORKSPACE%\\grpc-java-helper64\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\lib\\pkgconfig SET VC_PROTOBUF_LIBS=/LIBPATH:%ESCWORKSPACE%\\grpc-java-helper64\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\lib SET VC_PROTOBUF_INCLUDE=%ESCWORKSPACE%\\grpc-java-helper64\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\include diff --git a/buildscripts/make_dependencies.bat b/buildscripts/make_dependencies.bat index dce08ef7624..adb7a1fbcbc 100644 --- a/buildscripts/make_dependencies.bat +++ b/buildscripts/make_dependencies.bat @@ -1,8 +1,8 @@ choco install -y pkgconfiglite choco install -y openjdk --version=17.0 set PATH=%PATH%;"c:\Program Files\OpenJDK\jdk-17\bin" -set PROTOBUF_VER=22.5 -set ABSL_VERSION=20230125.4 +set PROTOBUF_VER=26.1 +set ABSL_VERSION=20250127.1 set CMAKE_NAME=cmake-3.26.3-windows-x86_64 if not exist "protobuf-%PROTOBUF_VER%\build\Release\" ( @@ -51,7 +51,7 @@ for /f "tokens=4 delims=\" %%a in ("%VCINSTALLDIR%") do ( for /f "tokens=1 delims=." %%a in ("%VisualStudioVersion%") do ( SET visual_studio_major_version=%%a ) -cmake -Dprotobuf_BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX=%cd%\protobuf-%PROTOBUF_VER% -DCMAKE_PREFIX_PATH=%cd%\protobuf-%PROTOBUF_VER% -G "Visual Studio %visual_studio_major_version% %VC_YEAR%" %CMAKE_VSARCH% .. || exit /b 1 +cmake -DABSL_MSVC_STATIC_RUNTIME=ON -Dprotobuf_BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX=%cd%\protobuf-%PROTOBUF_VER% -DCMAKE_PREFIX_PATH=%cd%\protobuf-%PROTOBUF_VER% -G "Visual Studio %visual_studio_major_version% %VC_YEAR%" %CMAKE_VSARCH% .. || exit /b 1 cmake --build . --config Release --target install || exit /b 1 popd goto :eof diff --git a/buildscripts/make_dependencies.sh b/buildscripts/make_dependencies.sh index 73cb54b7b68..7dc94299daf 100755 --- a/buildscripts/make_dependencies.sh +++ b/buildscripts/make_dependencies.sh @@ -3,8 +3,8 @@ # Build protoc set -evux -o pipefail -PROTOBUF_VERSION=22.5 -ABSL_VERSION=20230125.4 +PROTOBUF_VERSION=26.1 +ABSL_VERSION=20250127.1 CMAKE_VERSION=3.26.3 # ARCH is x86_64 bit unless otherwise specified. From 63fdaaccc025c243dd9f32f40e0f254d3ad2484a Mon Sep 17 00:00:00 2001 From: Kannan J Date: Thu, 25 Sep 2025 12:12:05 +0530 Subject: [PATCH 393/591] xds: SslContext updates handling when using system root certs (#12340) Fixes the issues in SslContext not updated when using system root certs because it was using `CertProviderClientSslContextProvider` that relies on watcher updates from the certificate file watcher. This change creates a separate handler for system root certs and updates the SslContext on the `SslContextProvider` callback as soon as the provider is created. --- .../CertProviderClientSslContextProvider.java | 23 ++-- .../CertProviderSslContextProvider.java | 67 ++++++---- .../certprovider/IgnoreUpdatesWatcher.java | 68 ++++++++++ .../SystemRootCertificateProvider.java | 71 ++++++++++ .../grpc/xds/XdsSecurityClientServerTest.java | 78 +++++++++-- .../ClientSslContextProviderFactoryTest.java | 32 +++-- .../ServerSslContextProviderFactoryTest.java | 16 +-- ...tProviderClientSslContextProviderTest.java | 124 +++++++++++++++--- 8 files changed, 399 insertions(+), 80 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/security/certprovider/IgnoreUpdatesWatcher.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/security/certprovider/SystemRootCertificateProvider.java diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java index d7c2267c48f..131ae6b6125 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java @@ -55,20 +55,19 @@ protected final SslContextBuilder getSslContextBuilder( CertificateValidationContext certificateValidationContextdationContext) throws CertStoreException { SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient(); - // Null rootCertInstance implies hasSystemRootCerts because of the check in - // CertProviderClientSslContextProviderFactory. - if (rootCertInstance != null) { - if (savedSpiffeTrustMap != null) { - sslContextBuilder = sslContextBuilder.trustManager( + if (savedSpiffeTrustMap != null) { + sslContextBuilder = sslContextBuilder.trustManager( + new XdsTrustManagerFactory( + savedSpiffeTrustMap, + certificateValidationContextdationContext)); + } else if (savedTrustedRoots != null) { + sslContextBuilder = sslContextBuilder.trustManager( new XdsTrustManagerFactory( - savedSpiffeTrustMap, + savedTrustedRoots.toArray(new X509Certificate[0]), certificateValidationContextdationContext)); - } else { - sslContextBuilder = sslContextBuilder.trustManager( - new XdsTrustManagerFactory( - savedTrustedRoots.toArray(new X509Certificate[0]), - certificateValidationContextdationContext)); - } + } else { + // Should be impossible because of the check in CertProviderClientSslContextProviderFactory + throw new IllegalStateException("There must be trusted roots or a SPIFFE trust map"); } if (isMtls()) { sslContextBuilder.keyManager(savedKey, savedCertChain); diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java index 801dabeecb7..2570dcb731b 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java @@ -24,6 +24,7 @@ import io.grpc.xds.client.Bootstrapper.CertificateProviderInfo; import io.grpc.xds.internal.security.CommonTlsContextUtil; import io.grpc.xds.internal.security.DynamicSslContextProvider; +import java.io.Closeable; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.List; @@ -34,8 +35,8 @@ abstract class CertProviderSslContextProvider extends DynamicSslContextProvider implements CertificateProvider.Watcher { - @Nullable private final CertificateProviderStore.Handle certHandle; - @Nullable private final CertificateProviderStore.Handle rootCertHandle; + @Nullable private final NoExceptionCloseable certHandle; + @Nullable private final NoExceptionCloseable rootCertHandle; @Nullable private final CertificateProviderInstance certInstance; @Nullable protected final CertificateProviderInstance rootCertInstance; @Nullable protected PrivateKey savedKey; @@ -55,24 +56,33 @@ protected CertProviderSslContextProvider( super(tlsContext, staticCertValidationContext); this.certInstance = certInstance; this.rootCertInstance = rootCertInstance; - String certInstanceName = null; - if (certInstance != null && certInstance.isInitialized()) { - certInstanceName = certInstance.getInstanceName(); + this.isUsingSystemRootCerts = rootCertInstance == null + && CommonTlsContextUtil.isUsingSystemRootCerts(tlsContext.getCommonTlsContext()); + boolean createCertInstance = certInstance != null && certInstance.isInitialized(); + boolean createRootCertInstance = rootCertInstance != null && rootCertInstance.isInitialized(); + boolean sharedCertInstance = createCertInstance && createRootCertInstance + && rootCertInstance.getInstanceName().equals(certInstance.getInstanceName()); + if (createCertInstance) { CertificateProviderInfo certProviderInstanceConfig = - getCertProviderConfig(certProviders, certInstanceName); + getCertProviderConfig(certProviders, certInstance.getInstanceName()); + CertificateProvider.Watcher watcher = this; + if (!sharedCertInstance && !isUsingSystemRootCerts) { + watcher = new IgnoreUpdatesWatcher(watcher, /* ignoreRootCertUpdates= */ true); + } + // TODO: Previously we'd hang if certProviderInstanceConfig were null or + // certInstance.isInitialized() == false. Now we'll proceed. Those should be errors, or are + // they impossible and should be assertions? certHandle = certProviderInstanceConfig == null ? null : certificateProviderStore.createOrGetProvider( certInstance.getCertificateName(), certProviderInstanceConfig.pluginName(), certProviderInstanceConfig.config(), - this, - true); + watcher, + true)::close; } else { certHandle = null; } - if (rootCertInstance != null - && rootCertInstance.isInitialized() - && !rootCertInstance.getInstanceName().equals(certInstanceName)) { + if (createRootCertInstance && !sharedCertInstance) { CertificateProviderInfo certProviderInstanceConfig = getCertProviderConfig(certProviders, rootCertInstance.getInstanceName()); rootCertHandle = certProviderInstanceConfig == null ? null @@ -80,13 +90,16 @@ protected CertProviderSslContextProvider( rootCertInstance.getCertificateName(), certProviderInstanceConfig.pluginName(), certProviderInstanceConfig.config(), - this, - true); + new IgnoreUpdatesWatcher(this, /* ignoreRootCertUpdates= */ false), + false)::close; + } else if (rootCertInstance == null + && CommonTlsContextUtil.isUsingSystemRootCerts(tlsContext.getCommonTlsContext())) { + SystemRootCertificateProvider systemRootProvider = new SystemRootCertificateProvider(this); + systemRootProvider.start(); + rootCertHandle = systemRootProvider::close; } else { rootCertHandle = null; } - this.isUsingSystemRootCerts = rootCertInstance == null - && CommonTlsContextUtil.isUsingSystemRootCerts(tlsContext.getCommonTlsContext()); } private static CertificateProviderInfo getCertProviderConfig( @@ -150,17 +163,16 @@ public final void updateSpiffeTrustMap(Map> spiffe private void updateSslContextWhenReady() { if (isMtls()) { - if (savedKey != null - && (savedTrustedRoots != null || isUsingSystemRootCerts || savedSpiffeTrustMap != null)) { + if (savedKey != null && (savedTrustedRoots != null || savedSpiffeTrustMap != null)) { updateSslContext(); clearKeysAndCerts(); } - } else if (isClientSideTls()) { + } else if (isRegularTlsAndClientSide()) { if (savedTrustedRoots != null || savedSpiffeTrustMap != null) { updateSslContext(); clearKeysAndCerts(); } - } else if (isServerSideTls()) { + } else if (isRegularTlsAndServerSide()) { if (savedKey != null) { updateSslContext(); clearKeysAndCerts(); @@ -170,8 +182,10 @@ private void updateSslContextWhenReady() { private void clearKeysAndCerts() { savedKey = null; - savedTrustedRoots = null; - savedSpiffeTrustMap = null; + if (!isUsingSystemRootCerts) { + savedTrustedRoots = null; + savedSpiffeTrustMap = null; + } savedCertChain = null; } @@ -179,11 +193,11 @@ protected final boolean isMtls() { return certInstance != null && (rootCertInstance != null || isUsingSystemRootCerts); } - protected final boolean isClientSideTls() { - return rootCertInstance != null && certInstance == null; + protected final boolean isRegularTlsAndClientSide() { + return (rootCertInstance != null || isUsingSystemRootCerts) && certInstance == null; } - protected final boolean isServerSideTls() { + protected final boolean isRegularTlsAndServerSide() { return certInstance != null && rootCertInstance == null; } @@ -201,4 +215,9 @@ public final void close() { rootCertHandle.close(); } } + + interface NoExceptionCloseable extends Closeable { + @Override + void close(); + } } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/IgnoreUpdatesWatcher.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/IgnoreUpdatesWatcher.java new file mode 100644 index 00000000000..cd9d88be41b --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/IgnoreUpdatesWatcher.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.security.certprovider; + +import static java.util.Objects.requireNonNull; + +import com.google.common.annotations.VisibleForTesting; +import io.grpc.Status; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +public final class IgnoreUpdatesWatcher implements CertificateProvider.Watcher { + private final CertificateProvider.Watcher delegate; + private final boolean ignoreRootCertUpdates; + + public IgnoreUpdatesWatcher( + CertificateProvider.Watcher delegate, boolean ignoreRootCertUpdates) { + this.delegate = requireNonNull(delegate, "delegate"); + this.ignoreRootCertUpdates = ignoreRootCertUpdates; + } + + @Override + public void updateCertificate(PrivateKey key, List certChain) { + if (ignoreRootCertUpdates) { + delegate.updateCertificate(key, certChain); + } + } + + @Override + public void updateTrustedRoots(List trustedRoots) { + if (!ignoreRootCertUpdates) { + delegate.updateTrustedRoots(trustedRoots); + } + } + + @Override + public void updateSpiffeTrustMap(Map> spiffeTrustMap) { + if (!ignoreRootCertUpdates) { + delegate.updateSpiffeTrustMap(spiffeTrustMap); + } + } + + @Override + public void onError(Status errorStatus) { + delegate.onError(errorStatus); + } + + @VisibleForTesting + public CertificateProvider.Watcher getDelegate() { + return delegate; + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/SystemRootCertificateProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/SystemRootCertificateProvider.java new file mode 100644 index 00000000000..7c60f714e71 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/SystemRootCertificateProvider.java @@ -0,0 +1,71 @@ +/* + * 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.internal.security.certprovider; + +import io.grpc.Status; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + * An non-registered provider for CertProviderSslContextProvider to use the same code path for + * system root certs as provider-obtained certs. + */ +final class SystemRootCertificateProvider extends CertificateProvider { + public SystemRootCertificateProvider(CertificateProvider.Watcher watcher) { + super(new DistributorWatcher(), false); + getWatcher().addWatcher(watcher); + } + + @Override + public void start() { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + + List trustManagers = Arrays.asList(trustManagerFactory.getTrustManagers()); + List rootCerts = trustManagers.stream() + .filter(X509TrustManager.class::isInstance) + .map(X509TrustManager.class::cast) + .map(trustManager -> Arrays.asList(trustManager.getAcceptedIssuers())) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + getWatcher().updateTrustedRoots(rootCerts); + } catch (KeyStoreException | NoSuchAlgorithmException ex) { + getWatcher().onError(Status.UNAVAILABLE + .withDescription("Could not load system root certs") + .withCause(ex)); + } + } + + @Override + public void close() { + // Unnecessary because there's no more callbacks, but do it for good measure + for (Watcher watcher : getWatcher().getDownstreamWatchers()) { + getWatcher().removeWatcher(watcher); + } + } +} diff --git a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java index 23068d665bf..e46e440475a 100644 --- a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java @@ -37,6 +37,7 @@ import com.google.common.util.concurrent.SettableFuture; import io.envoyproxy.envoy.config.core.v3.SocketAddress.Protocol; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; +import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; import io.grpc.Attributes; import io.grpc.EquivalentAddressGroup; import io.grpc.Grpc; @@ -117,6 +118,8 @@ @RunWith(Parameterized.class) public class XdsSecurityClientServerTest { + private static final String SAN_TO_MATCH = "waterzooi.test.google.be"; + @Parameter public Boolean enableSpiffe; private Boolean originalEnableSpiffe; @@ -206,7 +209,8 @@ public void tlsClientServer_noClientAuthentication() throws Exception { * Uses common_tls_context.combined_validation_context in upstream_tls_context. */ @Test - public void tlsClientServer_useSystemRootCerts_useCombinedValidationContext() throws Exception { + public void tlsClientServer_useSystemRootCerts_noMtls_useCombinedValidationContext() + throws Exception { Path trustStoreFilePath = getCacertFilePathForTestCa(); try { setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); @@ -217,7 +221,7 @@ public void tlsClientServer_useSystemRootCerts_useCombinedValidationContext() th UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, - CLIENT_PEM_FILE, true); + CLIENT_PEM_FILE, true, SAN_TO_MATCH, false); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); @@ -233,7 +237,7 @@ public void tlsClientServer_useSystemRootCerts_useCombinedValidationContext() th * Uses common_tls_context.validation_context in upstream_tls_context. */ @Test - public void tlsClientServer_useSystemRootCerts_validationContext() throws Exception { + public void tlsClientServer_useSystemRootCerts_noMtls_validationContext() throws Exception { Path trustStoreFilePath = getCacertFilePathForTestCa().toAbsolutePath(); try { setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); @@ -244,7 +248,7 @@ public void tlsClientServer_useSystemRootCerts_validationContext() throws Except UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, - CLIENT_PEM_FILE, false); + CLIENT_PEM_FILE, false, SAN_TO_MATCH, false); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); @@ -255,9 +259,60 @@ public void tlsClientServer_useSystemRootCerts_validationContext() throws Except } } + @Test + public void tlsClientServer_useSystemRootCerts_mtls() throws Exception { + Path trustStoreFilePath = getCacertFilePathForTestCa(); + try { + setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); + DownstreamTlsContext downstreamTlsContext = + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, false, true); + buildServerWithTlsContext(downstreamTlsContext); + + UpstreamTlsContext upstreamTlsContext = + setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, + CLIENT_PEM_FILE, true, SAN_TO_MATCH, true); + + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = + getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); + assertThat(unaryRpc(/* requestMessage= */ "buddy", blockingStub)).isEqualTo("Hello buddy"); + } finally { + Files.deleteIfExists(trustStoreFilePath); + clearTrustStoreSystemProperties(); + } + } + + @Test + public void tlsClientServer_useSystemRootCerts_failureToMatchSubjAltNames() throws Exception { + Path trustStoreFilePath = getCacertFilePathForTestCa(); + try { + setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); + DownstreamTlsContext downstreamTlsContext = + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, false, false); + buildServerWithTlsContext(downstreamTlsContext); + + UpstreamTlsContext upstreamTlsContext = + setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, + CLIENT_PEM_FILE, true, "server1.test.google.in", false); + + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = + getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); + unaryRpc(/* requestMessage= */ "buddy", blockingStub); + fail("Expected handshake failure exception"); + } catch (StatusRuntimeException e) { + assertThat(e.getCause()).isInstanceOf(SSLHandshakeException.class); + assertThat(e.getCause().getCause()).isInstanceOf(CertificateException.class); + assertThat(e.getCause().getCause().getMessage()).isEqualTo( + "Peer certificate SAN check failed"); + } finally { + Files.deleteIfExists(trustStoreFilePath); + clearTrustStoreSystemProperties(); + } + } + /** * Use system root ca cert for TLS channel - mTLS. - * Uses common_tls_context.combined_validation_context in upstream_tls_context. */ @Test public void tlsClientServer_useSystemRootCerts_requireClientAuth() throws Exception { @@ -266,12 +321,12 @@ public void tlsClientServer_useSystemRootCerts_requireClientAuth() throws Except setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); DownstreamTlsContext downstreamTlsContext = setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, - null, false, false); + null, false, true); buildServerWithTlsContext(downstreamTlsContext); UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, - CLIENT_PEM_FILE, true); + CLIENT_PEM_FILE, true, SAN_TO_MATCH, false); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); @@ -549,20 +604,25 @@ private UpstreamTlsContext setBootstrapInfoAndBuildUpstreamTlsContext(String cli .buildUpstreamTlsContext("google_cloud_private_spiffe-client", hasIdentityCert); } + @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names private UpstreamTlsContext setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts( String clientKeyFile, String clientPemFile, - boolean useCombinedValidationContext) { + boolean useCombinedValidationContext, String sanToMatch, boolean isMtls) { bootstrapInfoForClient = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-client", clientKeyFile, clientPemFile, CA_PEM_FILE, null, null, null, null, null); if (useCombinedValidationContext) { return CommonTlsContextTestsUtil.buildUpstreamTlsContextForCertProviderInstance( - "google_cloud_private_spiffe-client", "ROOT", null, + isMtls ? "google_cloud_private_spiffe-client" : null, + isMtls ? "ROOT" : null, null, null, null, CertificateValidationContext.newBuilder() .setSystemRootCerts( CertificateValidationContext.SystemRootCerts.newBuilder().build()) + .addMatchSubjectAltNames( + StringMatcher.newBuilder() + .setExact(sanToMatch)) .build()); } return CommonTlsContextTestsUtil.buildNewUpstreamTlsContextForCertProviderInstance( diff --git a/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java index 397fe01e0f5..a0eac581d5c 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/ClientSslContextProviderFactoryTest.java @@ -37,6 +37,7 @@ import io.grpc.xds.internal.security.certprovider.CertificateProviderProvider; import io.grpc.xds.internal.security.certprovider.CertificateProviderRegistry; import io.grpc.xds.internal.security.certprovider.CertificateProviderStore; +import io.grpc.xds.internal.security.certprovider.IgnoreUpdatesWatcher; import io.grpc.xds.internal.security.certprovider.TestCertificateProvider; import org.junit.Before; import org.junit.Test; @@ -84,7 +85,7 @@ public void createCertProviderClientSslContextProvider() throws XdsInitializatio clientSslContextProviderFactory.create(upstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderClientSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); + verifyWatcher(sslContextProvider, watcherCaptor[0], false); // verify that bootstrapInfo is cached... sslContextProvider = clientSslContextProviderFactory.create(upstreamTlsContext); @@ -119,7 +120,7 @@ public void bothPresent_expectCertProviderClientSslContextProvider() clientSslContextProviderFactory.create(upstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderClientSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); + verifyWatcher(sslContextProvider, watcherCaptor[0], true); } @Test @@ -145,7 +146,7 @@ public void createCertProviderClientSslContextProvider_onlyRootCert() clientSslContextProviderFactory.create(upstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderClientSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); + verifyWatcher(sslContextProvider, watcherCaptor[0], true); } @Test @@ -179,7 +180,7 @@ public void createCertProviderClientSslContextProvider_withStaticContext() clientSslContextProviderFactory.create(upstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderClientSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); + verifyWatcher(sslContextProvider, watcherCaptor[0], true); } @Test @@ -209,8 +210,8 @@ public void createCertProviderClientSslContextProvider_2providers() clientSslContextProviderFactory.create(upstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderClientSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); - verifyWatcher(sslContextProvider, watcherCaptor[1]); + verifyWatcher(sslContextProvider, watcherCaptor[0], true); + verifyWatcher(sslContextProvider, watcherCaptor[1], true); } @Test @@ -246,8 +247,8 @@ public void createNewCertProviderClientSslContextProvider_withSans() { clientSslContextProviderFactory.create(upstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderClientSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); - verifyWatcher(sslContextProvider, watcherCaptor[1]); + verifyWatcher(sslContextProvider, watcherCaptor[0], true); + verifyWatcher(sslContextProvider, watcherCaptor[1], true); } @Test @@ -280,7 +281,7 @@ public void createNewCertProviderClientSslContextProvider_onlyRootCert() { clientSslContextProviderFactory.create(upstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderClientSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); + verifyWatcher(sslContextProvider, watcherCaptor[0], true); } static void createAndRegisterProviderProvider( @@ -310,11 +311,18 @@ public CertificateProvider answer(InvocationOnMock invocation) throws Throwable } static void verifyWatcher( - SslContextProvider sslContextProvider, CertificateProvider.DistributorWatcher watcherCaptor) { + SslContextProvider sslContextProvider, CertificateProvider.DistributorWatcher watcherCaptor, + boolean usesDelegateWatcher) { assertThat(watcherCaptor).isNotNull(); assertThat(watcherCaptor.getDownstreamWatchers()).hasSize(1); - assertThat(watcherCaptor.getDownstreamWatchers().iterator().next()) - .isSameInstanceAs(sslContextProvider); + if (usesDelegateWatcher) { + assertThat(((IgnoreUpdatesWatcher) watcherCaptor.getDownstreamWatchers().iterator().next()) + .getDelegate()) + .isSameInstanceAs(sslContextProvider); + } else { + assertThat(watcherCaptor.getDownstreamWatchers().iterator().next()) + .isSameInstanceAs(sslContextProvider); + } } static CommonTlsContext.Builder addFilenames( diff --git a/xds/src/test/java/io/grpc/xds/internal/security/ServerSslContextProviderFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/security/ServerSslContextProviderFactoryTest.java index cf86b511f1f..7a5a6c00639 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/ServerSslContextProviderFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/ServerSslContextProviderFactoryTest.java @@ -78,7 +78,7 @@ public void createCertProviderServerSslContextProvider() throws XdsInitializatio serverSslContextProviderFactory.create(downstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderServerSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); + verifyWatcher(sslContextProvider, watcherCaptor[0], false); // verify that bootstrapInfo is cached... sslContextProvider = serverSslContextProviderFactory.create(downstreamTlsContext); @@ -117,7 +117,7 @@ public void bothPresent_expectCertProviderServerSslContextProvider() serverSslContextProviderFactory.create(downstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderServerSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); + verifyWatcher(sslContextProvider, watcherCaptor[0], true); } @Test @@ -144,7 +144,7 @@ public void createCertProviderServerSslContextProvider_onlyCertInstance() serverSslContextProviderFactory.create(downstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderServerSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); + verifyWatcher(sslContextProvider, watcherCaptor[0], true); } @Test @@ -179,7 +179,7 @@ public void createCertProviderServerSslContextProvider_withStaticContext() serverSslContextProviderFactory.create(downstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderServerSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); + verifyWatcher(sslContextProvider, watcherCaptor[0], false); } @Test @@ -210,8 +210,8 @@ public void createCertProviderServerSslContextProvider_2providers() serverSslContextProviderFactory.create(downstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderServerSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); - verifyWatcher(sslContextProvider, watcherCaptor[1]); + verifyWatcher(sslContextProvider, watcherCaptor[0], true); + verifyWatcher(sslContextProvider, watcherCaptor[1], true); } @Test @@ -249,7 +249,7 @@ public void createNewCertProviderServerSslContextProvider_withSans() serverSslContextProviderFactory.create(downstreamTlsContext); assertThat(sslContextProvider.getClass().getSimpleName()).isEqualTo( "CertProviderServerSslContextProvider"); - verifyWatcher(sslContextProvider, watcherCaptor[0]); - verifyWatcher(sslContextProvider, watcherCaptor[1]); + verifyWatcher(sslContextProvider, watcherCaptor[0], true); + verifyWatcher(sslContextProvider, watcherCaptor[1], true); } } diff --git a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java index b0800458d66..3c734df3f5a 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java @@ -72,15 +72,28 @@ private CertProviderClientSslContextProvider getSslContextProvider( String rootInstanceName, Bootstrapper.BootstrapInfo bootstrapInfo, Iterable alpnProtocols, - CertificateValidationContext staticCertValidationContext) { - EnvoyServerProtoData.UpstreamTlsContext upstreamTlsContext = - CommonTlsContextTestsUtil.buildUpstreamTlsContextForCertProviderInstance( - certInstanceName, - "cert-default", - rootInstanceName, - "root-default", - alpnProtocols, - staticCertValidationContext); + CertificateValidationContext staticCertValidationContext, + boolean useSystemRootCerts) { + EnvoyServerProtoData.UpstreamTlsContext upstreamTlsContext; + if (useSystemRootCerts) { + upstreamTlsContext = + CommonTlsContextTestsUtil.buildNewUpstreamTlsContextForCertProviderInstance( + certInstanceName, + "cert-default", + rootInstanceName, + "root-default", + alpnProtocols, + staticCertValidationContext); + } else { + upstreamTlsContext = + CommonTlsContextTestsUtil.buildUpstreamTlsContextForCertProviderInstance( + certInstanceName, + "cert-default", + rootInstanceName, + "root-default", + alpnProtocols, + staticCertValidationContext); + } return (CertProviderClientSslContextProvider) certProviderClientSslContextProviderFactory.getProvider( upstreamTlsContext, @@ -122,7 +135,7 @@ public void testProviderForClient_mtls() throws Exception { "gcp_id", CommonBootstrapperTestUtils.getTestBootstrapInfo(), /* alpnProtocols= */ null, - /* staticCertValidationContext= */ null); + /* staticCertValidationContext= */ null, false); assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); @@ -173,6 +186,87 @@ public void testProviderForClient_mtls() throws Exception { assertThat(testCallback1.updatedSslContext).isNotSameInstanceAs(testCallback.updatedSslContext); } + @Test + public void testProviderForClient_systemRootCerts_regularTls() { + final CertificateProvider.DistributorWatcher[] watcherCaptor = + new CertificateProvider.DistributorWatcher[1]; + TestCertificateProvider.createAndRegisterProviderProvider( + certificateProviderRegistry, watcherCaptor, "testca", 0); + CertProviderClientSslContextProvider provider = + getSslContextProvider( + null, + null, + CommonBootstrapperTestUtils.getTestBootstrapInfo(), + /* alpnProtocols= */ null, + CertificateValidationContext.newBuilder() + .setSystemRootCerts( + CertificateValidationContext.SystemRootCerts.getDefaultInstance()) + .build(), + true); + + assertThat(provider.savedKey).isNull(); + assertThat(provider.savedCertChain).isNull(); + assertThat(provider.savedTrustedRoots).isNotNull(); + assertThat(provider.getSslContext()).isNotNull(); + TestCallback testCallback = + CommonTlsContextTestsUtil.getValueThruCallback(provider); + assertThat(testCallback.updatedSslContext).isEqualTo(provider.getSslContext()); + + assertThat(watcherCaptor[0]).isNull(); + } + + @Test + public void testProviderForClient_systemRootCerts_mtls() throws Exception { + final CertificateProvider.DistributorWatcher[] watcherCaptor = + new CertificateProvider.DistributorWatcher[1]; + TestCertificateProvider.createAndRegisterProviderProvider( + certificateProviderRegistry, watcherCaptor, "testca", 0); + CertProviderClientSslContextProvider provider = + getSslContextProvider( + "gcp_id", + null, + CommonBootstrapperTestUtils.getTestBootstrapInfo(), + /* alpnProtocols= */ null, + CertificateValidationContext.newBuilder() + .setSystemRootCerts( + CertificateValidationContext.SystemRootCerts.getDefaultInstance()) + .build(), + true); + + assertThat(provider.savedKey).isNull(); + assertThat(provider.savedCertChain).isNull(); + assertThat(provider.savedTrustedRoots).isNotNull(); + assertThat(provider.getSslContext()).isNull(); + + // now generate cert update + watcherCaptor[0].updateCertificate( + CommonCertProviderTestUtils.getPrivateKey(CLIENT_KEY_FILE), + ImmutableList.of(getCertFromResourceName(CLIENT_PEM_FILE))); + assertThat(provider.savedKey).isNull(); + assertThat(provider.savedCertChain).isNull(); + assertThat(provider.savedTrustedRoots).isNotNull(); + assertThat(provider.getSslContext()).isNotNull(); + + TestCallback testCallback = + CommonTlsContextTestsUtil.getValueThruCallback(provider); + + doChecksOnSslContext(false, testCallback.updatedSslContext, /* expectedApnProtos= */ null); + TestCallback testCallback1 = + CommonTlsContextTestsUtil.getValueThruCallback(provider); + assertThat(testCallback1.updatedSslContext).isSameInstanceAs(testCallback.updatedSslContext); + + // now update id cert: sslContext should be updated i.e. different from the previous one + watcherCaptor[0].updateCertificate( + CommonCertProviderTestUtils.getPrivateKey(SERVER_1_KEY_FILE), + ImmutableList.of(getCertFromResourceName(SERVER_1_PEM_FILE))); + assertThat(provider.savedKey).isNull(); + assertThat(provider.savedCertChain).isNull(); + assertThat(provider.savedTrustedRoots).isNotNull(); + assertThat(provider.getSslContext()).isNotNull(); + testCallback1 = CommonTlsContextTestsUtil.getValueThruCallback(provider); + assertThat(testCallback1.updatedSslContext).isNotSameInstanceAs(testCallback.updatedSslContext); + } + @Test public void testProviderForClient_mtls_newXds() throws Exception { final CertificateProvider.DistributorWatcher[] watcherCaptor = @@ -248,7 +342,7 @@ public void testProviderForClient_queueExecutor() throws Exception { "gcp_id", CommonBootstrapperTestUtils.getTestBootstrapInfo(), /* alpnProtocols= */ null, - /* staticCertValidationContext= */ null); + /* staticCertValidationContext= */ null, false); QueuedExecutor queuedExecutor = new QueuedExecutor(); TestCallback testCallback = @@ -281,7 +375,7 @@ public void testProviderForClient_tls() throws Exception { "gcp_id", CommonBootstrapperTestUtils.getTestBootstrapInfo(), /* alpnProtocols= */ null, - /* staticCertValidationContext= */ null); + /* staticCertValidationContext= */ null, false); assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); @@ -318,7 +412,7 @@ public void testProviderForClient_sslContextException_onError() throws Exception "gcp_id", CommonBootstrapperTestUtils.getTestBootstrapInfo(), /* alpnProtocols= */null, - staticCertValidationContext); + staticCertValidationContext, false); TestCallback testCallback = new TestCallback(MoreExecutors.directExecutor()); provider.addCallback(testCallback); @@ -350,7 +444,7 @@ public void testProviderForClient_rootInstanceNull_and_notUsingSystemRootCerts_e /* rootInstanceName= */ null, CommonBootstrapperTestUtils.getTestBootstrapInfo(), /* alpnProtocols= */ null, - /* staticCertValidationContext= */ null); + /* staticCertValidationContext= */ null, false); fail("exception expected"); } catch (UnsupportedOperationException expected) { assertThat(expected).hasMessageThat().contains("Unsupported configurations in " @@ -373,7 +467,7 @@ public void testProviderForClient_rootInstanceNull_but_isUsingSystemRootCerts_va CertificateValidationContext.newBuilder() .setSystemRootCerts( CertificateValidationContext.SystemRootCerts.newBuilder().build()) - .build()); + .build(), false); } static class QueuedExecutor implements Executor { From 5b876cc864f98e02c1aed215ae3fd9daa2b0f227 Mon Sep 17 00:00:00 2001 From: vimanikag Date: Thu, 25 Sep 2025 13:29:36 +0530 Subject: [PATCH 394/591] xds: Handle wildcards in split patterns in DNS SAN exact matching (#12345) Makes DNS SAN exact matching in xds handle wildcards in split patterns similar to Envoy. Fixes #12326 --- .../bad_wildcard_dns_certificate.pem | 22 ++++++ .../security/trust/XdsX509TrustManager.java | 48 ++++++++++++ .../security/CommonTlsContextTestsUtil.java | 2 + .../trust/XdsX509TrustManagerTest.java | 74 ++++++++++++++++++- 4 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 testing/src/main/resources/certs/sni-test-certs/bad_wildcard_dns_certificate.pem diff --git a/testing/src/main/resources/certs/sni-test-certs/bad_wildcard_dns_certificate.pem b/testing/src/main/resources/certs/sni-test-certs/bad_wildcard_dns_certificate.pem new file mode 100644 index 00000000000..b015f62e51c --- /dev/null +++ b/testing/src/main/resources/certs/sni-test-certs/bad_wildcard_dns_certificate.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDsjCCApqgAwIBAgIUCs5j4C2KXgCRVFa48kc5TYRS1JwwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOTXkgSW50ZXJuYWwgQ0EwIBcNMjUwOTIzMDc1NDUzWhgP +MjEyNTA4MzAwNzU0NTNaMGUxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhJbGxpbm9p +czEQMA4GA1UEBwwHQ2hpY2FnbzEVMBMGA1UECgwMRXhhbXBsZSwgQ28uMRowGAYD +VQQDDBEqLnRlc3QuZ29vZ2xlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAKoqcnNh9MV39GH6JjC5KVMN6MO1IoTw6wHJN0JJ/nGNx6ycIsBK8SgJ +eYRR2BEpT6WZba+f04KChcB4Z9tiPISNvUBpmEv76rAsdtcAZwSpF06q4wxHVE5F +rX6mNT8hk448mDBDGHUXNAT6g/e/Vlt6U0XRyuu713gbZq1X6JH29FG7EJ3LUx35 +h6sEkvTlZZ3m6NJr7zYoqrYh/gRkPigtPxaNcoXo0gVm4IEde0sYz27SWyNH4v/o +23NynSulOwx4DwEhBOXekLb5QJHBqwMTPynaMncBQIXF+PXeuxN9a3zR6DSn+jGw +g008tS0tn2FuAvJDBl0paEykdOr2rNMCAwEAAaOBozCBoDALBgNVHQ8EBAMCBeAw +EwYDVR0lBAwwCgYIKwYBBQUHAwEwPAYDVR0RBDUwM4IKbHkqKmZ0LmNvbYIIKnlm +dC5jKm2CCS5seWZ0LmNvbYIOeG4tLSoubHlmdC5jb22CADAdBgNVHQ4EFgQUZoL2 +OzBtK/BUzSYfgXDx3iDjcIQwHwYDVR0jBBgwFoAUHlstFN5WSLSqyJgUDy6BB0z0 +BrgwDQYJKoZIhvcNAQELBQADggEBAMYwVOT7XZJMQ6n32pQqtZhJ/Z0wlVfCAbm0 +7xospeBt6KtOz2zIsvPpq0aqPjowMAeL1EZaBvmfm/XgWUU5e/3hLUIHOHyKfswB +czDbY0RE8nfVDoF4Ck1ljPjvrFr4tSAxTzVA4JU5o3UXkblBg0LG6tTuLlZ3x5aF +KtkZnszxjE+vOg6J9MDbFP/xtA1oVHyCvk+cUgnBxAoPShI+87DINGVTmztBSetK +nJN9dOh7Q88NhTLHOe67Ora9Y0ZP+uFKHaqFv8qj8B/Q6ptb0CAksdL5EunkIHrq +glKdVdYgIP2JpRwtvVHK5FzWBlGXCi3DxTyYi6FWqsSJ+heCS2w= +-----END CERTIFICATE----- diff --git a/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java b/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java index 1ecfe378d29..ebf7ea82184 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java @@ -147,6 +147,9 @@ private static boolean verifyDnsNameExact( if (Strings.isNullOrEmpty(sanToVerifyExact)) { return false; } + if (sanToVerifyExact.contains("*")) { + return verifyDnsNameWildcard(altNameFromCert, sanToVerifyExact, ignoreCase); + } return ignoreCase ? sanToVerifyExact.equalsIgnoreCase(altNameFromCert) : sanToVerifyExact.equals(altNameFromCert); @@ -303,4 +306,49 @@ public X509Certificate[] getAcceptedIssuers() { } return delegate.getAcceptedIssuers(); } + + private static boolean verifyDnsNameWildcard( + String altNameFromCert, String sanToVerify, boolean ignoreCase) { + String[] splitPattern = splitAtFirstDelimiter(ignoreCase + ? sanToVerify.toLowerCase(Locale.ROOT) : sanToVerify); + String[] splitDnsName = splitAtFirstDelimiter(ignoreCase + ? altNameFromCert.toLowerCase(Locale.ROOT) : altNameFromCert); + if (splitPattern == null || splitDnsName == null) { + return false; + } + if (splitDnsName[0].startsWith("xn--")) { + return false; + } + if (splitPattern[0].contains("*") + && !splitPattern[1].contains("*") + && !splitPattern[0].startsWith("xn--")) { + return splitDnsName[1].equals(splitPattern[1]) + && labelWildcardMatch(splitDnsName[0], splitPattern[0]); + } + return false; + } + + private static boolean labelWildcardMatch(String dnsLabel, String pattern) { + final char glob = '*'; + // Check the special case of a single * pattern, as it's common. + if (pattern.equals("*")) { + return !dnsLabel.isEmpty(); + } + int globIndex = pattern.indexOf(glob); + if (pattern.indexOf(glob, globIndex + 1) == -1) { + return dnsLabel.length() >= pattern.length() - 1 + && dnsLabel.startsWith(pattern.substring(0, globIndex)) + && dnsLabel.endsWith(pattern.substring(globIndex + 1)); + } + return false; + } + + @Nullable + private static String[] splitAtFirstDelimiter(String s) { + int index = s.indexOf('.'); + if (index == -1) { + return null; + } + return new String[]{s.substring(0, index), s.substring(index + 1)}; + } } diff --git a/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java b/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java index 48814dece1d..8f970c86366 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java @@ -60,6 +60,8 @@ public class CommonTlsContextTestsUtil { public static final String BAD_SERVER_KEY_FILE = "badserver.key"; public static final String BAD_CLIENT_PEM_FILE = "badclient.pem"; public static final String BAD_CLIENT_KEY_FILE = "badclient.key"; + public static final String BAD_WILDCARD_DNS_PEM_FILE = + "sni-test-certs/bad_wildcard_dns_certificate.pem"; /** takes additional values and creates CombinedCertificateValidationContext as needed. */ private static CommonTlsContext buildCommonTlsContextWithAdditionalValues( diff --git a/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsX509TrustManagerTest.java b/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsX509TrustManagerTest.java index 6fa3d2e7d24..ddd0a8e7f94 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsX509TrustManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsX509TrustManagerTest.java @@ -18,9 +18,11 @@ import static com.google.common.truth.Truth.assertThat; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.BAD_SERVER_PEM_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.BAD_WILDCARD_DNS_PEM_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CA_PEM_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CLIENT_PEM_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.CLIENT_SPIFFE_PEM_FILE; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SERVER_0_PEM_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SERVER_1_PEM_FILE; import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.SERVER_1_SPIFFE_PEM_FILE; import static org.junit.Assert.fail; @@ -42,6 +44,7 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import javax.net.ssl.SSLEngine; @@ -52,7 +55,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -60,7 +64,7 @@ /** * Unit tests for {@link XdsX509TrustManager}. */ -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class XdsX509TrustManagerTest { @Rule @@ -74,6 +78,12 @@ public class XdsX509TrustManagerTest { private XdsX509TrustManager trustManager; + private final TestParam testParam; + + public XdsX509TrustManagerTest(TestParam testParam) { + this.testParam = testParam; + } + @Test public void nullCertContextTest() throws CertificateException, IOException { trustManager = new XdsX509TrustManager(null, mockDelegate); @@ -691,6 +701,52 @@ public void unsupportedAltNameType() throws CertificateException, IOException { } } + @Test + public void testDnsWildcardPatterns() + throws CertificateException, IOException { + StringMatcher stringMatcher = + StringMatcher.newBuilder() + .setExact(testParam.sanPattern) + .setIgnoreCase(testParam.ignoreCase) + .build(); + @SuppressWarnings("deprecation") + CertificateValidationContext certContext = + CertificateValidationContext.newBuilder() + .addMatchSubjectAltNames(stringMatcher) + .build(); + trustManager = new XdsX509TrustManager(certContext, mockDelegate); + X509Certificate[] certs = + CertificateUtils.toX509Certificates(TlsTesting.loadCert(testParam.certFile)); + try { + trustManager.verifySubjectAltNameInChain(certs); + assertThat(testParam.expected).isTrue(); + } catch (CertificateException certException) { + assertThat(testParam.expected).isFalse(); + assertThat(certException).hasMessageThat().isEqualTo("Peer certificate SAN check failed"); + } + } + + @Parameters(name = "{index}: {0}") + public static Collection getParameters() { + return Arrays.asList(new Object[][] { + {new TestParam("*.test.google.fr", SERVER_1_PEM_FILE, false, true)}, + {new TestParam("*.test.youtube.com", SERVER_1_PEM_FILE, false, true)}, + {new TestParam("waterzooi.test.google.be", SERVER_1_PEM_FILE, false, true)}, + {new TestParam("192.168.1.3", SERVER_1_PEM_FILE, false, true)}, + {new TestParam("*.TEST.YOUTUBE.com", SERVER_1_PEM_FILE, true, true)}, + {new TestParam("w*i.test.google.be", SERVER_1_PEM_FILE, false, true)}, + {new TestParam("w*a.test.google.be", SERVER_1_PEM_FILE, false, false)}, + {new TestParam("*.test.google.com.au", SERVER_0_PEM_FILE, false, false)}, + {new TestParam("*.TEST.YOUTUBE.com", SERVER_1_PEM_FILE, false, false)}, + {new TestParam("*waterzooi", SERVER_1_PEM_FILE, false, false)}, + {new TestParam("*.lyft.com", BAD_WILDCARD_DNS_PEM_FILE, false, false)}, + {new TestParam("ly**ft.com", BAD_WILDCARD_DNS_PEM_FILE, false, false)}, + {new TestParam("*yft.c*m", BAD_WILDCARD_DNS_PEM_FILE, false, false)}, + {new TestParam("xn--*.lyft.com", BAD_WILDCARD_DNS_PEM_FILE, false, false)}, + {new TestParam("", BAD_WILDCARD_DNS_PEM_FILE, false, false)}, + }); + } + private TestSslEngine buildTrustManagerAndGetSslEngine() throws CertificateException, IOException, CertStoreException { SSLParameters sslParams = buildTrustManagerAndGetSslParameters(); @@ -754,4 +810,18 @@ public void setSSLParameters(SSLParameters sslParameters) { private SSLParameters sslParameters; } + + private static class TestParam { + final String sanPattern; + final String certFile; + final boolean ignoreCase; + final boolean expected; + + TestParam(String sanPattern, String certFile, boolean ignoreCase, boolean expected) { + this.sanPattern = sanPattern; + this.certFile = certFile; + this.ignoreCase = ignoreCase; + this.expected = expected; + } + } } From 9ade38a792f8a07ea8bc7387593d2c6d91804ba9 Mon Sep 17 00:00:00 2001 From: Sangamesh Date: Thu, 25 Sep 2025 18:13:57 +0530 Subject: [PATCH 395/591] Observe failOnWarnings for android build (#12040) Fixes: #6868 --- build.gradle | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/build.gradle b/build.gradle index f80b27a4476..35f7161d4bf 100644 --- a/build.gradle +++ b/build.gradle @@ -197,6 +197,25 @@ subprojects { } } + plugins.withId("com.android.base") { + android { + lint { + abortOnError true + if (rootProject.hasProperty('failOnWarnings') && rootProject.failOnWarnings.toBoolean()) { + warningsAsErrors true + } + } + } + tasks.withType(JavaCompile).configureEach { + it.options.compilerArgs += [ + "-Xlint:all" + ] + if (rootProject.hasProperty('failOnWarnings') && rootProject.failOnWarnings.toBoolean()) { + it.options.compilerArgs += ["-Werror"] + } + } + } + plugins.withId("java") { dependencies { testImplementation libraries.junit, From 82f9b8ec053418d4c970cb8fff5ef028cd50409c Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 29 Sep 2025 12:42:39 +0530 Subject: [PATCH 396/591] xds: Make cluster selection interceptor run before other filters (#12381) Needed for `GcpAuthenticationFilter` to retrieve the cluster key set into the `CallOptions`. --- xds/src/main/java/io/grpc/xds/XdsNameResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index c3bc7c2e326..20001b6558d 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -525,7 +525,7 @@ public void onClose(Status status, Metadata trailers) { Result.newBuilder() .setConfig(config) .setInterceptor(combineInterceptors( - ImmutableList.of(filters, new ClusterSelectionInterceptor()))) + ImmutableList.of(new ClusterSelectionInterceptor(), filters))) .build(); } From 05675316cf960f5ec06d5611187a1c13341e43b9 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 29 Sep 2025 21:51:09 +0530 Subject: [PATCH 397/591] xds: xDS based SNI setting and SAN validation (#12378) When using xDS credentials make SNI for the Tls handshake to be configured via xDS, rather than use the channel authority as the SNI, and make SAN validation to be able to use the SNI sent when so instructed via xDS. Implements A101. --- .../io/grpc/internal/CertificateUtils.java | 26 ++ .../netty/InternalProtocolNegotiators.java | 17 +- .../io/grpc/netty/NettyChannelBuilder.java | 2 +- .../io/grpc/netty/ProtocolNegotiators.java | 73 ++--- .../grpc/netty/NettyClientTransportTest.java | 2 +- .../grpc/netty/ProtocolNegotiatorsTest.java | 4 +- .../S2AProtocolNegotiatorFactory.java | 4 +- .../io/grpc/xds/ClusterImplLoadBalancer.java | 15 +- .../grpc/xds/ClusterResolverLoadBalancer.java | 12 +- .../io/grpc/xds/EnvoyServerProtoData.java | 38 ++- .../main/java/io/grpc/xds/XdsAttributes.java | 5 - .../xds/internal/XdsInternalAttributes.java | 27 ++ .../security/DynamicSslContextProvider.java | 49 ++-- .../security/SecurityProtocolNegotiators.java | 47 ++- .../internal/security/SslContextProvider.java | 12 +- .../security/SslContextProviderSupplier.java | 13 +- .../security/TlsContextManagerImpl.java | 2 - .../CertProviderClientSslContextProvider.java | 34 ++- .../CertProviderServerSslContextProvider.java | 16 +- .../security/trust/CertificateUtils.java | 3 + .../trust/XdsTrustManagerFactory.java | 38 ++- .../security/trust/XdsX509TrustManager.java | 68 ++++- .../grpc/xds/ClusterImplLoadBalancerTest.java | 32 ++- .../xds/ClusterResolverLoadBalancerTest.java | 30 +- .../grpc/xds/XdsSecurityClientServerTest.java | 160 +++++++++-- .../security/CommonTlsContextTestsUtil.java | 81 +++++- .../SecurityProtocolNegotiatorsTest.java | 270 ++++++++++++++---- .../SslContextProviderSupplierTest.java | 92 +++++- ...tProviderClientSslContextProviderTest.java | 84 +++--- ...tProviderServerSslContextProviderTest.java | 20 +- .../trust/XdsTrustManagerFactoryTest.java | 27 +- .../trust/XdsX509TrustManagerTest.java | 189 ++++++------ 32 files changed, 1073 insertions(+), 419 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/XdsInternalAttributes.java diff --git a/core/src/main/java/io/grpc/internal/CertificateUtils.java b/core/src/main/java/io/grpc/internal/CertificateUtils.java index 91d17de93cb..130a435bb1a 100644 --- a/core/src/main/java/io/grpc/internal/CertificateUtils.java +++ b/core/src/main/java/io/grpc/internal/CertificateUtils.java @@ -26,14 +26,29 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Collection; +import java.util.List; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; import javax.security.auth.x500.X500Principal; /** * Contains certificate/key PEM file utility method(s) for internal usage. */ public final class CertificateUtils { + private static final Class x509ExtendedTrustManagerClass; + + static { + Class x509ExtendedTrustManagerClass1; + try { + x509ExtendedTrustManagerClass1 = Class.forName("javax.net.ssl.X509ExtendedTrustManager"); + } catch (ClassNotFoundException e) { + x509ExtendedTrustManagerClass1 = null; + // Will disallow per-rpc authority override via call option. + } + x509ExtendedTrustManagerClass = x509ExtendedTrustManagerClass1; + } + /** * Creates X509TrustManagers using the provided CA certs. */ @@ -71,6 +86,17 @@ public static TrustManager[] createTrustManager(InputStream rootCerts) return trustManagerFactory.getTrustManagers(); } + public static X509TrustManager getX509ExtendedTrustManager(List trustManagers) { + if (x509ExtendedTrustManagerClass != null) { + for (TrustManager trustManager : trustManagers) { + if (x509ExtendedTrustManagerClass.isInstance(trustManager)) { + return (X509TrustManager) trustManager; + } + } + } + return null; + } + private static X509Certificate[] getX509Certificates(InputStream inputStream) throws CertificateException { CertificateFactory factory = CertificateFactory.getInstance("X.509"); diff --git a/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java index 039ea6c4f24..35dc1bbc2e8 100644 --- a/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java @@ -26,6 +26,7 @@ import io.netty.handler.ssl.SslContext; import io.netty.util.AsciiString; import java.util.concurrent.Executor; +import javax.net.ssl.X509TrustManager; /** * Internal accessor for {@link ProtocolNegotiators}. @@ -42,9 +43,11 @@ private InternalProtocolNegotiators() {} */ public static InternalProtocolNegotiator.ProtocolNegotiator tls(SslContext sslContext, ObjectPool executorPool, - Optional handshakeCompleteRunnable) { + Optional handshakeCompleteRunnable, + X509TrustManager extendedX509TrustManager, + String sni) { final io.grpc.netty.ProtocolNegotiator negotiator = ProtocolNegotiators.tls(sslContext, - executorPool, handshakeCompleteRunnable, null); + executorPool, handshakeCompleteRunnable, extendedX509TrustManager, sni); final class TlsNegotiator implements InternalProtocolNegotiator.ProtocolNegotiator { @Override @@ -62,17 +65,19 @@ public void close() { negotiator.close(); } } - + return new TlsNegotiator(); } - + /** * Returns a {@link ProtocolNegotiator} that ensures the pipeline is set up so that TLS will * be negotiated, the {@code handler} is added and writes to the {@link io.netty.channel.Channel} * may happen immediately, even before the TLS Handshake is complete. */ - public static InternalProtocolNegotiator.ProtocolNegotiator tls(SslContext sslContext) { - return tls(sslContext, null, Optional.absent()); + public static InternalProtocolNegotiator.ProtocolNegotiator tls( + SslContext sslContext, String sni, + X509TrustManager extendedX509TrustManager) { + return tls(sslContext, null, Optional.absent(), extendedX509TrustManager, sni); } /** diff --git a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java index 46566eaca1a..2db5ab20a91 100644 --- a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java +++ b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java @@ -652,7 +652,7 @@ static ProtocolNegotiator createProtocolNegotiatorByType( case PLAINTEXT_UPGRADE: return ProtocolNegotiators.plaintextUpgrade(); case TLS: - return ProtocolNegotiators.tls(sslContext, executorPool, Optional.absent(), null); + return ProtocolNegotiators.tls(sslContext, executorPool, Optional.absent(), null, null); default: throw new IllegalArgumentException("Unsupported negotiationType: " + negotiationType); } diff --git a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java index 77308c76ace..59e7e96b4a5 100644 --- a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java @@ -102,15 +102,6 @@ final class ProtocolNegotiators { private static final EnumSet understoodServerTlsFeatures = EnumSet.of( TlsServerCredentials.Feature.MTLS, TlsServerCredentials.Feature.CUSTOM_MANAGERS); - private static Class x509ExtendedTrustManagerClass; - - static { - try { - x509ExtendedTrustManagerClass = Class.forName("javax.net.ssl.X509ExtendedTrustManager"); - } catch (ClassNotFoundException e) { - // Will disallow per-rpc authority override via call option. - } - } private ProtocolNegotiators() { } @@ -147,15 +138,8 @@ public static FromChannelCredentialsResult from(ChannelCredentials creds) { trustManagers = Arrays.asList(tmf.getTrustManagers()); } builder.trustManager(new FixedTrustManagerFactory(trustManagers)); - TrustManager x509ExtendedTrustManager = null; - if (x509ExtendedTrustManagerClass != null) { - for (TrustManager trustManager : trustManagers) { - if (x509ExtendedTrustManagerClass.isInstance(trustManager)) { - x509ExtendedTrustManager = trustManager; - break; - } - } - } + TrustManager x509ExtendedTrustManager = + CertificateUtils.getX509ExtendedTrustManager(trustManagers); return FromChannelCredentialsResult.negotiator(tlsClientFactory(builder.build(), (X509TrustManager) x509ExtendedTrustManager)); } catch (SSLException | GeneralSecurityException ex) { @@ -579,7 +563,7 @@ static final class ClientTlsProtocolNegotiator implements ProtocolNegotiator { public ClientTlsProtocolNegotiator(SslContext sslContext, ObjectPool executorPool, Optional handshakeCompleteRunnable, - X509TrustManager x509ExtendedTrustManager) { + X509TrustManager x509ExtendedTrustManager, String sni) { this.sslContext = Preconditions.checkNotNull(sslContext, "sslContext"); this.executorPool = executorPool; if (this.executorPool != null) { @@ -587,12 +571,14 @@ public ClientTlsProtocolNegotiator(SslContext sslContext, } this.handshakeCompleteRunnable = handshakeCompleteRunnable; this.x509ExtendedTrustManager = x509ExtendedTrustManager; + this.sni = sni; } private final SslContext sslContext; private final ObjectPool executorPool; private final Optional handshakeCompleteRunnable; private final X509TrustManager x509ExtendedTrustManager; + private final String sni; private Executor executor; @Override @@ -604,9 +590,17 @@ public AsciiString scheme() { public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { ChannelHandler gnh = new GrpcNegotiationHandler(grpcHandler); ChannelLogger negotiationLogger = grpcHandler.getNegotiationLogger(); - ChannelHandler cth = new ClientTlsHandler(gnh, sslContext, grpcHandler.getAuthority(), - this.executor, negotiationLogger, handshakeCompleteRunnable, this, - x509ExtendedTrustManager); + String authority; + if ("".equals(sni)) { + authority = null; + } else if (sni != null) { + authority = sni; + } else { + authority = grpcHandler.getAuthority(); + } + ChannelHandler cth = new ClientTlsHandler(gnh, sslContext, + authority, this.executor, negotiationLogger, handshakeCompleteRunnable, this, + x509ExtendedTrustManager); return new WaitUntilActiveHandler(cth, negotiationLogger); } @@ -630,28 +624,40 @@ static final class ClientTlsHandler extends ProtocolNegotiationHandler { private final int port; private Executor executor; private final Optional handshakeCompleteRunnable; - private final X509TrustManager x509ExtendedTrustManager; + private final X509TrustManager x509TrustManager; private SSLEngine sslEngine; ClientTlsHandler(ChannelHandler next, SslContext sslContext, String authority, Executor executor, ChannelLogger negotiationLogger, Optional handshakeCompleteRunnable, ClientTlsProtocolNegotiator clientTlsProtocolNegotiator, - X509TrustManager x509ExtendedTrustManager) { + X509TrustManager x509TrustManager) { super(next, negotiationLogger); this.sslContext = Preconditions.checkNotNull(sslContext, "sslContext"); - HostPort hostPort = parseAuthority(authority); - this.host = hostPort.host; - this.port = hostPort.port; + // TODO: For empty authority and fallback flag + // GRPC_USE_CHANNEL_AUTHORITY_IF_NO_SNI_APPLICABLE present, we should parse authority + // but prevent it from being used for SAN validation in the TrustManager. + if (authority != null) { + HostPort hostPort = parseAuthority(authority); + this.host = hostPort.host; + this.port = hostPort.port; + } else { + this.host = null; + this.port = 0; + } this.executor = executor; this.handshakeCompleteRunnable = handshakeCompleteRunnable; - this.x509ExtendedTrustManager = x509ExtendedTrustManager; + this.x509TrustManager = x509TrustManager; } @Override @IgnoreJRERequirement protected void handlerAdded0(ChannelHandlerContext ctx) { - sslEngine = sslContext.newEngine(ctx.alloc(), host, port); + if (host != null) { + sslEngine = sslContext.newEngine(ctx.alloc(), host, port); + } else { + sslEngine = sslContext.newEngine(ctx.alloc()); + } SSLParameters sslParams = sslEngine.getSSLParameters(); sslParams.setEndpointIdentificationAlgorithm("HTTPS"); sslEngine.setSSLParameters(sslParams); @@ -709,7 +715,7 @@ private void propagateTlsComplete(ChannelHandlerContext ctx, SSLSession session) .set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.PRIVACY_AND_INTEGRITY) .set(Grpc.TRANSPORT_ATTR_SSL_SESSION, session) .set(GrpcAttributes.ATTR_AUTHORITY_VERIFIER, new X509AuthorityVerifier( - sslEngine, x509ExtendedTrustManager)) + sslEngine, x509TrustManager)) .build(); replaceProtocolNegotiationEvent(existingPne.withAttributes(attrs).withSecurity(security)); if (handshakeCompleteRunnable.isPresent()) { @@ -746,13 +752,14 @@ static HostPort parseAuthority(String authority) { * Returns a {@link ProtocolNegotiator} that ensures the pipeline is set up so that TLS will * be negotiated, the {@code handler} is added and writes to the {@link io.netty.channel.Channel} * may happen immediately, even before the TLS Handshake is complete. + * * @param executorPool a dedicated {@link Executor} pool for time-consuming TLS tasks */ public static ProtocolNegotiator tls(SslContext sslContext, ObjectPool executorPool, Optional handshakeCompleteRunnable, - X509TrustManager x509ExtendedTrustManager) { + X509TrustManager x509ExtendedTrustManager, String sni) { return new ClientTlsProtocolNegotiator(sslContext, executorPool, handshakeCompleteRunnable, - x509ExtendedTrustManager); + x509ExtendedTrustManager, sni); } /** @@ -762,7 +769,7 @@ public static ProtocolNegotiator tls(SslContext sslContext, */ public static ProtocolNegotiator tls(SslContext sslContext, X509TrustManager x509ExtendedTrustManager) { - return tls(sslContext, null, Optional.absent(), x509ExtendedTrustManager); + return tls(sslContext, null, Optional.absent(), x509ExtendedTrustManager, null); } public static ProtocolNegotiator.ClientFactory tlsClientFactory(SslContext sslContext, diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index 55abe29e93a..3f9759ee1d2 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -877,7 +877,7 @@ public void tlsNegotiationServerExecutorShouldSucceed() throws Exception { .keyManager(clientCert, clientKey) .build(); ProtocolNegotiator negotiator = ProtocolNegotiators.tls(clientContext, clientExecutorPool, - Optional.absent(), null); + Optional.absent(), null, null); // after starting the client, the Executor in the client pool should be used assertEquals(true, clientExecutorPool.isInUse()); final NettyClientTransport transport = newTransport(negotiator); diff --git a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java index 638fe960a32..9bb5a43d792 100644 --- a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java +++ b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java @@ -1026,7 +1026,7 @@ public void clientTlsHandler_closeDuringNegotiation() throws Exception { private ClientTlsProtocolNegotiator getClientTlsProtocolNegotiator() throws SSLException { return new ClientTlsProtocolNegotiator(GrpcSslContexts.forClient().trustManager( TlsTesting.loadCert("ca.pem")).build(), - null, Optional.absent(), null); + null, Optional.absent(), null, ""); } @Test @@ -1277,7 +1277,7 @@ public void clientTlsHandler_firesNegotiation() throws Exception { } FakeGrpcHttp2ConnectionHandler gh = FakeGrpcHttp2ConnectionHandler.newHandler(); ClientTlsProtocolNegotiator pn = new ClientTlsProtocolNegotiator(clientSslContext, - null, Optional.absent(), null); + null, Optional.absent(), null, null); WriteBufferingAndExceptionHandler clientWbaeh = new WriteBufferingAndExceptionHandler(pn.newHandler(gh)); diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java index 03976cc7d7b..3b52f61c9df 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java @@ -38,7 +38,6 @@ import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; import io.grpc.netty.InternalProtocolNegotiators; import io.grpc.netty.InternalProtocolNegotiators.ProtocolNegotiationHandler; -import io.grpc.s2a.internal.handshaker.S2AIdentity; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; @@ -259,7 +258,8 @@ public void onSuccess(SslContext sslContext) { public void run() { s2aStub.close(); } - })) + }), + null, null) .newHandler(grpcHandler); // Delegate the rest of the handshake to the TLS handler. and remove the diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index fba66e2e8d7..a506977d952 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -53,6 +53,7 @@ import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger.XdsLogLevel; +import io.grpc.xds.internal.XdsInternalAttributes; import io.grpc.xds.internal.security.SecurityProtocolNegotiators; import io.grpc.xds.internal.security.SslContextProviderSupplier; import io.grpc.xds.orca.OrcaPerRequestUtil; @@ -117,12 +118,12 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses); Attributes attributes = resolvedAddresses.getAttributes(); if (xdsClientPool == null) { - xdsClientPool = attributes.get(XdsAttributes.XDS_CLIENT_POOL); + xdsClientPool = attributes.get(io.grpc.xds.XdsAttributes.XDS_CLIENT_POOL); assert xdsClientPool != null; xdsClient = xdsClientPool.getObject(); } if (callCounterProvider == null) { - callCounterProvider = attributes.get(XdsAttributes.CALL_COUNTER_PROVIDER); + callCounterProvider = attributes.get(io.grpc.xds.XdsAttributes.CALL_COUNTER_PROVIDER); } ClusterImplConfig config = @@ -241,9 +242,9 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { .set(ATTR_CLUSTER_LOCALITY, localityAtomicReference); if (GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE", false)) { String hostname = args.getAddresses().get(0).getAttributes() - .get(XdsAttributes.ATTR_ADDRESS_NAME); + .get(XdsInternalAttributes.ATTR_ADDRESS_NAME); if (hostname != null) { - attrsBuilder.set(XdsAttributes.ATTR_ADDRESS_NAME, hostname); + attrsBuilder.set(XdsInternalAttributes.ATTR_ADDRESS_NAME, hostname); } } args = args.toBuilder().setAddresses(addresses).setAttributes(attrsBuilder.build()).build(); @@ -292,7 +293,7 @@ private List withAdditionalAttributes( List newAddresses = new ArrayList<>(); for (EquivalentAddressGroup eag : addresses) { Attributes.Builder attrBuilder = eag.getAttributes().toBuilder().set( - XdsAttributes.ATTR_CLUSTER_NAME, cluster); + io.grpc.xds.XdsAttributes.ATTR_CLUSTER_NAME, cluster); if (sslContextProviderSupplier != null) { attrBuilder.set( SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER, @@ -304,7 +305,7 @@ private List withAdditionalAttributes( } private ClusterLocality createClusterLocalityFromAttributes(Attributes addressAttributes) { - Locality locality = addressAttributes.get(XdsAttributes.ATTR_LOCALITY); + Locality locality = addressAttributes.get(io.grpc.xds.XdsAttributes.ATTR_LOCALITY); String localityName = addressAttributes.get(EquivalentAddressGroup.ATTR_LOCALITY_NAME); // Endpoint addresses resolved by ClusterResolverLoadBalancer should always contain @@ -438,7 +439,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { result = PickResult.withSubchannel(result.getSubchannel(), result.getStreamTracerFactory(), result.getSubchannel().getAttributes().get( - XdsAttributes.ATTR_ADDRESS_NAME)); + XdsInternalAttributes.ATTR_ADDRESS_NAME)); } } return result; diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index 06fafbb6cf1..4c4092632bf 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -47,6 +47,7 @@ import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger.XdsLogLevel; +import io.grpc.xds.internal.XdsInternalAttributes; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.ArrayList; @@ -97,7 +98,8 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses); ClusterResolverConfig config = (ClusterResolverConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); - XdsConfig xdsConfig = resolvedAddresses.getAttributes().get(XdsAttributes.XDS_CONFIG); + XdsConfig xdsConfig = resolvedAddresses.getAttributes().get( + io.grpc.xds.XdsAttributes.XDS_CONFIG); DiscoveryMechanism instance = config.discoveryMechanism; String cluster = instance.cluster; @@ -189,12 +191,12 @@ StatusOr edsUpdateToResult( String localityName = localityName(locality); Attributes attr = endpoint.eag().getAttributes().toBuilder() - .set(XdsAttributes.ATTR_LOCALITY, locality) + .set(io.grpc.xds.XdsAttributes.ATTR_LOCALITY, locality) .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, localityName) - .set(XdsAttributes.ATTR_LOCALITY_WEIGHT, + .set(io.grpc.xds.XdsAttributes.ATTR_LOCALITY_WEIGHT, localityLbInfo.localityWeight()) - .set(XdsAttributes.ATTR_SERVER_WEIGHT, weight) - .set(XdsAttributes.ATTR_ADDRESS_NAME, endpoint.hostname()) + .set(io.grpc.xds.XdsAttributes.ATTR_SERVER_WEIGHT, weight) + .set(XdsInternalAttributes.ATTR_ADDRESS_NAME, endpoint.hostname()) .build(); EquivalentAddressGroup eag; if (config.isHttp11ProxyAvailable()) { diff --git a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java index 9c2ee641423..01ef3d97b57 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java @@ -73,20 +73,54 @@ public int hashCode() { public static final class UpstreamTlsContext extends BaseTlsContext { + private final String sni; + private final boolean autoHostSni; + private final boolean autoSniSanValidation; + @VisibleForTesting public UpstreamTlsContext(CommonTlsContext commonTlsContext) { super(commonTlsContext); + this.sni = null; + this.autoHostSni = false; + this.autoSniSanValidation = false; + } + + @VisibleForTesting + public UpstreamTlsContext( + io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + upstreamTlsContext) { + super(upstreamTlsContext.getCommonTlsContext()); + this.sni = upstreamTlsContext.getSni(); + this.autoHostSni = upstreamTlsContext.getAutoHostSni(); + this.autoSniSanValidation = upstreamTlsContext.getAutoSniSanValidation(); } public static UpstreamTlsContext fromEnvoyProtoUpstreamTlsContext( io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext upstreamTlsContext) { - return new UpstreamTlsContext(upstreamTlsContext.getCommonTlsContext()); + return new UpstreamTlsContext(upstreamTlsContext); + } + + public String getSni() { + return sni; + } + + public boolean getAutoHostSni() { + return autoHostSni; + } + + public boolean getAutoSniSanValidation() { + return autoSniSanValidation; } @Override public String toString() { - return "UpstreamTlsContext{" + "commonTlsContext=" + commonTlsContext + '}'; + return "UpstreamTlsContext{" + + "commonTlsContext=" + commonTlsContext + + "\nsni=" + sni + + "\nauto_host_sni=" + autoHostSni + + "\nauto_sni_san_validation=" + autoSniSanValidation + + "}"; } } diff --git a/xds/src/main/java/io/grpc/xds/XdsAttributes.java b/xds/src/main/java/io/grpc/xds/XdsAttributes.java index 2e165201e5f..0e770173219 100644 --- a/xds/src/main/java/io/grpc/xds/XdsAttributes.java +++ b/xds/src/main/java/io/grpc/xds/XdsAttributes.java @@ -88,11 +88,6 @@ final class XdsAttributes { static final Attributes.Key ATTR_SERVER_WEIGHT = Attributes.Key.create("io.grpc.xds.XdsAttributes.serverWeight"); - /** Name associated with individual address, if available (e.g., DNS name). */ - @EquivalentAddressGroup.Attr - static final Attributes.Key ATTR_ADDRESS_NAME = - Attributes.Key.create("io.grpc.xds.XdsAttributes.addressName"); - /** * Filter chain match for network filters. */ diff --git a/xds/src/main/java/io/grpc/xds/internal/XdsInternalAttributes.java b/xds/src/main/java/io/grpc/xds/internal/XdsInternalAttributes.java new file mode 100644 index 00000000000..b05230ea30b --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/XdsInternalAttributes.java @@ -0,0 +1,27 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal; + +import io.grpc.Attributes; +import io.grpc.EquivalentAddressGroup; + +public final class XdsInternalAttributes { + /** Name associated with individual address, if available (e.g., DNS name). */ + @EquivalentAddressGroup.Attr + public static final Attributes.Key ATTR_ADDRESS_NAME = + Attributes.Key.create("io.grpc.xds.XdsAttributes.addressName"); +} diff --git a/xds/src/main/java/io/grpc/xds/internal/security/DynamicSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/DynamicSslContextProvider.java index 6bf66d022ff..e7b27cd644a 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/DynamicSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/DynamicSslContextProvider.java @@ -30,9 +30,11 @@ import java.io.IOException; import java.security.cert.CertStoreException; import java.security.cert.CertificateException; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; +import javax.net.ssl.X509TrustManager; /** Base class for dynamic {@link SslContextProvider}s. */ @Internal @@ -40,7 +42,8 @@ public abstract class DynamicSslContextProvider extends SslContextProvider { protected final List pendingCallbacks = new ArrayList<>(); @Nullable protected final CertificateValidationContext staticCertificateValidationContext; - @Nullable protected SslContext sslContext; + @Nullable protected AbstractMap.SimpleImmutableEntry + sslContextAndTrustManager; protected DynamicSslContextProvider( BaseTlsContext tlsContext, CertificateValidationContext staticCertValidationContext) { @@ -49,15 +52,17 @@ protected DynamicSslContextProvider( } @Nullable - public SslContext getSslContext() { - return sslContext; + public AbstractMap.SimpleImmutableEntry + getSslContextAndTrustManager() { + return sslContextAndTrustManager; } protected abstract CertificateValidationContext generateCertificateValidationContext(); /** Gets a server or client side SslContextBuilder. */ - protected abstract SslContextBuilder getSslContextBuilder( - CertificateValidationContext certificateValidationContext) + protected abstract AbstractMap.SimpleImmutableEntry + getSslContextBuilderAndTrustManager( + CertificateValidationContext certificateValidationContext) throws CertificateException, IOException, CertStoreException; // this gets called only when requested secrets are ready... @@ -65,7 +70,8 @@ protected final void updateSslContext() { try { CertificateValidationContext localCertValidationContext = generateCertificateValidationContext(); - SslContextBuilder sslContextBuilder = getSslContextBuilder(localCertValidationContext); + AbstractMap.SimpleImmutableEntry sslContextBuilderAndTm = + getSslContextBuilderAndTrustManager(localCertValidationContext); CommonTlsContext commonTlsContext = getCommonTlsContext(); if (commonTlsContext != null && commonTlsContext.getAlpnProtocolsCount() > 0) { List alpnList = commonTlsContext.getAlpnProtocolsList(); @@ -75,16 +81,18 @@ protected final void updateSslContext() { ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, alpnList); - sslContextBuilder.applicationProtocolConfig(apn); + sslContextBuilderAndTm.getKey().applicationProtocolConfig(apn); } List pendingCallbacksCopy; - SslContext sslContextCopy; + AbstractMap.SimpleImmutableEntry + sslContextAndExtendedX09TrustManagerCopy; synchronized (pendingCallbacks) { - sslContext = sslContextBuilder.build(); - sslContextCopy = sslContext; + sslContextAndTrustManager = new AbstractMap.SimpleImmutableEntry<>( + sslContextBuilderAndTm.getKey().build(), sslContextBuilderAndTm.getValue()); + sslContextAndExtendedX09TrustManagerCopy = sslContextAndTrustManager; pendingCallbacksCopy = clonePendingCallbacksAndClear(); } - makePendingCallbacks(sslContextCopy, pendingCallbacksCopy); + makePendingCallbacks(sslContextAndExtendedX09TrustManagerCopy, pendingCallbacksCopy); } catch (Exception e) { onError(Status.fromThrowable(e)); throw new RuntimeException(e); @@ -92,12 +100,13 @@ protected final void updateSslContext() { } protected final void callPerformCallback( - Callback callback, final SslContext sslContextCopy) { + Callback callback, + final AbstractMap.SimpleImmutableEntry sslContextAndTmCopy) { performCallback( new SslContextGetter() { @Override - public SslContext get() { - return sslContextCopy; + public AbstractMap.SimpleImmutableEntry get() { + return sslContextAndTmCopy; } }, callback @@ -108,10 +117,10 @@ public SslContext get() { public final void addCallback(Callback callback) { checkNotNull(callback, "callback"); // if there is a computed sslContext just send it - SslContext sslContextCopy = null; + AbstractMap.SimpleImmutableEntry sslContextCopy = null; synchronized (pendingCallbacks) { - if (sslContext != null) { - sslContextCopy = sslContext; + if (sslContextAndTrustManager != null) { + sslContextCopy = sslContextAndTrustManager; } else { pendingCallbacks.add(callback); } @@ -122,9 +131,11 @@ public final void addCallback(Callback callback) { } private final void makePendingCallbacks( - SslContext sslContextCopy, List pendingCallbacksCopy) { + AbstractMap.SimpleImmutableEntry + sslContextAndExtendedX509TrustManagerCopy, + List pendingCallbacksCopy) { for (Callback callback : pendingCallbacksCopy) { - callPerformCallback(callback, sslContextCopy); + callPerformCallback(callback, sslContextAndExtendedX509TrustManagerCopy); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java b/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java index c34fab74032..10e3a0bcda1 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import io.grpc.Attributes; import io.grpc.Grpc; import io.grpc.internal.GrpcUtil; @@ -29,6 +30,10 @@ import io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator; import io.grpc.netty.InternalProtocolNegotiators; import io.grpc.netty.ProtocolNegotiationEvent; +import io.grpc.xds.EnvoyServerProtoData; +import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; +import io.grpc.xds.internal.XdsInternalAttributes; +import io.grpc.xds.internal.security.trust.CertificateUtils; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; @@ -36,12 +41,14 @@ import io.netty.handler.ssl.SslContext; import io.netty.util.AsciiString; import java.security.cert.CertStoreException; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; +import javax.net.ssl.X509TrustManager; /** * Provides client and server side gRPC {@link ProtocolNegotiator}s to provide the SSL @@ -60,14 +67,14 @@ private SecurityProtocolNegotiators() { private static final AsciiString SCHEME = AsciiString.of("http"); public static final Attributes.Key - ATTR_SERVER_SSL_CONTEXT_PROVIDER_SUPPLIER = - Attributes.Key.create("io.grpc.xds.internal.security.server.sslContextProviderSupplier"); + ATTR_SERVER_SSL_CONTEXT_PROVIDER_SUPPLIER = + Attributes.Key.create("io.grpc.xds.internal.security.server.sslContextProviderSupplier"); /** Attribute key for SslContextProviderSupplier (used from client) for a subchannel. */ @Grpc.TransportAttr public static final Attributes.Key ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER = - Attributes.Key.create("io.grpc.xds.internal.security.SslContextProviderSupplier"); + Attributes.Key.create("io.grpc.xds.internal.security.SslContextProviderSupplier"); /** * Returns a {@link InternalProtocolNegotiator.ClientFactory}. @@ -142,7 +149,8 @@ public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { fallbackProtocolNegotiator, "No TLS config and no fallbackProtocolNegotiator!"); return fallbackProtocolNegotiator.newHandler(grpcHandler); } - return new ClientSecurityHandler(grpcHandler, localSslContextProviderSupplier); + return new ClientSecurityHandler(grpcHandler, localSslContextProviderSupplier, + grpcHandler.getEagAttributes().get(XdsInternalAttributes.ATTR_ADDRESS_NAME)); } @Override @@ -185,10 +193,12 @@ static final class ClientSecurityHandler extends InternalProtocolNegotiators.ProtocolNegotiationHandler { private final GrpcHttp2ConnectionHandler grpcHandler; private final SslContextProviderSupplier sslContextProviderSupplier; + private final String sni; ClientSecurityHandler( GrpcHttp2ConnectionHandler grpcHandler, - SslContextProviderSupplier sslContextProviderSupplier) { + SslContextProviderSupplier sslContextProviderSupplier, + String endpointHostname) { super( // superclass (InternalProtocolNegotiators.ProtocolNegotiationHandler) expects 'next' // handler but we don't have a next handler _yet_. So we "disable" superclass's behavior @@ -202,6 +212,19 @@ public void handlerAdded(ChannelHandlerContext ctx) throws Exception { checkNotNull(grpcHandler, "grpcHandler"); this.grpcHandler = grpcHandler; this.sslContextProviderSupplier = sslContextProviderSupplier; + EnvoyServerProtoData.BaseTlsContext tlsContext = sslContextProviderSupplier.getTlsContext(); + UpstreamTlsContext upstreamTlsContext = ((UpstreamTlsContext) tlsContext); + if (CertificateUtils.isXdsSniEnabled) { + sni = upstreamTlsContext.getAutoHostSni() && !Strings.isNullOrEmpty(endpointHostname) + ? endpointHostname : upstreamTlsContext.getSni(); + } else { + sni = grpcHandler.getAuthority(); + } + } + + @VisibleForTesting + String getSni() { + return sni; } @Override @@ -213,7 +236,8 @@ protected void handlerAdded0(final ChannelHandlerContext ctx) { new SslContextProvider.Callback(ctx.executor()) { @Override - public void updateSslContext(SslContext sslContext) { + public void updateSslContextAndExtendedX509TrustManager( + AbstractMap.SimpleImmutableEntry sslContextAndTm) { if (ctx.isRemoved()) { return; } @@ -222,7 +246,9 @@ public void updateSslContext(SslContext sslContext) { "ClientSecurityHandler.updateSslContext authority={0}, ctx.name={1}", new Object[]{grpcHandler.getAuthority(), ctx.name()}); ChannelHandler handler = - InternalProtocolNegotiators.tls(sslContext).newHandler(grpcHandler); + InternalProtocolNegotiators.tls( + sslContextAndTm.getKey(), sni, sslContextAndTm.getValue()) + .newHandler(grpcHandler); // Delegate rest of handshake to TLS handler ctx.pipeline().addAfter(ctx.name(), null, handler); @@ -356,9 +382,10 @@ protected void handlerAdded0(final ChannelHandlerContext ctx) { new SslContextProvider.Callback(ctx.executor()) { @Override - public void updateSslContext(SslContext sslContext) { - ChannelHandler handler = - InternalProtocolNegotiators.serverTls(sslContext).newHandler(grpcHandler); + public void updateSslContextAndExtendedX509TrustManager( + AbstractMap.SimpleImmutableEntry sslContextAndTm) { + ChannelHandler handler = InternalProtocolNegotiators.serverTls( + sslContextAndTm.getKey()).newHandler(grpcHandler); // Delegate rest of handshake to TLS handler if (!ctx.isRemoved()) { diff --git a/xds/src/main/java/io/grpc/xds/internal/security/SslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/SslContextProvider.java index a0c4ed37dfb..a5d14f72dc5 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/SslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/SslContextProvider.java @@ -32,7 +32,9 @@ import java.io.IOException; import java.security.cert.CertStoreException; import java.security.cert.CertificateException; +import java.util.AbstractMap; import java.util.concurrent.Executor; +import javax.net.ssl.X509TrustManager; /** * A SslContextProvider is a "container" or provider of SslContext. This is used by gRPC-xds to @@ -57,7 +59,8 @@ protected Callback(Executor executor) { } /** Informs callee of new/updated SslContext. */ - @VisibleForTesting public abstract void updateSslContext(SslContext sslContext); + @VisibleForTesting public abstract void updateSslContextAndExtendedX509TrustManager( + AbstractMap.SimpleImmutableEntry sslContext); /** Informs callee of an exception that was generated. */ @VisibleForTesting protected abstract void onException(Throwable throwable); @@ -119,8 +122,9 @@ protected final void performCallback( @Override public void run() { try { - SslContext sslContext = sslContextGetter.get(); - callback.updateSslContext(sslContext); + AbstractMap.SimpleImmutableEntry sslContextAndTm = + sslContextGetter.get(); + callback.updateSslContextAndExtendedX509TrustManager(sslContextAndTm); } catch (Throwable e) { callback.onException(e); } @@ -130,6 +134,6 @@ public void run() { /** Allows implementations to compute or get SslContext. */ protected interface SslContextGetter { - SslContext get() throws Exception; + AbstractMap.SimpleImmutableEntry get() throws Exception; } } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/SslContextProviderSupplier.java b/xds/src/main/java/io/grpc/xds/internal/security/SslContextProviderSupplier.java index 5f629273179..38ae15a88aa 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/SslContextProviderSupplier.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/SslContextProviderSupplier.java @@ -25,7 +25,9 @@ import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.TlsContextManager; import io.netty.handler.ssl.SslContext; +import java.util.AbstractMap; import java.util.Objects; +import javax.net.ssl.X509TrustManager; /** * Enables Client or server side to initialize this object with the received {@link BaseTlsContext} @@ -52,7 +54,8 @@ public BaseTlsContext getTlsContext() { } /** Updates SslContext via the passed callback. */ - public synchronized void updateSslContext(final SslContextProvider.Callback callback) { + public synchronized void updateSslContext( + final SslContextProvider.Callback callback) { checkNotNull(callback, "callback"); try { if (!shutdown) { @@ -66,8 +69,9 @@ public synchronized void updateSslContext(final SslContextProvider.Callback call new SslContextProvider.Callback(callback.getExecutor()) { @Override - public void updateSslContext(SslContext sslContext) { - callback.updateSslContext(sslContext); + public void updateSslContextAndExtendedX509TrustManager( + AbstractMap.SimpleImmutableEntry sslContextAndTm) { + callback.updateSslContextAndExtendedX509TrustManager(sslContextAndTm); releaseSslContextProvider(toRelease); } @@ -98,7 +102,8 @@ private void releaseSslContextProvider(SslContextProvider toRelease) { private SslContextProvider getSslContextProvider() { return tlsContext instanceof UpstreamTlsContext ? tlsContextManager.findOrCreateClientSslContextProvider((UpstreamTlsContext) tlsContext) - : tlsContextManager.findOrCreateServerSslContextProvider((DownstreamTlsContext) tlsContext); + : tlsContextManager.findOrCreateServerSslContextProvider( + (DownstreamTlsContext) tlsContext); } @VisibleForTesting public boolean isShutdown() { diff --git a/xds/src/main/java/io/grpc/xds/internal/security/TlsContextManagerImpl.java b/xds/src/main/java/io/grpc/xds/internal/security/TlsContextManagerImpl.java index 34a8863c52b..f56524d50b7 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/TlsContextManagerImpl.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/TlsContextManagerImpl.java @@ -71,8 +71,6 @@ public SslContextProvider findOrCreateServerSslContextProvider( public SslContextProvider findOrCreateClientSslContextProvider( UpstreamTlsContext upstreamTlsContext) { checkNotNull(upstreamTlsContext, "upstreamTlsContext"); - CommonTlsContext.Builder builder = upstreamTlsContext.getCommonTlsContext().toBuilder(); - upstreamTlsContext = new UpstreamTlsContext(builder.build()); return mapForClients.get(upstreamTlsContext); } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java index 131ae6b6125..e92f9ad1e54 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java @@ -26,8 +26,11 @@ import io.netty.handler.ssl.SslContextBuilder; import java.security.cert.CertStoreException; import java.security.cert.X509Certificate; +import java.util.AbstractMap; +import java.util.Arrays; import java.util.Map; import javax.annotation.Nullable; +import javax.net.ssl.X509TrustManager; /** A client SslContext provider using CertificateProviderInstance to fetch secrets. */ final class CertProviderClientSslContextProvider extends CertProviderSslContextProvider { @@ -51,27 +54,46 @@ final class CertProviderClientSslContextProvider extends CertProviderSslContextP } @Override - protected final SslContextBuilder getSslContextBuilder( - CertificateValidationContext certificateValidationContextdationContext) - throws CertStoreException { + protected final AbstractMap.SimpleImmutableEntry + getSslContextBuilderAndTrustManager( + CertificateValidationContext certificateValidationContext) + throws CertStoreException { SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient(); if (savedSpiffeTrustMap != null) { sslContextBuilder = sslContextBuilder.trustManager( new XdsTrustManagerFactory( savedSpiffeTrustMap, - certificateValidationContextdationContext)); + certificateValidationContext, + ((UpstreamTlsContext) tlsContext).getAutoSniSanValidation())); } else if (savedTrustedRoots != null) { sslContextBuilder = sslContextBuilder.trustManager( new XdsTrustManagerFactory( savedTrustedRoots.toArray(new X509Certificate[0]), - certificateValidationContextdationContext)); + certificateValidationContext, + ((UpstreamTlsContext) tlsContext).getAutoSniSanValidation())); } else { // Should be impossible because of the check in CertProviderClientSslContextProviderFactory throw new IllegalStateException("There must be trusted roots or a SPIFFE trust map"); } + XdsTrustManagerFactory trustManagerFactory; + if (savedSpiffeTrustMap != null) { + trustManagerFactory = new XdsTrustManagerFactory( + savedSpiffeTrustMap, + certificateValidationContext, + ((UpstreamTlsContext) tlsContext).getAutoSniSanValidation()); + sslContextBuilder = sslContextBuilder.trustManager(trustManagerFactory); + } else { + trustManagerFactory = new XdsTrustManagerFactory( + savedTrustedRoots.toArray(new X509Certificate[0]), + certificateValidationContext, + ((UpstreamTlsContext) tlsContext).getAutoSniSanValidation()); + sslContextBuilder = sslContextBuilder.trustManager(trustManagerFactory); + } if (isMtls()) { sslContextBuilder.keyManager(savedKey, savedCertChain); } - return sslContextBuilder; + return new AbstractMap.SimpleImmutableEntry<>(sslContextBuilder, + io.grpc.internal.CertificateUtils.getX509ExtendedTrustManager( + Arrays.asList(trustManagerFactory.getTrustManagers()))); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProvider.java index ef65bbfb6f9..3712b948142 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProvider.java @@ -30,8 +30,10 @@ import java.security.cert.CertStoreException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.AbstractMap; import java.util.Map; import javax.annotation.Nullable; +import javax.net.ssl.X509TrustManager; /** A server SslContext provider using CertificateProviderInstance to fetch secrets. */ final class CertProviderServerSslContextProvider extends CertProviderSslContextProvider { @@ -55,23 +57,25 @@ final class CertProviderServerSslContextProvider extends CertProviderSslContextP } @Override - protected final SslContextBuilder getSslContextBuilder( - CertificateValidationContext certificateValidationContextdationContext) - throws CertStoreException, CertificateException, IOException { + protected final AbstractMap.SimpleImmutableEntry + getSslContextBuilderAndTrustManager( + CertificateValidationContext certificateValidationContextdationContext) + throws CertStoreException, CertificateException, IOException { SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(savedKey, savedCertChain); XdsTrustManagerFactory trustManagerFactory = null; if (isMtls() && savedSpiffeTrustMap != null) { trustManagerFactory = new XdsTrustManagerFactory( savedSpiffeTrustMap, - certificateValidationContextdationContext); + certificateValidationContextdationContext, false); } else if (isMtls()) { trustManagerFactory = new XdsTrustManagerFactory( savedTrustedRoots.toArray(new X509Certificate[0]), - certificateValidationContextdationContext); + certificateValidationContextdationContext, false); } setClientAuthValues(sslContextBuilder, trustManagerFactory); sslContextBuilder = GrpcSslContexts.configure(sslContextBuilder); - return sslContextBuilder; + // TrustManager in the below return value is not used on the server side, so setting it to null + return new AbstractMap.SimpleImmutableEntry<>(sslContextBuilder, null); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java b/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java index 86b6dd95c3e..89b4abd3029 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java @@ -16,6 +16,7 @@ package io.grpc.xds.internal.security.trust; +import io.grpc.internal.GrpcUtil; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; @@ -29,6 +30,8 @@ * Contains certificate utility method(s). */ public final class CertificateUtils { + public static boolean isXdsSniEnabled = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_SNI", false); + /** * Generates X509Certificate array from a file on disk. * diff --git a/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactory.java b/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactory.java index 8cb44117065..664c5dd9362 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactory.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactory.java @@ -58,43 +58,50 @@ public XdsTrustManagerFactory(CertificateValidationContext certificateValidation this( getTrustedCaFromCertContext(certificateValidationContext), certificateValidationContext, + false, false); } public XdsTrustManagerFactory( - X509Certificate[] certs, CertificateValidationContext staticCertificateValidationContext) - throws CertStoreException { - this(certs, staticCertificateValidationContext, true); + X509Certificate[] certs, CertificateValidationContext staticCertificateValidationContext, + boolean autoSniSanValidation) throws CertStoreException { + this(certs, staticCertificateValidationContext, true, autoSniSanValidation); } public XdsTrustManagerFactory(Map> spiffeTrustMap, - CertificateValidationContext staticCertificateValidationContext) throws CertStoreException { - this(spiffeTrustMap, staticCertificateValidationContext, true); + CertificateValidationContext staticCertificateValidationContext, boolean autoSniSanValidation) + throws CertStoreException { + this(spiffeTrustMap, staticCertificateValidationContext, true, autoSniSanValidation); } private XdsTrustManagerFactory( X509Certificate[] certs, CertificateValidationContext certificateValidationContext, - boolean validationContextIsStatic) + boolean validationContextIsStatic, + boolean autoSniSanValidation) throws CertStoreException { if (validationContextIsStatic) { checkArgument( - certificateValidationContext == null || !certificateValidationContext.hasTrustedCa(), + certificateValidationContext == null || !certificateValidationContext.hasTrustedCa() + || certificateValidationContext.hasSystemRootCerts(), "only static certificateValidationContext expected"); } - xdsX509TrustManager = createX509TrustManager(certs, certificateValidationContext); + xdsX509TrustManager = createX509TrustManager( + certs, certificateValidationContext, autoSniSanValidation); } private XdsTrustManagerFactory( Map> spiffeTrustMap, CertificateValidationContext certificateValidationContext, - boolean validationContextIsStatic) + boolean validationContextIsStatic, + boolean autoSniSanValidation) throws CertStoreException { if (validationContextIsStatic) { checkArgument( certificateValidationContext == null || !certificateValidationContext.hasTrustedCa(), "only static certificateValidationContext expected"); - xdsX509TrustManager = createX509TrustManager(spiffeTrustMap, certificateValidationContext); + xdsX509TrustManager = createX509TrustManager( + spiffeTrustMap, certificateValidationContext, autoSniSanValidation); } } @@ -121,21 +128,24 @@ private static X509Certificate[] getTrustedCaFromCertContext( @VisibleForTesting static XdsX509TrustManager createX509TrustManager( - X509Certificate[] certs, CertificateValidationContext certContext) throws CertStoreException { - return new XdsX509TrustManager(certContext, createTrustManager(certs)); + X509Certificate[] certs, CertificateValidationContext certContext, + boolean autoSniSanValidation) + throws CertStoreException { + return new XdsX509TrustManager(certContext, createTrustManager(certs), autoSniSanValidation); } @VisibleForTesting static XdsX509TrustManager createX509TrustManager( Map> spiffeTrustMapFile, - CertificateValidationContext certContext) throws CertStoreException { + CertificateValidationContext certContext, boolean autoSniSanValidation) + throws CertStoreException { checkNotNull(spiffeTrustMapFile, "spiffeTrustMapFile"); Map delegates = new HashMap<>(); for (Map.Entry> entry:spiffeTrustMapFile.entrySet()) { delegates.put(entry.getKey(), createTrustManager( entry.getValue().toArray(new X509Certificate[0]))); } - return new XdsX509TrustManager(certContext, delegates); + return new XdsX509TrustManager(certContext, delegates, autoSniSanValidation); } private static X509ExtendedTrustManager createTrustManager(X509Certificate[] certs) diff --git a/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java b/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java index ebf7ea82184..e8e7243ce0e 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/trust/XdsX509TrustManager.java @@ -31,6 +31,7 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -39,6 +40,8 @@ import java.util.Map; import java.util.Set; import javax.annotation.Nullable; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocket; @@ -60,21 +63,26 @@ final class XdsX509TrustManager extends X509ExtendedTrustManager implements X509 private final X509ExtendedTrustManager delegate; private final Map spiffeTrustMapDelegates; private final CertificateValidationContext certContext; + private final boolean autoSniSanValidation; XdsX509TrustManager(@Nullable CertificateValidationContext certContext, - X509ExtendedTrustManager delegate) { + X509ExtendedTrustManager delegate, + boolean autoSniSanValidation) { checkNotNull(delegate, "delegate"); this.certContext = certContext; this.delegate = delegate; this.spiffeTrustMapDelegates = null; + this.autoSniSanValidation = autoSniSanValidation; } XdsX509TrustManager(@Nullable CertificateValidationContext certContext, - Map spiffeTrustMapDelegates) { + Map spiffeTrustMapDelegates, + boolean autoSniSanValidation) { checkNotNull(spiffeTrustMapDelegates, "spiffeTrustMapDelegates"); this.spiffeTrustMapDelegates = ImmutableMap.copyOf(spiffeTrustMapDelegates); this.certContext = certContext; this.delegate = null; + this.autoSniSanValidation = autoSniSanValidation; } private static boolean verifyDnsNameInPattern( @@ -206,12 +214,11 @@ private static void verifySubjectAltNameInLeaf( * This is called from various check*Trusted methods. */ @VisibleForTesting - void verifySubjectAltNameInChain(X509Certificate[] peerCertChain) throws CertificateException { + void verifySubjectAltNameInChain(X509Certificate[] peerCertChain, + List verifyList) throws CertificateException { if (certContext == null) { return; } - @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names - List verifyList = certContext.getMatchSubjectAltNamesList(); if (verifyList.isEmpty()) { return; } @@ -223,29 +230,36 @@ void verifySubjectAltNameInChain(X509Certificate[] peerCertChain) throws Certifi } @Override + @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { chooseDelegate(chain).checkClientTrusted(chain, authType, socket); - verifySubjectAltNameInChain(chain); + verifySubjectAltNameInChain(chain, certContext != null + ? certContext.getMatchSubjectAltNamesList() : new ArrayList<>()); } @Override + @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { chooseDelegate(chain).checkClientTrusted(chain, authType, sslEngine); - verifySubjectAltNameInChain(chain); + verifySubjectAltNameInChain(chain, certContext != null + ? certContext.getMatchSubjectAltNamesList() : new ArrayList<>()); } @Override + @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { chooseDelegate(chain).checkClientTrusted(chain, authType); - verifySubjectAltNameInChain(chain); + verifySubjectAltNameInChain(chain, certContext != null + ? certContext.getMatchSubjectAltNamesList() : new ArrayList<>()); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + List sniMatchers = null; if (socket instanceof SSLSocket) { SSLSocket sslSocket = (SSLSocket) socket; SSLParameters sslParams = sslSocket.getSSLParameters(); @@ -253,28 +267,60 @@ public void checkServerTrusted(X509Certificate[] chain, String authType, Socket sslParams.setEndpointIdentificationAlgorithm(""); sslSocket.setSSLParameters(sslParams); } + sniMatchers = getAutoSniSanMatchers(sslParams); + } + if (sniMatchers.isEmpty() && certContext != null) { + @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names + List sniMatchersTmp = certContext.getMatchSubjectAltNamesList(); + sniMatchers = sniMatchersTmp; } chooseDelegate(chain).checkServerTrusted(chain, authType, socket); - verifySubjectAltNameInChain(chain); + verifySubjectAltNameInChain(chain, sniMatchers); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { + List sniMatchers = null; SSLParameters sslParams = sslEngine.getSSLParameters(); if (sslParams != null) { sslParams.setEndpointIdentificationAlgorithm(""); sslEngine.setSSLParameters(sslParams); + sniMatchers = getAutoSniSanMatchers(sslParams); + } + if (sniMatchers.isEmpty() && certContext != null) { + @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names + List sniMatchersTmp = certContext.getMatchSubjectAltNamesList(); + sniMatchers = sniMatchersTmp; } chooseDelegate(chain).checkServerTrusted(chain, authType, sslEngine); - verifySubjectAltNameInChain(chain); + verifySubjectAltNameInChain(chain, sniMatchers); } @Override + @SuppressWarnings("deprecation") // gRFC A29 predates match_typed_subject_alt_names public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { chooseDelegate(chain).checkServerTrusted(chain, authType); - verifySubjectAltNameInChain(chain); + verifySubjectAltNameInChain(chain, certContext != null + ? certContext.getMatchSubjectAltNamesList() : new ArrayList<>()); + } + + private List getAutoSniSanMatchers(SSLParameters sslParams) { + List sniNamesToMatch = new ArrayList<>(); + if (CertificateUtils.isXdsSniEnabled && autoSniSanValidation) { + List serverNames = sslParams.getServerNames(); + if (serverNames != null) { + for (SNIServerName serverName : serverNames) { + if (serverName instanceof SNIHostName) { + SNIHostName sniHostName = (SNIHostName) serverName; + String hostName = sniHostName.getAsciiName(); + sniNamesToMatch.add(StringMatcher.newBuilder().setExact(hostName).build()); + } + } + } + } + return sniNamesToMatch; } private X509ExtendedTrustManager chooseDelegate(X509Certificate[] chain) diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index c5e3f80f170..a491d3f3612 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -77,6 +77,7 @@ import io.grpc.xds.client.Stats.ClusterStats; import io.grpc.xds.client.Stats.UpstreamLocalityStats; import io.grpc.xds.client.XdsClient; +import io.grpc.xds.internal.XdsInternalAttributes; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import io.grpc.xds.internal.security.SecurityProtocolNegotiators; import io.grpc.xds.internal.security.SslContextProvider; @@ -197,7 +198,7 @@ public void handleResolvedAddresses_propagateToChildPolicy() { FakeLoadBalancer childBalancer = Iterables.getOnlyElement(downstreamBalancers); assertThat(Iterables.getOnlyElement(childBalancer.addresses)).isEqualTo(endpoint); assertThat(childBalancer.config).isSameInstanceAs(weightedTargetConfig); - assertThat(childBalancer.attributes.get(XdsAttributes.XDS_CLIENT_POOL)) + assertThat(childBalancer.attributes.get(io.grpc.xds.XdsAttributes.XDS_CLIENT_POOL)) .isSameInstanceAs(xdsClientPool); assertThat(childBalancer.attributes.get(NameResolver.ATTR_BACKEND_SERVICE)).isEqualTo(CLUSTER); } @@ -561,7 +562,7 @@ public void dropRpcsWithRespectToLbConfigDropCategories() { .setAddresses(Collections.singletonList(endpoint)) .setAttributes( Attributes.newBuilder() - .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) + .set(io.grpc.xds.XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) .build()) .setLoadBalancingPolicyConfig(config) .build()); @@ -766,14 +767,14 @@ public void endpointAddressesAttachedWithClusterName() { .build(); Subchannel subchannel = leafBalancer.helper.createSubchannel(args); for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) { - assertThat(eag.getAttributes().get(XdsAttributes.ATTR_CLUSTER_NAME)) + assertThat(eag.getAttributes().get(io.grpc.xds.XdsAttributes.ATTR_CLUSTER_NAME)) .isEqualTo(CLUSTER); } // An address update should also retain the cluster attribute. subchannel.updateAddresses(leafBalancer.addresses); for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) { - assertThat(eag.getAttributes().get(XdsAttributes.ATTR_CLUSTER_NAME)) + assertThat(eag.getAttributes().get(io.grpc.xds.XdsAttributes.ATTR_CLUSTER_NAME)) .isEqualTo(CLUSTER); } } @@ -811,10 +812,10 @@ public void endpointAddressesAttachedWithClusterName() { new FixedResultPicker(PickResult.withSubchannel(subchannel))); } }); - assertThat(subchannel.getAttributes().get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo( + assertThat(subchannel.getAttributes().get(XdsInternalAttributes.ATTR_ADDRESS_NAME)).isEqualTo( "authority-host-name"); for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) { - assertThat(eag.getAttributes().get(XdsAttributes.ATTR_ADDRESS_NAME)) + assertThat(eag.getAttributes().get(XdsInternalAttributes.ATTR_ADDRESS_NAME)) .isEqualTo("authority-host-name"); } @@ -863,9 +864,9 @@ public void endpointAddressesAttachedWithClusterName() { } }); // Sub Channel wrapper args won't have the address name although addresses will. - assertThat(subchannel.getAttributes().get(XdsAttributes.ATTR_ADDRESS_NAME)).isNull(); + assertThat(subchannel.getAttributes().get(XdsInternalAttributes.ATTR_ADDRESS_NAME)).isNull(); for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) { - assertThat(eag.getAttributes().get(XdsAttributes.ATTR_ADDRESS_NAME)) + assertThat(eag.getAttributes().get(XdsInternalAttributes.ATTR_ADDRESS_NAME)) .isEqualTo("authority-host-name"); } @@ -881,7 +882,8 @@ public void endpointAddressesAttachedWithClusterName() { @Test public void endpointAddressesAttachedWithTlsConfig_securityEnabledByDefault() { UpstreamTlsContext upstreamTlsContext = - CommonTlsContextTestsUtil.buildUpstreamTlsContext("google_cloud_private_spiffe", true); + CommonTlsContextTestsUtil.buildUpstreamTlsContext( + "google_cloud_private_spiffe", true); LoadBalancerProvider weightedTargetProvider = new WeightedTargetLoadBalancerProvider(); WeightedTargetConfig weightedTargetConfig = buildWeightedTargetConfig(ImmutableMap.of(locality, 10)); @@ -925,8 +927,8 @@ public void endpointAddressesAttachedWithTlsConfig_securityEnabledByDefault() { } // Config with a new UpstreamTlsContext. - upstreamTlsContext = - CommonTlsContextTestsUtil.buildUpstreamTlsContext("google_cloud_private_spiffe1", true); + upstreamTlsContext = CommonTlsContextTestsUtil.buildUpstreamTlsContext( + "google_cloud_private_spiffe1", true); config = new ClusterImplConfig(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( @@ -957,8 +959,8 @@ private void deliverAddressesAndConfig(List addresses, .setAddresses(addresses) .setAttributes( Attributes.newBuilder() - .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) - .set(XdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider) + .set(io.grpc.xds.XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) + .set(io.grpc.xds.XdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider) .build()) .setLoadBalancingPolicyConfig(config) .build()); @@ -1015,11 +1017,11 @@ public String toString() { } Attributes.Builder attributes = Attributes.newBuilder() - .set(XdsAttributes.ATTR_LOCALITY, locality) + .set(io.grpc.xds.XdsAttributes.ATTR_LOCALITY, locality) // Unique but arbitrary string .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, locality.toString()); if (authorityHostname != null) { - attributes.set(XdsAttributes.ATTR_ADDRESS_NAME, authorityHostname); + attributes.set(XdsInternalAttributes.ATTR_ADDRESS_NAME, authorityHostname); } EquivalentAddressGroup eag = new EquivalentAddressGroup(new FakeSocketAddress(name), attributes.build()); diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index be68018792b..86f525c61f2 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -102,6 +102,7 @@ import io.grpc.xds.WrrLocalityLoadBalancer.WrrLocalityConfig; import io.grpc.xds.client.Bootstrapper.ServerInfo; import io.grpc.xds.client.XdsClient; +import io.grpc.xds.internal.XdsInternalAttributes; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import java.net.InetSocketAddress; import java.net.URI; @@ -307,15 +308,15 @@ public void edsClustersWithRingHashEndpointLbPolicy() throws Exception { // LOCALITY1 are equally weighted. assertThat(addr1.getAddresses()) .isEqualTo(Arrays.asList(newInetSocketAddress("127.0.0.1", 8080))); - assertThat(addr1.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT)) + assertThat(addr1.getAttributes().get(io.grpc.xds.XdsAttributes.ATTR_SERVER_WEIGHT)) .isEqualTo(10); assertThat(addr2.getAddresses()) .isEqualTo(Arrays.asList(newInetSocketAddress("127.0.0.2", 8080))); - assertThat(addr2.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT)) + assertThat(addr2.getAttributes().get(io.grpc.xds.XdsAttributes.ATTR_SERVER_WEIGHT)) .isEqualTo(10); assertThat(addr3.getAddresses()) .isEqualTo(Arrays.asList(newInetSocketAddress("127.0.1.1", 8080))); - assertThat(addr3.getAttributes().get(XdsAttributes.ATTR_SERVER_WEIGHT)) + assertThat(addr3.getAttributes().get(io.grpc.xds.XdsAttributes.ATTR_SERVER_WEIGHT)) .isEqualTo(50 * 60); assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); PriorityLbConfig priorityLbConfig = (PriorityLbConfig) childBalancer.config; @@ -382,7 +383,7 @@ public void edsClustersWithLeastRequestEndpointLbPolicy() { assertThat( childBalancer.addresses.get(0).getAttributes() - .get(XdsAttributes.ATTR_LOCALITY_WEIGHT)).isEqualTo(100); + .get(io.grpc.xds.XdsAttributes.ATTR_LOCALITY_WEIGHT)).isEqualTo(100); } @Test @@ -408,7 +409,7 @@ public void edsClustersEndpointHostname_addedToAddressAttribute() { assertThat( childBalancer.addresses.get(0).getAttributes() - .get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo("hostname1"); + .get(XdsInternalAttributes.ATTR_ADDRESS_NAME)).isEqualTo("hostname1"); } @Test @@ -580,12 +581,13 @@ public void onlyEdsClusters_receivedEndpoints() { io.grpc.xds.client.Locality locality2 = io.grpc.xds.client.Locality.create( LOCALITY2.getRegion(), LOCALITY2.getZone(), LOCALITY2.getSubZone()); for (EquivalentAddressGroup eag : childBalancer.addresses) { - io.grpc.xds.client.Locality locality = eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY); + io.grpc.xds.client.Locality locality = + eag.getAttributes().get(io.grpc.xds.XdsAttributes.ATTR_LOCALITY); if (locality.equals(locality1)) { - assertThat(eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY_WEIGHT)) + assertThat(eag.getAttributes().get(io.grpc.xds.XdsAttributes.ATTR_LOCALITY_WEIGHT)) .isEqualTo(70); } else if (locality.equals(locality2)) { - assertThat(eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY_WEIGHT)) + assertThat(eag.getAttributes().get(io.grpc.xds.XdsAttributes.ATTR_LOCALITY_WEIGHT)) .isEqualTo(30); } else { throw new AssertionError("Unexpected locality region: " + locality.region()); @@ -813,7 +815,8 @@ public void handleEdsResource_ignoreLocalitiesWithNoHealthyEndpoints() { io.grpc.xds.client.Locality locality2 = io.grpc.xds.client.Locality.create( LOCALITY2.getRegion(), LOCALITY2.getZone(), LOCALITY2.getSubZone()); for (EquivalentAddressGroup eag : childBalancer.addresses) { - assertThat(eag.getAttributes().get(XdsAttributes.ATTR_LOCALITY)).isEqualTo(locality2); + assertThat(eag.getAttributes().get(io.grpc.xds.XdsAttributes.ATTR_LOCALITY)) + .isEqualTo(locality2); } } @@ -897,7 +900,7 @@ public void onlyLogicalDnsCluster_endpointsResolved() { newInetSocketAddress("127.0.2.1", 9000), newInetSocketAddress("127.0.2.2", 9000)))), childBalancer.addresses); assertThat(childBalancer.addresses.get(0).getAttributes() - .get(XdsAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME + ":9000"); + .get(XdsInternalAttributes.ATTR_ADDRESS_NAME)).isEqualTo(DNS_HOST_NAME + ":9000"); } @Test @@ -995,7 +998,8 @@ public void config_equalsTester() { ServerInfo lrsServerInfo = ServerInfo.create("lrs.googleapis.com", InsecureChannelCredentials.create()); UpstreamTlsContext tlsContext = - CommonTlsContextTestsUtil.buildUpstreamTlsContext("google_cloud_private_spiffe", true); + CommonTlsContextTestsUtil.buildUpstreamTlsContext( + "google_cloud_private_spiffe", true); DiscoveryMechanism edsDiscoveryMechanism1 = DiscoveryMechanism.forEds(CLUSTER, EDS_SERVICE_NAME, lrsServerInfo, 100L, tlsContext, Collections.emptyMap(), null); @@ -1053,8 +1057,8 @@ private void startXdsDepManager(final CdsConfig cdsConfig, boolean forwardTime) loadBalancer.acceptResolvedAddresses(ResolvedAddresses.newBuilder() .setAddresses(Collections.emptyList()) .setAttributes(Attributes.newBuilder() - .set(XdsAttributes.XDS_CONFIG, xdsConfig.getValue()) - .set(XdsAttributes.XDS_CLUSTER_SUBSCRIPT_REGISTRY, xdsDepManager) + .set(io.grpc.xds.XdsAttributes.XDS_CONFIG, xdsConfig.getValue()) + .set(io.grpc.xds.XdsAttributes.XDS_CLUSTER_SUBSCRIPT_REGISTRY, xdsDepManager) .build()) .setLoadBalancingPolicyConfig(cdsConfig) .build()); diff --git a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java index e46e440475a..6e4d243e904 100644 --- a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java @@ -71,11 +71,13 @@ import io.grpc.xds.client.Bootstrapper; import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.internal.Matchers.HeaderMatcher; +import io.grpc.xds.internal.XdsInternalAttributes; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import io.grpc.xds.internal.security.SecurityProtocolNegotiators; import io.grpc.xds.internal.security.SslContextProviderSupplier; import io.grpc.xds.internal.security.TlsContextManagerImpl; import io.grpc.xds.internal.security.certprovider.FileWatcherCertificateProviderProvider; +import io.grpc.xds.internal.security.trust.CertificateUtils; import io.netty.handler.ssl.NotSslRecordException; import java.io.File; import java.io.FileOutputStream; @@ -84,7 +86,6 @@ import java.net.Inet4Address; import java.net.InetSocketAddress; import java.net.URI; -import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyStore; @@ -117,8 +118,8 @@ */ @RunWith(Parameterized.class) public class XdsSecurityClientServerTest { - - private static final String SAN_TO_MATCH = "waterzooi.test.google.be"; + + private static final String SNI_IN_UTC = "waterzooi.test.google.be"; @Parameter public Boolean enableSpiffe; @@ -221,7 +222,7 @@ public void tlsClientServer_useSystemRootCerts_noMtls_useCombinedValidationConte UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, - CLIENT_PEM_FILE, true, SAN_TO_MATCH, false); + CLIENT_PEM_FILE, true, SNI_IN_UTC, false, "", false, false); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); @@ -248,7 +249,7 @@ public void tlsClientServer_useSystemRootCerts_noMtls_validationContext() throws UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, - CLIENT_PEM_FILE, false, SAN_TO_MATCH, false); + CLIENT_PEM_FILE, false, SNI_IN_UTC, false, null, false, false); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); @@ -271,7 +272,7 @@ public void tlsClientServer_useSystemRootCerts_mtls() throws Exception { UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, - CLIENT_PEM_FILE, true, SAN_TO_MATCH, true); + CLIENT_PEM_FILE, true, SNI_IN_UTC, true, "", false, false); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); @@ -283,7 +284,8 @@ public void tlsClientServer_useSystemRootCerts_mtls() throws Exception { } @Test - public void tlsClientServer_useSystemRootCerts_failureToMatchSubjAltNames() throws Exception { + public void tlsClientServer_noAutoSniValidation_failureToMatchSubjAltNames() + throws Exception { Path trustStoreFilePath = getCacertFilePathForTestCa(); try { setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); @@ -294,7 +296,7 @@ public void tlsClientServer_useSystemRootCerts_failureToMatchSubjAltNames() thro UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, - CLIENT_PEM_FILE, true, "server1.test.google.in", false); + CLIENT_PEM_FILE, true, "server1.test.google.in", false, "", false, false); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); @@ -311,6 +313,103 @@ public void tlsClientServer_useSystemRootCerts_failureToMatchSubjAltNames() thro } } + + @Test + public void tlsClientServer_autoSniValidation_sniInUtc() + throws Exception { + CertificateUtils.isXdsSniEnabled = true; + Path trustStoreFilePath = getCacertFilePathForTestCa(); + try { + setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); + DownstreamTlsContext downstreamTlsContext = + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, false, false); + buildServerWithTlsContext(downstreamTlsContext); + + UpstreamTlsContext upstreamTlsContext = + setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, + CLIENT_PEM_FILE, true, + // SAN matcher in CommonValidationContext. Will be overridden by autoSniSanValidation + "server1.test.google.in", + false, + SNI_IN_UTC, + false, true); + + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = + getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); + unaryRpc(/* requestMessage= */ "buddy", blockingStub); + } finally { + Files.deleteIfExists(trustStoreFilePath); + clearTrustStoreSystemProperties(); + CertificateUtils.isXdsSniEnabled = false; + } + } + + @Test + public void tlsClientServer_autoSniValidation_sniFromHostname() + throws Exception { + CertificateUtils.isXdsSniEnabled = true; + Path trustStoreFilePath = getCacertFilePathForTestCa(); + try { + setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); + DownstreamTlsContext downstreamTlsContext = + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, false, false); + buildServerWithTlsContext(downstreamTlsContext); + + UpstreamTlsContext upstreamTlsContext = + setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, + CLIENT_PEM_FILE, true, + // SAN matcher in CommonValidationContext. Will be overridden by autoSniSanValidation + "server1.test.google.in", + false, + "", + true, true); + + // TODO: Change this to foo.test.gooogle.fr that needs wildcard matching after + // https://github.com/grpc/grpc-java/pull/12345 is done + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = + getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY, + "waterzooi.test.google.be"); + unaryRpc(/* requestMessage= */ "buddy", blockingStub); + } finally { + Files.deleteIfExists(trustStoreFilePath); + clearTrustStoreSystemProperties(); + CertificateUtils.isXdsSniEnabled = false; + } + } + + @Test + public void tlsClientServer_autoSniValidation_noSniApplicable_usesMatcherFromCmnVdnCtx() + throws Exception { + CertificateUtils.isXdsSniEnabled = true; + Path trustStoreFilePath = getCacertFilePathForTestCa(); + try { + setTrustStoreSystemProperties(trustStoreFilePath.toAbsolutePath().toString()); + DownstreamTlsContext downstreamTlsContext = + setBootstrapInfoAndBuildDownstreamTlsContext(SERVER_1_PEM_FILE, null, null, null, null, + null, false, false); + buildServerWithTlsContext(downstreamTlsContext); + + UpstreamTlsContext upstreamTlsContext = + setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, + CLIENT_PEM_FILE, true, + // This is what will get used for the SAN validation since no SNI was used + "waterzooi.test.google.be", + false, + "", + false, true); + + SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = + getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); + unaryRpc(/* requestMessage= */ "buddy", blockingStub); + } finally { + Files.deleteIfExists(trustStoreFilePath); + clearTrustStoreSystemProperties(); + CertificateUtils.isXdsSniEnabled = false; + } + } + /** * Use system root ca cert for TLS channel - mTLS. */ @@ -326,8 +425,7 @@ public void tlsClientServer_useSystemRootCerts_requireClientAuth() throws Except UpstreamTlsContext upstreamTlsContext = setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts(CLIENT_KEY_FILE, - CLIENT_PEM_FILE, true, SAN_TO_MATCH, false); - + CLIENT_PEM_FILE, true, SNI_IN_UTC, false, "", false, false); SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = getBlockingStub(upstreamTlsContext, /* overrideAuthority= */ OVERRIDE_AUTHORITY); assertThat(unaryRpc(/* requestMessage= */ "buddy", blockingStub)).isEqualTo("Hello buddy"); @@ -608,7 +706,11 @@ private UpstreamTlsContext setBootstrapInfoAndBuildUpstreamTlsContext(String cli private UpstreamTlsContext setBootstrapInfoAndBuildUpstreamTlsContextForUsingSystemRootCerts( String clientKeyFile, String clientPemFile, - boolean useCombinedValidationContext, String sanToMatch, boolean isMtls) { + boolean useCombinedValidationContext, + String sanToMatch, + boolean isMtls, + String sniInUpstreamTlsContext, + boolean autoHostSni, boolean autoSniSanValidation) { bootstrapInfoForClient = CommonBootstrapperTestUtils .buildBootstrapInfo("google_cloud_private_spiffe-client", clientKeyFile, clientPemFile, CA_PEM_FILE, null, null, null, null, null); @@ -623,7 +725,7 @@ private UpstreamTlsContext setBootstrapInfoAndBuildUpstreamTlsContextForUsingSys .addMatchSubjectAltNames( StringMatcher.newBuilder() .setExact(sanToMatch)) - .build()); + .build(), sniInUpstreamTlsContext, autoHostSni, autoSniSanValidation); } return CommonTlsContextTestsUtil.buildNewUpstreamTlsContextForCertProviderInstance( "google_cloud_private_spiffe-client", "ROOT", null, @@ -708,8 +810,20 @@ static EnvoyServerProtoData.Listener buildListener( } private SimpleServiceGrpc.SimpleServiceBlockingStub getBlockingStub( - final UpstreamTlsContext upstreamTlsContext, String overrideAuthority) - throws URISyntaxException { + final UpstreamTlsContext upstreamTlsContext, String overrideAuthority) { + return getBlockingStub(upstreamTlsContext, overrideAuthority, overrideAuthority); + } + + // Two separate parameters for overrideAuthority and addrAttribute is for the SAN SNI validation + // test tlsClientServer_useSystemRootCerts_sni_san_validation_from_hostname that uses hostname + // passed for SNI. foo.test.google.fr is used for virtual host matching via authority but it + // can't be used for SNI in this testcase because foo.test.google.fr needs wildcard matching to + // match against *.test.google.fr in the certificate SNI, which isn't implemented yet + // (https://github.com/grpc/grpc-java/pull/12345 implements it) + // so use an exact match SAN such as waterzooi.test.google.be for SNI for this testcase. + private SimpleServiceGrpc.SimpleServiceBlockingStub getBlockingStub( + final UpstreamTlsContext upstreamTlsContext, String overrideAuthority, + String addrNameAttribute) { ManagedChannelBuilder channelBuilder = Grpc.newChannelBuilder( "sectest://localhost:" + port, @@ -721,14 +835,16 @@ private SimpleServiceGrpc.SimpleServiceBlockingStub getBlockingStub( InetSocketAddress socketAddress = new InetSocketAddress(Inet4Address.getLoopbackAddress(), port); tlsContextManagerForClient = new TlsContextManagerImpl(bootstrapInfoForClient); - sslContextAttributes = - (upstreamTlsContext != null) - ? Attributes.newBuilder() - .set(SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER, - new SslContextProviderSupplier( - upstreamTlsContext, tlsContextManagerForClient)) - .build() - : Attributes.EMPTY; + Attributes.Builder sslContextAttributesBuilder = (upstreamTlsContext != null) + ? Attributes.newBuilder() + .set(SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER, + new SslContextProviderSupplier( + upstreamTlsContext, tlsContextManagerForClient)) + : Attributes.newBuilder(); + if (addrNameAttribute != null) { + sslContextAttributesBuilder.set(XdsInternalAttributes.ATTR_ADDRESS_NAME, addrNameAttribute); + } + sslContextAttributes = sslContextAttributesBuilder.build(); fakeNameResolverFactory.setServers( ImmutableList.of(new EquivalentAddressGroup(socketAddress, sslContextAttributes))); return SimpleServiceGrpc.newBlockingStub(cleanupRule.register(channelBuilder.build())); diff --git a/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java b/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java index 8f970c86366..80a8083fb27 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java @@ -36,10 +36,12 @@ import java.io.InputStream; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.AbstractMap; import java.util.Arrays; import java.util.List; import java.util.concurrent.Executor; import javax.annotation.Nullable; +import javax.net.ssl.X509TrustManager; /** Utility class for client and server ssl provider tests. */ public class CommonTlsContextTestsUtil { @@ -151,11 +153,24 @@ public static String getTempFileNameForResourcesFile(String resFile) throws IOEx * Helper method to build UpstreamTlsContext for above tests. Called from other classes as well. */ static EnvoyServerProtoData.UpstreamTlsContext buildUpstreamTlsContext( - CommonTlsContext commonTlsContext) { - UpstreamTlsContext upstreamTlsContext = - UpstreamTlsContext.newBuilder().setCommonTlsContext(commonTlsContext).build(); + CommonTlsContext commonTlsContext) { + return buildUpstreamTlsContext(commonTlsContext, "", false, false); + } + + /** + * Helper method to build UpstreamTlsContext with SNI info. + */ + static EnvoyServerProtoData.UpstreamTlsContext buildUpstreamTlsContext( + CommonTlsContext commonTlsContext, String sni, boolean autoHostSni, + boolean autoSniSanValidation) { + UpstreamTlsContext.Builder upstreamTlsContext = + UpstreamTlsContext.newBuilder() + .setCommonTlsContext(commonTlsContext) + .setAutoHostSni(autoHostSni) + .setAutoSniSanValidation(autoSniSanValidation) + .setSni(sni); return EnvoyServerProtoData.UpstreamTlsContext.fromEnvoyProtoUpstreamTlsContext( - upstreamTlsContext); + upstreamTlsContext.build()); } /** Helper method to build UpstreamTlsContext for multiple test classes. */ @@ -170,6 +185,21 @@ public static EnvoyServerProtoData.UpstreamTlsContext buildUpstreamTlsContext( null); } + /** Helper method to build UpstreamTlsContext with SNI info. */ + public static EnvoyServerProtoData.UpstreamTlsContext buildUpstreamTlsContext( + String commonInstanceName, boolean hasIdentityCert, String sni, boolean autoHostSni) { + return buildUpstreamTlsContextForCertProviderInstance( + hasIdentityCert ? commonInstanceName : null, + hasIdentityCert ? "default" : null, + commonInstanceName, + "ROOT", + null, + null, + sni, + autoHostSni, + false); + } + /** Gets a cert from contents of a resource. */ public static X509Certificate getCertFromResourceName(String resourceName) throws IOException, CertificateException { @@ -286,7 +316,31 @@ private static CommonTlsContext.Builder addNewCertificateValidationContext( rootInstanceName, rootCertName, alpnProtocols, - staticCertValidationContext)); + staticCertValidationContext), + "", false, false); + } + + /** Helper method to build UpstreamTlsContext with SNI info for CertProvider tests. */ + public static EnvoyServerProtoData.UpstreamTlsContext + buildUpstreamTlsContextForCertProviderInstance( + @Nullable String certInstanceName, + @Nullable String certName, + @Nullable String rootInstanceName, + @Nullable String rootCertName, + Iterable alpnProtocols, + CertificateValidationContext staticCertValidationContext, + String sni, + boolean autoHostSni, + boolean autoSniSanValidation) { + return buildUpstreamTlsContext( + buildCommonTlsContextForCertProviderInstance( + certInstanceName, + certName, + rootInstanceName, + rootCertName, + alpnProtocols, + staticCertValidationContext), + sni, autoHostSni, autoSniSanValidation); } /** Helper method to build UpstreamTlsContext for CertProvider tests. */ @@ -305,7 +359,8 @@ private static CommonTlsContext.Builder addNewCertificateValidationContext( rootInstanceName, rootCertName, alpnProtocols, - staticCertValidationContext)); + staticCertValidationContext), + "", false, false); } /** Helper method to build DownstreamTlsContext for CertProvider tests. */ @@ -349,14 +404,15 @@ private static CommonTlsContext.Builder addNewCertificateValidationContext( } /** Perform some simple checks on sslContext. */ - public static void doChecksOnSslContext(boolean server, SslContext sslContext, + public static void doChecksOnSslContext(boolean server, + AbstractMap.SimpleImmutableEntry sslContextAndTm, List expectedApnProtos) { if (server) { - assertThat(sslContext.isServer()).isTrue(); + assertThat(sslContextAndTm.getKey().isServer()).isTrue(); } else { - assertThat(sslContext.isClient()).isTrue(); + assertThat(sslContextAndTm.getKey().isClient()).isTrue(); } - List apnProtos = sslContext.applicationProtocolNegotiator().protocols(); + List apnProtos = sslContextAndTm.getKey().applicationProtocolNegotiator().protocols(); assertThat(apnProtos).isNotNull(); if (expectedApnProtos != null) { assertThat(apnProtos).isEqualTo(expectedApnProtos); @@ -382,7 +438,7 @@ public static TestCallback getValueThruCallback(SslContextProvider provider, Exe public static class TestCallback extends SslContextProvider.Callback { - public SslContext updatedSslContext; + public AbstractMap.SimpleImmutableEntry updatedSslContext; public Throwable updatedThrowable; public TestCallback(Executor executor) { @@ -390,7 +446,8 @@ public TestCallback(Executor executor) { } @Override - public void updateSslContext(SslContext sslContext) { + public void updateSslContextAndExtendedX509TrustManager( + AbstractMap.SimpleImmutableEntry sslContext) { updatedSslContext = sslContext; } diff --git a/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java b/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java index a0139618f9f..061e6bad581 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java @@ -50,9 +50,11 @@ import io.grpc.xds.TlsContextManager; import io.grpc.xds.client.Bootstrapper; import io.grpc.xds.client.CommonBootstrapperTestUtils; +import io.grpc.xds.internal.XdsInternalAttributes; import io.grpc.xds.internal.security.SecurityProtocolNegotiators.ClientSecurityHandler; import io.grpc.xds.internal.security.SecurityProtocolNegotiators.ClientSecurityProtocolNegotiator; import io.grpc.xds.internal.security.certprovider.CommonCertProviderTestUtils; +import io.grpc.xds.internal.security.trust.CertificateUtils; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; @@ -73,11 +75,13 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.cert.CertStoreException; +import java.util.AbstractMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import javax.net.ssl.X509TrustManager; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -86,6 +90,10 @@ @RunWith(JUnit4.class) public class SecurityProtocolNegotiatorsTest { + private static final String HOSTNAME = "hostname"; + private static final String SNI_IN_UTC = "sni-in-upstream-tls-context"; + private static final String FAKE_AUTHORITY = "authority"; + private final GrpcHttp2ConnectionHandler grpcHandler = FakeGrpcHttp2ConnectionHandler.newHandler(); @@ -121,8 +129,8 @@ public void clientSecurityProtocolNegotiatorNewHandler_noFallback_expectExceptio @Test public void clientSecurityProtocolNegotiatorNewHandler_withTlsContextAttribute() { - UpstreamTlsContext upstreamTlsContext = - CommonTlsContextTestsUtil.buildUpstreamTlsContext(CommonTlsContext.newBuilder().build()); + UpstreamTlsContext upstreamTlsContext = CommonTlsContextTestsUtil.buildUpstreamTlsContext( + CommonTlsContext.newBuilder().build()); ClientSecurityProtocolNegotiator pn = new ClientSecurityProtocolNegotiator(InternalProtocolNegotiators.plaintext()); GrpcHttp2ConnectionHandler mockHandler = mock(GrpcHttp2ConnectionHandler.class); @@ -141,6 +149,36 @@ public void clientSecurityProtocolNegotiatorNewHandler_withTlsContextAttribute() assertThat(newHandler).isInstanceOf(ClientSecurityHandler.class); } + @Test + public void clientSecurityProtocolNegotiator_autoHostSni_hostnamePassedToClientSecurityHandlr() { + CertificateUtils.isXdsSniEnabled = true; + try { + UpstreamTlsContext upstreamTlsContext = + CommonTlsContextTestsUtil.buildUpstreamTlsContext( + CommonTlsContext.newBuilder().build(), "", true, false); + ClientSecurityProtocolNegotiator pn = + new ClientSecurityProtocolNegotiator(InternalProtocolNegotiators.plaintext()); + GrpcHttp2ConnectionHandler mockHandler = mock(GrpcHttp2ConnectionHandler.class); + ChannelLogger logger = mock(ChannelLogger.class); + doNothing().when(logger).log(any(ChannelLogLevel.class), anyString()); + when(mockHandler.getNegotiationLogger()).thenReturn(logger); + TlsContextManager mockTlsContextManager = mock(TlsContextManager.class); + when(mockHandler.getEagAttributes()) + .thenReturn( + Attributes.newBuilder() + .set(SecurityProtocolNegotiators.ATTR_SSL_CONTEXT_PROVIDER_SUPPLIER, + new SslContextProviderSupplier(upstreamTlsContext, mockTlsContextManager)) + .set(XdsInternalAttributes.ATTR_ADDRESS_NAME, FAKE_AUTHORITY) + .build()); + ChannelHandler newHandler = pn.newHandler(mockHandler); + assertThat(newHandler).isNotNull(); + assertThat(newHandler).isInstanceOf(ClientSecurityHandler.class); + assertThat(((ClientSecurityHandler) newHandler).getSni()).isEqualTo(FAKE_AUTHORITY); + } finally { + CertificateUtils.isXdsSniEnabled = false; + } + } + @Test public void clientSecurityHandler_addLast() throws InterruptedException, TimeoutException, ExecutionException { @@ -157,7 +195,7 @@ public void clientSecurityHandler_addLast() new SslContextProviderSupplier(upstreamTlsContext, new TlsContextManagerImpl(bootstrapInfoForClient)); ClientSecurityHandler clientSecurityHandler = - new ClientSecurityHandler(grpcHandler, sslContextProviderSupplier); + new ClientSecurityHandler(grpcHandler, sslContextProviderSupplier, HOSTNAME); pipeline.addLast(clientSecurityHandler); channelHandlerCtx = pipeline.context(clientSecurityHandler); assertNotNull(channelHandlerCtx); @@ -168,8 +206,9 @@ public void clientSecurityHandler_addLast() sslContextProviderSupplier .updateSslContext(new SslContextProvider.Callback(MoreExecutors.directExecutor()) { @Override - public void updateSslContext(SslContext sslContext) { - future.set(sslContext); + public void updateSslContextAndExtendedX509TrustManager( + AbstractMap.SimpleImmutableEntry sslContextAndTm) { + future.set(sslContextAndTm); } @Override @@ -180,7 +219,7 @@ protected void onException(Throwable throwable) { assertThat(executor.runDueTasks()).isEqualTo(1); channel.runPendingTasks(); Object fromFuture = future.get(2, TimeUnit.SECONDS); - assertThat(fromFuture).isInstanceOf(SslContext.class); + assertThat(fromFuture).isInstanceOf(AbstractMap.SimpleImmutableEntry.class); channel.runPendingTasks(); channelHandlerCtx = pipeline.context(clientSecurityHandler); assertThat(channelHandlerCtx).isNull(); @@ -194,6 +233,114 @@ protected void onException(Throwable throwable) { CommonCertProviderTestUtils.register0(); } + @Test + public void sniInClientSecurityHandler_autoHostSniIsTrue_usesEndpointHostname() { + CertificateUtils.isXdsSniEnabled = true; + try { + Bootstrapper.BootstrapInfo bootstrapInfoForClient = CommonBootstrapperTestUtils + .buildBootstrapInfo("google_cloud_private_spiffe-client", CLIENT_KEY_FILE, + CLIENT_PEM_FILE, CA_PEM_FILE, null, null, null, null, null); + UpstreamTlsContext upstreamTlsContext = + CommonTlsContextTestsUtil + .buildUpstreamTlsContext("google_cloud_private_spiffe-client", true, "", true); + SslContextProviderSupplier sslContextProviderSupplier = + new SslContextProviderSupplier(upstreamTlsContext, + new TlsContextManagerImpl(bootstrapInfoForClient)); + + ClientSecurityHandler clientSecurityHandler = + new ClientSecurityHandler(grpcHandler, sslContextProviderSupplier, HOSTNAME); + + assertThat(clientSecurityHandler.getSni()).isEqualTo(HOSTNAME); + } finally { + CertificateUtils.isXdsSniEnabled = false; + } + } + + @Test + public void sniInClientSecurityHandler_autoHostSni_endpointHostnameIsEmpty_usesSniFromUtc() { + CertificateUtils.isXdsSniEnabled = true; + try { + Bootstrapper.BootstrapInfo bootstrapInfoForClient = CommonBootstrapperTestUtils + .buildBootstrapInfo("google_cloud_private_spiffe-client", CLIENT_KEY_FILE, + CLIENT_PEM_FILE, CA_PEM_FILE, null, null, null, null, null); + UpstreamTlsContext upstreamTlsContext = CommonTlsContextTestsUtil.buildUpstreamTlsContext( + "google_cloud_private_spiffe-client", true, SNI_IN_UTC, true); + SslContextProviderSupplier sslContextProviderSupplier = + new SslContextProviderSupplier(upstreamTlsContext, + new TlsContextManagerImpl(bootstrapInfoForClient)); + + ClientSecurityHandler clientSecurityHandler = + new ClientSecurityHandler(grpcHandler, sslContextProviderSupplier, ""); + + assertThat(clientSecurityHandler.getSni()).isEqualTo(SNI_IN_UTC); + } finally { + CertificateUtils.isXdsSniEnabled = false; + } + } + + @Test + public void sniInClientSecurityHandler_autoHostSni_endpointHostnameIsNull_usesSniFromUtc() { + CertificateUtils.isXdsSniEnabled = true; + try { + Bootstrapper.BootstrapInfo bootstrapInfoForClient = CommonBootstrapperTestUtils + .buildBootstrapInfo("google_cloud_private_spiffe-client", CLIENT_KEY_FILE, + CLIENT_PEM_FILE, CA_PEM_FILE, null, null, null, null, null); + UpstreamTlsContext upstreamTlsContext = CommonTlsContextTestsUtil.buildUpstreamTlsContext( + "google_cloud_private_spiffe-client", true, SNI_IN_UTC, true); + SslContextProviderSupplier sslContextProviderSupplier = + new SslContextProviderSupplier(upstreamTlsContext, + new TlsContextManagerImpl(bootstrapInfoForClient)); + + ClientSecurityHandler clientSecurityHandler = + new ClientSecurityHandler(grpcHandler, sslContextProviderSupplier, null); + + assertThat(clientSecurityHandler.getSni()).isEqualTo(SNI_IN_UTC); + } finally { + CertificateUtils.isXdsSniEnabled = false; + } + } + + @Test + public void sniInClientSecurityHandler_autoHostSniIsFalse_usesSniFromUpstreamTlsContext() { + CertificateUtils.isXdsSniEnabled = true; + try { + Bootstrapper.BootstrapInfo bootstrapInfoForClient = CommonBootstrapperTestUtils + .buildBootstrapInfo("google_cloud_private_spiffe-client", CLIENT_KEY_FILE, + CLIENT_PEM_FILE, CA_PEM_FILE, null, null, null, null, null); + UpstreamTlsContext upstreamTlsContext = CommonTlsContextTestsUtil.buildUpstreamTlsContext( + "google_cloud_private_spiffe-client", true, SNI_IN_UTC, false); + SslContextProviderSupplier sslContextProviderSupplier = + new SslContextProviderSupplier(upstreamTlsContext, + new TlsContextManagerImpl(bootstrapInfoForClient)); + + ClientSecurityHandler clientSecurityHandler = + new ClientSecurityHandler(grpcHandler, sslContextProviderSupplier, HOSTNAME); + + assertThat(clientSecurityHandler.getSni()).isEqualTo(SNI_IN_UTC); + } finally { + CertificateUtils.isXdsSniEnabled = false; + } + } + + @Test + public void sniFeatureNotEnabled_usesChannelAuthorityForSni() { + CertificateUtils.isXdsSniEnabled = false; + Bootstrapper.BootstrapInfo bootstrapInfoForClient = CommonBootstrapperTestUtils + .buildBootstrapInfo("google_cloud_private_spiffe-client", CLIENT_KEY_FILE, + CLIENT_PEM_FILE, CA_PEM_FILE, null, null, null, null, null); + UpstreamTlsContext upstreamTlsContext = + CommonTlsContextTestsUtil + .buildUpstreamTlsContext("google_cloud_private_spiffe-client", true); + SslContextProviderSupplier sslContextProviderSupplier = + new SslContextProviderSupplier(upstreamTlsContext, + new TlsContextManagerImpl(bootstrapInfoForClient)); + + ClientSecurityHandler clientSecurityHandler = + new ClientSecurityHandler(grpcHandler, sslContextProviderSupplier, HOSTNAME); + + assertThat(clientSecurityHandler.getSni()).isEqualTo(FAKE_AUTHORITY); + } + @Test public void serverSecurityHandler_addLast() throws InterruptedException, TimeoutException, ExecutionException { @@ -245,8 +392,9 @@ public SocketAddress remoteAddress() { sslContextProviderSupplier .updateSslContext(new SslContextProvider.Callback(MoreExecutors.directExecutor()) { @Override - public void updateSslContext(SslContext sslContext) { - future.set(sslContext); + public void updateSslContextAndExtendedX509TrustManager( + AbstractMap.SimpleImmutableEntry sslContextAndTm) { + future.set(sslContextAndTm); } @Override @@ -257,7 +405,7 @@ protected void onException(Throwable throwable) { channel.runPendingTasks(); // need this for tasks to execute on eventLoop assertThat(executor.runDueTasks()).isEqualTo(1); Object fromFuture = future.get(2, TimeUnit.SECONDS); - assertThat(fromFuture).isInstanceOf(SslContext.class); + assertThat(fromFuture).isInstanceOf(AbstractMap.SimpleImmutableEntry.class); channel.runPendingTasks(); channelHandlerCtx = pipeline.context(SecurityProtocolNegotiators.ServerSecurityHandler.class); assertThat(channelHandlerCtx).isNull(); @@ -356,53 +504,59 @@ public void nullTlsContext_nullFallbackProtocolNegotiator_expectException() { @Test public void clientSecurityProtocolNegotiatorNewHandler_fireProtocolNegotiationEvent() throws InterruptedException, TimeoutException, ExecutionException { - FakeClock executor = new FakeClock(); - CommonCertProviderTestUtils.register(executor); - Bootstrapper.BootstrapInfo bootstrapInfoForClient = CommonBootstrapperTestUtils - .buildBootstrapInfo("google_cloud_private_spiffe-client", CLIENT_KEY_FILE, CLIENT_PEM_FILE, - CA_PEM_FILE, null, null, null, null, null); - UpstreamTlsContext upstreamTlsContext = - CommonTlsContextTestsUtil - .buildUpstreamTlsContext("google_cloud_private_spiffe-client", true); - - SslContextProviderSupplier sslContextProviderSupplier = - new SslContextProviderSupplier(upstreamTlsContext, - new TlsContextManagerImpl(bootstrapInfoForClient)); - ClientSecurityHandler clientSecurityHandler = - new ClientSecurityHandler(grpcHandler, sslContextProviderSupplier); - - pipeline.addLast(clientSecurityHandler); - channelHandlerCtx = pipeline.context(clientSecurityHandler); - assertNotNull(channelHandlerCtx); // non-null since we just added it - - // kick off protocol negotiation. - pipeline.fireUserEventTriggered(InternalProtocolNegotiationEvent.getDefault()); - final SettableFuture future = SettableFuture.create(); - sslContextProviderSupplier - .updateSslContext(new SslContextProvider.Callback(MoreExecutors.directExecutor()) { - @Override - public void updateSslContext(SslContext sslContext) { - future.set(sslContext); - } - - @Override - protected void onException(Throwable throwable) { - future.set(throwable); - } - }); - executor.runDueTasks(); - channel.runPendingTasks(); // need this for tasks to execute on eventLoop - Object fromFuture = future.get(5, TimeUnit.SECONDS); - assertThat(fromFuture).isInstanceOf(SslContext.class); - channel.runPendingTasks(); - channelHandlerCtx = pipeline.context(clientSecurityHandler); - assertThat(channelHandlerCtx).isNull(); - Object sslEvent = SslHandshakeCompletionEvent.SUCCESS; - - pipeline.fireUserEventTriggered(sslEvent); - channel.runPendingTasks(); // need this for tasks to execute on eventLoop - assertTrue(channel.isOpen()); - CommonCertProviderTestUtils.register0(); + CertificateUtils.isXdsSniEnabled = true; + try { + FakeClock executor = new FakeClock(); + CommonCertProviderTestUtils.register(executor); + Bootstrapper.BootstrapInfo bootstrapInfoForClient = CommonBootstrapperTestUtils + .buildBootstrapInfo("google_cloud_private_spiffe-client", CLIENT_KEY_FILE, + CLIENT_PEM_FILE, CA_PEM_FILE, null, null, null, null, null); + UpstreamTlsContext upstreamTlsContext = + CommonTlsContextTestsUtil + .buildUpstreamTlsContext("google_cloud_private_spiffe-client", true); + + SslContextProviderSupplier sslContextProviderSupplier = + new SslContextProviderSupplier(upstreamTlsContext, + new TlsContextManagerImpl(bootstrapInfoForClient)); + ClientSecurityHandler clientSecurityHandler = + new ClientSecurityHandler(grpcHandler, sslContextProviderSupplier, HOSTNAME); + + pipeline.addLast(clientSecurityHandler); + channelHandlerCtx = pipeline.context(clientSecurityHandler); + assertNotNull(channelHandlerCtx); // non-null since we just added it + + // kick off protocol negotiation. + pipeline.fireUserEventTriggered(InternalProtocolNegotiationEvent.getDefault()); + final SettableFuture future = SettableFuture.create(); + sslContextProviderSupplier + .updateSslContext(new SslContextProvider.Callback(MoreExecutors.directExecutor()) { + @Override + public void updateSslContextAndExtendedX509TrustManager( + AbstractMap.SimpleImmutableEntry sslContextAndTm) { + future.set(sslContextAndTm); + } + + @Override + protected void onException(Throwable throwable) { + future.set(throwable); + } + }); + executor.runDueTasks(); + channel.runPendingTasks(); // need this for tasks to execute on eventLoop + Object fromFuture = future.get(5, TimeUnit.SECONDS); + assertThat(fromFuture).isInstanceOf(AbstractMap.SimpleImmutableEntry.class); + channel.runPendingTasks(); + channelHandlerCtx = pipeline.context(clientSecurityHandler); + assertThat(channelHandlerCtx).isNull(); + Object sslEvent = SslHandshakeCompletionEvent.SUCCESS; + + pipeline.fireUserEventTriggered(sslEvent); + channel.runPendingTasks(); // need this for tasks to execute on eventLoop + assertTrue(channel.isOpen()); + CommonCertProviderTestUtils.register0(); + } finally { + CertificateUtils.isXdsSniEnabled = false; + } } @Test @@ -420,7 +574,7 @@ public void clientSecurityProtocolNegotiatorNewHandler_handleHandlerRemoved() { new SslContextProviderSupplier(upstreamTlsContext, new TlsContextManagerImpl(bootstrapInfoForClient)); ClientSecurityHandler clientSecurityHandler = - new ClientSecurityHandler(grpcHandler, sslContextProviderSupplier); + new ClientSecurityHandler(grpcHandler, sslContextProviderSupplier, HOSTNAME); pipeline.addLast(clientSecurityHandler); channelHandlerCtx = pipeline.context(clientSecurityHandler); @@ -458,7 +612,7 @@ static FakeGrpcHttp2ConnectionHandler newHandler() { @Override public String getAuthority() { - return "authority"; + return FAKE_AUTHORITY; } } } diff --git a/xds/src/test/java/io/grpc/xds/internal/security/SslContextProviderSupplierTest.java b/xds/src/test/java/io/grpc/xds/internal/security/SslContextProviderSupplierTest.java index f476818297d..9b6e1ecbc74 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/SslContextProviderSupplierTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/SslContextProviderSupplierTest.java @@ -17,8 +17,9 @@ package io.grpc.xds.internal.security; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.xds.internal.security.CommonTlsContextTestsUtil.buildUpstreamTlsContext; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -26,10 +27,13 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateValidationContext; import io.grpc.xds.EnvoyServerProtoData; import io.grpc.xds.TlsContextManager; import io.netty.handler.ssl.SslContext; +import java.util.AbstractMap; import java.util.concurrent.Executor; +import javax.net.ssl.X509TrustManager; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -47,14 +51,14 @@ public class SslContextProviderSupplierTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @Mock private TlsContextManager mockTlsContextManager; + @Mock private Executor mockExecutor; private SslContextProviderSupplier supplier; private SslContextProvider mockSslContextProvider; - private EnvoyServerProtoData.UpstreamTlsContext upstreamTlsContext; + private EnvoyServerProtoData.UpstreamTlsContext upstreamTlsContext = + buildUpstreamTlsContext("google_cloud_private_spiffe", true); private SslContextProvider.Callback mockCallback; private void prepareSupplier() { - upstreamTlsContext = - CommonTlsContextTestsUtil.buildUpstreamTlsContext("google_cloud_private_spiffe", true); mockSslContextProvider = mock(SslContextProvider.class); doReturn(mockSslContextProvider) .when(mockTlsContextManager) @@ -64,7 +68,6 @@ private void prepareSupplier() { private void callUpdateSslContext() { mockCallback = mock(SslContextProvider.Callback.class); - Executor mockExecutor = mock(Executor.class); doReturn(mockExecutor).when(mockCallback).getExecutor(); supplier.updateSslContext(mockCallback); } @@ -82,9 +85,12 @@ public void get_updateSecret() { verify(mockSslContextProvider, times(1)).addCallback(callbackCaptor.capture()); SslContextProvider.Callback capturedCallback = callbackCaptor.getValue(); assertThat(capturedCallback).isNotNull(); - SslContext mockSslContext = mock(SslContext.class); - capturedCallback.updateSslContext(mockSslContext); - verify(mockCallback, times(1)).updateSslContext(eq(mockSslContext)); + @SuppressWarnings("unchecked") + AbstractMap.SimpleImmutableEntry mockSslContextAndTm = + mock(AbstractMap.SimpleImmutableEntry.class); + capturedCallback.updateSslContextAndExtendedX509TrustManager(mockSslContextAndTm); + verify(mockCallback, times(1)) + .updateSslContextAndExtendedX509TrustManager(eq(mockSslContextAndTm)); verify(mockTlsContextManager, times(1)) .releaseClientSslContextProvider(eq(mockSslContextProvider)); SslContextProvider.Callback mockCallback = mock(SslContextProvider.Callback.class); @@ -94,14 +100,42 @@ public void get_updateSecret() { } @Test - public void get_onException() { + public void autoHostSniFalse_usesSniFromUpstreamTlsContext() { prepareSupplier(); callUpdateSslContext(); + verify(mockTlsContextManager, times(2)) + .findOrCreateClientSslContextProvider(eq(upstreamTlsContext)); + verify(mockTlsContextManager, times(0)) + .releaseClientSslContextProvider(any(SslContextProvider.class)); ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(SslContextProvider.Callback.class); verify(mockSslContextProvider, times(1)).addCallback(callbackCaptor.capture()); SslContextProvider.Callback capturedCallback = callbackCaptor.getValue(); assertThat(capturedCallback).isNotNull(); + @SuppressWarnings("unchecked") + AbstractMap.SimpleImmutableEntry mockSslContextAndTm = + mock(AbstractMap.SimpleImmutableEntry.class); + capturedCallback.updateSslContextAndExtendedX509TrustManager(mockSslContextAndTm); + verify(mockCallback, times(1)) + .updateSslContextAndExtendedX509TrustManager(eq(mockSslContextAndTm)); + verify(mockTlsContextManager, times(1)) + .releaseClientSslContextProvider(eq(mockSslContextProvider)); + SslContextProvider.Callback mockCallback = mock(SslContextProvider.Callback.class); + supplier.updateSslContext(mockCallback); + verify(mockTlsContextManager, times(3)) + .findOrCreateClientSslContextProvider(eq(upstreamTlsContext)); + } + + @Test + public void get_onException() { + prepareSupplier(); + callUpdateSslContext(); + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(SslContextProvider.Callback.class); + verify(mockSslContextProvider, times(1)) + .addCallback(callbackCaptor.capture()); + SslContextProvider.Callback capturedCallback = callbackCaptor.getValue(); + assertThat(capturedCallback).isNotNull(); Exception exception = new Exception("test"); capturedCallback.onException(exception); verify(mockCallback, times(1)).onException(eq(exception)); @@ -109,6 +143,46 @@ public void get_onException() { .releaseClientSslContextProvider(eq(mockSslContextProvider)); } + @Test + public void systemRootCertsWithMtls_callbackExecutedFromProvider() { + upstreamTlsContext = + CommonTlsContextTestsUtil.buildNewUpstreamTlsContextForCertProviderInstance( + "gcp_id", + "cert-default", + null, + "root-default", + null, + CertificateValidationContext.newBuilder() + .setSystemRootCerts( + CertificateValidationContext.SystemRootCerts.getDefaultInstance()) + .build()); + prepareSupplier(); + + callUpdateSslContext(); + + verify(mockTlsContextManager, times(2)) + .findOrCreateClientSslContextProvider(eq(upstreamTlsContext)); + verify(mockTlsContextManager, times(0)) + .releaseClientSslContextProvider(any(SslContextProvider.class)); + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(SslContextProvider.Callback.class); + verify(mockSslContextProvider, times(1)).addCallback(callbackCaptor.capture()); + SslContextProvider.Callback capturedCallback = callbackCaptor.getValue(); + assertThat(capturedCallback).isNotNull(); + @SuppressWarnings("unchecked") + AbstractMap.SimpleImmutableEntry mockSslContextAndTm = + mock(AbstractMap.SimpleImmutableEntry.class); + capturedCallback.updateSslContextAndExtendedX509TrustManager(mockSslContextAndTm); + verify(mockCallback, times(1)) + .updateSslContextAndExtendedX509TrustManager(eq(mockSslContextAndTm)); + verify(mockTlsContextManager, times(1)) + .releaseClientSslContextProvider(eq(mockSslContextProvider)); + SslContextProvider.Callback mockCallback = mock(SslContextProvider.Callback.class); + supplier.updateSslContext(mockCallback); + verify(mockTlsContextManager, times(3)) + .findOrCreateClientSslContextProvider(eq(upstreamTlsContext)); + } + @Test public void testClose() { prepareSupplier(); diff --git a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java index 3c734df3f5a..875a57b8f3d 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java @@ -140,7 +140,7 @@ public void testProviderForClient_mtls() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); - assertThat(provider.getSslContext()).isNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); // now generate cert update watcherCaptor[0].updateCertificate( @@ -148,11 +148,11 @@ public void testProviderForClient_mtls() throws Exception { ImmutableList.of(getCertFromResourceName(CLIENT_PEM_FILE))); assertThat(provider.savedKey).isNotNull(); assertThat(provider.savedCertChain).isNotNull(); - assertThat(provider.getSslContext()).isNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); // now generate root cert update watcherCaptor[0].updateTrustedRoots(ImmutableList.of(getCertFromResourceName(CA_PEM_FILE))); - assertThat(provider.getSslContext()).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); @@ -181,40 +181,11 @@ public void testProviderForClient_mtls() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); - assertThat(provider.getSslContext()).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); testCallback1 = CommonTlsContextTestsUtil.getValueThruCallback(provider); assertThat(testCallback1.updatedSslContext).isNotSameInstanceAs(testCallback.updatedSslContext); } - @Test - public void testProviderForClient_systemRootCerts_regularTls() { - final CertificateProvider.DistributorWatcher[] watcherCaptor = - new CertificateProvider.DistributorWatcher[1]; - TestCertificateProvider.createAndRegisterProviderProvider( - certificateProviderRegistry, watcherCaptor, "testca", 0); - CertProviderClientSslContextProvider provider = - getSslContextProvider( - null, - null, - CommonBootstrapperTestUtils.getTestBootstrapInfo(), - /* alpnProtocols= */ null, - CertificateValidationContext.newBuilder() - .setSystemRootCerts( - CertificateValidationContext.SystemRootCerts.getDefaultInstance()) - .build(), - true); - - assertThat(provider.savedKey).isNull(); - assertThat(provider.savedCertChain).isNull(); - assertThat(provider.savedTrustedRoots).isNotNull(); - assertThat(provider.getSslContext()).isNotNull(); - TestCallback testCallback = - CommonTlsContextTestsUtil.getValueThruCallback(provider); - assertThat(testCallback.updatedSslContext).isEqualTo(provider.getSslContext()); - - assertThat(watcherCaptor[0]).isNull(); - } - @Test public void testProviderForClient_systemRootCerts_mtls() throws Exception { final CertificateProvider.DistributorWatcher[] watcherCaptor = @@ -236,7 +207,7 @@ public void testProviderForClient_systemRootCerts_mtls() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNotNull(); - assertThat(provider.getSslContext()).isNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); // now generate cert update watcherCaptor[0].updateCertificate( @@ -245,7 +216,7 @@ public void testProviderForClient_systemRootCerts_mtls() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNotNull(); - assertThat(provider.getSslContext()).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); TestCallback testCallback = CommonTlsContextTestsUtil.getValueThruCallback(provider); @@ -262,11 +233,40 @@ public void testProviderForClient_systemRootCerts_mtls() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNotNull(); - assertThat(provider.getSslContext()).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); testCallback1 = CommonTlsContextTestsUtil.getValueThruCallback(provider); assertThat(testCallback1.updatedSslContext).isNotSameInstanceAs(testCallback.updatedSslContext); } + @Test + public void testProviderForClient_systemRootCerts_regularTls() { + final CertificateProvider.DistributorWatcher[] watcherCaptor = + new CertificateProvider.DistributorWatcher[1]; + TestCertificateProvider.createAndRegisterProviderProvider( + certificateProviderRegistry, watcherCaptor, "testca", 0); + CertProviderClientSslContextProvider provider = + getSslContextProvider( + null, + null, + CommonBootstrapperTestUtils.getTestBootstrapInfo(), + /* alpnProtocols= */ null, + CertificateValidationContext.newBuilder() + .setSystemRootCerts( + CertificateValidationContext.SystemRootCerts.getDefaultInstance()) + .build(), + true); + + assertThat(provider.savedKey).isNull(); + assertThat(provider.savedCertChain).isNull(); + assertThat(provider.savedTrustedRoots).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); + TestCallback testCallback = + CommonTlsContextTestsUtil.getValueThruCallback(provider); + assertThat(testCallback.updatedSslContext).isEqualTo(provider.getSslContextAndTrustManager()); + + assertThat(watcherCaptor[0]).isNull(); + } + @Test public void testProviderForClient_mtls_newXds() throws Exception { final CertificateProvider.DistributorWatcher[] watcherCaptor = @@ -284,7 +284,7 @@ public void testProviderForClient_mtls_newXds() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); - assertThat(provider.getSslContext()).isNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); // now generate cert update watcherCaptor[0].updateCertificate( @@ -292,11 +292,11 @@ public void testProviderForClient_mtls_newXds() throws Exception { ImmutableList.of(getCertFromResourceName(CLIENT_PEM_FILE))); assertThat(provider.savedKey).isNotNull(); assertThat(provider.savedCertChain).isNotNull(); - assertThat(provider.getSslContext()).isNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); // now generate root cert update watcherCaptor[0].updateTrustedRoots(ImmutableList.of(getCertFromResourceName(CA_PEM_FILE))); - assertThat(provider.getSslContext()).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); @@ -325,7 +325,7 @@ public void testProviderForClient_mtls_newXds() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); - assertThat(provider.getSslContext()).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); testCallback1 = CommonTlsContextTestsUtil.getValueThruCallback(provider); assertThat(testCallback1.updatedSslContext).isNotSameInstanceAs(testCallback.updatedSslContext); } @@ -380,11 +380,11 @@ public void testProviderForClient_tls() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); - assertThat(provider.getSslContext()).isNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); // now generate root cert update watcherCaptor[0].updateTrustedRoots(ImmutableList.of(getCertFromResourceName(CA_PEM_FILE))); - assertThat(provider.getSslContext()).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); diff --git a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProviderTest.java index 423829ff5af..93559f47245 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderServerSslContextProviderTest.java @@ -127,7 +127,7 @@ public void testProviderForServer_mtls() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); - assertThat(provider.getSslContext()).isNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); // now generate cert update watcherCaptor[0].updateCertificate( @@ -135,11 +135,11 @@ public void testProviderForServer_mtls() throws Exception { ImmutableList.of(getCertFromResourceName(SERVER_0_PEM_FILE))); assertThat(provider.savedKey).isNotNull(); assertThat(provider.savedCertChain).isNotNull(); - assertThat(provider.getSslContext()).isNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); // now generate root cert update watcherCaptor[0].updateTrustedRoots(ImmutableList.of(getCertFromResourceName(CA_PEM_FILE))); - assertThat(provider.getSslContext()).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); @@ -168,7 +168,7 @@ public void testProviderForServer_mtls() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); - assertThat(provider.getSslContext()).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); testCallback1 = CommonTlsContextTestsUtil.getValueThruCallback(provider); assertThat(testCallback1.updatedSslContext).isNotSameInstanceAs(testCallback.updatedSslContext); } @@ -196,7 +196,7 @@ public void testProviderForServer_mtls_newXds() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); - assertThat(provider.getSslContext()).isNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); // now generate cert update watcherCaptor[0].updateCertificate( @@ -204,11 +204,11 @@ public void testProviderForServer_mtls_newXds() throws Exception { ImmutableList.of(getCertFromResourceName(SERVER_0_PEM_FILE))); assertThat(provider.savedKey).isNotNull(); assertThat(provider.savedCertChain).isNotNull(); - assertThat(provider.getSslContext()).isNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); // now generate root cert update watcherCaptor[0].updateTrustedRoots(ImmutableList.of(getCertFromResourceName(CA_PEM_FILE))); - assertThat(provider.getSslContext()).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); @@ -237,7 +237,7 @@ public void testProviderForServer_mtls_newXds() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); - assertThat(provider.getSslContext()).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); testCallback1 = CommonTlsContextTestsUtil.getValueThruCallback(provider); assertThat(testCallback1.updatedSslContext).isNotSameInstanceAs(testCallback.updatedSslContext); } @@ -294,14 +294,14 @@ public void testProviderForServer_tls() throws Exception { assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); - assertThat(provider.getSslContext()).isNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); // now generate cert update watcherCaptor[0].updateCertificate( CommonCertProviderTestUtils.getPrivateKey(SERVER_0_KEY_FILE), ImmutableList.of(getCertFromResourceName(SERVER_0_PEM_FILE))); - assertThat(provider.getSslContext()).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); assertThat(provider.savedKey).isNull(); assertThat(provider.savedCertChain).isNull(); assertThat(provider.savedTrustedRoots).isNull(); diff --git a/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactoryTest.java index db83961cfc3..0b81c9249dd 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactoryTest.java @@ -91,7 +91,7 @@ public void constructor_fromRootCert() CertificateValidationContext staticValidationContext = buildStaticValidationContext("san1", "san2"); XdsTrustManagerFactory factory = - new XdsTrustManagerFactory(new X509Certificate[]{x509Cert}, staticValidationContext); + new XdsTrustManagerFactory(new X509Certificate[]{x509Cert}, staticValidationContext, false); assertThat(factory).isNotNull(); TrustManager[] tms = factory.getTrustManagers(); assertThat(tms).isNotNull(); @@ -115,7 +115,7 @@ public void constructor_fromSpiffeTrustMap() "san2"); // Single domain and single cert XdsTrustManagerFactory factory = new XdsTrustManagerFactory(ImmutableMap - .of("example.com", ImmutableList.of(x509Cert)), staticValidationContext); + .of("example.com", ImmutableList.of(x509Cert)), staticValidationContext, false); assertThat(factory).isNotNull(); TrustManager[] tms = factory.getTrustManagers(); assertThat(tms).isNotNull(); @@ -131,7 +131,7 @@ public void constructor_fromSpiffeTrustMap() X509Certificate anotherCert = TestUtils.loadX509Cert(CLIENT_PEM_FILE); factory = new XdsTrustManagerFactory(ImmutableMap .of("example.com", ImmutableList.of(x509Cert), - "google.com", ImmutableList.of(x509Cert, anotherCert)), staticValidationContext); + "google.com", ImmutableList.of(x509Cert, anotherCert)), staticValidationContext, false); assertThat(factory).isNotNull(); tms = factory.getTrustManagers(); assertThat(tms).isNotNull(); @@ -154,7 +154,7 @@ public void constructorRootCert_checkServerTrusted() CertificateValidationContext staticValidationContext = buildStaticValidationContext("san1", "waterzooi.test.google.be"); XdsTrustManagerFactory factory = - new XdsTrustManagerFactory(new X509Certificate[]{x509Cert}, staticValidationContext); + new XdsTrustManagerFactory(new X509Certificate[]{x509Cert}, staticValidationContext, false); XdsX509TrustManager xdsX509TrustManager = (XdsX509TrustManager) factory.getTrustManagers()[0]; X509Certificate[] serverChain = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); @@ -167,7 +167,7 @@ public void constructorRootCert_nonStaticContext_throwsException() X509Certificate x509Cert = TestUtils.loadX509Cert(CA_PEM_FILE); try { new XdsTrustManagerFactory( - new X509Certificate[] {x509Cert}, getCertContextFromPath(CA_PEM_FILE)); + new X509Certificate[] {x509Cert}, getCertContextFromPath(CA_PEM_FILE), false); Assert.fail("no exception thrown"); } catch (IllegalArgumentException expected) { assertThat(expected) @@ -176,6 +176,19 @@ public void constructorRootCert_nonStaticContext_throwsException() } } + @Test + public void constructorRootCert_nonStaticContext_systemRootCerts_valid() + throws CertificateException, IOException, CertStoreException { + X509Certificate x509Cert = TestUtils.loadX509Cert(CA_PEM_FILE); + CertificateValidationContext certValidationContext = CertificateValidationContext.newBuilder() + .setTrustedCa( + DataSource.newBuilder().setFilename(TestUtils.loadCert(CA_PEM_FILE).getAbsolutePath())) + .setSystemRootCerts(CertificateValidationContext.SystemRootCerts.getDefaultInstance()) + .build(); + new XdsTrustManagerFactory( + new X509Certificate[] {x509Cert}, certValidationContext, false); + } + @Test public void constructorRootCert_checkServerTrusted_throwsException() throws CertificateException, IOException, CertStoreException { @@ -183,7 +196,7 @@ public void constructorRootCert_checkServerTrusted_throwsException() CertificateValidationContext staticValidationContext = buildStaticValidationContext("san1", "san2"); XdsTrustManagerFactory factory = - new XdsTrustManagerFactory(new X509Certificate[]{x509Cert}, staticValidationContext); + new XdsTrustManagerFactory(new X509Certificate[]{x509Cert}, staticValidationContext, false); XdsX509TrustManager xdsX509TrustManager = (XdsX509TrustManager) factory.getTrustManagers()[0]; X509Certificate[] serverChain = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); @@ -204,7 +217,7 @@ public void constructorRootCert_checkClientTrusted_throwsException() CertificateValidationContext staticValidationContext = buildStaticValidationContext("san1", "san2"); XdsTrustManagerFactory factory = - new XdsTrustManagerFactory(new X509Certificate[]{x509Cert}, staticValidationContext); + new XdsTrustManagerFactory(new X509Certificate[]{x509Cert}, staticValidationContext, false); XdsX509TrustManager xdsX509TrustManager = (XdsX509TrustManager) factory.getTrustManagers()[0]; X509Certificate[] clientChain = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); diff --git a/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsX509TrustManagerTest.java b/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsX509TrustManagerTest.java index ddd0a8e7f94..ffe0536f25b 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsX509TrustManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsX509TrustManagerTest.java @@ -43,6 +43,7 @@ import java.security.cert.CertStoreException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -86,30 +87,32 @@ public XdsX509TrustManagerTest(TestParam testParam) { @Test public void nullCertContextTest() throws CertificateException, IOException { - trustManager = new XdsX509TrustManager(null, mockDelegate); + trustManager = new XdsX509TrustManager(null, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, new ArrayList<>()); } @Test + @SuppressWarnings("deprecation") public void emptySanListContextTest() throws CertificateException, IOException { CertificateValidationContext certContext = CertificateValidationContext.getDefaultInstance(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void missingPeerCerts() { StringMatcher stringMatcher = StringMatcher.newBuilder().setExact("foo.com").build(); @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); try { - trustManager.verifySubjectAltNameInChain(null); + trustManager.verifySubjectAltNameInChain(null, certContext.getMatchSubjectAltNamesList()); fail("no exception thrown"); } catch (CertificateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Peer certificate(s) missing"); @@ -117,14 +120,15 @@ public void missingPeerCerts() { } @Test + @SuppressWarnings("deprecation") public void emptyArrayPeerCerts() { StringMatcher stringMatcher = StringMatcher.newBuilder().setExact("foo.com").build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); try { - trustManager.verifySubjectAltNameInChain(new X509Certificate[0]); + trustManager.verifySubjectAltNameInChain( + new X509Certificate[0], certContext.getMatchSubjectAltNamesList()); fail("no exception thrown"); } catch (CertificateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Peer certificate(s) missing"); @@ -132,16 +136,16 @@ public void emptyArrayPeerCerts() { } @Test + @SuppressWarnings("deprecation") public void noSansInPeerCerts() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setExact("foo.com").build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(CLIENT_PEM_FILE)); try { - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); fail("no exception thrown"); } catch (CertificateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Peer certificate SAN check failed"); @@ -149,22 +153,23 @@ public void noSansInPeerCerts() throws CertificateException, IOException { } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCertsVerifies() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder() .setExact("waterzooi.test.google.be") .setIgnoreCase(false) .build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCertsVerifies_differentCase_expectException() throws CertificateException, IOException { StringMatcher stringMatcher = @@ -172,14 +177,13 @@ public void oneSanInPeerCertsVerifies_differentCase_expectException() .setExact("waterZooi.test.Google.be") .setIgnoreCase(false) .build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); try { - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); fail("no exception thrown"); } catch (CertificateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Peer certificate SAN check failed"); @@ -187,47 +191,48 @@ public void oneSanInPeerCertsVerifies_differentCase_expectException() } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCertsVerifies_ignoreCase() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setExact("Waterzooi.Test.google.be").setIgnoreCase(true).build(); @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCerts_prefix() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder() .setPrefix("waterzooi.") // test.google.be .setIgnoreCase(false) .build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCertsPrefix_differentCase_expectException() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setPrefix("waterZooi.").setIgnoreCase(false).build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); try { - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); fail("no exception thrown"); } catch (CertificateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Peer certificate SAN check failed"); @@ -235,47 +240,47 @@ public void oneSanInPeerCertsPrefix_differentCase_expectException() } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCerts_prefixIgnoreCase() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder() .setPrefix("WaterZooi.") // test.google.be .setIgnoreCase(true) .build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCerts_suffix() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setSuffix(".google.be").setIgnoreCase(false).build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCertsSuffix_differentCase_expectException() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setSuffix(".gooGle.bE").setIgnoreCase(false).build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); try { - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); fail("no exception thrown"); } catch (CertificateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Peer certificate SAN check failed"); @@ -283,44 +288,45 @@ public void oneSanInPeerCertsSuffix_differentCase_expectException() } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCerts_suffixIgnoreCase() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setSuffix(".GooGle.BE").setIgnoreCase(true).build(); @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCerts_substring() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setContains("zooi.test.google").setIgnoreCase(false).build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCertsSubstring_differentCase_expectException() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setContains("zooi.Test.gooGle").setIgnoreCase(false).build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); try { - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); fail("no exception thrown"); } catch (CertificateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Peer certificate SAN check failed"); @@ -328,81 +334,81 @@ public void oneSanInPeerCertsSubstring_differentCase_expectException() } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCerts_substringIgnoreCase() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setContains("zooI.Test.Google").setIgnoreCase(true).build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCerts_safeRegex() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder() .setSafeRegex( RegexMatcher.newBuilder().setRegex("water[[:alpha:]]{1}ooi\\.test\\.google\\.be")) .build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCerts_safeRegex1() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder() .setSafeRegex( RegexMatcher.newBuilder().setRegex("no-match-string|\\*\\.test\\.youtube\\.com")) .build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCerts_safeRegex_ipAddress() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder() .setSafeRegex( RegexMatcher.newBuilder().setRegex("([[:digit:]]{1,3}\\.){3}[[:digit:]]{1,3}")) .build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCerts_safeRegex_noMatch() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder() .setSafeRegex( RegexMatcher.newBuilder().setRegex("water[[:alpha:]]{2}ooi\\.test\\.google\\.be")) .build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); try { - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); fail("no exception thrown"); } catch (CertificateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Peer certificate SAN check failed"); @@ -410,35 +416,35 @@ public void oneSanInPeerCerts_safeRegex_noMatch() throws CertificateException, I } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCertsVerifiesMultipleVerifySans() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setExact("x.foo.com").build(); StringMatcher stringMatcher1 = StringMatcher.newBuilder().setExact("waterzooi.test.google.be").build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder() .addMatchSubjectAltNames(stringMatcher) .addMatchSubjectAltNames(stringMatcher1) .build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneSanInPeerCertsNotFoundException() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setExact("x.foo.com").build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); try { - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); fail("no exception thrown"); } catch (CertificateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Peer certificate SAN check failed"); @@ -446,42 +452,43 @@ public void oneSanInPeerCertsNotFoundException() } @Test + @SuppressWarnings("deprecation") public void wildcardSanInPeerCertsVerifiesMultipleVerifySans() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setExact("x.foo.com").build(); StringMatcher stringMatcher1 = StringMatcher.newBuilder().setSuffix("test.youTube.Com").setIgnoreCase(true).build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder() .addMatchSubjectAltNames(stringMatcher) .addMatchSubjectAltNames(stringMatcher1) // should match suffix test.youTube.Com .build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void wildcardSanInPeerCertsVerifiesMultipleVerifySans1() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setExact("x.foo.com").build(); StringMatcher stringMatcher1 = StringMatcher.newBuilder().setContains("est.Google.f").setIgnoreCase(true).build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder() .addMatchSubjectAltNames(stringMatcher) .addMatchSubjectAltNames(stringMatcher1) // should contain est.Google.f .build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void wildcardSanInPeerCertsSubdomainMismatch() throws CertificateException, IOException { // 2. Asterisk (*) cannot match across domain name labels. @@ -489,14 +496,13 @@ public void wildcardSanInPeerCertsSubdomainMismatch() // sub.test.example.com. StringMatcher stringMatcher = StringMatcher.newBuilder().setExact("sub.abc.test.youtube.com").build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); try { - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); fail("no exception thrown"); } catch (CertificateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Peer certificate SAN check failed"); @@ -504,36 +510,36 @@ public void wildcardSanInPeerCertsSubdomainMismatch() } @Test + @SuppressWarnings("deprecation") public void oneIpAddressInPeerCertsVerifies() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setExact("x.foo.com").build(); StringMatcher stringMatcher1 = StringMatcher.newBuilder().setExact("192.168.1.3").build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder() .addMatchSubjectAltNames(stringMatcher) .addMatchSubjectAltNames(stringMatcher1) .build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); } @Test + @SuppressWarnings("deprecation") public void oneIpAddressInPeerCertsMismatch() throws CertificateException, IOException { StringMatcher stringMatcher = StringMatcher.newBuilder().setExact("x.foo.com").build(); StringMatcher stringMatcher1 = StringMatcher.newBuilder().setExact("192.168.2.3").build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder() .addMatchSubjectAltNames(stringMatcher) .addMatchSubjectAltNames(stringMatcher1) .build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(SERVER_1_PEM_FILE)); try { - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); fail("no exception thrown"); } catch (CertificateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Peer certificate SAN check failed"); @@ -560,7 +566,7 @@ public void checkServerTrustedSslEngineSpiffeTrustMap() List caCerts = Arrays.asList(CertificateUtils .toX509Certificates(TlsTesting.loadCert(CA_PEM_FILE))); trustManager = XdsTrustManagerFactory.createX509TrustManager( - ImmutableMap.of("example.com", caCerts), null); + ImmutableMap.of("example.com", caCerts), null, false); trustManager.checkServerTrusted(serverCerts, "ECDHE_ECDSA", sslEngine); verify(sslEngine, times(1)).getHandshakeSession(); assertThat(sslEngine.getSSLParameters().getEndpointIdentificationAlgorithm()).isEmpty(); @@ -575,7 +581,7 @@ public void checkServerTrustedSslEngineSpiffeTrustMap_missing_spiffe_id() List caCerts = Arrays.asList(CertificateUtils .toX509Certificates(TlsTesting.loadCert(CA_PEM_FILE))); trustManager = XdsTrustManagerFactory.createX509TrustManager( - ImmutableMap.of("example.com", caCerts), null); + ImmutableMap.of("example.com", caCerts), null, false); try { trustManager.checkServerTrusted(serverCerts, "ECDHE_ECDSA", sslEngine); fail("exception expected"); @@ -594,7 +600,7 @@ public void checkServerTrustedSpiffeSslEngineTrustMap_missing_trust_domain() List caCerts = Arrays.asList(CertificateUtils .toX509Certificates(TlsTesting.loadCert(CA_PEM_FILE))); trustManager = XdsTrustManagerFactory.createX509TrustManager( - ImmutableMap.of("unknown.com", caCerts), null); + ImmutableMap.of("unknown.com", caCerts), null, false); try { trustManager.checkServerTrusted(serverCerts, "ECDHE_ECDSA", sslEngine); fail("exception expected"); @@ -612,7 +618,7 @@ public void checkClientTrustedSpiffeTrustMap() List caCerts = Arrays.asList(CertificateUtils .toX509Certificates(TlsTesting.loadCert(CA_PEM_FILE))); trustManager = XdsTrustManagerFactory.createX509TrustManager( - ImmutableMap.of("foo.bar.com", caCerts), null); + ImmutableMap.of("foo.bar.com", caCerts), null, false); trustManager.checkClientTrusted(clientCerts, "RSA"); } @@ -653,7 +659,7 @@ public void checkServerTrustedSslSocketSpiffeTrustMap() List caCerts = Arrays.asList(CertificateUtils .toX509Certificates(TlsTesting.loadCert(CA_PEM_FILE))); trustManager = XdsTrustManagerFactory.createX509TrustManager( - ImmutableMap.of("example.com", caCerts), null); + ImmutableMap.of("example.com", caCerts), null, false); trustManager.checkServerTrusted(serverCerts, "ECDHE_ECDSA", sslSocket); verify(sslSocket, times(1)).isConnected(); verify(sslSocket, times(1)).getHandshakeSession(); @@ -678,23 +684,23 @@ public void checkServerTrustedSslSocket_untrustedServer_expectException() } @Test - public void unsupportedAltNameType() throws CertificateException, IOException { + @SuppressWarnings("deprecation") + public void unsupportedAltNameType() throws CertificateException { StringMatcher stringMatcher = StringMatcher.newBuilder() .setExact("waterzooi.test.google.be") .setIgnoreCase(false) .build(); - @SuppressWarnings("deprecation") CertificateValidationContext certContext = CertificateValidationContext.newBuilder().addMatchSubjectAltNames(stringMatcher).build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate mockCert = mock(X509Certificate.class); when(mockCert.getSubjectAlternativeNames()) .thenReturn(Collections.>singleton(ImmutableList.of(Integer.valueOf(1), "foo"))); X509Certificate[] certs = new X509Certificate[] {mockCert}; try { - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); fail("no exception thrown"); } catch (CertificateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Peer certificate SAN check failed"); @@ -702,6 +708,7 @@ public void unsupportedAltNameType() throws CertificateException, IOException { } @Test + @SuppressWarnings("deprecation") public void testDnsWildcardPatterns() throws CertificateException, IOException { StringMatcher stringMatcher = @@ -714,11 +721,11 @@ public void testDnsWildcardPatterns() CertificateValidationContext.newBuilder() .addMatchSubjectAltNames(stringMatcher) .build(); - trustManager = new XdsX509TrustManager(certContext, mockDelegate); + trustManager = new XdsX509TrustManager(certContext, mockDelegate, false); X509Certificate[] certs = CertificateUtils.toX509Certificates(TlsTesting.loadCert(testParam.certFile)); try { - trustManager.verifySubjectAltNameInChain(certs); + trustManager.verifySubjectAltNameInChain(certs, certContext.getMatchSubjectAltNamesList()); assertThat(testParam.expected).isTrue(); } catch (CertificateException certException) { assertThat(testParam.expected).isFalse(); @@ -773,7 +780,7 @@ private SSLParameters buildTrustManagerAndGetSslParameters() X509Certificate[] caCerts = CertificateUtils.toX509Certificates(TlsTesting.loadCert(CA_PEM_FILE)); trustManager = XdsTrustManagerFactory.createX509TrustManager(caCerts, - null); + null, false); when(mockSession.getProtocol()).thenReturn("TLSv1.2"); when(mockSession.getPeerHost()).thenReturn("peer-host-from-mock"); SSLParameters sslParams = new SSLParameters(); From 5c145963eb0bea285782847367d4d588037e210e Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 30 Sep 2025 13:23:08 -0700 Subject: [PATCH 398/591] Clean up ServiceBindingTest. (#12386) - Don't store shadows in variables - removing confusing references to uid (these tests are all about Android users, which are different) - remove duplicated logic --- .../binder/internal/ServiceBindingTest.java | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java index 0875881dcc5..0acd7a75f22 100644 --- a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java @@ -52,7 +52,6 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplication; -import org.robolectric.shadows.ShadowDevicePolicyManager; @RunWith(RobolectricTestRunner.class) public final class ServiceBindingTest { @@ -365,12 +364,12 @@ public void testBindServiceAsUser_returnsErrorWhenSdkBelowR() { @Test @Config(sdk = 28) public void testDevicePolicyBlind_returnsErrorWhenSdkBelowR() { - String deviceAdminClassName = "DevicePolicyAdmin"; - ComponentName adminComponent = new ComponentName(appContext, deviceAdminClassName); - allowBindDeviceAdminForUser(appContext, adminComponent, 10); + ComponentName adminComponent = new ComponentName(appContext, "DevicePolicyAdmin"); + UserHandle user10 = generateUserHandle(/* userId= */ 10); + allowBindDeviceAdminForUser(appContext, adminComponent, user10); binding = newBuilder() - .setTargetUserHandle(UserHandle.getUserHandleForUid(10)) + .setTargetUserHandle(user10) .setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent)) .build(); binding.bind(); @@ -384,13 +383,12 @@ public void testDevicePolicyBlind_returnsErrorWhenSdkBelowR() { @Test @Config(sdk = 30) public void testBindWithDeviceAdmin() throws Exception { - String deviceAdminClassName = "DevicePolicyAdmin"; - ComponentName adminComponent = new ComponentName(appContext, deviceAdminClassName); - allowBindDeviceAdminForUser(appContext, adminComponent, /* userId= */ 0); + ComponentName adminComponent = new ComponentName(appContext, "DevicePolicyAdmin"); + UserHandle user0 = generateUserHandle(/* userId= */ 0); + allowBindDeviceAdminForUser(appContext, adminComponent, user0); binding = newBuilder() - .setTargetUserHandle(UserHandle.getUserHandleForUid(/* uid= */ 0)) - .setTargetUserHandle(generateUserHandle(/* userId= */ 0)) + .setTargetUserHandle(user0) .setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent)) .build(); shadowOf(getMainLooper()).idle(); @@ -418,15 +416,10 @@ private void assertNoLockHeld() { } private static void allowBindDeviceAdminForUser( - Context context, ComponentName admin, int userId) { - ShadowDevicePolicyManager devicePolicyManager = - shadowOf(context.getSystemService(DevicePolicyManager.class)); - devicePolicyManager.setDeviceOwner(admin); - devicePolicyManager.setBindDeviceAdminTargetUsers( - Arrays.asList(UserHandle.getUserHandleForUid(userId))); - shadowOf((DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE)); - devicePolicyManager.setDeviceOwner(admin); - devicePolicyManager.setBindDeviceAdminTargetUsers(Arrays.asList(generateUserHandle(userId))); + Context context, ComponentName admin, UserHandle user) { + DevicePolicyManager devicePolicyManager = context.getSystemService(DevicePolicyManager.class); + shadowOf(devicePolicyManager).setBindDeviceAdminTargetUsers(Arrays.asList(user)); + shadowOf(devicePolicyManager).setDeviceOwner(admin); } /** Generate UserHandles the hard way. */ From b33c17fcea482402d0cd2c3541ac5b1cce408e50 Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Tue, 30 Sep 2025 23:30:58 +0200 Subject: [PATCH 399/591] api: remove nullable from StatusOr value methods (#12338) It someone wants it nullable, that should be defined via the type parameter, e.g. `StatusOr<@Nullable Foo> foo` --- api/src/main/java/io/grpc/StatusOr.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/io/grpc/StatusOr.java b/api/src/main/java/io/grpc/StatusOr.java index 0f1e9da3c75..b7dd68cfd7b 100644 --- a/api/src/main/java/io/grpc/StatusOr.java +++ b/api/src/main/java/io/grpc/StatusOr.java @@ -33,7 +33,7 @@ private StatusOr(Status status, T value) { } /** Construct from a value. */ - public static StatusOr fromValue(@Nullable T value) { + public static StatusOr fromValue(T value) { StatusOr result = new StatusOr(null, value); return result; } @@ -54,7 +54,7 @@ public boolean hasValue() { * Returns the value if set or throws exception if there is no value set. This method is meant * to be called after checking the return value of hasValue() first. */ - public @Nullable T getValue() { + public T getValue() { if (status != null) { throw new IllegalStateException("No value present."); } @@ -105,6 +105,7 @@ public String toString() { return stringHelper.toString(); } + @Nullable private final Status status; private final T value; } From 6b83959ff47211e1f8387f2b51060d3d2c13097a Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 30 Sep 2025 19:05:47 -0700 Subject: [PATCH 400/591] netty: Unconditionally disable adaptive cumulator (#12390) io.netty.util.Version is unreliable, so we stop using it. grpc-netty and grpc-netty-shaded have their version.properties mix, and you can't tell which is which. Changed the tests to use assume, so it is clear in the results that they weren't run. --- netty/shaded/build.gradle | 6 ++- .../netty/GrpcHttp2ConnectionHandler.java | 40 ------------------- .../netty/NettyAdaptiveCumulatorTest.java | 13 +++--- 3 files changed, 12 insertions(+), 47 deletions(-) diff --git a/netty/shaded/build.gradle b/netty/shaded/build.gradle index f805a4f4a1f..27816f9380b 100644 --- a/netty/shaded/build.gradle +++ b/netty/shaded/build.gradle @@ -153,7 +153,11 @@ class NettyResourceTransformer implements Transformer { @Override boolean canTransformResource(FileTreeElement fileTreeElement) { - fileTreeElement.name.startsWith("META-INF/native-image/io.netty") + // io.netty.versions.properties can't actually be shaded successfully, + // as io.netty.util.Version still looks for the unshaded name. But we + // keep the file for manual inspection. + fileTreeElement.name.startsWith("META-INF/native-image/io.netty") || + fileTreeElement.name.startsWith("META-INF/io.netty.versions.properties") } @Override diff --git a/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java b/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java index a463cf01d95..ee5227484fb 100644 --- a/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java +++ b/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java @@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkState; -import com.google.common.annotations.VisibleForTesting; import io.grpc.Attributes; import io.grpc.ChannelLogger; import io.grpc.Internal; @@ -28,7 +27,6 @@ import io.netty.handler.codec.http2.Http2ConnectionEncoder; import io.netty.handler.codec.http2.Http2ConnectionHandler; import io.netty.handler.codec.http2.Http2Settings; -import io.netty.util.Version; import javax.annotation.Nullable; /** @@ -36,37 +34,9 @@ */ @Internal public abstract class GrpcHttp2ConnectionHandler extends Http2ConnectionHandler { - static final int ADAPTIVE_CUMULATOR_COMPOSE_MIN_SIZE_DEFAULT = 1024; - static final Cumulator ADAPTIVE_CUMULATOR = - new NettyAdaptiveCumulator(ADAPTIVE_CUMULATOR_COMPOSE_MIN_SIZE_DEFAULT); - @Nullable protected final ChannelPromise channelUnused; private final ChannelLogger negotiationLogger; - private static final boolean usingPre4_1_111_Netty; - - static { - // Netty 4.1.111 introduced a change in the behavior of duplicate() method - // that breaks the assumption of the cumulator. We need to detect this version - // and adjust the behavior accordingly. - - boolean identifiedOldVersion = false; - try { - Version version = Version.identify().get("netty-buffer"); - if (version != null) { - String[] split = version.artifactVersion().split("\\."); - if (split.length >= 3 - && Integer.parseInt(split[0]) == 4 - && Integer.parseInt(split[1]) <= 1 - && Integer.parseInt(split[2]) < 111) { - identifiedOldVersion = true; - } - } - } catch (Exception e) { - // Ignore, we'll assume it's a new version. - } - usingPre4_1_111_Netty = identifiedOldVersion; - } @SuppressWarnings("this-escape") protected GrpcHttp2ConnectionHandler( @@ -78,16 +48,6 @@ protected GrpcHttp2ConnectionHandler( super(decoder, encoder, initialSettings); this.channelUnused = channelUnused; this.negotiationLogger = negotiationLogger; - if (usingPre4_1_111_Netty()) { - // We need to use the adaptive cumulator only if we're using a version of Netty that - // doesn't have the behavior that breaks it. - setCumulator(ADAPTIVE_CUMULATOR); - } - } - - @VisibleForTesting - static boolean usingPre4_1_111_Netty() { - return usingPre4_1_111_Netty; } /** diff --git a/netty/src/test/java/io/grpc/netty/NettyAdaptiveCumulatorTest.java b/netty/src/test/java/io/grpc/netty/NettyAdaptiveCumulatorTest.java index 68dc6dc0c80..b19f247b5cf 100644 --- a/netty/src/test/java/io/grpc/netty/NettyAdaptiveCumulatorTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyAdaptiveCumulatorTest.java @@ -52,6 +52,9 @@ @RunWith(Enclosed.class) public class NettyAdaptiveCumulatorTest { + private static boolean usingPre4_1_111_Netty() { + return false; // Disabled detection because it was unreliable + } private static Collection cartesianProductParams(List... lists) { return Lists.transform(Lists.cartesianProduct(lists), List::toArray); @@ -385,9 +388,8 @@ public void mergeWithCompositeTail_tailExpandable_reallocateInMemory() { } private void assertTailExpanded(String expectedTailReadableData, int expectedNewTailCapacity) { - if (!GrpcHttp2ConnectionHandler.usingPre4_1_111_Netty()) { - return; // Netty 4.1.111 doesn't work with NettyAdaptiveCumulator - } + assume().withMessage("Netty 4.1.111 doesn't work with NettyAdaptiveCumulator") + .that(usingPre4_1_111_Netty()).isTrue(); int originalNumComponents = composite.numComponents(); // Handle the case when reader index is beyond all readable bytes of the cumulation. @@ -628,9 +630,8 @@ public void mergeWithCompositeTail_outOfSyncComposite() { alloc.compositeBuffer(8).addFlattenedComponents(true, composite1); assertThat(composite2.toString(US_ASCII)).isEqualTo("01234"); - if (!GrpcHttp2ConnectionHandler.usingPre4_1_111_Netty()) { - return; // Netty 4.1.111 doesn't work with NettyAdaptiveCumulator - } + assume().withMessage("Netty 4.1.111 doesn't work with NettyAdaptiveCumulator") + .that(usingPre4_1_111_Netty()).isTrue(); // The previous operation does not adjust the read indexes of the underlying buffers, // only the internal Component offsets. When the cumulator attempts to append the input to From 7d5749f39d2d1665ad9a2fc5d6d7adf1533effdc Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Wed, 1 Oct 2025 11:37:29 +0530 Subject: [PATCH 401/591] xds: Use `unused` variable to ignore the return value (#12388) Internal: cl/813142534 --- .../internal/security/trust/XdsTrustManagerFactoryTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactoryTest.java index 0b81c9249dd..3077482b10b 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/trust/XdsTrustManagerFactoryTest.java @@ -185,8 +185,8 @@ public void constructorRootCert_nonStaticContext_systemRootCerts_valid() DataSource.newBuilder().setFilename(TestUtils.loadCert(CA_PEM_FILE).getAbsolutePath())) .setSystemRootCerts(CertificateValidationContext.SystemRootCerts.getDefaultInstance()) .build(); - new XdsTrustManagerFactory( - new X509Certificate[] {x509Cert}, certValidationContext, false); + XdsTrustManagerFactory unused = + new XdsTrustManagerFactory(new X509Certificate[] {x509Cert}, certValidationContext, false); } @Test From b37ee67cf7e657ee1835bf9b35490d640d7bd9fa Mon Sep 17 00:00:00 2001 From: ZachChuba <49295341+ZachChuba@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:14:22 -0400 Subject: [PATCH 402/591] Upgrade netty to 4.1.127.Final And netty-tcnative to 2.0.74.Final. Removes CVE-2025-58057 from security reports. --- MODULE.bazel | 28 ++++++++++++++-------------- SECURITY.md | 3 ++- gradle/libs.versions.toml | 4 ++-- repositories.bzl | 28 ++++++++++++++-------------- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 83ff9eaa539..9e83053fb0b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -23,20 +23,20 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.truth:truth:1.4.2", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day - "io.netty:netty-buffer:4.1.124.Final", - "io.netty:netty-codec-http2:4.1.124.Final", - "io.netty:netty-codec-http:4.1.124.Final", - "io.netty:netty-codec-socks:4.1.124.Final", - "io.netty:netty-codec:4.1.124.Final", - "io.netty:netty-common:4.1.124.Final", - "io.netty:netty-handler-proxy:4.1.124.Final", - "io.netty:netty-handler:4.1.124.Final", - "io.netty:netty-resolver:4.1.124.Final", - "io.netty:netty-tcnative-boringssl-static:2.0.70.Final", - "io.netty:netty-tcnative-classes:2.0.70.Final", - "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.124.Final", - "io.netty:netty-transport-native-unix-common:4.1.124.Final", - "io.netty:netty-transport:4.1.124.Final", + "io.netty:netty-buffer:4.1.127.Final", + "io.netty:netty-codec-http2:4.1.127.Final", + "io.netty:netty-codec-http:4.1.127.Final", + "io.netty:netty-codec-socks:4.1.127.Final", + "io.netty:netty-codec:4.1.127.Final", + "io.netty:netty-common:4.1.127.Final", + "io.netty:netty-handler-proxy:4.1.127.Final", + "io.netty:netty-handler:4.1.127.Final", + "io.netty:netty-resolver:4.1.127.Final", + "io.netty:netty-tcnative-boringssl-static:2.0.74.Final", + "io.netty:netty-tcnative-classes:2.0.74.Final", + "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.127.Final", + "io.netty:netty-transport-native-unix-common:4.1.127.Final", + "io.netty:netty-transport:4.1.127.Final", "io.opencensus:opencensus-api:0.31.0", "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", diff --git a/SECURITY.md b/SECURITY.md index 1555882c3c6..c0ef797238f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -397,7 +397,8 @@ grpc-netty version | netty-handler version | netty-tcnative-boringssl-static ver 1.60.x-1.66.x | 4.1.100.Final | 2.0.61.Final 1.67.x-1.70.x | 4.1.110.Final | 2.0.65.Final 1.71.x-1.74.x | 4.1.110.Final | 2.0.70.Final -1.75.x- | 4.1.124.Final | 2.0.72.Final +1.75.x-1.76.x | 4.1.124.Final | 2.0.72.Final +1.77.x- | 4.1.127.Final | 2.0.74.Final _(grpc-netty-shaded avoids issues with keeping these versions in sync.)_ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cb5dce02843..bf03cc6e18e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -netty = '4.1.124.Final' +netty = '4.1.127.Final' # Keep the following references of tcnative version in sync whenever it's updated: # SECURITY.md -nettytcnative = '2.0.72.Final' +nettytcnative = '2.0.74.Final' opencensus = "0.31.1" # Not upgrading to 4.x as it is not yet ABI compatible. # https://github.com/protocolbuffers/protobuf/issues/17247 diff --git a/repositories.bzl b/repositories.bzl index 4b9d0327b66..2c9b0d46bb6 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -27,20 +27,20 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.truth:truth:1.4.2", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day - "io.netty:netty-buffer:4.1.124.Final", - "io.netty:netty-codec-http2:4.1.124.Final", - "io.netty:netty-codec-http:4.1.124.Final", - "io.netty:netty-codec-socks:4.1.124.Final", - "io.netty:netty-codec:4.1.124.Final", - "io.netty:netty-common:4.1.124.Final", - "io.netty:netty-handler-proxy:4.1.124.Final", - "io.netty:netty-handler:4.1.124.Final", - "io.netty:netty-resolver:4.1.124.Final", - "io.netty:netty-tcnative-boringssl-static:2.0.70.Final", - "io.netty:netty-tcnative-classes:2.0.70.Final", - "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.124.Final", - "io.netty:netty-transport-native-unix-common:4.1.124.Final", - "io.netty:netty-transport:4.1.124.Final", + "io.netty:netty-buffer:4.1.127.Final", + "io.netty:netty-codec-http2:4.1.127.Final", + "io.netty:netty-codec-http:4.1.127.Final", + "io.netty:netty-codec-socks:4.1.127.Final", + "io.netty:netty-codec:4.1.127.Final", + "io.netty:netty-common:4.1.127.Final", + "io.netty:netty-handler-proxy:4.1.127.Final", + "io.netty:netty-handler:4.1.127.Final", + "io.netty:netty-resolver:4.1.127.Final", + "io.netty:netty-tcnative-boringssl-static:2.0.74.Final", + "io.netty:netty-tcnative-classes:2.0.74.Final", + "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.127.Final", + "io.netty:netty-transport-native-unix-common:4.1.127.Final", + "io.netty:netty-transport:4.1.127.Final", "io.opencensus:opencensus-api:0.31.0", "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", From f96ce0670fbe2f2a71b76ccda770e52b1affc063 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Fri, 3 Oct 2025 13:44:43 -0700 Subject: [PATCH 403/591] Remove experimental bindAsUser() method, deprecated since 1.69 (#12401) --- .../io/grpc/binder/BinderChannelBuilder.java | 28 ------------------- .../internal/BinderClientTransport.java | 4 +-- .../BinderClientTransportFactory.java | 10 ------- 3 files changed, 1 insertion(+), 41 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java index 18928339fbd..9a20d35ddb5 100644 --- a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java +++ b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java @@ -20,8 +20,6 @@ import static com.google.common.base.Preconditions.checkState; import android.content.Context; -import android.os.UserHandle; -import androidx.annotation.RequiresApi; import com.google.errorprone.annotations.DoNotCall; import io.grpc.ExperimentalApi; import io.grpc.ForwardingChannelBuilder; @@ -235,32 +233,6 @@ public BinderChannelBuilder securityPolicy(SecurityPolicy securityPolicy) { return this; } - /** - * Specifies the {@link UserHandle} to be searched for the remote Android Service by default. - * - *

    Used only as a fallback if the direct or resolved {@link AndroidComponentAddress} doesn't - * specify a {@link UserHandle}. If neither the Channel nor the {@link AndroidComponentAddress} - * specifies a target user, the {@link UserHandle} of the current process will be used. - * - *

    Connecting to a server in a different Android user is uncommon and can only be done by a - * "system app" client with special permissions. See {@link - * AndroidComponentAddress.Builder#setTargetUser(UserHandle)} for details. - * - * @deprecated This method's name is misleading because it implies an impersonated client identity - * when it's actually specifying part of the server's location. It's also no longer necessary - * since the target user is part of {@link AndroidComponentAddress}. Prefer to specify target - * user in the address instead, either directly or via a {@link io.grpc.NameResolverProvider}. - * @param targetUserHandle the target user to bind into. - * @return this - */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173") - @RequiresApi(30) - @Deprecated - public BinderChannelBuilder bindAsUser(UserHandle targetUserHandle) { - transportFactoryBuilder.setDefaultTargetUserHandle(targetUserHandle); - return this; - } - /** Sets the policy for inbound parcelable objects. */ public BinderChannelBuilder inboundParcelablePolicy( InboundParcelablePolicy inboundParcelablePolicy) { diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 144ad56eec3..6b68552722c 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -131,9 +131,7 @@ public BinderClientTransport( factory.sourceContext, factory.channelCredentials, targetAddress.asBindIntent(), - targetAddress.getTargetUser() != null - ? targetAddress.getTargetUser() - : factory.defaultTargetUserHandle, + targetAddress.getTargetUser(), factory.bindServiceFlags.toInteger(), this); } diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java index 3f51452c90c..e156a6a7b92 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java @@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import android.content.Context; -import android.os.UserHandle; import androidx.core.content.ContextCompat; import io.grpc.ChannelCredentials; import io.grpc.ChannelLogger; @@ -39,7 +38,6 @@ import java.util.Collections; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; -import javax.annotation.Nullable; /** Creates new binder transports. */ @Internal @@ -50,7 +48,6 @@ public final class BinderClientTransportFactory implements ClientTransportFactor final ObjectPool scheduledExecutorPool; final ObjectPool offloadExecutorPool; final SecurityPolicy securityPolicy; - @Nullable final UserHandle defaultTargetUserHandle; final BindServiceFlags bindServiceFlags; final InboundParcelablePolicy inboundParcelablePolicy; final OneWayBinderProxy.Decorator binderDecorator; @@ -71,7 +68,6 @@ private BinderClientTransportFactory(Builder builder) { scheduledExecutorPool = checkNotNull(builder.scheduledExecutorPool); offloadExecutorPool = checkNotNull(builder.offloadExecutorPool); securityPolicy = checkNotNull(builder.securityPolicy); - defaultTargetUserHandle = builder.defaultTargetUserHandle; bindServiceFlags = checkNotNull(builder.bindServiceFlags); inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy); binderDecorator = checkNotNull(builder.binderDecorator); @@ -125,7 +121,6 @@ public static final class Builder implements ClientTransportFactoryBuilder { ObjectPool scheduledExecutorPool = SharedResourcePool.forResource(GrpcUtil.TIMER_SERVICE); SecurityPolicy securityPolicy = SecurityPolicies.internalOnly(); - @Nullable UserHandle defaultTargetUserHandle; BindServiceFlags bindServiceFlags = BindServiceFlags.DEFAULTS; InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT; OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR; @@ -172,11 +167,6 @@ public Builder setSecurityPolicy(SecurityPolicy securityPolicy) { return this; } - public Builder setDefaultTargetUserHandle(@Nullable UserHandle defaultTargetUserHandle) { - this.defaultTargetUserHandle = defaultTargetUserHandle; - return this; - } - public Builder setBindServiceFlags(BindServiceFlags bindServiceFlags) { this.bindServiceFlags = checkNotNull(bindServiceFlags, "bindServiceFlags"); return this; From 349a35a9b121f744a47caa9c6be293e2f33ade33 Mon Sep 17 00:00:00 2001 From: vimanikag Date: Tue, 7 Oct 2025 02:22:23 +0530 Subject: [PATCH 404/591] android: Fix UdsChannelBuilder with WiFi Proxy We want to avoid "[{0}] Failed to resolve name. status={1}" I/O failures for UDS channels when WiFi Proxies are enabled, as it should be local communication. Fixes #11922 --- android/src/main/java/io/grpc/android/UdsChannelBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/io/grpc/android/UdsChannelBuilder.java b/android/src/main/java/io/grpc/android/UdsChannelBuilder.java index 7d41301704c..6f03aa0ee5e 100644 --- a/android/src/main/java/io/grpc/android/UdsChannelBuilder.java +++ b/android/src/main/java/io/grpc/android/UdsChannelBuilder.java @@ -21,6 +21,7 @@ import io.grpc.ExperimentalApi; import io.grpc.InsecureChannelCredentials; import io.grpc.ManagedChannelBuilder; +import io.grpc.internal.GrpcUtil; import java.lang.reflect.InvocationTargetException; import javax.annotation.Nullable; import javax.net.SocketFactory; @@ -81,7 +82,7 @@ public static ManagedChannelBuilder forPath(String path, Namespace namespace) OKHTTP_CHANNEL_BUILDER_CLASS .getMethod("socketFactory", SocketFactory.class) .invoke(builder, new UdsSocketFactory(path, namespace)); - return builder; + return builder.proxyDetector(GrpcUtil.NOOP_PROXY_DETECTOR); } catch (IllegalAccessException e) { throw new RuntimeException("Failed to create OkHttpChannelBuilder", e); } catch (NoSuchMethodException e) { From 032d2928ebfacc4c5a237e21233acdff9a62416b Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Mon, 13 Oct 2025 13:27:31 +0530 Subject: [PATCH 405/591] xds: ORCA to LRS propagation changes (#12203) Implements gRFC A85 (https://github.com/grpc/proposal/pull/454). --- .../java/io/grpc/xds/CdsLoadBalancer2.java | 6 +- .../io/grpc/xds/ClusterImplLoadBalancer.java | 24 ++- .../xds/ClusterImplLoadBalancerProvider.java | 7 +- .../grpc/xds/ClusterResolverLoadBalancer.java | 3 +- .../ClusterResolverLoadBalancerProvider.java | 24 ++- .../java/io/grpc/xds/XdsClusterResource.java | 33 +++- .../xds/client/BackendMetricPropagation.java | 133 +++++++++++++++ .../io/grpc/xds/client/LoadStatsManager2.java | 65 +++++++- .../java/io/grpc/xds/client/XdsClient.java | 17 ++ .../io/grpc/xds/client/XdsClientImpl.java | 15 +- .../io/grpc/xds/CdsLoadBalancer2Test.java | 12 +- .../grpc/xds/ClusterImplLoadBalancerTest.java | 154 +++++++++++++++--- .../xds/ClusterResolverLoadBalancerTest.java | 27 ++- .../grpc/xds/GcpAuthenticationFilterTest.java | 6 +- .../grpc/xds/GrpcXdsClientImplDataTest.java | 39 +++++ .../io/grpc/xds/XdsClientFederationTest.java | 1 - .../java/io/grpc/xds/XdsNameResolverTest.java | 2 +- .../test/java/io/grpc/xds/XdsTestUtils.java | 2 +- .../client/BackendMetricPropagationTest.java | 151 +++++++++++++++++ .../xds/client/LoadStatsManager2Test.java | 54 ++++++ 20 files changed, 707 insertions(+), 68 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/client/BackendMetricPropagation.java create mode 100644 xds/src/test/java/io/grpc/xds/client/BackendMetricPropagationTest.java diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java index 87963476265..688626d9b3d 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java @@ -122,7 +122,8 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { result.maxConcurrentRequests(), result.upstreamTlsContext(), result.filterMetadata(), - result.outlierDetection()); + result.outlierDetection(), + result.backendMetricPropagation()); } else { instance = DiscoveryMechanism.forLogicalDns( clusterName, @@ -130,7 +131,8 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { result.lrsServerInfo(), result.maxConcurrentRequests(), result.upstreamTlsContext(), - result.filterMetadata()); + result.filterMetadata(), + result.backendMetricPropagation()); } gracefulConfig = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( lbRegistry.getProvider(CLUSTER_RESOLVER_POLICY_NAME), diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index a506977d952..00fe736761b 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.xds.client.LoadStatsManager2.isEnabledOrcaLrsPropagation; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; @@ -46,6 +47,7 @@ import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl; import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider; +import io.grpc.xds.client.BackendMetricPropagation; import io.grpc.xds.client.Bootstrapper.ServerInfo; import io.grpc.xds.client.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.client.LoadStatsManager2.ClusterLocalityStats; @@ -149,6 +151,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { childLbHelper.updateMaxConcurrentRequests(config.maxConcurrentRequests); childLbHelper.updateSslContextProviderSupplier(config.tlsContext); childLbHelper.updateFilterMetadata(config.filterMetadata); + childLbHelper.updateBackendMetricPropagation(config.backendMetricPropagation); childSwitchLb.handleResolvedAddresses( resolvedAddresses.toBuilder() @@ -209,6 +212,8 @@ private final class ClusterImplLbHelper extends ForwardingLoadBalancerHelper { private Map filterMetadata = ImmutableMap.of(); @Nullable private final ServerInfo lrsServerInfo; + @Nullable + private BackendMetricPropagation backendMetricPropagation; private ClusterImplLbHelper(AtomicLong inFlights, @Nullable ServerInfo lrsServerInfo) { this.inFlights = checkNotNull(inFlights, "inFlights"); @@ -321,7 +326,7 @@ private ClusterLocality createClusterLocalityFromAttributes(Attributes addressAt (lrsServerInfo == null) ? null : xdsClient.addClusterLocalityStats(lrsServerInfo, cluster, - edsServiceName, locality); + edsServiceName, locality, backendMetricPropagation); return new ClusterLocality(localityStats, localityName); } @@ -371,6 +376,11 @@ private void updateFilterMetadata(Map filterMetadata) { this.filterMetadata = ImmutableMap.copyOf(filterMetadata); } + private void updateBackendMetricPropagation( + @Nullable BackendMetricPropagation backendMetricPropagation) { + this.backendMetricPropagation = backendMetricPropagation; + } + private class RequestLimitingSubchannelPicker extends SubchannelPicker { private final SubchannelPicker delegate; private final List dropPolicies; @@ -506,11 +516,19 @@ private OrcaPerRpcListener(ClusterLocalityStats stats) { } /** - * Copies {@link MetricReport#getNamedMetrics()} to {@link ClusterLocalityStats} such that it is - * included in the snapshot for the LRS report sent to the LRS server. + * Copies ORCA metrics from {@link MetricReport} to {@link ClusterLocalityStats} + * such that they are included in the snapshot for the LRS report sent to the LRS server. + * This includes both top-level metrics (CPU, memory, application utilization) and named + * metrics, filtered according to the backend metric propagation configuration. */ @Override public void onLoadReport(MetricReport report) { + if (isEnabledOrcaLrsPropagation) { + stats.recordTopLevelMetrics( + report.getCpuUtilization(), + report.getMemoryUtilization(), + report.getApplicationUtilization()); + } stats.recordBackendLoadMetricStats(report.getNamedMetrics()); } } diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancerProvider.java index 4c9c14ba5f5..f369c3b99b4 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancerProvider.java @@ -31,6 +31,7 @@ import io.grpc.Status; import io.grpc.xds.Endpoints.DropOverload; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; +import io.grpc.xds.client.BackendMetricPropagation; import io.grpc.xds.client.Bootstrapper.ServerInfo; import java.util.ArrayList; import java.util.Collections; @@ -98,11 +99,14 @@ static final class ClusterImplConfig { // Provides the direct child policy and its config. final Object childConfig; final Map filterMetadata; + @Nullable + final BackendMetricPropagation backendMetricPropagation; ClusterImplConfig(String cluster, @Nullable String edsServiceName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, List dropCategories, Object childConfig, - @Nullable UpstreamTlsContext tlsContext, Map filterMetadata) { + @Nullable UpstreamTlsContext tlsContext, Map filterMetadata, + @Nullable BackendMetricPropagation backendMetricPropagation) { this.cluster = checkNotNull(cluster, "cluster"); this.edsServiceName = edsServiceName; this.lrsServerInfo = lrsServerInfo; @@ -112,6 +116,7 @@ static final class ClusterImplConfig { this.dropCategories = Collections.unmodifiableList( new ArrayList<>(checkNotNull(dropCategories, "dropCategories"))); this.childConfig = checkNotNull(childConfig, "childConfig"); + this.backendMetricPropagation = backendMetricPropagation; } @Override diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index 4c4092632bf..6cfed96a54e 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -44,6 +44,7 @@ import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; import io.grpc.xds.XdsConfig.XdsClusterConfig; import io.grpc.xds.XdsEndpointResource.EdsUpdate; +// import io.grpc.xds.client.BackendMetricPropagation;] import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger.XdsLogLevel; @@ -336,7 +337,7 @@ private static Map generatePriorityChildConfigs( new ClusterImplConfig( discovery.cluster, discovery.edsServiceName, discovery.lrsServerInfo, discovery.maxConcurrentRequests, dropOverloads, endpointLbConfig, - discovery.tlsContext, discovery.filterMetadata); + discovery.tlsContext, discovery.filterMetadata, discovery.backendMetricPropagation); LoadBalancerProvider clusterImplLbProvider = lbRegistry.getProvider(XdsLbPolicies.CLUSTER_IMPL_POLICY_NAME); Object priorityChildPolicy = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java index 8cff272fcba..f6f92a7a9a7 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java @@ -30,6 +30,7 @@ import io.grpc.Status; import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; +import io.grpc.xds.client.BackendMetricPropagation; import io.grpc.xds.client.Bootstrapper.ServerInfo; import java.util.Map; import java.util.Objects; @@ -152,6 +153,8 @@ static final class DiscoveryMechanism { @Nullable final OutlierDetection outlierDetection; final Map filterMetadata; + @Nullable + final BackendMetricPropagation backendMetricPropagation; enum Type { EDS, @@ -161,7 +164,8 @@ enum Type { private DiscoveryMechanism(String cluster, Type type, @Nullable String edsServiceName, @Nullable String dnsHostName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext tlsContext, - Map filterMetadata, @Nullable OutlierDetection outlierDetection) { + Map filterMetadata, @Nullable OutlierDetection outlierDetection, + @Nullable BackendMetricPropagation backendMetricPropagation) { this.cluster = checkNotNull(cluster, "cluster"); this.type = checkNotNull(type, "type"); this.edsServiceName = edsServiceName; @@ -171,27 +175,33 @@ private DiscoveryMechanism(String cluster, Type type, @Nullable String edsServic this.tlsContext = tlsContext; this.filterMetadata = ImmutableMap.copyOf(checkNotNull(filterMetadata, "filterMetadata")); this.outlierDetection = outlierDetection; + this.backendMetricPropagation = backendMetricPropagation; } static DiscoveryMechanism forEds(String cluster, @Nullable String edsServiceName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext tlsContext, Map filterMetadata, - OutlierDetection outlierDetection) { - return new DiscoveryMechanism(cluster, Type.EDS, edsServiceName, null, lrsServerInfo, - maxConcurrentRequests, tlsContext, filterMetadata, outlierDetection); + OutlierDetection outlierDetection, + @Nullable BackendMetricPropagation backendMetricPropagation) { + return new DiscoveryMechanism(cluster, Type.EDS, edsServiceName, + null, lrsServerInfo, maxConcurrentRequests, tlsContext, + filterMetadata, outlierDetection, backendMetricPropagation); } static DiscoveryMechanism forLogicalDns(String cluster, String dnsHostName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, - @Nullable UpstreamTlsContext tlsContext, Map filterMetadata) { + @Nullable UpstreamTlsContext tlsContext, Map filterMetadata, + @Nullable BackendMetricPropagation backendMetricPropagation) { return new DiscoveryMechanism(cluster, Type.LOGICAL_DNS, null, dnsHostName, - lrsServerInfo, maxConcurrentRequests, tlsContext, filterMetadata, null); + lrsServerInfo, maxConcurrentRequests, tlsContext, filterMetadata, null, + backendMetricPropagation); } @Override public int hashCode() { return Objects.hash(cluster, type, lrsServerInfo, maxConcurrentRequests, tlsContext, - edsServiceName, dnsHostName, filterMetadata, outlierDetection); + edsServiceName, dnsHostName, filterMetadata, + outlierDetection, backendMetricPropagation); } @Override diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index a5220515b6c..54089491671 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.xds.client.Bootstrapper.ServerInfo; +import static io.grpc.xds.client.LoadStatsManager2.isEnabledOrcaLrsPropagation; import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; @@ -47,6 +48,7 @@ import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.XdsClusterResource.CdsUpdate; +import io.grpc.xds.client.BackendMetricPropagation; import io.grpc.xds.client.XdsClient.ResourceUpdate; import io.grpc.xds.client.XdsResourceType; import io.grpc.xds.internal.security.CommonTlsContextUtil; @@ -227,6 +229,12 @@ private static StructOrError parseNonAggregateCluster( UpstreamTlsContext upstreamTlsContext = null; OutlierDetection outlierDetection = null; boolean isHttp11ProxyAvailable = false; + BackendMetricPropagation backendMetricPropagation = null; + + if (isEnabledOrcaLrsPropagation) { + backendMetricPropagation = BackendMetricPropagation.fromMetricSpecs( + cluster.getLrsReportEndpointMetricsList()); + } if (cluster.hasLrsServer()) { if (!cluster.getLrsServer().hasSelf()) { return StructOrError.fromError( @@ -326,7 +334,7 @@ private static StructOrError parseNonAggregateCluster( return StructOrError.fromStruct(CdsUpdate.forEds( clusterName, edsServiceName, lrsServerInfo, maxConcurrentRequests, upstreamTlsContext, - outlierDetection, isHttp11ProxyAvailable)); + outlierDetection, isHttp11ProxyAvailable, backendMetricPropagation)); } else if (type.equals(Cluster.DiscoveryType.LOGICAL_DNS)) { if (!cluster.hasLoadAssignment()) { return StructOrError.fromError( @@ -362,7 +370,7 @@ private static StructOrError parseNonAggregateCluster( Locale.US, "%s:%d", socketAddress.getAddress(), socketAddress.getPortValue()); return StructOrError.fromStruct(CdsUpdate.forLogicalDns( clusterName, dnsHostName, lrsServerInfo, maxConcurrentRequests, - upstreamTlsContext, isHttp11ProxyAvailable)); + upstreamTlsContext, isHttp11ProxyAvailable, backendMetricPropagation)); } return StructOrError.fromError( "Cluster " + clusterName + ": unsupported built-in discovery type: " + type); @@ -614,6 +622,9 @@ abstract static class CdsUpdate implements ResourceUpdate { abstract ImmutableMap parsedMetadata(); + @Nullable + abstract BackendMetricPropagation backendMetricPropagation(); + private static Builder newBuilder(String clusterName) { return new AutoValue_XdsClusterResource_CdsUpdate.Builder() .clusterName(clusterName) @@ -622,7 +633,8 @@ private static Builder newBuilder(String clusterName) { .choiceCount(0) .filterMetadata(ImmutableMap.of()) .parsedMetadata(ImmutableMap.of()) - .isHttp11ProxyAvailable(false); + .isHttp11ProxyAvailable(false) + .backendMetricPropagation(null); } static Builder forAggregate(String clusterName, List prioritizedClusterNames) { @@ -636,7 +648,8 @@ static Builder forEds(String clusterName, @Nullable String edsServiceName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext upstreamTlsContext, @Nullable OutlierDetection outlierDetection, - boolean isHttp11ProxyAvailable) { + boolean isHttp11ProxyAvailable, + BackendMetricPropagation backendMetricPropagation) { return newBuilder(clusterName) .clusterType(ClusterType.EDS) .edsServiceName(edsServiceName) @@ -644,21 +657,24 @@ static Builder forEds(String clusterName, @Nullable String edsServiceName, .maxConcurrentRequests(maxConcurrentRequests) .upstreamTlsContext(upstreamTlsContext) .outlierDetection(outlierDetection) - .isHttp11ProxyAvailable(isHttp11ProxyAvailable); + .isHttp11ProxyAvailable(isHttp11ProxyAvailable) + .backendMetricPropagation(backendMetricPropagation); } static Builder forLogicalDns(String clusterName, String dnsHostName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext upstreamTlsContext, - boolean isHttp11ProxyAvailable) { + boolean isHttp11ProxyAvailable, + BackendMetricPropagation backendMetricPropagation) { return newBuilder(clusterName) .clusterType(ClusterType.LOGICAL_DNS) .dnsHostName(dnsHostName) .lrsServerInfo(lrsServerInfo) .maxConcurrentRequests(maxConcurrentRequests) .upstreamTlsContext(upstreamTlsContext) - .isHttp11ProxyAvailable(isHttp11ProxyAvailable); + .isHttp11ProxyAvailable(isHttp11ProxyAvailable) + .backendMetricPropagation(backendMetricPropagation); } enum ClusterType { @@ -749,6 +765,9 @@ Builder leastRequestLbPolicy(Integer choiceCount) { protected abstract Builder parsedMetadata(ImmutableMap parsedMetadata); + protected abstract Builder backendMetricPropagation( + BackendMetricPropagation backendMetricPropagation); + abstract CdsUpdate build(); } } diff --git a/xds/src/main/java/io/grpc/xds/client/BackendMetricPropagation.java b/xds/src/main/java/io/grpc/xds/client/BackendMetricPropagation.java new file mode 100644 index 00000000000..f0e2c9484b4 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/client/BackendMetricPropagation.java @@ -0,0 +1,133 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.client; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableSet; +import io.grpc.Internal; +import java.util.Objects; +import javax.annotation.Nullable; + +/** + * Represents the configuration for which ORCA metrics should be propagated from backend + * to LRS load reports, as defined in gRFC A85. + */ +@Internal +public final class BackendMetricPropagation { + + public final boolean propagateCpuUtilization; + public final boolean propagateMemUtilization; + public final boolean propagateApplicationUtilization; + + private final boolean propagateAllNamedMetrics; + private final ImmutableSet namedMetricKeys; + + private BackendMetricPropagation( + boolean propagateCpuUtilization, + boolean propagateMemUtilization, + boolean propagateApplicationUtilization, + boolean propagateAllNamedMetrics, + ImmutableSet namedMetricKeys) { + this.propagateCpuUtilization = propagateCpuUtilization; + this.propagateMemUtilization = propagateMemUtilization; + this.propagateApplicationUtilization = propagateApplicationUtilization; + this.propagateAllNamedMetrics = propagateAllNamedMetrics; + this.namedMetricKeys = checkNotNull(namedMetricKeys, "namedMetricKeys"); + } + + /** + * Creates a BackendMetricPropagation from a list of metric specifications. + * + * @param metricSpecs list of metric specification strings from CDS resource + * @return BackendMetricPropagation instance + */ + public static BackendMetricPropagation fromMetricSpecs( + @Nullable java.util.List metricSpecs) { + if (metricSpecs == null || metricSpecs.isEmpty()) { + return new BackendMetricPropagation(false, false, false, false, ImmutableSet.of()); + } + + boolean propagateCpuUtilization = false; + boolean propagateMemUtilization = false; + boolean propagateApplicationUtilization = false; + boolean propagateAllNamedMetrics = false; + ImmutableSet.Builder namedMetricKeysBuilder = ImmutableSet.builder(); + for (String spec : metricSpecs) { + if (spec == null) { + continue; + } + switch (spec) { + case "cpu_utilization": + propagateCpuUtilization = true; + break; + case "mem_utilization": + propagateMemUtilization = true; + break; + case "application_utilization": + propagateApplicationUtilization = true; + break; + case "named_metrics.*": + propagateAllNamedMetrics = true; + break; + default: + if (spec.startsWith("named_metrics.")) { + String metricKey = spec.substring("named_metrics.".length()); + if (!metricKey.isEmpty()) { + namedMetricKeysBuilder.add(metricKey); + } + } + } + } + + return new BackendMetricPropagation( + propagateCpuUtilization, + propagateMemUtilization, + propagateApplicationUtilization, + propagateAllNamedMetrics, + namedMetricKeysBuilder.build()); + } + + /** + * Returns whether the given named metric key should be propagated. + */ + public boolean shouldPropagateNamedMetric(String metricKey) { + return propagateAllNamedMetrics || namedMetricKeys.contains(metricKey); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BackendMetricPropagation that = (BackendMetricPropagation) o; + return propagateCpuUtilization == that.propagateCpuUtilization + && propagateMemUtilization == that.propagateMemUtilization + && propagateApplicationUtilization == that.propagateApplicationUtilization + && propagateAllNamedMetrics == that.propagateAllNamedMetrics + && Objects.equals(namedMetricKeys, that.namedMetricKeys); + } + + @Override + public int hashCode() { + return Objects.hash(propagateCpuUtilization, propagateMemUtilization, + propagateApplicationUtilization, propagateAllNamedMetrics, namedMetricKeys); + } +} \ No newline at end of file diff --git a/xds/src/main/java/io/grpc/xds/client/LoadStatsManager2.java b/xds/src/main/java/io/grpc/xds/client/LoadStatsManager2.java index be9d3587d14..cd858dccd99 100644 --- a/xds/src/main/java/io/grpc/xds/client/LoadStatsManager2.java +++ b/xds/src/main/java/io/grpc/xds/client/LoadStatsManager2.java @@ -25,6 +25,7 @@ import com.google.common.collect.Sets; import io.grpc.Internal; import io.grpc.Status; +import io.grpc.internal.GrpcUtil; import io.grpc.xds.client.Stats.BackendLoadMetricStats; import io.grpc.xds.client.Stats.ClusterStats; import io.grpc.xds.client.Stats.DroppedRequests; @@ -57,6 +58,8 @@ public final class LoadStatsManager2 { private final Map>>> allLoadStats = new HashMap<>(); private final Supplier stopwatchSupplier; + public static boolean isEnabledOrcaLrsPropagation = + GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_ORCA_LRS_PROPAGATION", false); @VisibleForTesting public LoadStatsManager2(Supplier stopwatchSupplier) { @@ -98,13 +101,20 @@ private synchronized void releaseClusterDropCounter( /** * Gets or creates the stats object for recording loads for the specified locality (in the - * specified cluster with edsServiceName). The returned object is reference counted and the - * caller should use {@link ClusterLocalityStats#release} to release its hard reference + * specified cluster with edsServiceName) with the specified backend metric propagation + * configuration. The returned object is reference counted and the caller should + * use {@link ClusterLocalityStats#release} to release its hard reference * when it is safe to discard the future stats for the locality. */ @VisibleForTesting public synchronized ClusterLocalityStats getClusterLocalityStats( String cluster, @Nullable String edsServiceName, Locality locality) { + return getClusterLocalityStats(cluster, edsServiceName, locality, null); + } + + public synchronized ClusterLocalityStats getClusterLocalityStats( + String cluster, @Nullable String edsServiceName, Locality locality, + @Nullable BackendMetricPropagation backendMetricPropagation) { if (!allLoadStats.containsKey(cluster)) { allLoadStats.put( cluster, @@ -121,8 +131,8 @@ public synchronized ClusterLocalityStats getClusterLocalityStats( if (!localityStats.containsKey(locality)) { localityStats.put( locality, - ReferenceCounted.wrap(new ClusterLocalityStats( - cluster, edsServiceName, locality, stopwatchSupplier.get()))); + ReferenceCounted.wrap(new ClusterLocalityStats(cluster, edsServiceName, + locality, stopwatchSupplier.get(), backendMetricPropagation))); } ReferenceCounted ref = localityStats.get(locality); ref.retain(); @@ -325,6 +335,8 @@ public final class ClusterLocalityStats { private final String edsServiceName; private final Locality locality; private final Stopwatch stopwatch; + @Nullable + private final BackendMetricPropagation backendMetricPropagation; private final AtomicLong callsInProgress = new AtomicLong(); private final AtomicLong callsSucceeded = new AtomicLong(); private final AtomicLong callsFailed = new AtomicLong(); @@ -333,11 +345,12 @@ public final class ClusterLocalityStats { private ClusterLocalityStats( String clusterName, @Nullable String edsServiceName, Locality locality, - Stopwatch stopwatch) { + Stopwatch stopwatch, BackendMetricPropagation backendMetricPropagation) { this.clusterName = checkNotNull(clusterName, "clusterName"); this.edsServiceName = edsServiceName; this.locality = checkNotNull(locality, "locality"); this.stopwatch = checkNotNull(stopwatch, "stopwatch"); + this.backendMetricPropagation = backendMetricPropagation; stopwatch.reset().start(); } @@ -367,17 +380,51 @@ public void recordCallFinished(Status status) { * requests counter of 1 and the {@code value} if the key is not present in the map. Otherwise, * increments the finished requests counter and adds the {@code value} to the existing * {@link BackendLoadMetricStats}. + * Metrics are filtered based on the backend metric propagation configuration if configured. */ public synchronized void recordBackendLoadMetricStats(Map namedMetrics) { + if (!isEnabledOrcaLrsPropagation) { + namedMetrics.forEach((name, value) -> updateLoadMetricStats(name, value)); + return; + } + namedMetrics.forEach((name, value) -> { - if (!loadMetricStatsMap.containsKey(name)) { - loadMetricStatsMap.put(name, new BackendLoadMetricStats(1, value)); - } else { - loadMetricStatsMap.get(name).addMetricValueAndIncrementRequestsFinished(value); + if (backendMetricPropagation.shouldPropagateNamedMetric(name)) { + updateLoadMetricStats("named_metrics." + name, value); } }); } + private void updateLoadMetricStats(String metricName, double value) { + if (!loadMetricStatsMap.containsKey(metricName)) { + loadMetricStatsMap.put(metricName, new BackendLoadMetricStats(1, value)); + } else { + loadMetricStatsMap.get(metricName).addMetricValueAndIncrementRequestsFinished(value); + } + } + + /** + * Records top-level ORCA metrics (CPU, memory, application utilization) for per-call load + * reporting. Metrics are filtered based on the backend metric propagation configuration + * if configured. + * + * @param cpuUtilization CPU utilization metric value + * @param memUtilization Memory utilization metric value + * @param applicationUtilization Application utilization metric value + */ + public synchronized void recordTopLevelMetrics(double cpuUtilization, double memUtilization, + double applicationUtilization) { + if (backendMetricPropagation.propagateCpuUtilization && cpuUtilization > 0) { + updateLoadMetricStats("cpu_utilization", cpuUtilization); + } + if (backendMetricPropagation.propagateMemUtilization && memUtilization > 0) { + updateLoadMetricStats("mem_utilization", memUtilization); + } + if (backendMetricPropagation.propagateApplicationUtilization && applicationUtilization > 0) { + updateLoadMetricStats("application_utilization", applicationUtilization); + } + } + /** * Release the hard reference for this stats object (previously obtained via {@link * LoadStatsManager2#getClusterLocalityStats}). The object may still be diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClient.java b/xds/src/main/java/io/grpc/xds/client/XdsClient.java index e2dd4169bca..cd545c00c1d 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClient.java @@ -388,6 +388,23 @@ public LoadStatsManager2.ClusterDropStats addClusterDropStats( public LoadStatsManager2.ClusterLocalityStats addClusterLocalityStats( Bootstrapper.ServerInfo serverInfo, String clusterName, @Nullable String edsServiceName, Locality locality) { + return addClusterLocalityStats(serverInfo, clusterName, edsServiceName, locality, null); + } + + /** + * Adds load stats for the specified locality (in the specified cluster with edsServiceName) by + * using the returned object to record RPCs. Load stats recorded with the returned object will + * be reported to the load reporting server. The returned object is reference counted and the + * caller should use {@link LoadStatsManager2.ClusterLocalityStats#release} to release its + * hard reference when it is safe to stop reporting RPC loads for the specified locality + * in the future. + * + * @param backendMetricPropagation Configuration for which backend metrics should be propagated + * to LRS load reports. If null, all metrics will be propagated (legacy behavior). + */ + public LoadStatsManager2.ClusterLocalityStats addClusterLocalityStats( + Bootstrapper.ServerInfo serverInfo, String clusterName, @Nullable String edsServiceName, + Locality locality, @Nullable BackendMetricPropagation backendMetricPropagation) { throw new UnsupportedOperationException(); } diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index 4c6823f844a..6a4c20cf02b 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -409,9 +409,22 @@ public void run() { public LoadStatsManager2.ClusterLocalityStats addClusterLocalityStats( final ServerInfo serverInfo, String clusterName, @Nullable String edsServiceName, Locality locality) { + return addClusterLocalityStats(serverInfo, clusterName, edsServiceName, locality, null); + } + + @Override + public LoadStatsManager2.ClusterLocalityStats addClusterLocalityStats( + final ServerInfo serverInfo, + String clusterName, + @Nullable String edsServiceName, + Locality locality, + @Nullable BackendMetricPropagation backendMetricPropagation) { LoadStatsManager2 loadStatsManager = loadStatsManagerMap.get(serverInfo); + LoadStatsManager2.ClusterLocalityStats loadCounter = - loadStatsManager.getClusterLocalityStats(clusterName, edsServiceName, locality); + loadStatsManager.getClusterLocalityStats( + clusterName, edsServiceName, locality, backendMetricPropagation); + syncContext.execute(new Runnable() { @Override public void run() { diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java index 2059022c203..790f4445823 100644 --- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java +++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java @@ -257,7 +257,7 @@ public void discoverTopLevelEdsCluster() { CLUSTER, EDS_SERVICE_NAME, lrsServerInfo, 100L, upstreamTlsContext, Collections.emptyMap(), io.grpc.xds.EnvoyServerProtoData.OutlierDetection.create( null, null, null, null, SuccessRateEjection.create(null, null, null, null), - FailurePercentageEjection.create(null, null, null, null)))); + FailurePercentageEjection.create(null, null, null, null)), null)); assertThat( GracefulSwitchLoadBalancerAccessor.getChildProvider(childLbConfig.lbConfig).getPolicyName()) .isEqualTo("wrr_locality_experimental"); @@ -304,7 +304,7 @@ public void discoverTopLevelLogicalDnsCluster() { assertThat(childLbConfig.discoveryMechanism).isEqualTo( DiscoveryMechanism.forLogicalDns( CLUSTER, "dns.example.com:1111", lrsServerInfo, 100L, upstreamTlsContext, - Collections.emptyMap())); + Collections.emptyMap(), null)); assertThat( GracefulSwitchLoadBalancerAccessor.getChildProvider(childLbConfig.lbConfig).getPolicyName()) .isEqualTo("wrr_locality_experimental"); @@ -338,7 +338,7 @@ public void nonAggregateCluster_resourceUpdate() { ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; assertThat(childLbConfig.discoveryMechanism).isEqualTo( DiscoveryMechanism.forEds( - CLUSTER, EDS_SERVICE_NAME, null, 100L, null, Collections.emptyMap(), null)); + CLUSTER, EDS_SERVICE_NAME, null, 100L, null, Collections.emptyMap(), null, null)); cluster = EDS_CLUSTER.toBuilder() .setCircuitBreakers(CircuitBreakers.newBuilder() @@ -353,7 +353,7 @@ public void nonAggregateCluster_resourceUpdate() { childLbConfig = (ClusterResolverConfig) childBalancer.config; assertThat(childLbConfig.discoveryMechanism).isEqualTo( DiscoveryMechanism.forEds( - CLUSTER, EDS_SERVICE_NAME, null, 200L, null, Collections.emptyMap(), null)); + CLUSTER, EDS_SERVICE_NAME, null, 200L, null, Collections.emptyMap(), null, null)); } @Test @@ -367,7 +367,7 @@ public void nonAggregateCluster_resourceRevoked() { ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; assertThat(childLbConfig.discoveryMechanism).isEqualTo( DiscoveryMechanism.forEds( - CLUSTER, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null)); + CLUSTER, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null, null)); controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of()); @@ -398,7 +398,7 @@ public void dynamicCluster() { ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; assertThat(childLbConfig.discoveryMechanism).isEqualTo( DiscoveryMechanism.forEds( - clusterName, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null)); + clusterName, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null, null)); assertThat(this.lastXdsConfig.getClusters()).containsKey(clusterName); shutdownLoadBalancer(); diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index a491d3f3612..50138cb8e5a 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -68,6 +68,7 @@ import io.grpc.xds.WeightedTargetLoadBalancerProvider.WeightedPolicySelection; import io.grpc.xds.WeightedTargetLoadBalancerProvider.WeightedTargetConfig; import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider; +import io.grpc.xds.client.BackendMetricPropagation; import io.grpc.xds.client.Bootstrapper.ServerInfo; import io.grpc.xds.client.LoadReportClient; import io.grpc.xds.client.LoadStatsManager2; @@ -192,7 +193,7 @@ public void handleResolvedAddresses_propagateToChildPolicy() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); deliverAddressesAndConfig(Collections.singletonList(endpoint), config); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(downstreamBalancers); @@ -220,7 +221,7 @@ public void handleResolvedAddresses_childPolicyChanges() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); deliverAddressesAndConfig(Collections.singletonList(endpoint), configWithWeightedTarget); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(downstreamBalancers); @@ -235,7 +236,7 @@ public void handleResolvedAddresses_childPolicyChanges() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( wrrLocalityProvider, wrrLocalityConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); deliverAddressesAndConfig(Collections.singletonList(endpoint), configWithWrrLocality); childBalancer = Iterables.getOnlyElement(downstreamBalancers); assertThat(childBalancer.name).isEqualTo(XdsLbPolicies.WRR_LOCALITY_POLICY_NAME); @@ -261,7 +262,7 @@ public void nameResolutionError_afterChildPolicyInstantiated_propagateToDownstre null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); deliverAddressesAndConfig(Collections.singletonList(endpoint), config); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(downstreamBalancers); @@ -282,7 +283,7 @@ public void pick_addsOptionalLabels() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); deliverAddressesAndConfig(Collections.singletonList(endpoint), config); FakeLoadBalancer leafBalancer = Iterables.getOnlyElement(downstreamBalancers); @@ -313,7 +314,7 @@ public void pick_noResult_addsClusterLabel() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); deliverAddressesAndConfig(Collections.singletonList(endpoint), config); FakeLoadBalancer leafBalancer = Iterables.getOnlyElement(downstreamBalancers); @@ -337,7 +338,7 @@ public void recordLoadStats() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); deliverAddressesAndConfig(Collections.singletonList(endpoint), config); FakeLoadBalancer leafBalancer = Iterables.getOnlyElement(downstreamBalancers); @@ -416,6 +417,116 @@ public void recordLoadStats() { assertThat(clusterStats.upstreamLocalityStatsList()).isEmpty(); // no longer reported } + @Test + public void recordLoadStats_orcaLrsPropagationEnabled() { + boolean originalVal = LoadStatsManager2.isEnabledOrcaLrsPropagation; + LoadStatsManager2.isEnabledOrcaLrsPropagation = true; + BackendMetricPropagation backendMetricPropagation = BackendMetricPropagation.fromMetricSpecs( + Arrays.asList("application_utilization", "cpu_utilization", "named_metrics.named1")); + LoadBalancerProvider weightedTargetProvider = new WeightedTargetLoadBalancerProvider(); + WeightedTargetConfig weightedTargetConfig = + buildWeightedTargetConfig(ImmutableMap.of(locality, 10)); + ClusterImplConfig config = new ClusterImplConfig(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, + null, Collections.emptyList(), + GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + weightedTargetProvider, weightedTargetConfig), + null, Collections.emptyMap(), backendMetricPropagation); + EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); + deliverAddressesAndConfig(Collections.singletonList(endpoint), config); + FakeLoadBalancer leafBalancer = Iterables.getOnlyElement(downstreamBalancers); + Subchannel subchannel = leafBalancer.createSubChannel(); + FakeSubchannel fakeSubchannel = helper.subchannels.poll(); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.CONNECTING)); + fakeSubchannel.setConnectedEagIndex(0); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); + assertThat(currentState).isEqualTo(ConnectivityState.READY); + PickResult result = currentPicker.pickSubchannel(pickSubchannelArgs); + assertThat(result.getStatus().isOk()).isTrue(); + ClientStreamTracer streamTracer = result.getStreamTracerFactory().newClientStreamTracer( + ClientStreamTracer.StreamInfo.newBuilder().build(), new Metadata()); + Metadata trailersWithOrcaLoadReport = new Metadata(); + trailersWithOrcaLoadReport.put(ORCA_ENDPOINT_LOAD_METRICS_KEY, + OrcaLoadReport.newBuilder() + .setApplicationUtilization(1.414) + .setCpuUtilization(0.5) + .setMemUtilization(0.034) + .putNamedMetrics("named1", 3.14159) + .putNamedMetrics("named2", -1.618).build()); + streamTracer.inboundTrailers(trailersWithOrcaLoadReport); + streamTracer.streamClosed(Status.OK); + ClusterStats clusterStats = + Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); + UpstreamLocalityStats localityStats = + Iterables.getOnlyElement(clusterStats.upstreamLocalityStatsList()); + + assertThat(localityStats.loadMetricStatsMap()).containsKey("application_utilization"); + assertThat(localityStats.loadMetricStatsMap().get("application_utilization").totalMetricValue()) + .isWithin(TOLERANCE).of(1.414); + assertThat(localityStats.loadMetricStatsMap()).containsKey("cpu_utilization"); + assertThat(localityStats.loadMetricStatsMap().get("cpu_utilization").totalMetricValue()) + .isWithin(TOLERANCE).of(0.5); + assertThat(localityStats.loadMetricStatsMap()).doesNotContainKey("mem_utilization"); + assertThat(localityStats.loadMetricStatsMap()).containsKey("named_metrics.named1"); + assertThat(localityStats.loadMetricStatsMap().get("named_metrics.named1").totalMetricValue()) + .isWithin(TOLERANCE).of(3.14159); + assertThat(localityStats.loadMetricStatsMap()).doesNotContainKey("named_metrics.named2"); + subchannel.shutdown(); + LoadStatsManager2.isEnabledOrcaLrsPropagation = originalVal; + } + + @Test + public void recordLoadStats_orcaLrsPropagationDisabled() { + boolean originalVal = LoadStatsManager2.isEnabledOrcaLrsPropagation; + LoadStatsManager2.isEnabledOrcaLrsPropagation = false; + BackendMetricPropagation backendMetricPropagation = BackendMetricPropagation.fromMetricSpecs( + Arrays.asList("application_utilization", "cpu_utilization", "named_metrics.named1")); + LoadBalancerProvider weightedTargetProvider = new WeightedTargetLoadBalancerProvider(); + WeightedTargetConfig weightedTargetConfig = + buildWeightedTargetConfig(ImmutableMap.of(locality, 10)); + ClusterImplConfig config = new ClusterImplConfig(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, + null, Collections.emptyList(), + GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + weightedTargetProvider, weightedTargetConfig), + null, Collections.emptyMap(), backendMetricPropagation); + EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); + deliverAddressesAndConfig(Collections.singletonList(endpoint), config); + FakeLoadBalancer leafBalancer = Iterables.getOnlyElement(downstreamBalancers); + Subchannel subchannel = leafBalancer.createSubChannel(); + FakeSubchannel fakeSubchannel = helper.subchannels.poll(); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.CONNECTING)); + fakeSubchannel.setConnectedEagIndex(0); + fakeSubchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); + assertThat(currentState).isEqualTo(ConnectivityState.READY); + PickResult result = currentPicker.pickSubchannel(pickSubchannelArgs); + assertThat(result.getStatus().isOk()).isTrue(); + ClientStreamTracer streamTracer = result.getStreamTracerFactory().newClientStreamTracer( + ClientStreamTracer.StreamInfo.newBuilder().build(), new Metadata()); + Metadata trailersWithOrcaLoadReport = new Metadata(); + trailersWithOrcaLoadReport.put(ORCA_ENDPOINT_LOAD_METRICS_KEY, + OrcaLoadReport.newBuilder() + .setApplicationUtilization(1.414) + .setCpuUtilization(0.5) + .setMemUtilization(0.034) + .putNamedMetrics("named1", 3.14159) + .putNamedMetrics("named2", -1.618).build()); + streamTracer.inboundTrailers(trailersWithOrcaLoadReport); + streamTracer.streamClosed(Status.OK); + ClusterStats clusterStats = + Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); + UpstreamLocalityStats localityStats = + Iterables.getOnlyElement(clusterStats.upstreamLocalityStatsList()); + + assertThat(localityStats.loadMetricStatsMap()).doesNotContainKey("application_utilization"); + assertThat(localityStats.loadMetricStatsMap()).doesNotContainKey("cpu_utilization"); + assertThat(localityStats.loadMetricStatsMap()).doesNotContainKey("mem_utilization"); + assertThat(localityStats.loadMetricStatsMap()).doesNotContainKey("named_metrics.named1"); + assertThat(localityStats.loadMetricStatsMap()).doesNotContainKey("named_metrics.named2"); + assertThat(localityStats.loadMetricStatsMap().containsKey("named1")).isTrue(); + assertThat(localityStats.loadMetricStatsMap().containsKey("named2")).isTrue(); + subchannel.shutdown(); + LoadStatsManager2.isEnabledOrcaLrsPropagation = originalVal; + } + // Verifies https://github.com/grpc/grpc-java/issues/11434. @Test public void pickFirstLoadReport_onUpdateAddress() { @@ -432,7 +543,7 @@ public void pickFirstLoadReport_onUpdateAddress() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig(pickFirstProvider, pickFirstConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr1", locality1); EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr2", locality2); deliverAddressesAndConfig(Arrays.asList(endpoint1, endpoint2), config); @@ -522,7 +633,7 @@ public void dropRpcsWithRespectToLbConfigDropCategories() { null, Collections.singletonList(DropOverload.create("throttle", 500_000)), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); deliverAddressesAndConfig(Collections.singletonList(endpoint), config); when(mockRandom.nextInt(anyInt())).thenReturn(499_999, 999_999, 1_000_000); @@ -556,7 +667,7 @@ public void dropRpcsWithRespectToLbConfigDropCategories() { Collections.singletonList(DropOverload.create("lb", 1_000_000)), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); loadBalancer.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(Collections.singletonList(endpoint)) @@ -605,7 +716,7 @@ private void subtest_maxConcurrentRequests_appliedByLbConfig(boolean enableCircu maxConcurrentRequests, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); deliverAddressesAndConfig(Collections.singletonList(endpoint), config); assertThat(downstreamBalancers).hasSize(1); // one leaf balancer @@ -652,7 +763,7 @@ private void subtest_maxConcurrentRequests_appliedByLbConfig(boolean enableCircu maxConcurrentRequests, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); deliverAddressesAndConfig(Collections.singletonList(endpoint), config); result = currentPicker.pickSubchannel(pickSubchannelArgs); @@ -700,7 +811,7 @@ private void subtest_maxConcurrentRequests_appliedWithDefaultValue( null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); deliverAddressesAndConfig(Collections.singletonList(endpoint), config); assertThat(downstreamBalancers).hasSize(1); // one leaf balancer @@ -751,7 +862,7 @@ public void endpointAddressesAttachedWithClusterName() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); // One locality with two endpoints. EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr1", locality); EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr2", locality); @@ -791,7 +902,7 @@ public void endpointAddressesAttachedWithClusterName() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr1", locality, "authority-host-name"); deliverAddressesAndConfig(Arrays.asList(endpoint1), config); @@ -842,7 +953,7 @@ public void endpointAddressesAttachedWithClusterName() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr1", locality, "authority-host-name"); deliverAddressesAndConfig(Arrays.asList(endpoint1), config); @@ -891,7 +1002,7 @@ public void endpointAddressesAttachedWithTlsConfig_securityEnabledByDefault() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - upstreamTlsContext, Collections.emptyMap()); + upstreamTlsContext, Collections.emptyMap(), null); // One locality with two endpoints. EquivalentAddressGroup endpoint1 = makeAddress("endpoint-addr1", locality); EquivalentAddressGroup endpoint2 = makeAddress("endpoint-addr2", locality); @@ -916,7 +1027,7 @@ public void endpointAddressesAttachedWithTlsConfig_securityEnabledByDefault() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - null, Collections.emptyMap()); + null, Collections.emptyMap(), null); deliverAddressesAndConfig(Arrays.asList(endpoint1, endpoint2), config); assertThat(Iterables.getOnlyElement(downstreamBalancers)).isSameInstanceAs(leafBalancer); subchannel = leafBalancer.helper.createSubchannel(args); // creates new connections @@ -933,7 +1044,7 @@ public void endpointAddressesAttachedWithTlsConfig_securityEnabledByDefault() { null, Collections.emptyList(), GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( weightedTargetProvider, weightedTargetConfig), - upstreamTlsContext, Collections.emptyMap()); + upstreamTlsContext, Collections.emptyMap(), null); deliverAddressesAndConfig(Arrays.asList(endpoint1, endpoint2), config); assertThat(Iterables.getOnlyElement(downstreamBalancers)).isSameInstanceAs(leafBalancer); subchannel = leafBalancer.helper.createSubchannel(args); // creates new connections @@ -1243,8 +1354,9 @@ public ClusterDropStats addClusterDropStats( @Override public ClusterLocalityStats addClusterLocalityStats( ServerInfo lrsServerInfo, String clusterName, @Nullable String edsServiceName, - Locality locality) { - return loadStatsManager.getClusterLocalityStats(clusterName, edsServiceName, locality); + Locality locality, BackendMetricPropagation backendMetricPropagation) { + return loadStatsManager.getClusterLocalityStats( + clusterName, edsServiceName, locality, backendMetricPropagation); } @Override diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index 86f525c61f2..983ae17818c 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -100,7 +100,9 @@ import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; import io.grpc.xds.WrrLocalityLoadBalancer.WrrLocalityConfig; +import io.grpc.xds.client.BackendMetricPropagation; import io.grpc.xds.client.Bootstrapper.ServerInfo; +import io.grpc.xds.client.LoadStatsManager2; import io.grpc.xds.client.XdsClient; import io.grpc.xds.internal.XdsInternalAttributes; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; @@ -270,14 +272,19 @@ public void tearDown() throws Exception { @Test public void edsClustersWithRingHashEndpointLbPolicy() throws Exception { + boolean originalVal = LoadStatsManager2.isEnabledOrcaLrsPropagation; + LoadStatsManager2.isEnabledOrcaLrsPropagation = true; + List metricSpecs = Arrays.asList("cpu_utilization"); + BackendMetricPropagation backendMetricPropagation = + BackendMetricPropagation.fromMetricSpecs(metricSpecs); Cluster cluster = EDS_CLUSTER.toBuilder() .setLbPolicy(Cluster.LbPolicy.RING_HASH) .setRingHashLbConfig(Cluster.RingHashLbConfig.newBuilder() .setMinimumRingSize(UInt64Value.of(10)) .setMaximumRingSize(UInt64Value.of(100)) .build()) + .addAllLrsReportEndpointMetrics(metricSpecs) .build(); - // One priority with two localities of different weights. ClusterLoadAssignment clusterLoadAssignment = ClusterLoadAssignment.newBuilder() .setClusterName(EDS_SERVICE_NAME) .addEndpoints(LocalityLbEndpoints.newBuilder() @@ -331,6 +338,8 @@ public void edsClustersWithRingHashEndpointLbPolicy() throws Exception { GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig.childConfig); assertClusterImplConfig(clusterImplConfig, CLUSTER, EDS_SERVICE_NAME, null, null, null, Collections.emptyList(), "ring_hash_experimental"); + assertThat(clusterImplConfig.backendMetricPropagation).isEqualTo(backendMetricPropagation); + LoadStatsManager2.isEnabledOrcaLrsPropagation = originalVal; RingHashConfig ringHashConfig = (RingHashConfig) GracefulSwitchLoadBalancerAccessor.getChildConfig(clusterImplConfig.childConfig); assertThat(ringHashConfig.minRingSize).isEqualTo(10L); @@ -871,8 +880,16 @@ public void handleEdsResource_noHealthyEndpoint() { @Test public void onlyLogicalDnsCluster_endpointsResolved() { + boolean originalVal = LoadStatsManager2.isEnabledOrcaLrsPropagation; + LoadStatsManager2.isEnabledOrcaLrsPropagation = true; + List metricSpecs = Arrays.asList("cpu_utilization"); + BackendMetricPropagation backendMetricPropagation = + BackendMetricPropagation.fromMetricSpecs(metricSpecs); + Cluster logicalDnsClusterWithMetrics = LOGICAL_DNS_CLUSTER.toBuilder() + .addAllLrsReportEndpointMetrics(metricSpecs) + .build(); controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( - CLUSTER, LOGICAL_DNS_CLUSTER)); + CLUSTER, logicalDnsClusterWithMetrics)); startXdsDepManager(new CdsConfig(CLUSTER), /* forwardTime= */ false); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME + ":9000"); assertThat(childBalancers).isEmpty(); @@ -895,6 +912,8 @@ public void onlyLogicalDnsCluster_endpointsResolved() { GracefulSwitchLoadBalancerAccessor.getChildConfig(priorityChildConfig.childConfig); assertClusterImplConfig(clusterImplConfig, CLUSTER, null, null, null, null, Collections.emptyList(), "wrr_locality_experimental"); + assertThat(clusterImplConfig.backendMetricPropagation).isEqualTo(backendMetricPropagation); + LoadStatsManager2.isEnabledOrcaLrsPropagation = originalVal; assertAddressesEqual( Arrays.asList(new EquivalentAddressGroup(Arrays.asList( newInetSocketAddress("127.0.2.1", 9000), newInetSocketAddress("127.0.2.2", 9000)))), @@ -1002,14 +1021,14 @@ public void config_equalsTester() { "google_cloud_private_spiffe", true); DiscoveryMechanism edsDiscoveryMechanism1 = DiscoveryMechanism.forEds(CLUSTER, EDS_SERVICE_NAME, lrsServerInfo, 100L, tlsContext, - Collections.emptyMap(), null); + Collections.emptyMap(), null, null); io.grpc.xds.EnvoyServerProtoData.OutlierDetection outlierDetection = io.grpc.xds.EnvoyServerProtoData.OutlierDetection.create( 100L, 100L, 100L, 100, SuccessRateEjection.create(100, 100, 100, 100), FailurePercentageEjection.create(100, 100, 100, 100)); DiscoveryMechanism edsDiscoveryMechanismWithOutlierDetection = DiscoveryMechanism.forEds(CLUSTER, EDS_SERVICE_NAME, lrsServerInfo, 100L, tlsContext, - Collections.emptyMap(), outlierDetection); + Collections.emptyMap(), outlierDetection, null); Object roundRobin = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( new FakeLoadBalancerProvider("wrr_locality_experimental"), new WrrLocalityConfig( GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( diff --git a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java index 1d6c97d81e6..7f203e036b7 100644 --- a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java @@ -491,7 +491,7 @@ private static CdsUpdate getCdsUpdate() { parsedMetadata.put("FILTER_INSTANCE_NAME", new AudienceWrapper("TEST_AUDIENCE")); try { CdsUpdate.Builder cdsUpdate = CdsUpdate.forEds( - CLUSTER_NAME, EDS_NAME, null, null, null, null, false) + CLUSTER_NAME, EDS_NAME, null, null, null, null, false, null) .lbPolicyConfig(getWrrLbConfigAsMap()); return cdsUpdate.parsedMetadata(parsedMetadata.build()).build(); } catch (IOException ex) { @@ -504,7 +504,7 @@ private static CdsUpdate getCdsUpdate2() { parsedMetadata.put("FILTER_INSTANCE_NAME", new AudienceWrapper("NEW_TEST_AUDIENCE")); try { CdsUpdate.Builder cdsUpdate = CdsUpdate.forEds( - CLUSTER_NAME, EDS_NAME, null, null, null, null, false) + CLUSTER_NAME, EDS_NAME, null, null, null, null, false, null) .lbPolicyConfig(getWrrLbConfigAsMap()); return cdsUpdate.parsedMetadata(parsedMetadata.build()).build(); } catch (IOException ex) { @@ -516,7 +516,7 @@ private static CdsUpdate getCdsUpdateWithIncorrectAudienceWrapper() throws IOExc ImmutableMap.Builder parsedMetadata = ImmutableMap.builder(); parsedMetadata.put("FILTER_INSTANCE_NAME", "TEST_AUDIENCE"); CdsUpdate.Builder cdsUpdate = CdsUpdate.forEds( - CLUSTER_NAME, EDS_NAME, null, null, null, null, false) + CLUSTER_NAME, EDS_NAME, null, null, null, null, false, null) .lbPolicyConfig(getWrrLbConfigAsMap()); return cdsUpdate.parsedMetadata(parsedMetadata.build()).build(); } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 3650e5d5bb9..2c75ee04d91 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.fail; import com.github.udpa.udpa.type.v1.TypedStruct; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; @@ -140,7 +141,9 @@ import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; import io.grpc.xds.XdsClusterResource.CdsUpdate; +import io.grpc.xds.client.BackendMetricPropagation; import io.grpc.xds.client.Bootstrapper.ServerInfo; +import io.grpc.xds.client.LoadStatsManager2; import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsResourceType; import io.grpc.xds.client.XdsResourceType.ResourceInvalidException; @@ -2640,6 +2643,42 @@ public void parseNonAggregateCluster_withHttp11ProxyTransportSocket() throws Exc assertThat(result.isHttp11ProxyAvailable()).isTrue(); } + @Test + public void processCluster_parsesOrcaLrsPropagationMetrics() throws ResourceInvalidException { + LoadStatsManager2.isEnabledOrcaLrsPropagation = true; + + ImmutableList metricSpecs = ImmutableList.of( + "cpu_utilization", + "named_metrics.foo", + "unknown_metric_spec" + ); + Cluster cluster = Cluster.newBuilder() + .setName("cluster-orca.googleapis.com") + .setType(DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) + .setServiceName("service-orca.googleapis.com")) + .setLbPolicy(LbPolicy.ROUND_ROBIN) + .addAllLrsReportEndpointMetrics(metricSpecs) + .build(); + + CdsUpdate update = XdsClusterResource.processCluster( + cluster, null, LRS_SERVER_INFO, LoadBalancerRegistry.getDefaultRegistry()); + + BackendMetricPropagation propagationConfig = update.backendMetricPropagation(); + assertThat(propagationConfig).isNotNull(); + assertThat(propagationConfig.propagateCpuUtilization).isTrue(); + assertThat(propagationConfig.propagateMemUtilization).isFalse(); + assertThat(propagationConfig.shouldPropagateNamedMetric("foo")).isTrue(); + assertThat(propagationConfig.shouldPropagateNamedMetric("bar")).isFalse(); + assertThat(propagationConfig.shouldPropagateNamedMetric("unknown_metric_spec")) + .isFalse(); + + LoadStatsManager2.isEnabledOrcaLrsPropagation = false; + } + @Test public void parseServerSideListener_invalidTrafficDirection() throws ResourceInvalidException { Listener listener = diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java index b2b713e9a8e..ee32c4490af 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java @@ -154,7 +154,6 @@ public void lrsClientsStartedForLocalityStats() throws InterruptedException, Exe } } - /** * Assures that when an {@link XdsClient} is asked to add cluster locality stats it appropriately * starts {@link LoadReportClient}s to do that. diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index e80fcd008bd..6edd98f923e 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -1246,7 +1246,7 @@ private static void createAndDeliverClusterUpdates( FakeXdsClient xdsClient, String... clusterNames) { for (String clusterName : clusterNames) { CdsUpdate.Builder forEds = CdsUpdate - .forEds(clusterName, clusterName, null, null, null, null, false) + .forEds(clusterName, clusterName, null, null, null, null, false, null) .roundRobinLbPolicy(); xdsClient.deliverCdsUpdate(clusterName, forEds.build()); EdsUpdate edsUpdate = new EdsUpdate(clusterName, diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java index b1818445bea..63bd139c2cd 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -268,7 +268,7 @@ static XdsConfig getDefaultXdsConfig(String serverHostName) XdsEndpointResource.EdsUpdate edsUpdate = new XdsEndpointResource.EdsUpdate( EDS_NAME, lbEndpointsMap, Collections.emptyList()); XdsClusterResource.CdsUpdate cdsUpdate = XdsClusterResource.CdsUpdate.forEds( - CLUSTER_NAME, EDS_NAME, serverInfo, null, null, null, false) + CLUSTER_NAME, EDS_NAME, serverInfo, null, null, null, false, null) .lbPolicyConfig(getWrrLbConfigAsMap()).build(); XdsConfig.XdsClusterConfig clusterConfig = new XdsConfig.XdsClusterConfig( CLUSTER_NAME, cdsUpdate, new EndpointConfig(StatusOr.fromValue(edsUpdate))); diff --git a/xds/src/test/java/io/grpc/xds/client/BackendMetricPropagationTest.java b/xds/src/test/java/io/grpc/xds/client/BackendMetricPropagationTest.java new file mode 100644 index 00000000000..31ad6f9c47f --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/client/BackendMetricPropagationTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.client; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.asList; + +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@link BackendMetricPropagation}. + */ +@RunWith(JUnit4.class) +public class BackendMetricPropagationTest { + + @Test + public void fromMetricSpecs_nullInput() { + BackendMetricPropagation config = BackendMetricPropagation.fromMetricSpecs(null); + + assertThat(config.propagateCpuUtilization).isFalse(); + assertThat(config.propagateMemUtilization).isFalse(); + assertThat(config.propagateApplicationUtilization).isFalse(); + assertThat(config.shouldPropagateNamedMetric("any")).isFalse(); + } + + @Test + public void fromMetricSpecs_emptyInput() { + BackendMetricPropagation config = BackendMetricPropagation.fromMetricSpecs(ImmutableList.of()); + + assertThat(config.propagateCpuUtilization).isFalse(); + assertThat(config.propagateMemUtilization).isFalse(); + assertThat(config.propagateApplicationUtilization).isFalse(); + assertThat(config.shouldPropagateNamedMetric("any")).isFalse(); + } + + @Test + public void fromMetricSpecs_partialStandardMetrics() { + BackendMetricPropagation config = BackendMetricPropagation.fromMetricSpecs( + ImmutableList.of("cpu_utilization", "mem_utilization")); + + assertThat(config.propagateCpuUtilization).isTrue(); + assertThat(config.propagateMemUtilization).isTrue(); + assertThat(config.propagateApplicationUtilization).isFalse(); + assertThat(config.shouldPropagateNamedMetric("any")).isFalse(); + } + + @Test + public void fromMetricSpecs_allStandardMetrics() { + BackendMetricPropagation config = BackendMetricPropagation.fromMetricSpecs( + ImmutableList.of("cpu_utilization", "mem_utilization", "application_utilization")); + + assertThat(config.propagateCpuUtilization).isTrue(); + assertThat(config.propagateMemUtilization).isTrue(); + assertThat(config.propagateApplicationUtilization).isTrue(); + assertThat(config.shouldPropagateNamedMetric("any")).isFalse(); + } + + @Test + public void fromMetricSpecs_wildcardNamedMetrics() { + BackendMetricPropagation config = BackendMetricPropagation.fromMetricSpecs( + ImmutableList.of("named_metrics.*")); + + assertThat(config.propagateCpuUtilization).isFalse(); + assertThat(config.propagateMemUtilization).isFalse(); + assertThat(config.propagateApplicationUtilization).isFalse(); + assertThat(config.shouldPropagateNamedMetric("any_key")).isTrue(); + assertThat(config.shouldPropagateNamedMetric("another_key")).isTrue(); + } + + @Test + public void fromMetricSpecs_specificNamedMetrics() { + BackendMetricPropagation config = BackendMetricPropagation.fromMetricSpecs( + ImmutableList.of("named_metrics.foo", "named_metrics.bar")); + + assertThat(config.shouldPropagateNamedMetric("foo")).isTrue(); + assertThat(config.shouldPropagateNamedMetric("bar")).isTrue(); + assertThat(config.shouldPropagateNamedMetric("baz")).isFalse(); + assertThat(config.shouldPropagateNamedMetric("any")).isFalse(); + } + + @Test + public void fromMetricSpecs_mixedStandardAndNamed() { + BackendMetricPropagation config = BackendMetricPropagation.fromMetricSpecs( + ImmutableList.of("cpu_utilization", "named_metrics.foo", "named_metrics.bar")); + + assertThat(config.propagateCpuUtilization).isTrue(); + assertThat(config.propagateMemUtilization).isFalse(); + assertThat(config.shouldPropagateNamedMetric("foo")).isTrue(); + assertThat(config.shouldPropagateNamedMetric("bar")).isTrue(); + assertThat(config.shouldPropagateNamedMetric("baz")).isFalse(); + } + + @Test + public void fromMetricSpecs_wildcardAndSpecificNamedMetrics() { + BackendMetricPropagation config = BackendMetricPropagation.fromMetricSpecs( + ImmutableList.of("named_metrics.foo", "named_metrics.*")); + + assertThat(config.shouldPropagateNamedMetric("foo")).isTrue(); + assertThat(config.shouldPropagateNamedMetric("bar")).isTrue(); + assertThat(config.shouldPropagateNamedMetric("any_other_key")).isTrue(); + } + + @Test + public void fromMetricSpecs_malformedAndUnknownSpecs_areIgnored() { + BackendMetricPropagation config = BackendMetricPropagation.fromMetricSpecs( + asList( + "cpu_utilization", + null, // ignored + "disk_utilization", + "named_metrics.", // empty key + "named_metrics.valid" + )); + + assertThat(config.propagateCpuUtilization).isTrue(); + assertThat(config.propagateMemUtilization).isFalse(); + assertThat(config.shouldPropagateNamedMetric("disk_utilization")).isFalse(); + assertThat(config.shouldPropagateNamedMetric("valid")).isTrue(); + assertThat(config.shouldPropagateNamedMetric("")).isFalse(); // from the empty key + } + + @Test + public void fromMetricSpecs_duplicateSpecs_areHandledGracefully() { + BackendMetricPropagation config = BackendMetricPropagation.fromMetricSpecs( + ImmutableList.of( + "cpu_utilization", + "named_metrics.foo", + "cpu_utilization", + "named_metrics.foo")); + + assertThat(config.propagateCpuUtilization).isTrue(); + assertThat(config.shouldPropagateNamedMetric("foo")).isTrue(); + assertThat(config.shouldPropagateNamedMetric("bar")).isFalse(); + } +} diff --git a/xds/src/test/java/io/grpc/xds/client/LoadStatsManager2Test.java b/xds/src/test/java/io/grpc/xds/client/LoadStatsManager2Test.java index 9a90a92dcbd..a0642f7e4bb 100644 --- a/xds/src/test/java/io/grpc/xds/client/LoadStatsManager2Test.java +++ b/xds/src/test/java/io/grpc/xds/client/LoadStatsManager2Test.java @@ -27,6 +27,7 @@ import io.grpc.xds.client.Stats.ClusterStats; import io.grpc.xds.client.Stats.DroppedRequests; import io.grpc.xds.client.Stats.UpstreamLocalityStats; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -254,6 +255,59 @@ public void sharedLoadCounterStatsAggregation() { 2.718); } + @Test + public void recordMetrics_orcaLrsPropagationEnabled_specificMetrics() { + boolean originalVal = LoadStatsManager2.isEnabledOrcaLrsPropagation; + LoadStatsManager2.isEnabledOrcaLrsPropagation = true; + BackendMetricPropagation backendMetricPropagation = BackendMetricPropagation.fromMetricSpecs( + Arrays.asList("cpu_utilization", "named_metrics.named1")); + ClusterLocalityStats stats = loadStatsManager.getClusterLocalityStats( + CLUSTER_NAME1, EDS_SERVICE_NAME1, LOCALITY1, backendMetricPropagation); + + stats.recordTopLevelMetrics(0.8, 0.5, 0.0); // cpu, mem, app + stats.recordBackendLoadMetricStats(ImmutableMap.of("named1", 123.4, "named2", 567.8)); + stats.recordCallFinished(Status.OK); + ClusterStats report = Iterables.getOnlyElement( + loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)); + UpstreamLocalityStats localityStats = + Iterables.getOnlyElement(report.upstreamLocalityStatsList()); + + assertThat(localityStats.loadMetricStatsMap()).containsKey("cpu_utilization"); + assertThat(localityStats.loadMetricStatsMap().get("cpu_utilization").totalMetricValue()) + .isWithin(TOLERANCE).of(0.8); + assertThat(localityStats.loadMetricStatsMap()).doesNotContainKey("mem_utilization"); + assertThat(localityStats.loadMetricStatsMap()).containsKey("named_metrics.named1"); + assertThat(localityStats.loadMetricStatsMap().get("named_metrics.named1").totalMetricValue()) + .isWithin(TOLERANCE).of(123.4); + assertThat(localityStats.loadMetricStatsMap()).doesNotContainKey("named_metrics.named2"); + LoadStatsManager2.isEnabledOrcaLrsPropagation = originalVal; + } + + @Test + public void recordMetrics_orcaLrsPropagationEnabled_wildcardNamedMetrics() { + boolean originalVal = LoadStatsManager2.isEnabledOrcaLrsPropagation; + LoadStatsManager2.isEnabledOrcaLrsPropagation = true; + BackendMetricPropagation backendMetricPropagation = BackendMetricPropagation.fromMetricSpecs( + Arrays.asList("named_metrics.*")); + ClusterLocalityStats stats = loadStatsManager.getClusterLocalityStats( + CLUSTER_NAME1, EDS_SERVICE_NAME1, LOCALITY1, backendMetricPropagation); + + stats.recordBackendLoadMetricStats(ImmutableMap.of("named1", 123.4, "named2", 567.8)); + stats.recordCallFinished(Status.OK); + ClusterStats report = Iterables.getOnlyElement( + loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)); + UpstreamLocalityStats localityStats = + Iterables.getOnlyElement(report.upstreamLocalityStatsList()); + + assertThat(localityStats.loadMetricStatsMap()).containsKey("named_metrics.named1"); + assertThat(localityStats.loadMetricStatsMap().get("named_metrics.named1").totalMetricValue()) + .isWithin(TOLERANCE).of(123.4); + assertThat(localityStats.loadMetricStatsMap()).containsKey("named_metrics.named2"); + assertThat(localityStats.loadMetricStatsMap().get("named_metrics.named2").totalMetricValue()) + .isWithin(TOLERANCE).of(567.8); + LoadStatsManager2.isEnabledOrcaLrsPropagation = originalVal; + } + @Test public void loadCounterDelayedDeletionAfterAllInProgressRequestsReported() { ClusterLocalityStats counter = loadStatsManager.getClusterLocalityStats( From 7f0a19d41a4c2bb11afb01dbcece3703b9de4f18 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 14 Oct 2025 09:36:52 +0530 Subject: [PATCH 406/591] xds: Add documentation for /sni-test-certs and refactoring (#12415) --- .../resources/certs/sni-test-certs/README | 55 +++++++++++++++++++ .../bad_wildcard_dns_certificate.pem | 0 2 files changed, 55 insertions(+) create mode 100644 xds/src/test/resources/certs/sni-test-certs/README rename {testing/src/main => xds/src/test}/resources/certs/sni-test-certs/bad_wildcard_dns_certificate.pem (100%) diff --git a/xds/src/test/resources/certs/sni-test-certs/README b/xds/src/test/resources/certs/sni-test-certs/README new file mode 100644 index 00000000000..25e66021192 --- /dev/null +++ b/xds/src/test/resources/certs/sni-test-certs/README @@ -0,0 +1,55 @@ +Bad Wildcard DNS Certificate (bad_wildcard_dns_certificate.pem) +This certificate is used for testing SNI with invalid wildcard DNS SANs. It is issued by a custom, self-signed Certificate Authority (CA). + +1. Create the Certificate Authority (CA) +Create the CA's private key: +$ openssl genpkey -algorithm RSA -out ca.key -pkeyopt rsa_keygen_bits:2048 +Create the CA's self-signed certificate: +$ openssl req -x509 -new -nodes -key ca.key -sha256 -days 365 -out ca.pem -subj "/CN=My Internal CA" + +2. Generate the Server Certificate +Next, generate the server's private key and a Certificate Signing Request (CSR). +Create the server's private key: +$ openssl genpkey -algorithm RSA -out bad_wildcard_dns.key -pkeyopt rsa_keygen_bits:2048 +Create a configuration file named san.cnf with the following content. This file specifies the Subject Alternative Names (SANs) for the certificate. +[req] +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no + +[req_distinguished_name] +C = US +ST = Illinois +L = Chicago +O = "Example, Co." +CN = *.test.google.com + +[v3_req] +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = *.test.google.fr +DNS.2 = *.test.youtube.com +DNS.3 = waterzooi.test.google.be +DNS.4 = 192.168.1.3 +DNS.5 = *.TEST.YOUTUBE.com +DNS.6 = w*i.test.google.be +DNS.7 = w*a.test.google.be +DNS.8 = *.test.google.com.au +DNS.9 = *waterzooi +DNS.10 = *.lyft.com +DNS.11 = ly**ft.com +DNS.12 = *yft.c*m +DNS.13 = xn--*.lyft.com + +Create the Certificate Signing Request (CSR): +$ openssl req -new -key bad_wildcard_dns.key -out bad_wildcard_dns.csr -config san.cnf + +3. Sign the Server Certificate +Finally, use the CA to sign the CSR, which will create the server certificate. +$ openssl x509 -req -in bad_wildcard_dns.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out bad_wildcard_dns_certificate.pem -days 365 -sha256 -extensions v3_req -extfile san.cnf + +4. Clean Up +$ rm bad_wildcard_dns.key san.cnf bad_wildcard_dns.csr ca.key ca.pem ca.srl diff --git a/testing/src/main/resources/certs/sni-test-certs/bad_wildcard_dns_certificate.pem b/xds/src/test/resources/certs/sni-test-certs/bad_wildcard_dns_certificate.pem similarity index 100% rename from testing/src/main/resources/certs/sni-test-certs/bad_wildcard_dns_certificate.pem rename to xds/src/test/resources/certs/sni-test-certs/bad_wildcard_dns_certificate.pem From 4725ced9981e714c9745f22123e3c621342e0b70 Mon Sep 17 00:00:00 2001 From: HYUNSANG HAN Date: Wed, 15 Oct 2025 05:35:58 +0900 Subject: [PATCH 407/591] binder: Avoid potential deadlock when canceling AsyncSecurityPolicy futures (#12283) Move future cancellation outside of synchronized block in `BinderClientTransport.notifyTerminated()` to prevent deadlock if `AsyncSecurityPolicy` uses `directExecutor()` for callbacks. Fixes grpc#12190 --------- Signed-off-by: Hyunsang Han --- .../internal/BinderClientTransport.java | 20 +++++-------------- .../grpc/binder/internal/BinderTransport.java | 19 ++++++++++++++++++ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 6b68552722c..c2447daf751 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -87,13 +87,6 @@ public final class BinderClientTransport extends BinderTransport @GuardedBy("this") private ScheduledFuture readyTimeoutFuture; // != null iff timeout scheduled. - @GuardedBy("this") - @Nullable - private ListenableFuture authResultFuture; // null before we check auth. - - @GuardedBy("this") - @Nullable - private ListenableFuture preAuthResultFuture; // null before we pre-auth. /** * Constructs a new transport instance. @@ -193,7 +186,8 @@ private void preAuthorize(ServiceInfo serviceInfo) { // unauthorized server a chance to run, but the connection will still fail by SecurityPolicy // check later in handshake. Pre-auth remains effective at mitigating abuse because malware // can't typically control the exact timing of its installation. - preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid); + ListenableFuture preAuthResultFuture = + register(checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid)); Futures.addCallback( preAuthResultFuture, new FutureCallback() { @@ -314,12 +308,6 @@ void notifyTerminated() { readyTimeoutFuture.cancel(false); readyTimeoutFuture = null; } - if (preAuthResultFuture != null) { - preAuthResultFuture.cancel(false); // No effect if already complete. - } - if (authResultFuture != null) { - authResultFuture.cancel(false); // No effect if already complete. - } serviceBinding.unbind(); clientTransportListener.transportTerminated(); } @@ -339,7 +327,8 @@ protected void handleSetupTransport(Parcel parcel) { } else { restrictIncomingBinderToCallsFrom(remoteUid); attributes = setSecurityAttrs(attributes, remoteUid); - authResultFuture = checkServerAuthorizationAsync(remoteUid); + ListenableFuture authResultFuture = + register(checkServerAuthorizationAsync(remoteUid)); Futures.addCallback( authResultFuture, new FutureCallback() { @@ -398,6 +387,7 @@ protected void handlePingResponse(Parcel parcel) { pingTracker.onPingResponse(parcel.readInt()); } + private static ClientStream newFailingClientStream( Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) { StatsTraceContext statsTraceContext = diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 904b7e83001..0f03d5fc9f2 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -45,9 +45,11 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.logging.Level; import java.util.logging.Logger; @@ -166,6 +168,9 @@ protected enum TransportState { @GuardedBy("this") private final LinkedHashSet callIdsToNotifyWhenReady = new LinkedHashSet<>(); + @GuardedBy("this") + private final List> ownedFutures = new ArrayList<>(); // To cancel upon terminate. + @GuardedBy("this") protected Attributes attributes; @@ -249,6 +254,13 @@ void releaseExecutors() { executorServicePool.returnObject(scheduledExecutorService); } + // Registers the specified future for eventual safe cancellation upon shutdown/terminate. + @GuardedBy("this") + protected final > T register(T future) { + ownedFutures.add(future); + return future; + } + @GuardedBy("this") boolean inState(TransportState transportState) { return this.transportState == transportState; @@ -299,6 +311,8 @@ final void shutdownInternal(Status shutdownStatus, boolean forceTerminate) { sendShutdownTransaction(); ArrayList> calls = new ArrayList<>(ongoingCalls.values()); ongoingCalls.clear(); + ArrayList> futuresToCancel = new ArrayList<>(ownedFutures); + ownedFutures.clear(); scheduledExecutorService.execute( () -> { for (Inbound inbound : calls) { @@ -310,6 +324,11 @@ final void shutdownInternal(Status shutdownStatus, boolean forceTerminate) { notifyTerminated(); } releaseExecutors(); + + for (Future future : futuresToCancel) { + // Not holding any locks here just in case some listener runs on a direct Executor. + future.cancel(false); // No effect if already isDone(). + } }); } } From f0a606710bc87da8848225d20710044c34fa4780 Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Wed, 15 Oct 2025 17:41:45 +0530 Subject: [PATCH 408/591] Bump readme (#12410) to reference 1.76.0 --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d696164ee32..c25fc73934f 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.75.0/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.75.0/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.76.0/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.76.0/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,34 +56,34 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.75.0 + 1.76.0 runtime io.grpc grpc-protobuf - 1.75.0 + 1.76.0 io.grpc grpc-stub - 1.75.0 + 1.76.0 ``` Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.75.0' -implementation 'io.grpc:grpc-protobuf:1.75.0' -implementation 'io.grpc:grpc-stub:1.75.0' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.76.0' +implementation 'io.grpc:grpc-protobuf:1.76.0' +implementation 'io.grpc:grpc-stub:1.76.0' ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.75.0' -implementation 'io.grpc:grpc-protobuf-lite:1.75.0' -implementation 'io.grpc:grpc-stub:1.75.0' +implementation 'io.grpc:grpc-okhttp:1.76.0' +implementation 'io.grpc:grpc-protobuf-lite:1.76.0' +implementation 'io.grpc:grpc-stub:1.76.0' ``` For [Bazel](https://bazel.build), you can either @@ -91,7 +91,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.75.0 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.76.0 Development snapshots are available in [Sonatypes's snapshot repository](https://central.sonatype.com/repository/maven-snapshots/). @@ -121,9 +121,9 @@ For protobuf-based codegen integrated with the Maven build system, you can use protobuf-maven-plugin 0.6.1 - com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier} + com.google.protobuf:protoc:3.25.8:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.75.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.76.0:exe:${os.detected.classifier} @@ -149,11 +149,11 @@ plugins { protobuf { protoc { - artifact = "com.google.protobuf:protoc:3.25.5" + artifact = "com.google.protobuf:protoc:3.25.8" } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0' } } generateProtoTasks { @@ -182,11 +182,11 @@ plugins { protobuf { protoc { - artifact = "com.google.protobuf:protoc:3.25.5" + artifact = "com.google.protobuf:protoc:3.25.8" } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0' } } generateProtoTasks { From 53393e06f3adfcecf929caf3b571f1e4ec46ae12 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Fri, 17 Oct 2025 09:53:22 +0530 Subject: [PATCH 409/591] xds: use UNKNOWN for auth algorithm type during per-rpc authority verification (#12421) While we can get the cipher suite name with `sslEngine.getHandshakeSession().getCipherSuite()`, for the `authType` to use in `X509ExtendedTrustManager.checkServerTrusted` it needs to go through a mapping, for example, for the cipher suite name "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" the `authType` to use is actually `ECDHE_RSA`. (JDK code maintains such a [mapping](https://github.com/openjdk/jdk/blob/844118a9d854459778f88d299b148c2288131344/src/java.base/share/classes/sun/security/ssl/CipherSuite.java#L113)). Since we don't have all this information handy to use, and UNKNOWN for `authType` works and has actually been observed being used during Tls handshake, we are using the same during the per-rpc authority verification check as the Tls connection has already been established by then. --- netty/src/main/java/io/grpc/netty/X509AuthorityVerifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netty/src/main/java/io/grpc/netty/X509AuthorityVerifier.java b/netty/src/main/java/io/grpc/netty/X509AuthorityVerifier.java index 8a8d426662f..a2df3dbc431 100644 --- a/netty/src/main/java/io/grpc/netty/X509AuthorityVerifier.java +++ b/netty/src/main/java/io/grpc/netty/X509AuthorityVerifier.java @@ -103,6 +103,6 @@ private void verifyAuthorityAllowedForPeerCert(String authority) throw new IllegalStateException("checkServerTrustedMethod not found"); } checkServerTrustedMethod.invoke( - x509ExtendedTrustManager, x509PeerCertificates, "RSA", sslEngineWrapper); + x509ExtendedTrustManager, x509PeerCertificates, "UNKNOWN", sslEngineWrapper); } } From 559e3ba41d7ab8798a52cdd6bdd7cf762c6739d5 Mon Sep 17 00:00:00 2001 From: Gregory Cooke Date: Fri, 17 Oct 2025 05:17:00 -0400 Subject: [PATCH 410/591] internal: Allow EC Keys in SPIFFE Bundle Map parsing (#12399) SPIFFE Bundle Map parsing was originally implemented to only support RSA keys. It should also support EC keys. --- .../java/io/grpc/internal/SpiffeUtil.java | 13 +- .../java/io/grpc/internal/SpiffeUtilTest.java | 19 ++- .../io/grpc/internal/spiffebundle_ec.json | 116 ++++++++++++++++++ 3 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 core/src/test/resources/io/grpc/internal/spiffebundle_ec.json diff --git a/core/src/main/java/io/grpc/internal/SpiffeUtil.java b/core/src/main/java/io/grpc/internal/SpiffeUtil.java index 44ef343b6dc..9eafc9950e2 100644 --- a/core/src/main/java/io/grpc/internal/SpiffeUtil.java +++ b/core/src/main/java/io/grpc/internal/SpiffeUtil.java @@ -23,6 +23,7 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.io.Files; import java.io.ByteArrayInputStream; import java.io.File; @@ -51,7 +52,7 @@ public final class SpiffeUtil { private static final Integer URI_SAN_TYPE = 6; private static final String USE_PARAMETER_VALUE = "x509-svid"; - private static final String KTY_PARAMETER_VALUE = "RSA"; + private static final ImmutableSet KTY_PARAMETER_VALUES = ImmutableSet.of("RSA", "EC"); private static final String CERTIFICATE_PREFIX = "-----BEGIN CERTIFICATE-----\n"; private static final String CERTIFICATE_SUFFIX = "-----END CERTIFICATE-----"; private static final String PREFIX = "spiffe://"; @@ -205,10 +206,12 @@ public static SpiffeBundle loadTrustBundleFromFile(String trustBundleFile) throw private static void checkJwkEntry(Map jwkNode, String trustDomainName) { String kty = JsonUtil.getString(jwkNode, "kty"); - if (kty == null || !kty.equals(KTY_PARAMETER_VALUE)) { - throw new IllegalArgumentException(String.format("'kty' parameter must be '%s' but '%s' " - + "found. Certificate loading for trust domain '%s' failed.", KTY_PARAMETER_VALUE, - kty, trustDomainName)); + if (kty == null || !KTY_PARAMETER_VALUES.contains(kty)) { + throw new IllegalArgumentException( + String.format( + "'kty' parameter must be one of %s but '%s' " + + "found. Certificate loading for trust domain '%s' failed.", + KTY_PARAMETER_VALUES, kty, trustDomainName)); } if (jwkNode.containsKey("kid")) { throw new IllegalArgumentException(String.format("'kid' parameter must not be set. " diff --git a/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java b/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java index d5155728936..57824cf207f 100644 --- a/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java +++ b/core/src/test/java/io/grpc/internal/SpiffeUtilTest.java @@ -218,6 +218,7 @@ public static class CertificateApiTest { private static final String SERVER_0_PEM_FILE = "server0.pem"; private static final String TEST_DIRECTORY_PREFIX = "io/grpc/internal/"; private static final String SPIFFE_TRUST_BUNDLE = "spiffebundle.json"; + private static final String SPIFFE_TRUST_BUNDLE_WITH_EC_KTY = "spiffebundle_ec.json"; private static final String SPIFFE_TRUST_BUNDLE_MALFORMED = "spiffebundle_malformed.json"; private static final String SPIFFE_TRUST_BUNDLE_CORRUPTED_CERT = "spiffebundle_corrupted_cert.json"; @@ -311,6 +312,21 @@ public void loadTrustBundleFromFileSuccessTest() throws Exception { .toArray(new X509Certificate[0])); assertTrue(spiffeId.isPresent()); assertEquals("foo.bar.com", spiffeId.get().getTrustDomain()); + + SpiffeBundle tb_ec = SpiffeUtil.loadTrustBundleFromFile( + copyFileToTmp(SPIFFE_TRUST_BUNDLE_WITH_EC_KTY)); + assertEquals(2, tb_ec.getSequenceNumbers().size()); + assertEquals(12035488L, (long) tb_ec.getSequenceNumbers().get("example.com")); + assertEquals(-1L, (long) tb_ec.getSequenceNumbers().get("test.example.com")); + assertEquals(3, tb_ec.getBundleMap().size()); + assertEquals(0, tb_ec.getBundleMap().get("test.google.com.au").size()); + assertEquals(1, tb_ec.getBundleMap().get("example.com").size()); + assertEquals(2, tb_ec.getBundleMap().get("test.example.com").size()); + Optional spiffeId_ec = + SpiffeUtil.extractSpiffeId(tb_ec.getBundleMap().get("example.com") + .toArray(new X509Certificate[0])); + assertTrue(spiffeId_ec.isPresent()); + assertEquals("foo.bar.com", spiffeId_ec.get().getTrustDomain()); } @Test @@ -338,7 +354,8 @@ public void loadTrustBundleFromFileFailureTest() { // Check the exception if 'kty' value differs from 'RSA' iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil .loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_KTY))); - assertEquals("'kty' parameter must be 'RSA' but 'null' found." + DOMAIN_ERROR_MESSAGE, + assertEquals( + "'kty' parameter must be one of [RSA, EC] but 'null' found." + DOMAIN_ERROR_MESSAGE, iae.getMessage()); // Check the exception if 'kid' has a value iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil diff --git a/core/src/test/resources/io/grpc/internal/spiffebundle_ec.json b/core/src/test/resources/io/grpc/internal/spiffebundle_ec.json new file mode 100644 index 00000000000..1732310f8cf --- /dev/null +++ b/core/src/test/resources/io/grpc/internal/spiffebundle_ec.json @@ -0,0 +1,116 @@ +{ + "trust_domains": { + "test.google.com.au": {}, + "example.com": { + "spiffe_sequence": 12035488, + "keys": [ + { + + "kty": "EC", + "use": "x509-svid", + "x5c": ["MIIFsjCCA5qgAwIBAgIURygVMMzdr+Q7rsUaz189JozyHMwwDQYJKoZIhvcNAQEL + BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL + BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMTEyMjMxODQy + NTJaFw0zMTEyMjExODQyNTJaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM + MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu + dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ4AqpGetyVSqGUuBJ + LVFla+7bEfca7UYzfVSSZLZ/X+JDmWIVN8UIPuFib5jhMEc3XaUnFXUmM7zEtz/Z + G5hapwLwOb2C3ZxOP6PQjYCJxbkLie+b43UQrFu1xxd3vMhVJgcj/AIxEpmszuqO + a6kUrkYifjJADQ+64kZgl66bsTdXMCzpxyFl9xUfff59L8OX+HUfAcoZz3emjg3Z + JPYURQEmjdZTOau1EjFilwHgd989Jt7NKgx30NXoHmw7nusVBIY94fL2VKN3f1XV + m0dHu5NI279Q6zr0ZBU7k5T3IeHnzsUesQS4NGlklDWoVTKk73Uv9Pna8yQsSW75 + 7PEbHOGp9Knu4bnoGPOlsG81yIPipO6hTgGFK24pF97M9kpGbWqYX4+2vLlrCAfc + msHqaUPmQlYeRVTT6vw7ctYo2kyUYGtnODXk76LqewRBVvkzx75QUhfjAyb740Yc + DmIenc56Tq6gebJHjhEmVSehR6xIpXP7SVeurTyhPsEQnpJHtgs4dcwWOZp7BvPN + zHXmJqfr7vsshie3vS5kQ0u1e1yqAqXgyDjqKXOkx+dpgUTehSJHhPNHvTc5LXRs + vvXKYz6FrwR/DZ8t7BNEvPeLjFgxpH7QVJFLCvCbXs5K6yYbsnLfxFIBPRnrbJkI + sK+sQwnRdnsiUdPsTkG5B2lQfQIDAQABo4GHMIGEMB0GA1UdDgQWBBQ2lBp0PiRH + HvQ5IRURm8aHsj4RETAfBgNVHSMEGDAWgBQ2lBp0PiRHHvQ5IRURm8aHsj4RETAP + BgNVHRMBAf8EBTADAQH/MDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29t + L2NsaWVudC93b3JrbG9hZC8xMA0GCSqGSIb3DQEBCwUAA4ICAQA1mSkgRclAl+E/ + aS9zJ7t8+Y4n3T24nOKKveSIjxXm/zjhWqVsLYBI6kglWtih2+PELvU8JdPqNZK3 + 4Kl0Q6FWpVSGDdWN1i6NyORt2ocggL3ke3iXxRk3UpUKJmqwz81VhA2KUHnMlyE0 + IufFfZNwNWWHBv13uJfRbjeQpKPhU+yf4DeXrsWcvrZlGvAET+mcplafUzCp7Iv+ + PcISJtUerbxbVtuHVeZCLlgDXWkLAWJN8rf0dIG4x060LJ+j6j9uRVhb9sZn1HJV + +j4XdIYm1VKilluhOtNwP2d3Ox/JuTBxf7hFHXZPfMagQE5k5PzmxRaCAEMJ1l2D + vUbZw+shJfSNoWcBo2qadnUaWT3BmmJRBDh7ZReib/RQ1Rd4ygOyzP3E0vkV4/gq + yjLdApXh5PZP8KLQZ+1JN/sdWt7VfIt9wYOpkIqujdll51ESHzwQeAK9WVCB4UvV + z6zdhItB9CRbXPreWC+wCB1xDovIzFKOVsLs5+Gqs1m7VinG2LxbDqaKyo/FB0Hx + x0acBNzezLWoDwXYQrN0T0S4pnqhKD1CYPpdArBkNezUYAjS725FkApuK+mnBX3U + 0msBffEaUEOkcyar1EW2m/33vpetD/k3eQQkmvQf4Hbiu9AF+9cNDm/hMuXEw5EX + GA91fn0891b5eEW8BJHXX0jri0aN8g=="], + "n": "", + "e": "AQAB" + } + ] + }, + "test.example.com": { + "keys": [ + { + "kty": "RSA", + "use": "x509-svid", + "x5c": ["MIIFsjCCA5qgAwIBAgIURygVMMzdr+Q7rsUaz189JozyHMwwDQYJKoZIhvcNAQEL + BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL + BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMTEyMjMxODQy + NTJaFw0zMTEyMjExODQyNTJaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM + MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu + dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ4AqpGetyVSqGUuBJ + LVFla+7bEfca7UYzfVSSZLZ/X+JDmWIVN8UIPuFib5jhMEc3XaUnFXUmM7zEtz/Z + G5hapwLwOb2C3ZxOP6PQjYCJxbkLie+b43UQrFu1xxd3vMhVJgcj/AIxEpmszuqO + a6kUrkYifjJADQ+64kZgl66bsTdXMCzpxyFl9xUfff59L8OX+HUfAcoZz3emjg3Z + JPYURQEmjdZTOau1EjFilwHgd989Jt7NKgx30NXoHmw7nusVBIY94fL2VKN3f1XV + m0dHu5NI279Q6zr0ZBU7k5T3IeHnzsUesQS4NGlklDWoVTKk73Uv9Pna8yQsSW75 + 7PEbHOGp9Knu4bnoGPOlsG81yIPipO6hTgGFK24pF97M9kpGbWqYX4+2vLlrCAfc + msHqaUPmQlYeRVTT6vw7ctYo2kyUYGtnODXk76LqewRBVvkzx75QUhfjAyb740Yc + DmIenc56Tq6gebJHjhEmVSehR6xIpXP7SVeurTyhPsEQnpJHtgs4dcwWOZp7BvPN + zHXmJqfr7vsshie3vS5kQ0u1e1yqAqXgyDjqKXOkx+dpgUTehSJHhPNHvTc5LXRs + vvXKYz6FrwR/DZ8t7BNEvPeLjFgxpH7QVJFLCvCbXs5K6yYbsnLfxFIBPRnrbJkI + sK+sQwnRdnsiUdPsTkG5B2lQfQIDAQABo4GHMIGEMB0GA1UdDgQWBBQ2lBp0PiRH + HvQ5IRURm8aHsj4RETAfBgNVHSMEGDAWgBQ2lBp0PiRHHvQ5IRURm8aHsj4RETAP + BgNVHRMBAf8EBTADAQH/MDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29t + L2NsaWVudC93b3JrbG9hZC8xMA0GCSqGSIb3DQEBCwUAA4ICAQA1mSkgRclAl+E/ + aS9zJ7t8+Y4n3T24nOKKveSIjxXm/zjhWqVsLYBI6kglWtih2+PELvU8JdPqNZK3 + 4Kl0Q6FWpVSGDdWN1i6NyORt2ocggL3ke3iXxRk3UpUKJmqwz81VhA2KUHnMlyE0 + IufFfZNwNWWHBv13uJfRbjeQpKPhU+yf4DeXrsWcvrZlGvAET+mcplafUzCp7Iv+ + PcISJtUerbxbVtuHVeZCLlgDXWkLAWJN8rf0dIG4x060LJ+j6j9uRVhb9sZn1HJV + +j4XdIYm1VKilluhOtNwP2d3Ox/JuTBxf7hFHXZPfMagQE5k5PzmxRaCAEMJ1l2D + vUbZw+shJfSNoWcBo2qadnUaWT3BmmJRBDh7ZReib/RQ1Rd4ygOyzP3E0vkV4/gq + yjLdApXh5PZP8KLQZ+1JN/sdWt7VfIt9wYOpkIqujdll51ESHzwQeAK9WVCB4UvV + z6zdhItB9CRbXPreWC+wCB1xDovIzFKOVsLs5+Gqs1m7VinG2LxbDqaKyo/FB0Hx + x0acBNzezLWoDwXYQrN0T0S4pnqhKD1CYPpdArBkNezUYAjS725FkApuK+mnBX3U + 0msBffEaUEOkcyar1EW2m/33vpetD/k3eQQkmvQf4Hbiu9AF+9cNDm/hMuXEw5EX + GA91fn0891b5eEW8BJHXX0jri0aN8g=="], + "n": "", + "e": "AQAB" + }, + { + "kty": "RSA", + "use": "x509-svid", + "x5c": ["MIIELTCCAxWgAwIBAgIUVXGlXjNENtOZbI12epjgIhMaShEwDQYJKoZIhvcNAQEL + BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM + GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTI0 + MDkxNzE2MTk0NFoXDTM0MDkxNTE2MTk0NFowTjELMAkGA1UEBhMCVVMxCzAJBgNV + BAgMAkNBMQwwCgYDVQQHDANTVkwxDTALBgNVBAoMBGdSUEMxFTATBgNVBAMMDHRl + c3QtY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOcTjjcS + SfG/EGrr6G+f+3T2GXyHHfroQFi9mZUz80L7uKBdECOImID+YhoK8vcxLQjPmEEv + FIYgJT5amugDcYIgUhMjBx/8RPJaP/nGmBngAqsuuNCaZfyaHBRqN8XdS/AwmsI5 + Wo+nru0+0/7aQFdqqtd2+e9dHjUWwgHxXvMgC4hkHpsdCGIZWVzWyBliwTYQYb1Y + yYe1LzqqQA5OMbZfKOY9MYDCEYOliRiunOn30iIOHj9V5qLzWGfSyxCRuvLRdEP8 + iDeNweHbdaKuI80nQmxuBdRIspE9k5sD1WA4vLZpeg3zggxp4rfLL5zBJgb/33D3 + d9Rkm14xfDPihhkCAwEAAaOB+jCB9zBZBgNVHREEUjBQhiZzcGlmZmU6Ly9mb28u + YmFyLmNvbS9jbGllbnQvd29ya2xvYWQvMYYmc3BpZmZlOi8vZm9vLmJhci5jb20v + Y2xpZW50L3dvcmtsb2FkLzIwHQYDVR0OBBYEFG9GkBgdBg/p0U9/lXv8zIJ+2c2N + MHsGA1UdIwR0MHKhWqRYMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0 + YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMM + BnRlc3RjYYIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQELBQADggEB + AJ4Cbxv+02SpUgkEu4hP/1+8DtSBXUxNxI0VG4e3Ap2+Rhjm3YiFeS/UeaZhNrrw + UEjkSTPFODyXR7wI7UO9OO1StyD6CMkp3SEvevU5JsZtGL6mTiTLTi3Qkywa91Bt + GlyZdVMghA1bBJLBMwiD5VT5noqoJBD7hDy6v9yNmt1Sw2iYBJPqI3Gnf5bMjR3s + UICaxmFyqaMCZsPkfJh0DmZpInGJys3m4QqGz6ZE2DWgcSr1r/ML7/5bSPjjr8j4 + WFFSqFR3dMu8CbGnfZTCTXa4GTX/rARXbAO67Z/oJbJBK7VKayskL+PzKuohb9ox + jGL772hQMbwtFCOFXu5VP0s="] + } + ] + } + } +} \ No newline at end of file From 9662f0fdccf1cb93a056236b0c93ca4bc4adfbf5 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 16 Oct 2025 16:07:21 -0700 Subject: [PATCH 411/591] buildscripts: Convert GAE CI to Cloud Build The Google App Engine build now requires Java 17, because the App Engine libraries are now using Java 17 bytecode. The Kokoro environment doesn't include Java 17, and while we could make some custom pools to resolve it, it is easier to swap to Cloud Build than to fight and maintain the Kokoro images. With Cloud Build we can also restrict permissions easier, as the same workers aren't used for multiple tasks. However, the Gradle App Engine plugin doesn't support choosing a service account for GAE, so I swapped to using gcloud app deploy. Although we'll be using restricted service accounts, we'll configure Cloud Build to require a "/gcbrun" GitHub comment except for owners and collaborators of the repository, similar to the "kokoro:run" label today. I swapped the Gradle code to use project properties instead of system properties, as we really should have been using project properties to begin with and I didn't want to add new system properties. The sleep has probably been unnecessary since the turndown of GAE Java 7, when the architecture of GAE changed considerably. But today it is very possible a new instance is spun up for that request and GAE does a warmup request, so the delay seems unlikely to help anything and was excessive at 20 seconds. The Cloud Build file _doesn't_ include GAE in its name because it can do more than GAE testing and it is easy to run things in parallel in Cloud Build (although they share the worker). In particular, some of the Android tests may make sense to migrate away from Kokoro. We're using e2-standard-16 for Kokoro and it takes about 10 minutes. With the default Cloud Build worker e2-standard-2 it takes 20 minutes, and with e2-highcpu-8 it takes 10 minutes with 4 minutes spent on app deploy. The expectation is to run this with a Java-CI-specific service account, so we have configure logging ourselves. I chose CLOUD_LOGGING_ONLY because it was easy, but we'll want to configure GCS in the future to allow external contributors to see the logs. --- buildscripts/cloudbuild-testing.yaml | 64 +++++++++++++++++++++++ buildscripts/gae-build/Dockerfile | 10 ++++ buildscripts/kokoro/gae-interop.sh | 55 ------------------- gae-interop-testing/gae-jdk8/build.gradle | 20 +++---- 4 files changed, 85 insertions(+), 64 deletions(-) create mode 100644 buildscripts/cloudbuild-testing.yaml create mode 100644 buildscripts/gae-build/Dockerfile delete mode 100755 buildscripts/kokoro/gae-interop.sh diff --git a/buildscripts/cloudbuild-testing.yaml b/buildscripts/cloudbuild-testing.yaml new file mode 100644 index 00000000000..623b85b6882 --- /dev/null +++ b/buildscripts/cloudbuild-testing.yaml @@ -0,0 +1,64 @@ +substitutions: + _GAE_SERVICE_ACCOUNT: appengine-testing-java@grpc-testing.iam.gserviceaccount.com +options: + env: + - BUILD_ID=$BUILD_ID + - KOKORO_GAE_SERVICE=java-gae-interop-test + - DUMMY_DEFAULT_VERSION=dummy-default + - GRADLE_OPTS=-Dorg.gradle.jvmargs='-Xmx1g' + - GRADLE_FLAGS=-PskipCodegen=true -PskipAndroid=true + logging: CLOUD_LOGGING_ONLY + machineType: E2_HIGHCPU_8 + +steps: +- id: clean-stale-deploys + name: gcr.io/cloud-builders/gcloud + allowFailure: true + script: | + #!/usr/bin/env bash + set -e + echo "Cleaning out stale deploys from previous runs, it is ok if this part fails" + # If the test fails, the deployment is leaked. + # Delete all versions whose name is not 'dummy-default' and is older than 1 hour. + # This expression is an ISO8601 relative date: + # https://cloud.google.com/sdk/gcloud/reference/topic/datetimes + (gcloud app versions list --format="get(version.id)" \ + --filter="service=$KOKORO_GAE_SERVICE AND NOT version : '$DUMMY_DEFAULT_VERSION' AND version.createTime<'-p1h'" \ + | xargs -i gcloud app services delete "$KOKORO_GAE_SERVICE" --version {} --quiet) || true + +- name: gcr.io/cloud-builders/docker + args: ['build', '-t', 'gae-build', 'buildscripts/gae-build/'] + +- id: build + name: gae-build + script: | + #!/usr/bin/env bash + exec ./gradlew $GRADLE_FLAGS :grpc-gae-interop-testing-jdk8:appengineStage + +- id: deploy + name: gcr.io/cloud-builders/gcloud + args: + - app + - deploy + - gae-interop-testing/gae-jdk8/build/staged-app/app.yaml + - --service-account=$_GAE_SERVICE_ACCOUNT + - --no-promote + - --no-stop-previous-version + - --version=cb-$BUILD_ID + +- id: runInteropTestRemote + name: eclipse-temurin:17-jdk + env: + - PROJECT_ID=$PROJECT_ID + script: | + #!/usr/bin/env bash + exec ./gradlew $GRADLE_FLAGS --stacktrace -PgaeDeployVersion="cb-$BUILD_ID" \ + -PgaeProjectId="$PROJECT_ID" :grpc-gae-interop-testing-jdk8:runInteropTestRemote + +- id: cleanup + name: gcr.io/cloud-builders/gcloud + script: | + #!/usr/bin/env bash + set -e + echo "Performing cleanup now." + gcloud app services delete "$KOKORO_GAE_SERVICE" --version "cb-$BUILD_ID" --quiet diff --git a/buildscripts/gae-build/Dockerfile b/buildscripts/gae-build/Dockerfile new file mode 100644 index 00000000000..7e68b270801 --- /dev/null +++ b/buildscripts/gae-build/Dockerfile @@ -0,0 +1,10 @@ +FROM eclipse-temurin:17-jdk + +# The AppEngine Gradle plugin downloads and runs its own gcloud to get the .jar +# to link against, so we need Python even if we use gcloud deploy directly +# instead of using the plugin. +RUN export DEBIAN_FRONTEND=noninteractive && \ + apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends python3 && \ + rm -rf /var/lib/apt/lists/* diff --git a/buildscripts/kokoro/gae-interop.sh b/buildscripts/kokoro/gae-interop.sh deleted file mode 100755 index c4ce56cac52..00000000000 --- a/buildscripts/kokoro/gae-interop.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -set -exu -o pipefail -if [[ -f /VERSION ]]; then - cat /VERSION -fi - -KOKORO_GAE_SERVICE="java-gae-interop-test" - -# We deploy as different versions of a single service, this way any stale -# lingering deploys can be easily cleaned up by purging all running versions -# of this service. -KOKORO_GAE_APP_VERSION=$(hostname) - -# A dummy version that can be the recipient of all traffic, so that the kokoro test version can be -# set to 0 traffic. This is a requirement in order to delete it. -DUMMY_DEFAULT_VERSION='dummy-default' - -function cleanup() { - echo "Performing cleanup now." - gcloud app services delete $KOKORO_GAE_SERVICE --version $KOKORO_GAE_APP_VERSION --quiet -} -trap cleanup SIGHUP SIGINT SIGTERM EXIT - -readonly GRPC_JAVA_DIR="$(cd "$(dirname "$0")"/../.. && pwd)" -cd "$GRPC_JAVA_DIR" - -## -## Deploy the dummy 'default' version of the service -## -GRADLE_FLAGS="--stacktrace -DgaeStopPreviousVersion=false -PskipCodegen=true -PskipAndroid=true" -export GRADLE_OPTS="-Dorg.gradle.jvmargs='-Xmx1g'" - -# Deploy the dummy 'default' version. We only require that it exists when cleanup() is called. -# It ok if we race with another run and fail here, because the end result is idempotent. -set +e -if ! gcloud app versions describe "$DUMMY_DEFAULT_VERSION" --service="$KOKORO_GAE_SERVICE"; then - ./gradlew $GRADLE_FLAGS -DgaeDeployVersion="$DUMMY_DEFAULT_VERSION" -DgaePromote=true :grpc-gae-interop-testing-jdk8:appengineDeploy -else - echo "default version already exists: $DUMMY_DEFAULT_VERSION" -fi -set -e - -# Deploy and test the real app (jdk8) -./gradlew $GRADLE_FLAGS -DgaeDeployVersion="$KOKORO_GAE_APP_VERSION" :grpc-gae-interop-testing-jdk8:runInteropTestRemote - -set +e -echo "Cleaning out stale deploys from previous runs, it is ok if this part fails" - -# Sometimes the trap based cleanup fails. -# Delete all versions whose name is not 'dummy-default' and is older than 1 hour. -# This expression is an ISO8601 relative date: -# https://cloud.google.com/sdk/gcloud/reference/topic/datetimes -gcloud app versions list --format="get(version.id)" --filter="service=$KOKORO_GAE_SERVICE AND NOT version : 'dummy-default' AND version.createTime<'-p1h'" | xargs -i gcloud app services delete "$KOKORO_GAE_SERVICE" --version {} --quiet -exit 0 diff --git a/gae-interop-testing/gae-jdk8/build.gradle b/gae-interop-testing/gae-jdk8/build.gradle index 14abbc05a9b..07033f403de 100644 --- a/gae-interop-testing/gae-jdk8/build.gradle +++ b/gae-interop-testing/gae-jdk8/build.gradle @@ -58,6 +58,7 @@ def createDefaultVersion() { return new java.text.SimpleDateFormat("yyyyMMdd't'HHmmss").format(new Date()) } +def nonShadowedProject = project // [START model] appengine { // App Engine tasks configuration @@ -67,13 +68,13 @@ appengine { deploy { // deploy configuration - projectId = 'GCLOUD_CONFIG' + projectId = nonShadowedProject.findProperty('gaeProjectId') ?: 'GCLOUD_CONFIG' // default - stop the current version - stopPreviousVersion = System.getProperty('gaeStopPreviousVersion') ?: true + stopPreviousVersion = nonShadowedProject.findProperty('gaeStopPreviousVersion') ?: true // default - do not make this the promoted version - promote = System.getProperty('gaePromote') ?: false - // Use -DgaeDeployVersion if set, otherwise the version is null and the plugin will generate it - version = System.getProperty('gaeDeployVersion', createDefaultVersion()) + promote = nonShadowedProject.findProperty('gaePromote') ?: false + // Use -PgaeDeployVersion if set, otherwise the version is null and the plugin will generate it + version = nonShadowedProject.findProperty('gaeDeployVersion') ?: createDefaultVersion() } } // [END model] @@ -83,6 +84,10 @@ version = '1.0-SNAPSHOT' // Version in generated output /** Returns the service name. */ String getGaeProject() { + def configuredProjectId = appengine.deploy.projectId + if (!"GCLOUD_CONFIG".equals(configuredProjectId)) { + return configuredProjectId + } def stream = new ByteArrayOutputStream() exec { executable 'gcloud' @@ -110,11 +115,8 @@ String getAppUrl(String project, String service, String version) { } tasks.register("runInteropTestRemote") { - dependsOn appengineDeploy + mustRunAfter appengineDeploy doLast { - // give remote app some time to settle down - sleep(20000) - def appUrl = getAppUrl( getGaeProject(), getService(project.getProjectDir().toPath()), From ab20a12c50558d3c84f6371f740589ba2b68f642 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Mon, 20 Oct 2025 21:48:11 -0700 Subject: [PATCH 412/591] Cancel owned Futures *before* declaring termination. (#12426) Fixes BinderClientTransportTest#testAsyncSecurityPolicyCancelledUponExternalTermination and others which have been flaky since #12283. @HyunSangHan --- .../java/io/grpc/binder/internal/BinderTransport.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 0f03d5fc9f2..a5b8d94ba0e 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -320,15 +320,16 @@ final void shutdownInternal(Status shutdownStatus, boolean forceTerminate) { inbound.closeAbnormal(shutdownStatus); } } - synchronized (this) { - notifyTerminated(); - } - releaseExecutors(); - + for (Future future : futuresToCancel) { // Not holding any locks here just in case some listener runs on a direct Executor. future.cancel(false); // No effect if already isDone(). } + + synchronized (this) { + notifyTerminated(); + } + releaseExecutors(); }); } } From 7ea47445be44f625f2d37c20172da716f5f1e2f6 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 21 Oct 2025 21:38:01 +0530 Subject: [PATCH 413/591] xds: Introduce flag for fallback to use the xds channel authority if no SNI is determined to be used. (#12422) This is to allow the previous behavior if needed, and when the xds channel authority is used as the SNI, it won't be used for the SAN validation. --- .../io/grpc/netty/ProtocolNegotiators.java | 3 --- .../security/DynamicSslContextProvider.java | 5 +++++ .../security/SecurityProtocolNegotiators.java | 20 ++++++++++++++----- .../security/SslContextProviderSupplier.java | 5 ++++- .../CertProviderClientSslContextProvider.java | 6 ++++-- .../security/trust/CertificateUtils.java | 2 ++ .../XdsClientWrapperForServerSdsTestMisc.java | 2 +- .../SecurityProtocolNegotiatorsTest.java | 6 +++--- .../SslContextProviderSupplierTest.java | 10 +++++----- 9 files changed, 39 insertions(+), 20 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java index 59e7e96b4a5..d30a6292d38 100644 --- a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java @@ -634,9 +634,6 @@ static final class ClientTlsHandler extends ProtocolNegotiationHandler { X509TrustManager x509TrustManager) { super(next, negotiationLogger); this.sslContext = Preconditions.checkNotNull(sslContext, "sslContext"); - // TODO: For empty authority and fallback flag - // GRPC_USE_CHANNEL_AUTHORITY_IF_NO_SNI_APPLICABLE present, we should parse authority - // but prevent it from being used for SAN validation in the TrustManager. if (authority != null) { HostPort hostPort = parseAuthority(authority); this.host = hostPort.host; diff --git a/xds/src/main/java/io/grpc/xds/internal/security/DynamicSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/DynamicSslContextProvider.java index e7b27cd644a..59e114a89ff 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/DynamicSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/DynamicSslContextProvider.java @@ -44,6 +44,7 @@ public abstract class DynamicSslContextProvider extends SslContextProvider { @Nullable protected final CertificateValidationContext staticCertificateValidationContext; @Nullable protected AbstractMap.SimpleImmutableEntry sslContextAndTrustManager; + protected boolean autoSniSanValidationDoesNotApply; protected DynamicSslContextProvider( BaseTlsContext tlsContext, CertificateValidationContext staticCertValidationContext) { @@ -59,6 +60,10 @@ protected DynamicSslContextProvider( protected abstract CertificateValidationContext generateCertificateValidationContext(); + public void setAutoSniSanValidationDoesNotApply() { + autoSniSanValidationDoesNotApply = true; + } + /** Gets a server or client side SslContextBuilder. */ protected abstract AbstractMap.SimpleImmutableEntry getSslContextBuilderAndTrustManager( diff --git a/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java b/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java index 10e3a0bcda1..0da06b3a753 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java @@ -194,6 +194,7 @@ static final class ClientSecurityHandler private final GrpcHttp2ConnectionHandler grpcHandler; private final SslContextProviderSupplier sslContextProviderSupplier; private final String sni; + private final boolean autoSniSanValidationDoesNotApply; ClientSecurityHandler( GrpcHttp2ConnectionHandler grpcHandler, @@ -215,10 +216,19 @@ public void handlerAdded(ChannelHandlerContext ctx) throws Exception { EnvoyServerProtoData.BaseTlsContext tlsContext = sslContextProviderSupplier.getTlsContext(); UpstreamTlsContext upstreamTlsContext = ((UpstreamTlsContext) tlsContext); if (CertificateUtils.isXdsSniEnabled) { - sni = upstreamTlsContext.getAutoHostSni() && !Strings.isNullOrEmpty(endpointHostname) + String sniToUse = upstreamTlsContext.getAutoHostSni() + && !Strings.isNullOrEmpty(endpointHostname) ? endpointHostname : upstreamTlsContext.getSni(); + if (sniToUse.isEmpty() && CertificateUtils.useChannelAuthorityIfNoSniApplicable) { + sniToUse = grpcHandler.getAuthority(); + autoSniSanValidationDoesNotApply = true; + } else { + autoSniSanValidationDoesNotApply = false; + } + sni = sniToUse; } else { sni = grpcHandler.getAuthority(); + autoSniSanValidationDoesNotApply = false; } } @@ -260,8 +270,8 @@ public void updateSslContextAndExtendedX509TrustManager( public void onException(Throwable throwable) { ctx.fireExceptionCaught(throwable); } - } - ); + }, + autoSniSanValidationDoesNotApply); } @Override @@ -399,8 +409,8 @@ public void updateSslContextAndExtendedX509TrustManager( public void onException(Throwable throwable) { ctx.fireExceptionCaught(throwable); } - } - ); + }, + false); } } } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/SslContextProviderSupplier.java b/xds/src/main/java/io/grpc/xds/internal/security/SslContextProviderSupplier.java index 38ae15a88aa..e5960dd95e8 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/SslContextProviderSupplier.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/SslContextProviderSupplier.java @@ -55,12 +55,15 @@ public BaseTlsContext getTlsContext() { /** Updates SslContext via the passed callback. */ public synchronized void updateSslContext( - final SslContextProvider.Callback callback) { + final SslContextProvider.Callback callback, boolean autoSniSanValidationDoesNotApply) { checkNotNull(callback, "callback"); try { if (!shutdown) { if (sslContextProvider == null) { sslContextProvider = getSslContextProvider(); + if (tlsContext instanceof UpstreamTlsContext && autoSniSanValidationDoesNotApply) { + ((DynamicSslContextProvider) sslContextProvider).setAutoSniSanValidationDoesNotApply(); + } } } // we want to increment the ref-count so call findOrCreate again... diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java index e92f9ad1e54..b4b72ae11c6 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProvider.java @@ -64,13 +64,15 @@ final class CertProviderClientSslContextProvider extends CertProviderSslContextP new XdsTrustManagerFactory( savedSpiffeTrustMap, certificateValidationContext, - ((UpstreamTlsContext) tlsContext).getAutoSniSanValidation())); + autoSniSanValidationDoesNotApply + ? false : ((UpstreamTlsContext) tlsContext).getAutoSniSanValidation())); } else if (savedTrustedRoots != null) { sslContextBuilder = sslContextBuilder.trustManager( new XdsTrustManagerFactory( savedTrustedRoots.toArray(new X509Certificate[0]), certificateValidationContext, - ((UpstreamTlsContext) tlsContext).getAutoSniSanValidation())); + autoSniSanValidationDoesNotApply + ? false : ((UpstreamTlsContext) tlsContext).getAutoSniSanValidation())); } else { // Should be impossible because of the check in CertProviderClientSslContextProviderFactory throw new IllegalStateException("There must be trusted roots or a SPIFFE trust map"); diff --git a/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java b/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java index 89b4abd3029..30535df836e 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java @@ -31,6 +31,8 @@ */ public final class CertificateUtils { public static boolean isXdsSniEnabled = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_SNI", false); + public static boolean useChannelAuthorityIfNoSniApplicable + = GrpcUtil.getFlag("GRPC_USE_CHANNEL_AUTHORITY_IF_NO_SNI_APPLICABLE", false); /** * Generates X509Certificate array from a file on disk. diff --git a/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java b/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java index ff97afe6916..f997f583898 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java @@ -301,7 +301,7 @@ public void releaseOldSupplierOnTemporaryError_noClose() throws Exception { private void callUpdateSslContext(SslContextProviderSupplier sslContextProviderSupplier) { assertThat(sslContextProviderSupplier).isNotNull(); SslContextProvider.Callback callback = mock(SslContextProvider.Callback.class); - sslContextProviderSupplier.updateSslContext(callback); + sslContextProviderSupplier.updateSslContext(callback, false); } private void sendListenerUpdate( diff --git a/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java b/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java index 061e6bad581..dcb2fa051a3 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java @@ -215,7 +215,7 @@ public void updateSslContextAndExtendedX509TrustManager( protected void onException(Throwable throwable) { future.set(throwable); } - }); + }, false); assertThat(executor.runDueTasks()).isEqualTo(1); channel.runPendingTasks(); Object fromFuture = future.get(2, TimeUnit.SECONDS); @@ -401,7 +401,7 @@ public void updateSslContextAndExtendedX509TrustManager( protected void onException(Throwable throwable) { future.set(throwable); } - }); + }, false); channel.runPendingTasks(); // need this for tasks to execute on eventLoop assertThat(executor.runDueTasks()).isEqualTo(1); Object fromFuture = future.get(2, TimeUnit.SECONDS); @@ -540,7 +540,7 @@ public void updateSslContextAndExtendedX509TrustManager( protected void onException(Throwable throwable) { future.set(throwable); } - }); + }, false); executor.runDueTasks(); channel.runPendingTasks(); // need this for tasks to execute on eventLoop Object fromFuture = future.get(5, TimeUnit.SECONDS); diff --git a/xds/src/test/java/io/grpc/xds/internal/security/SslContextProviderSupplierTest.java b/xds/src/test/java/io/grpc/xds/internal/security/SslContextProviderSupplierTest.java index 9b6e1ecbc74..70a53c53205 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/SslContextProviderSupplierTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/SslContextProviderSupplierTest.java @@ -69,7 +69,7 @@ private void prepareSupplier() { private void callUpdateSslContext() { mockCallback = mock(SslContextProvider.Callback.class); doReturn(mockExecutor).when(mockCallback).getExecutor(); - supplier.updateSslContext(mockCallback); + supplier.updateSslContext(mockCallback, false); } @Test @@ -94,7 +94,7 @@ public void get_updateSecret() { verify(mockTlsContextManager, times(1)) .releaseClientSslContextProvider(eq(mockSslContextProvider)); SslContextProvider.Callback mockCallback = mock(SslContextProvider.Callback.class); - supplier.updateSslContext(mockCallback); + supplier.updateSslContext(mockCallback, false); verify(mockTlsContextManager, times(3)) .findOrCreateClientSslContextProvider(eq(upstreamTlsContext)); } @@ -121,7 +121,7 @@ public void autoHostSniFalse_usesSniFromUpstreamTlsContext() { verify(mockTlsContextManager, times(1)) .releaseClientSslContextProvider(eq(mockSslContextProvider)); SslContextProvider.Callback mockCallback = mock(SslContextProvider.Callback.class); - supplier.updateSslContext(mockCallback); + supplier.updateSslContext(mockCallback, false); verify(mockTlsContextManager, times(3)) .findOrCreateClientSslContextProvider(eq(upstreamTlsContext)); } @@ -178,7 +178,7 @@ public void systemRootCertsWithMtls_callbackExecutedFromProvider() { verify(mockTlsContextManager, times(1)) .releaseClientSslContextProvider(eq(mockSslContextProvider)); SslContextProvider.Callback mockCallback = mock(SslContextProvider.Callback.class); - supplier.updateSslContext(mockCallback); + supplier.updateSslContext(mockCallback, false); verify(mockTlsContextManager, times(3)) .findOrCreateClientSslContextProvider(eq(upstreamTlsContext)); } @@ -190,7 +190,7 @@ public void testClose() { supplier.close(); verify(mockTlsContextManager, times(1)) .releaseClientSslContextProvider(eq(mockSslContextProvider)); - supplier.updateSslContext(mockCallback); + supplier.updateSslContext(mockCallback, false); verify(mockTlsContextManager, times(3)) .findOrCreateClientSslContextProvider(eq(upstreamTlsContext)); verify(mockTlsContextManager, times(1)) From 096c4d9f915fcbef342311a794378194f526bd54 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 21 Oct 2025 10:38:15 -0700 Subject: [PATCH 414/591] Javadoc and rename BinderServerTransport's init method. (#12427) Call this `start()` to match the other transports and so readers know this is way more than a setter. --- .../io/grpc/binder/internal/BinderServer.java | 2 +- .../binder/internal/BinderServerTransport.java | 17 ++++++++++++++--- .../internal/BinderServerTransportTest.java | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderServer.java b/binder/src/main/java/io/grpc/binder/internal/BinderServer.java index fca8e3d88e1..b1ae344d5f9 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderServer.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderServer.java @@ -185,7 +185,7 @@ public synchronized boolean handleTransaction(int code, Parcel parcel) { streamTracerFactories, OneWayBinderProxy.IDENTITY_DECORATOR, callbackBinder); - transport.setServerTransportListener(listener.transportCreated(transport)); + transport.start(listener.transportCreated(transport)); return true; } } diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java index 1c345249735..1556b838142 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java @@ -15,6 +15,9 @@ */ package io.grpc.binder.internal; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + import android.os.IBinder; import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; @@ -57,9 +60,17 @@ public BinderServerTransport( setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService())); } - public synchronized void setServerTransportListener( - ServerTransportListener serverTransportListener) { - this.serverTransportListener = serverTransportListener; + /** + * Initializes this transport instance. + * + *

    Must be called exactly once, even if {@link #shutdown} or {@link #shutdownNow} was called + * first. + * + * @param serverTransportListener where this transport will report events + */ + public synchronized void start(ServerTransportListener serverTransportListener) { + checkState(this.serverTransportListener == null, "Already started!"); + this.serverTransportListener = checkNotNull(serverTransportListener, "serverTransportListener"); if (isShutdown()) { setState(TransportState.SHUTDOWN_TERMINATED); notifyTerminated(); diff --git a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java index e7e73e6d4b0..42bfa387044 100644 --- a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java @@ -74,7 +74,7 @@ public void setUp() throws Exception { public void testSetupTransactionFailureCausesMultipleShutdowns_b153460678() throws Exception { // Make the binder fail the setup transaction. when(mockBinder.transact(anyInt(), any(Parcel.class), isNull(), anyInt())).thenReturn(false); - transport.setServerTransportListener(transportListener); + transport.start(transportListener); // Now shut it down. transport.shutdownNow(Status.UNKNOWN.withDescription("reasons")); From 6da93db6fc038d86c43a81b5485a54f73a80db63 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 21 Oct 2025 11:47:40 -0700 Subject: [PATCH 415/591] xds: Remove commented out+broken import from cluster resolver It was added by mistake in 032d2928e. --- xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index 6cfed96a54e..61bfd3c0839 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -44,7 +44,6 @@ import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; import io.grpc.xds.XdsConfig.XdsClusterConfig; import io.grpc.xds.XdsEndpointResource.EdsUpdate; -// import io.grpc.xds.client.BackendMetricPropagation;] import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger.XdsLogLevel; From ea7444607565f9d3ba8e6fe6e9d70a828f7645de Mon Sep 17 00:00:00 2001 From: John Cormie Date: Wed, 22 Oct 2025 09:51:14 -0700 Subject: [PATCH 416/591] binder: Fix a "fail the setup transaction" test case. (#12434) The setup transaction hasn't actually been failing in this test case since #8987. That's when I changed Robolectric tests to start ignoring the return value of transact() to match how real Android doesn't expose the peers' return value for 'oneway' cross-process transactions. The fix is as simple as mocking transact() to throw rather than return false. --- .../internal/BinderServerTransportTest.java | 74 ++++++++++++++++--- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java index 42bfa387044..b3b99ae34e8 100644 --- a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java @@ -20,17 +20,21 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.doThrow; import static org.robolectric.Shadows.shadowOf; import android.os.IBinder; import android.os.Looper; import android.os.Parcel; +import android.os.RemoteException; import com.google.common.collect.ImmutableList; import io.grpc.Attributes; +import io.grpc.ServerStreamTracer; import io.grpc.Status; import io.grpc.internal.FixedObjectPool; import io.grpc.internal.MockServerTransportListener; +import io.grpc.internal.ObjectPool; +import java.util.List; import java.util.concurrent.ScheduledExecutorService; import org.junit.Before; import org.junit.Rule; @@ -60,26 +64,74 @@ public final class BinderServerTransportTest { @Before public void setUp() throws Exception { - transport = - new BinderServerTransport( - new FixedObjectPool<>(executorService), - Attributes.EMPTY, - ImmutableList.of(), - OneWayBinderProxy.IDENTITY_DECORATOR, - mockBinder); transportListener = new MockServerTransportListener(transport); } + // Provide defaults so that we can "include only relevant details in tests." + BinderServerTransportBuilder newBinderServerTransportBuilder() { + return new BinderServerTransportBuilder() + .setExecutorServicePool(new FixedObjectPool<>(executorService)) + .setAttributes(Attributes.EMPTY) + .setStreamTracerFactories(ImmutableList.of()) + .setBinderDecorator(OneWayBinderProxy.IDENTITY_DECORATOR) + .setCallbackBinder(mockBinder); + } + @Test - public void testSetupTransactionFailureCausesMultipleShutdowns_b153460678() throws Exception { + public void testSetupTransactionFailureReportsMultipleTerminations_b153460678() throws Exception { // Make the binder fail the setup transaction. - when(mockBinder.transact(anyInt(), any(Parcel.class), isNull(), anyInt())).thenReturn(false); + doThrow(new RemoteException()) + .when(mockBinder) + .transact(anyInt(), any(Parcel.class), isNull(), anyInt()); + transport = newBinderServerTransportBuilder().setCallbackBinder(mockBinder).build(); + shadowOf(Looper.getMainLooper()).idle(); transport.start(transportListener); - // Now shut it down. + // Now shut it down externally *before* executing Runnables scheduled on the executor. transport.shutdownNow(Status.UNKNOWN.withDescription("reasons")); shadowOf(Looper.getMainLooper()).idle(); assertThat(transportListener.isTerminated()).isTrue(); } + + static class BinderServerTransportBuilder { + ObjectPool executorServicePool; + Attributes attributes; + List streamTracerFactories; + OneWayBinderProxy.Decorator binderDecorator; + IBinder callbackBinder; + + public BinderServerTransport build() { + return new BinderServerTransport( + executorServicePool, attributes, streamTracerFactories, binderDecorator, callbackBinder); + } + + public BinderServerTransportBuilder setExecutorServicePool( + ObjectPool executorServicePool) { + this.executorServicePool = executorServicePool; + return this; + } + + public BinderServerTransportBuilder setAttributes(Attributes attributes) { + this.attributes = attributes; + return this; + } + + public BinderServerTransportBuilder setStreamTracerFactories( + List streamTracerFactories) { + this.streamTracerFactories = streamTracerFactories; + return this; + } + + public BinderServerTransportBuilder setBinderDecorator( + OneWayBinderProxy.Decorator binderDecorator) { + this.binderDecorator = binderDecorator; + return this; + } + + public BinderServerTransportBuilder setCallbackBinder(IBinder callbackBinder) { + this.callbackBinder = callbackBinder; + return this; + } + } } From b769f966a051a6e6607bc4ddfb2a56a5964b1281 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 22 Oct 2025 07:45:48 -0700 Subject: [PATCH 417/591] alts: Remove dep on grpclb There are no longer any users of grpclb on directpath, so remove the special-casing logic to choose between TLS and ALTS for grpclb-provided backends. Removing the grpclb dep can speed channel startup, as grpclb's DNS resolver does SRV lookups which are no longer needed. --- alts/BUILD.bazel | 1 - alts/build.gradle | 2 -- .../io/grpc/alts/internal/AltsProtocolNegotiator.java | 5 +---- .../internal/GoogleDefaultProtocolNegotiatorTest.java | 8 -------- gcp-observability/build.gradle | 1 - 5 files changed, 1 insertion(+), 16 deletions(-) diff --git a/alts/BUILD.bazel b/alts/BUILD.bazel index 2d9d3508461..d2c01449dc3 100644 --- a/alts/BUILD.bazel +++ b/alts/BUILD.bazel @@ -13,7 +13,6 @@ java_library( ":handshaker_java_proto", "//api", "//core:internal", - "//grpclb", "//netty", "//stub", "@com_google_protobuf//:protobuf_java", diff --git a/alts/build.gradle b/alts/build.gradle index fe2e27784fc..c206a37bcef 100644 --- a/alts/build.gradle +++ b/alts/build.gradle @@ -14,12 +14,10 @@ dependencies { implementation project(':grpc-auth'), project(':grpc-core'), project(":grpc-context"), // Override google-auth dependency with our newer version - project(':grpc-grpclb'), project(':grpc-protobuf'), project(':grpc-stub'), libraries.protobuf.java, libraries.conscrypt, - libraries.guava.jre, // JRE required by protobuf-java-util from grpclb libraries.google.auth.oauth2Http def nettyDependency = implementation project(':grpc-netty') diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsProtocolNegotiator.java b/alts/src/main/java/io/grpc/alts/internal/AltsProtocolNegotiator.java index e0343f83c51..9c51cf6a053 100644 --- a/alts/src/main/java/io/grpc/alts/internal/AltsProtocolNegotiator.java +++ b/alts/src/main/java/io/grpc/alts/internal/AltsProtocolNegotiator.java @@ -30,7 +30,6 @@ import io.grpc.SecurityLevel; import io.grpc.Status; import io.grpc.alts.internal.RpcProtocolVersionsUtil.RpcVersionsCheckResult; -import io.grpc.grpclb.GrpclbConstants; import io.grpc.internal.ObjectPool; import io.grpc.netty.GrpcHttp2ConnectionHandler; import io.grpc.netty.InternalProtocolNegotiator; @@ -299,9 +298,7 @@ public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { isXdsDirectPath = isDirectPathCluster( grpcHandler.getEagAttributes().get(clusterNameAttrKey)); } - if (grpcHandler.getEagAttributes().get(GrpclbConstants.ATTR_LB_ADDR_AUTHORITY) != null - || grpcHandler.getEagAttributes().get(GrpclbConstants.ATTR_LB_PROVIDED_BACKEND) != null - || isXdsDirectPath) { + if (isXdsDirectPath) { TsiHandshaker handshaker = handshakerFactory.newHandshaker(grpcHandler.getAuthority(), negotiationLogger); NettyTsiHandshaker nettyHandshaker = new NettyTsiHandshaker(handshaker); diff --git a/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java b/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java index 9a520720beb..14c19e554ae 100644 --- a/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java +++ b/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java @@ -29,7 +29,6 @@ import io.grpc.ChannelLogger; import io.grpc.ChannelLogger.ChannelLogLevel; import io.grpc.ManagedChannel; -import io.grpc.grpclb.GrpclbConstants; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.internal.ObjectPool; import io.grpc.netty.GrpcHttp2ConnectionHandler; @@ -95,13 +94,6 @@ public void tearDown() { @Nullable abstract Attributes.Key getClusterNameAttrKey(); - @Test - public void altsHandler_lbProvidedBackend() { - Attributes attrs = - Attributes.newBuilder().set(GrpclbConstants.ATTR_LB_PROVIDED_BACKEND, true).build(); - subtest_altsHandler(attrs); - } - @Test public void tlsHandler_emptyAttributes() { subtest_tlsHandler(Attributes.EMPTY); diff --git a/gcp-observability/build.gradle b/gcp-observability/build.gradle index c6d6fa28ddc..1d8c7a9f961 100644 --- a/gcp-observability/build.gradle +++ b/gcp-observability/build.gradle @@ -59,7 +59,6 @@ dependencies { project(path: ':grpc-alts', configuration: 'shadow'), project(':grpc-auth'), // Align grpc versions project(':grpc-core'), // Align grpc versions - project(':grpc-grpclb'), // Align grpc versions project(':grpc-services'), // Align grpc versions libraries.animalsniffer.annotations, // Use our newer version libraries.auto.value.annotations, // Use our newer version From 91f3f4dc176f69f15e5b4c2a77e4fb7f07426eae Mon Sep 17 00:00:00 2001 From: John Cormie Date: Fri, 24 Oct 2025 16:03:06 -0700 Subject: [PATCH 418/591] Fix a BinderServerTransport crash in the rare shutdown-before-start case (#12440) The `isShutdown()` clause of `BinderServerTransport#start()` code was completely untested and did not in fact work. The problem is that if the listener does in fact arrive via start() after shutdown, BinderTransport's `shutdownInternal()` has already set the state to `SHUTDOWN_TERMINATED` (which is not a valid transition from itself). It has also already scheduled a call to notifyTerminated() and releaseExecutors(). This causes a duplicate call to `transportTerminated` and releasing the same executor twice. This commit changes `start()` to leave changing state and releasing executors to `shutdownInternal()`'s. `notifyTerminated()` either runs then (if already started) or from within `start()` (if not yet started) Fixes #12439. --- .../internal/BinderServerTransport.java | 27 ++-- .../grpc/binder/internal/SimplePromise.java | 97 ++++++++++++ .../internal/BinderServerTransportTest.java | 23 +++ .../binder/internal/SimplePromiseTest.java | 143 ++++++++++++++++++ 4 files changed, 275 insertions(+), 15 deletions(-) create mode 100644 binder/src/main/java/io/grpc/binder/internal/SimplePromise.java create mode 100644 binder/src/test/java/io/grpc/binder/internal/SimplePromiseTest.java diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java index 1556b838142..cbe64149e15 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java @@ -15,9 +15,6 @@ */ package io.grpc.binder.internal; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - import android.os.IBinder; import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; @@ -41,7 +38,9 @@ public final class BinderServerTransport extends BinderTransport implements ServerTransport { private final List streamTracerFactories; - @Nullable private ServerTransportListener serverTransportListener; + + @GuardedBy("this") + private final SimplePromise listenerPromise = new SimplePromise<>(); /** * Constructs a new transport instance. @@ -69,13 +68,8 @@ public BinderServerTransport( * @param serverTransportListener where this transport will report events */ public synchronized void start(ServerTransportListener serverTransportListener) { - checkState(this.serverTransportListener == null, "Already started!"); - this.serverTransportListener = checkNotNull(serverTransportListener, "serverTransportListener"); - if (isShutdown()) { - setState(TransportState.SHUTDOWN_TERMINATED); - notifyTerminated(); - releaseExecutors(); - } else { + this.listenerPromise.set(serverTransportListener); + if (!isShutdown()) { sendSetupTransaction(); // Check we're not shutdown again, since a failure inside sendSetupTransaction (or a callback // it triggers), could have shut us down. @@ -90,11 +84,16 @@ StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) { return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers); } + /** + * Reports a new ServerStream requested by the remote client. + * + *

    Precondition: {@link #start(ServerTransportListener)} must already have been called. + */ synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) { if (isShutdown()) { return Status.UNAVAILABLE.withDescription("transport is shutdown"); } else { - serverTransportListener.streamCreated(stream, methodName, headers); + listenerPromise.get().streamCreated(stream, methodName, headers); return Status.OK; } } @@ -108,9 +107,7 @@ void notifyShutdown(Status status) { @Override @GuardedBy("this") void notifyTerminated() { - if (serverTransportListener != null) { - serverTransportListener.transportTerminated(); - } + listenerPromise.runWhenSet(ServerTransportListener::transportTerminated); } @Override diff --git a/binder/src/main/java/io/grpc/binder/internal/SimplePromise.java b/binder/src/main/java/io/grpc/binder/internal/SimplePromise.java new file mode 100644 index 00000000000..c7d227fbf64 --- /dev/null +++ b/binder/src/main/java/io/grpc/binder/internal/SimplePromise.java @@ -0,0 +1,97 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.binder.internal; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import java.util.ArrayList; +import java.util.List; + +/** + * Placeholder for an object that will be provided later. + * + *

    Similar to {@link com.google.common.util.concurrent.SettableFuture}, except it cannot fail or + * be cancelled. Most importantly, this class guarantees that {@link Listener}s run one-at-a-time + * and in the same order that they were scheduled. This conveniently matches the expectations of + * most listener interfaces in the io.grpc universe. + * + *

    Not safe for concurrent use by multiple threads. Thread-compatible for callers that provide + * synchronization externally. + */ +public class SimplePromise { + private T value; + private List> pendingListeners; // Allocated lazily in the hopes it's never needed. + + /** + * Provides the promised object and runs any pending listeners. + * + * @throws IllegalStateException if this method has already been called + * @throws RuntimeException if some pending listener threw when we tried to run it + */ + public void set(T value) { + checkNotNull(value, "value"); + checkState(this.value == null, "Already set!"); + this.value = value; + if (pendingListeners != null) { + for (Listener listener : pendingListeners) { + listener.notify(value); + } + pendingListeners = null; + } + } + + /** + * Returns the promised object, under the assumption that it's already been set. + * + *

    Compared to {@link #runWhenSet(Listener)}, this method may be a more efficient way to access + * the promised value in the case where you somehow know externally that {@link #set(T)} has + * "happened-before" this call. + * + * @throws IllegalStateException if {@link #set(T)} has not yet been called + */ + public T get() { + checkState(value != null, "Not yet set!"); + return value; + } + + /** + * Runs the given listener when this promise is fulfilled, or immediately if already fulfilled. + * + * @throws RuntimeException if already fulfilled and 'listener' threw when we tried to run it + */ + public void runWhenSet(Listener listener) { + if (value != null) { + listener.notify(value); + } else { + if (pendingListeners == null) { + pendingListeners = new ArrayList<>(); + } + pendingListeners.add(listener); + } + } + + /** + * An object that wants to get notified when a SimplePromise has been fulfilled. + */ + public interface Listener { + /** + * Indicates that the associated SimplePromise has been fulfilled with the given `value`. + */ + void notify(T value); + } +} diff --git a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java index b3b99ae34e8..12416922fc9 100644 --- a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java @@ -21,8 +21,10 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; +import android.os.DeadObjectException; import android.os.IBinder; import android.os.Looper; import android.os.Parcel; @@ -94,6 +96,27 @@ public void testSetupTransactionFailureReportsMultipleTerminations_b153460678() assertThat(transportListener.isTerminated()).isTrue(); } + @Test + public void testStartAfterShutdownAndIdle() throws Exception { + transport = newBinderServerTransportBuilder().build(); + transport.shutdownNow(Status.UNKNOWN.withDescription("reasons")); + shadowOf(Looper.getMainLooper()).idle(); + transport.start(transportListener); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(transportListener.isTerminated()).isTrue(); + } + + @Test + public void testStartAfterShutdownNoIdle() throws Exception { + transport = newBinderServerTransportBuilder().build(); + transport.shutdownNow(Status.UNKNOWN.withDescription("reasons")); + transport.start(transportListener); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(transportListener.isTerminated()).isTrue(); + } + static class BinderServerTransportBuilder { ObjectPool executorServicePool; Attributes attributes; diff --git a/binder/src/test/java/io/grpc/binder/internal/SimplePromiseTest.java b/binder/src/test/java/io/grpc/binder/internal/SimplePromiseTest.java new file mode 100644 index 00000000000..6486ff5e8a1 --- /dev/null +++ b/binder/src/test/java/io/grpc/binder/internal/SimplePromiseTest.java @@ -0,0 +1,143 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.binder.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import io.grpc.binder.internal.SimplePromise.Listener; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public final class SimplePromiseTest { + + private static final String FULFILLED_VALUE = "a fulfilled value"; + + @Mock private Listener mockListener1; + @Mock private Listener mockListener2; + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + private SimplePromise promise = new SimplePromise<>(); + + @Before + public void setUp() { + } + + @Test + public void get_beforeFulfilled_throws() { + IllegalStateException e = assertThrows(IllegalStateException.class, () -> promise.get()); + assertThat(e).hasMessageThat().isEqualTo("Not yet set!"); + } + + @Test + public void get_afterFulfilled_returnsValue() { + promise.set(FULFILLED_VALUE); + assertThat(promise.get()).isEqualTo(FULFILLED_VALUE); + } + + @Test + public void set_withNull_throws() { + assertThrows(NullPointerException.class, () -> promise.set(null)); + } + + @Test + public void set_calledTwice_throws() { + promise.set(FULFILLED_VALUE); + IllegalStateException e = + assertThrows(IllegalStateException.class, () -> promise.set("another value")); + assertThat(e).hasMessageThat().isEqualTo("Already set!"); + } + + @Test + public void runWhenSet_beforeFulfill_listenerIsNotifiedUponSet() { + promise.runWhenSet(mockListener1); + + // Should not have been called yet. + verify(mockListener1, never()).notify(FULFILLED_VALUE); + + promise.set(FULFILLED_VALUE); + + // Now it should be called. + verify(mockListener1, times(1)).notify(FULFILLED_VALUE); + } + + @Test + public void runWhenSet_afterSet_listenerIsNotifiedImmediately() { + promise.set(FULFILLED_VALUE); + promise.runWhenSet(mockListener1); + + // Should have been called immediately. + verify(mockListener1, times(1)).notify(FULFILLED_VALUE); + } + + @Test + public void multipleListeners_addedBeforeSet_allNotifiedInOrder() { + promise.runWhenSet(mockListener1); + promise.runWhenSet(mockListener2); + + promise.set(FULFILLED_VALUE); + + InOrder inOrder = inOrder(mockListener1, mockListener2); + inOrder.verify(mockListener1).notify(FULFILLED_VALUE); + inOrder.verify(mockListener2).notify(FULFILLED_VALUE); + } + + @Test + public void listenerThrows_duringSet_propagatesException() { + // A listener that will throw when notified. + Listener throwingListener = + (value) -> { + throw new UnsupportedOperationException("Listener failed"); + }; + + promise.runWhenSet(throwingListener); + + // Fulfilling the promise should now throw the exception from the listener. + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, () -> promise.set(FULFILLED_VALUE)); + assertThat(e).hasMessageThat().isEqualTo("Listener failed"); + } + + @Test + public void listenerThrows_whenAddedAfterSet_propagatesException() { + promise.set(FULFILLED_VALUE); + + // A listener that will throw when notified. + Listener throwingListener = + (value) -> { + throw new UnsupportedOperationException("Listener failed"); + }; + + // Running the listener should throw immediately because the promise is already fulfilled. + UnsupportedOperationException e = + assertThrows( + UnsupportedOperationException.class, () -> promise.runWhenSet(throwingListener)); + assertThat(e).hasMessageThat().isEqualTo("Listener failed"); + } +} From 599a0a1469c3c9e773c1bd90868aa1b54f0e9072 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Mon, 27 Oct 2025 13:52:28 -0700 Subject: [PATCH 419/591] binder: Let the server know when the client fails to authorize it. (#12445) Avoids waiting for the handshake timeout on the server side in this case. I also add test coverage for the `!setOutgoingBinder()` case to make sure it works in the new location. My ulterior motive for this change is simplifying the client handshake code in preparation for #12398 -- An (impossible) !isShutdown() clause goes away for easy to understand reasons and I'll no longer have to pass the server's binder as an arg from async function to function in two separate handshake impls. Fixes #12438 --- .../internal/BinderClientTransportTest.java | 15 ++++ .../internal/BinderClientTransport.java | 28 +++---- .../RobolectricBinderTransportTest.java | 24 ++++++ .../java/io/grpc/binder/FakeDeadBinder.java | 74 +++++++++++++++++++ 4 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 binder/src/testFixtures/java/io/grpc/binder/FakeDeadBinder.java diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java index 0038a054854..ee28f132f79 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java @@ -39,6 +39,7 @@ import io.grpc.Status.Code; import io.grpc.binder.AndroidComponentAddress; import io.grpc.binder.BinderServerBuilder; +import io.grpc.binder.FakeDeadBinder; import io.grpc.binder.HostServices; import io.grpc.binder.SecurityPolicy; import io.grpc.binder.internal.OneWayBinderProxies.BlackHoleOneWayBinderProxy; @@ -358,6 +359,20 @@ public void testTxnFailurePostSetup() throws Exception { assertThat(streamStatus.getCause()).isSameInstanceAs(doe); } + @Test + public void testServerBinderDeadOnArrival() throws Exception { + BlockingBinderDecorator decorator = new BlockingBinderDecorator<>(); + transport = new BinderClientTransportBuilder().setBinderDecorator(decorator).build(); + transport.start(transportListener).run(); + decorator.putNextResult(decorator.takeNextRequest()); // Server's "Endpoint" Binder. + OneWayBinderProxy unusedServerBinder = decorator.takeNextRequest(); + decorator.putNextResult( + OneWayBinderProxy.wrap(new FakeDeadBinder(), offloadServicePool.getObject())); + Status clientStatus = transportListener.awaitShutdown(); + assertThat(clientStatus.getCode()).isEqualTo(Code.UNAVAILABLE); + assertThat(clientStatus.getDescription()).contains("Failed to observe outgoing binder"); + } + @Test public void testBlackHoleEndpointConnectTimeout() throws Exception { BlockingBinderDecorator decorator = new BlockingBinderDecorator<>(); diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index c2447daf751..c6aa195544e 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -87,7 +87,6 @@ public final class BinderClientTransport extends BinderTransport @GuardedBy("this") private ScheduledFuture readyTimeoutFuture; // != null iff timeout scheduled. - /** * Constructs a new transport instance. * @@ -324,6 +323,9 @@ protected void handleSetupTransport(Parcel parcel) { } else if (binder == null) { shutdownInternal( Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); + } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); } else { restrictIncomingBinderToCallsFrom(remoteUid); attributes = setSecurityAttrs(attributes, remoteUid); @@ -334,7 +336,7 @@ protected void handleSetupTransport(Parcel parcel) { new FutureCallback() { @Override public void onSuccess(Status result) { - handleAuthResult(binder, result); + handleAuthResult(result); } @Override @@ -353,24 +355,17 @@ private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); } - private synchronized void handleAuthResult(IBinder binder, Status authorization) { + private synchronized void handleAuthResult(Status authorization) { if (inState(TransportState.SETUP)) { if (!authorization.isOk()) { shutdownInternal(authorization, true); - } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); } else { - // Check state again, since a failure inside setOutgoingBinder (or a callback it - // triggers), could have shut us down. - if (!isShutdown()) { - setState(TransportState.READY); - attributes = clientTransportListener.filterTransport(attributes); - clientTransportListener.transportReady(); - if (readyTimeoutFuture != null) { - readyTimeoutFuture.cancel(false); - readyTimeoutFuture = null; - } + setState(TransportState.READY); + attributes = clientTransportListener.filterTransport(attributes); + clientTransportListener.transportReady(); + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; } } } @@ -387,7 +382,6 @@ protected void handlePingResponse(Parcel parcel) { pingTracker.onPingResponse(parcel.readInt()); } - private static ClientStream newFailingClientStream( Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) { StatsTraceContext statsTraceContext = diff --git a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java index d3d73f0e9eb..6beb92c1691 100644 --- a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java @@ -43,6 +43,7 @@ import androidx.test.core.content.pm.ApplicationInfoBuilder; import androidx.test.core.content.pm.PackageInfoBuilder; import com.google.common.collect.ImmutableList; +import com.google.common.truth.TruthJUnit; import io.grpc.Attributes; import io.grpc.InternalChannelz.SocketStats; import io.grpc.ServerStreamTracer; @@ -353,6 +354,29 @@ static void sendShutdownTransportTransactionAsUid(ClientTransport client, int se } } + @Test + public void clientReportsAuthzErrorToServer() throws Exception { + server.start(serverListener); + client = + newClientTransportBuilder() + .setFactory( + newClientTransportFactoryBuilder() + .setSecurityPolicy(SecurityPolicies.permissionDenied("test")) + .buildClientTransportFactory()) + .build(); + runIfNotNull(client.start(mockClientTransportListener)); + verify(mockClientTransportListener, timeout(TIMEOUT_MS)) + .transportShutdown(statusCaptor.capture()); + assertThat(statusCaptor.getValue().getCode()).isEqualTo(Status.Code.PERMISSION_DENIED); + + TruthJUnit.assume().that(preAuthServersParam).isFalse(); + + MockServerTransportListener serverTransportListener = + serverListener.takeListenerOrFail(TIMEOUT_MS, MILLISECONDS); + serverTransportListener.waitForTermination(TIMEOUT_MS, MILLISECONDS); + assertThat(serverTransportListener.isTerminated()).isTrue(); + } + @Test @Override // We don't quite pass the official/abstract version of this test yet because diff --git a/binder/src/testFixtures/java/io/grpc/binder/FakeDeadBinder.java b/binder/src/testFixtures/java/io/grpc/binder/FakeDeadBinder.java new file mode 100644 index 00000000000..b1658c13812 --- /dev/null +++ b/binder/src/testFixtures/java/io/grpc/binder/FakeDeadBinder.java @@ -0,0 +1,74 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.binder; + +import android.os.DeadObjectException; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.os.RemoteException; +import java.io.FileDescriptor; + +/** An {@link IBinder} that behaves as if its hosting process has died, for testing. */ +public class FakeDeadBinder implements IBinder { + @Override + public boolean isBinderAlive() { + return false; + } + + @Override + public IInterface queryLocalInterface(String descriptor) { + return null; + } + + @Override + public String getInterfaceDescriptor() throws RemoteException { + throw new DeadObjectException(); + } + + @Override + public boolean pingBinder() { + return false; + } + + @Override + public void dump(FileDescriptor fd, String[] args) throws RemoteException { + throw new DeadObjectException(); + } + + @Override + public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException { + throw new DeadObjectException(); + } + + @Override + public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { + throw new DeadObjectException(); + } + + @Override + public void linkToDeath(DeathRecipient r, int flags) throws RemoteException { + throw new DeadObjectException(); + } + + @Override + public boolean unlinkToDeath(DeathRecipient deathRecipient, int flags) { + // No need to check whether 'deathRecipient' was ever actually passed to linkToDeath(): Per our + // API contract, if "the IBinder has already died" we never throw and always return false. + return false; + } +} From 9b424b3186d35d1185658c5f60003ebe2a477c47 Mon Sep 17 00:00:00 2001 From: Kim Jin Young Date: Tue, 28 Oct 2025 21:31:36 +0900 Subject: [PATCH 420/591] stub: remove unnecessary condition in if statement (#12437) This PR removes unnecessary condition inside an `if` statement in BlockingClientCall class. There is no functional change. Fixes #12425 --- stub/src/main/java/io/grpc/stub/BlockingClientCall.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stub/src/main/java/io/grpc/stub/BlockingClientCall.java b/stub/src/main/java/io/grpc/stub/BlockingClientCall.java index 27bd42e53bb..6a52ce50776 100644 --- a/stub/src/main/java/io/grpc/stub/BlockingClientCall.java +++ b/stub/src/main/java/io/grpc/stub/BlockingClientCall.java @@ -225,7 +225,7 @@ private boolean write(boolean waitForever, ReqT request, long endNanoTime) (x) -> x.call.isReady() || x.closeState.get() != null; executor.waitAndDrainWithTimeout(waitForever, endNanoTime, predicate, this); CloseState savedCloseState = closeState.get(); - if (savedCloseState == null || savedCloseState.status == null) { + if (savedCloseState == null) { call.sendMessage(request); return true; } else if (savedCloseState.status.isOk()) { From acbbf869a160153e636f95f6fd1f37a6a4ae0b5f Mon Sep 17 00:00:00 2001 From: oliviamariacodes Date: Wed, 29 Oct 2025 01:58:05 -0700 Subject: [PATCH 421/591] api: Fix name resolver bridge listener handling for address resolution errors (#12441) Fixes https://github.com/grpc/grpc-java/issues/12444 This PR addresses a bug in the `NameResolver.Listener` to `NameResolver.Listener2` bridge affecting custom NameResolver implementations using Listener. The bridge in `NameResolver.start(Listener)` at https://github.com/grpc/grpc-java/blob/master/api/src/main/java/io/grpc/NameResolver.java#L100 unconditionally calls `getValue()` on the `StatusOr`, throwing `java.lang.IllegalStateException: No value present.` when the result contains an error. This was identified when upgrading from gRPC `v1.63.3` to `v1.75.0`. The bug occurs due to `DnsNameResolver`'s error handling changes between versions: - `v1.63.3`: Errors reported via `Listener.onError()` (https://github.com/grpc/grpc-java/blob/v1.63.x/core/src/main/java/io/grpc/internal/DnsNameResolver.java#L319) - `v1.75.0`: Errors passed via `Listener2.onResult2()` with a ResolutionResult containing either addresses OR an error (https://github.com/grpc/grpc-java/blob/master/core/src/main/java/io/grpc/internal/DnsNameResolver.java#L322) This PR updates the bridge to check whether `ResolutionResult` contains addresses or an error. It passes the error via `onError` and addresses via `onAddresses`. **Reproducing the Issue** The `startOnOldListener_resolverReportsError` test reproduces a similar issue. It creates a custom `NameResolver` that reports errors through the `ResolutionResult` like the `DNSNameResolver` in `v1.75.0`, passes an old Listener to `resolver.start`, which triggers the bridge code path. Without the fix, the bridge calls `getValue()` on the error containing `StatusOr`, throwing `IllegalStateException: No value present`. With the fix, the bridge checks `hasValue()` first and correctly routes to `listener.onError()` when appropriate. This ensures backward compatibility for `Listener` implementations when resolvers report errors via `ResolutionResult`. --- api/src/main/java/io/grpc/NameResolver.java | 10 +++- .../test/java/io/grpc/NameResolverTest.java | 50 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java index 2ac4ecae69e..0e8315e812c 100644 --- a/api/src/main/java/io/grpc/NameResolver.java +++ b/api/src/main/java/io/grpc/NameResolver.java @@ -97,8 +97,14 @@ public void onError(Status error) { @Override public void onResult(ResolutionResult resolutionResult) { - listener.onAddresses(resolutionResult.getAddressesOrError().getValue(), - resolutionResult.getAttributes()); + StatusOr> addressesOrError = + resolutionResult.getAddressesOrError(); + if (addressesOrError.hasValue()) { + listener.onAddresses(addressesOrError.getValue(), + resolutionResult.getAttributes()); + } else { + listener.onError(addressesOrError.getStatus()); + } } }); } diff --git a/api/src/test/java/io/grpc/NameResolverTest.java b/api/src/test/java/io/grpc/NameResolverTest.java index ae8c080bd5c..82abe5c7505 100644 --- a/api/src/test/java/io/grpc/NameResolverTest.java +++ b/api/src/test/java/io/grpc/NameResolverTest.java @@ -192,6 +192,56 @@ public void resolutionResult_hashCode() { Objects.hashCode(StatusOr.fromValue(ADDRESSES), ATTRIBUTES, CONFIG)); } + @Test + public void startOnOldListener_resolverReportsError() { + final boolean[] onErrorCalled = new boolean[1]; + final Status[] receivedError = new Status[1]; + + NameResolver resolver = new NameResolver() { + @Override + public String getServiceAuthority() { + return "example.com"; + } + + @Override + public void shutdown() { + } + + @Override + public void start(Listener2 listener2) { + ResolutionResult errorResult = ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromStatus( + Status.UNAVAILABLE + .withDescription("DNS resolution failed with UNAVAILABLE"))) + .build(); + + listener2.onResult(errorResult); + } + }; + + NameResolver.Listener listener = new NameResolver.Listener() { + @Override + public void onAddresses( + List servers, + Attributes attributes) { + throw new AssertionError("Called onAddresses on error"); + } + + @Override + public void onError(Status error) { + onErrorCalled[0] = true; + receivedError[0] = error; + } + }; + + resolver.start(listener); + + assertThat(onErrorCalled[0]).isTrue(); + assertThat(receivedError[0].getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(receivedError[0].getDescription()).isEqualTo( + "DNS resolution failed with UNAVAILABLE"); + } + private static class FakeSocketAddress extends SocketAddress { final String name; From 27d150890e165dbaf6869e79484139d6c0058ea5 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 29 Oct 2025 09:43:58 -0700 Subject: [PATCH 422/591] xds,googleapis: Allow wrapping NameResolver to inject XdsClient (#12450) Since there is no longer a single global XdsClient, it makes more sense to allow things like the c2p name resolver to inject its own bootstrap even if there is one defined in an environment variable. GoogleCloudToProdNameResolver can now pass an XdsClient instance to XdsNameResolver, and SharedXdsClientPoolProvider allows GoogleCloudToProdNameResolver to choose the bootstrap for that one specific target. Since XdsNameResolver is no longer in control of the XdsClient pool the XdsClient instance is now passed to ClusterImplLb. A channel will now only access the global XdsClient pool exactly once: in the name resolver. BootstrapInfo is purposefully being shared across channels, as we really want to share things like credentials which can have significant memory use and may have caches which reduce I/O when shared. That is why SharedXdsClientPoolProvider receives BootstrapInfo instead of Map. Verifying BootstrapInfo.server() is not empty was moved from SharedXdsClientPoolProvider to GrpcBootstrapperImpl so avoid getOrCreate() throwing an exception in only that one case. It might make sense to move that to BootstrapperImpl, but that will need more investigation. A lot of tests needed updating because XdsClientPoolProvider is no longer responsible for parsing the bootstrap, so we now need bootstraps even if XdsClientPoolProvider will ignore it. This also fixes a bug in GoogleCloudToProdNameResolver where it would initialize the delegate even when it failed to create the bootstrap. That would certainly cause all RPCs on the channel to fail because of the missing bootstrap and it defeated the point of `succeeded == false` and `refresh()` which was supposed to retry contacting the metadata server. The server tests were enhanced to give a useful error when server.start() throws an exception, as otherwise the real error is lost. b/442819521 --- googleapis/BUILD.bazel | 1 + .../GoogleCloudToProdNameResolver.java | 135 +++++++++------- ...GoogleCloudToProdNameResolverProvider.java | 13 +- .../GoogleCloudToProdNameResolverTest.java | 145 ++++-------------- .../io/grpc/xds/ClusterImplLoadBalancer.java | 12 +- .../io/grpc/xds/GrpcBootstrapperImpl.java | 17 +- .../xds/InternalGrpcBootstrapperImpl.java | 7 + .../InternalSharedXdsClientPoolProvider.java | 56 +++++++ .../grpc/xds/SharedXdsClientPoolProvider.java | 42 ++--- .../main/java/io/grpc/xds/XdsAttributes.java | 5 +- .../io/grpc/xds/XdsClientPoolFactory.java | 9 +- .../java/io/grpc/xds/XdsNameResolver.java | 98 ++++++++++-- .../io/grpc/xds/XdsNameResolverProvider.java | 9 ++ .../java/io/grpc/xds/XdsServerBuilder.java | 6 +- .../java/io/grpc/xds/XdsServerWrapper.java | 25 ++- .../grpc/xds/ClusterImplLoadBalancerTest.java | 22 +-- .../java/io/grpc/xds/CsdsServiceTest.java | 9 +- .../io/grpc/xds/GrpcBootstrapperImplTest.java | 44 +++++- .../xds/SharedXdsClientPoolProviderTest.java | 40 +++-- .../io/grpc/xds/XdsClientFallbackTest.java | 6 +- .../io/grpc/xds/XdsClientFederationTest.java | 6 +- .../XdsClientWrapperForServerSdsTestMisc.java | 3 +- .../java/io/grpc/xds/XdsNameResolverTest.java | 90 +++++------ .../grpc/xds/XdsSecurityClientServerTest.java | 14 +- .../io/grpc/xds/XdsServerBuilderTest.java | 22 ++- .../java/io/grpc/xds/XdsServerTestHelper.java | 24 +-- .../io/grpc/xds/XdsServerWrapperTest.java | 17 +- 27 files changed, 526 insertions(+), 351 deletions(-) diff --git a/googleapis/BUILD.bazel b/googleapis/BUILD.bazel index 42632e0f99d..5b62b21cb3a 100644 --- a/googleapis/BUILD.bazel +++ b/googleapis/BUILD.bazel @@ -13,5 +13,6 @@ java_library( "//core:internal", "//xds", artifact("com.google.guava:guava"), + artifact("com.google.errorprone:error_prone_annotations"), ], ) diff --git a/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java index ebc7dd05ea4..0aee17b6b9f 100644 --- a/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java +++ b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java @@ -20,10 +20,11 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.CharStreams; +import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.grpc.MetricRecorder; import io.grpc.NameResolver; import io.grpc.NameResolverRegistry; import io.grpc.Status; @@ -32,6 +33,13 @@ import io.grpc.internal.GrpcUtil; import io.grpc.internal.SharedResourceHolder; import io.grpc.internal.SharedResourceHolder.Resource; +import io.grpc.xds.InternalGrpcBootstrapperImpl; +import io.grpc.xds.InternalSharedXdsClientPoolProvider; +import io.grpc.xds.InternalSharedXdsClientPoolProvider.XdsClientResult; +import io.grpc.xds.XdsNameResolverProvider; +import io.grpc.xds.client.Bootstrapper.BootstrapInfo; +import io.grpc.xds.client.XdsClient; +import io.grpc.xds.client.XdsInitializationException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -41,7 +49,6 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.Map; import java.util.Random; import java.util.concurrent.Executor; import java.util.logging.Level; @@ -63,52 +70,54 @@ final class GoogleCloudToProdNameResolver extends NameResolver { static final String C2P_AUTHORITY = "traffic-director-c2p.xds.googleapis.com"; @VisibleForTesting static boolean isOnGcp = InternalCheckGcpEnvironment.isOnGcp(); - @VisibleForTesting - static boolean xdsBootstrapProvided = - System.getenv("GRPC_XDS_BOOTSTRAP") != null - || System.getProperty("io.grpc.xds.bootstrap") != null - || System.getenv("GRPC_XDS_BOOTSTRAP_CONFIG") != null - || System.getProperty("io.grpc.xds.bootstrapConfig") != null; - @VisibleForTesting - static boolean enableFederation = - Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_XDS_FEDERATION")) - || Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_XDS_FEDERATION")); private static final String serverUriOverride = System.getenv("GRPC_TEST_ONLY_GOOGLE_C2P_RESOLVER_TRAFFIC_DIRECTOR_URI"); - private HttpConnectionProvider httpConnectionProvider = HttpConnectionFactory.INSTANCE; + @GuardedBy("GoogleCloudToProdNameResolver.class") + private static BootstrapInfo bootstrapInfo; + private static HttpConnectionProvider httpConnectionProvider = HttpConnectionFactory.INSTANCE; + private static int c2pId = new Random().nextInt(); + + private static synchronized BootstrapInfo getBootstrapInfo() + throws XdsInitializationException, IOException { + if (bootstrapInfo != null) { + return bootstrapInfo; + } + BootstrapInfo bootstrapInfoTmp = + InternalGrpcBootstrapperImpl.parseBootstrap(generateBootstrap()); + // Avoid setting global when testing + if (httpConnectionProvider == HttpConnectionFactory.INSTANCE) { + bootstrapInfo = bootstrapInfoTmp; + } + return bootstrapInfoTmp; + } + private final String authority; private final SynchronizationContext syncContext; private final Resource executorResource; - private final BootstrapSetter bootstrapSetter; + private final String target; + private final MetricRecorder metricRecorder; private final NameResolver delegate; - private final Random rand; private final boolean usingExecutorResource; - // It's not possible to use both PSM and DirectPath C2P in the same application. - // Delegate to DNS if user-provided bootstrap is found. - private final String schemeOverride = - !isOnGcp - || (xdsBootstrapProvided && !enableFederation) - ? "dns" : "xds"; + private final String schemeOverride = !isOnGcp ? "dns" : "xds"; + private XdsClientResult xdsClientPool; + private XdsClient xdsClient; private Executor executor; private Listener2 listener; private boolean succeeded; private boolean resolving; private boolean shutdown; - GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource executorResource, - BootstrapSetter bootstrapSetter) { - this(targetUri, args, executorResource, new Random(), bootstrapSetter, + GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource executorResource) { + this(targetUri, args, executorResource, NameResolverRegistry.getDefaultRegistry().asFactory()); } @VisibleForTesting GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource executorResource, - Random rand, BootstrapSetter bootstrapSetter, NameResolver.Factory nameResolverFactory) { + NameResolver.Factory nameResolverFactory) { this.executorResource = checkNotNull(executorResource, "executorResource"); - this.bootstrapSetter = checkNotNull(bootstrapSetter, "bootstrapSetter"); - this.rand = checkNotNull(rand, "rand"); String targetPath = checkNotNull(checkNotNull(targetUri, "targetUri").getPath(), "targetPath"); Preconditions.checkArgument( targetPath.startsWith("/"), @@ -118,9 +127,14 @@ final class GoogleCloudToProdNameResolver extends NameResolver { authority = GrpcUtil.checkAuthority(targetPath.substring(1)); syncContext = checkNotNull(args, "args").getSynchronizationContext(); targetUri = overrideUriScheme(targetUri, schemeOverride); - if (schemeOverride.equals("xds") && enableFederation) { + if (schemeOverride.equals("xds")) { targetUri = overrideUriAuthority(targetUri, C2P_AUTHORITY); + args = args.toBuilder() + .setArg(XdsNameResolverProvider.XDS_CLIENT_SUPPLIER, () -> xdsClient) + .build(); } + target = targetUri.toString(); + metricRecorder = args.getMetricRecorder(); delegate = checkNotNull(nameResolverFactory, "nameResolverFactory").newNameResolver( targetUri, args); executor = args.getOffloadExecutor(); @@ -150,7 +164,7 @@ private void resolve() { resolving = true; if (logger.isLoggable(Level.FINE)) { - logger.fine("resolve with schemaOverride = " + schemeOverride); + logger.log(Level.FINE, "start with schemaOverride = {0}", schemeOverride); } if (schemeOverride.equals("dns")) { @@ -168,28 +182,28 @@ private void resolve() { class Resolve implements Runnable { @Override public void run() { - ImmutableMap rawBootstrap = null; + BootstrapInfo bootstrapInfo = null; try { - // User provided bootstrap configs are only supported with federation. If federation is - // not enabled or there is no user provided config, we set a custom bootstrap override. - // Otherwise, we don't set the override, which will allow a user provided bootstrap config - // to take effect. - if (!enableFederation || !xdsBootstrapProvided) { - rawBootstrap = generateBootstrap(queryZoneMetadata(METADATA_URL_ZONE), - queryIpv6SupportMetadata(METADATA_URL_SUPPORT_IPV6)); - } + bootstrapInfo = getBootstrapInfo(); } catch (IOException e) { listener.onError( Status.INTERNAL.withDescription("Unable to get metadata").withCause(e)); + } catch (XdsInitializationException e) { + listener.onError( + Status.INTERNAL.withDescription("Unable to create c2p bootstrap").withCause(e)); + } catch (Throwable t) { + listener.onError( + Status.INTERNAL.withDescription("Unexpected error creating c2p bootstrap") + .withCause(t)); } finally { - final ImmutableMap finalRawBootstrap = rawBootstrap; + final BootstrapInfo finalBootstrapInfo = bootstrapInfo; syncContext.execute(new Runnable() { @Override public void run() { - if (!shutdown) { - if (finalRawBootstrap != null) { - bootstrapSetter.setBootstrap(finalRawBootstrap); - } + if (!shutdown && finalBootstrapInfo != null) { + xdsClientPool = InternalSharedXdsClientPoolProvider.getOrCreate( + target, finalBootstrapInfo, metricRecorder, null); + xdsClient = xdsClientPool.getObject(); delegate.start(listener); succeeded = true; } @@ -203,9 +217,16 @@ public void run() { executor.execute(new Resolve()); } - private ImmutableMap generateBootstrap(String zone, boolean supportIpv6) { + @VisibleForTesting + static ImmutableMap generateBootstrap() throws IOException { + return generateBootstrap( + queryZoneMetadata(METADATA_URL_ZONE), + queryIpv6SupportMetadata(METADATA_URL_SUPPORT_IPV6)); + } + + private static ImmutableMap generateBootstrap(String zone, boolean supportIpv6) { ImmutableMap.Builder nodeBuilder = ImmutableMap.builder(); - nodeBuilder.put("id", "C2P-" + (rand.nextInt() & Integer.MAX_VALUE)); + nodeBuilder.put("id", "C2P-" + (c2pId & Integer.MAX_VALUE)); if (!zone.isEmpty()) { nodeBuilder.put("locality", ImmutableMap.of("zone", zone)); } @@ -250,12 +271,15 @@ public void shutdown() { if (delegate != null) { delegate.shutdown(); } + if (xdsClient != null) { + xdsClient = xdsClientPool.returnObject(xdsClient); + } if (executor != null && usingExecutorResource) { executor = SharedResourceHolder.release(executorResource, executor); } } - private String queryZoneMetadata(String url) throws IOException { + private static String queryZoneMetadata(String url) throws IOException { HttpURLConnection con = null; String respBody; try { @@ -275,7 +299,7 @@ private String queryZoneMetadata(String url) throws IOException { return index == -1 ? "" : respBody.substring(index + 1); } - private boolean queryIpv6SupportMetadata(String url) throws IOException { + private static boolean queryIpv6SupportMetadata(String url) throws IOException { HttpURLConnection con = null; try { con = httpConnectionProvider.createConnection(url); @@ -294,8 +318,17 @@ private boolean queryIpv6SupportMetadata(String url) throws IOException { } @VisibleForTesting - void setHttpConnectionProvider(HttpConnectionProvider httpConnectionProvider) { - this.httpConnectionProvider = httpConnectionProvider; + static void setHttpConnectionProvider(HttpConnectionProvider httpConnectionProvider) { + if (httpConnectionProvider == null) { + GoogleCloudToProdNameResolver.httpConnectionProvider = HttpConnectionFactory.INSTANCE; + } else { + GoogleCloudToProdNameResolver.httpConnectionProvider = httpConnectionProvider; + } + } + + @VisibleForTesting + static void setC2pId(int c2pId) { + GoogleCloudToProdNameResolver.c2pId = c2pId; } private static URI overrideUriScheme(URI uri, String scheme) { @@ -335,8 +368,4 @@ public HttpURLConnection createConnection(String url) throws IOException { interface HttpConnectionProvider { HttpURLConnection createConnection(String url) throws IOException; } - - public interface BootstrapSetter { - void setBootstrap(Map bootstrap); - } } diff --git a/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolverProvider.java b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolverProvider.java index 8ad292a3d98..c02cf53c2d2 100644 --- a/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolverProvider.java +++ b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolverProvider.java @@ -22,13 +22,11 @@ import io.grpc.NameResolver.Args; import io.grpc.NameResolverProvider; import io.grpc.internal.GrpcUtil; -import io.grpc.xds.InternalSharedXdsClientPoolProvider; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; import java.util.Collection; import java.util.Collections; -import java.util.Map; /** * A provider for {@link GoogleCloudToProdNameResolver}. @@ -52,8 +50,7 @@ public GoogleCloudToProdNameResolverProvider() { public NameResolver newNameResolver(URI targetUri, Args args) { if (scheme.equals(targetUri.getScheme())) { return new GoogleCloudToProdNameResolver( - targetUri, args, GrpcUtil.SHARED_CHANNEL_EXECUTOR, - new SharedXdsClientPoolProviderBootstrapSetter()); + targetUri, args, GrpcUtil.SHARED_CHANNEL_EXECUTOR); } return null; } @@ -77,12 +74,4 @@ protected int priority() { public Collection> getProducedSocketAddressTypes() { return Collections.singleton(InetSocketAddress.class); } - - private static final class SharedXdsClientPoolProviderBootstrapSetter - implements GoogleCloudToProdNameResolver.BootstrapSetter { - @Override - public void setBootstrap(Map bootstrap) { - InternalSharedXdsClientPoolProvider.setDefaultProviderBootstrapOverride(bootstrap); - } - } } diff --git a/googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverTest.java b/googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverTest.java index edb3126d1e3..6a144696447 100644 --- a/googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverTest.java +++ b/googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverTest.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import io.grpc.ChannelLogger; +import io.grpc.MetricRecorder; import io.grpc.NameResolver; import io.grpc.NameResolver.Args; import io.grpc.NameResolver.ServiceConfigParser; @@ -47,7 +48,6 @@ import java.util.Map; import java.util.Random; import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -77,15 +77,16 @@ public void uncaughtException(Thread t, Throwable e) { throw new AssertionError(e); } }); + private final FakeClock fakeExecutor = new FakeClock(); private final NameResolver.Args args = NameResolver.Args.newBuilder() .setDefaultPort(DEFAULT_PORT) .setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR) .setSynchronizationContext(syncContext) + .setScheduledExecutorService(fakeExecutor.getScheduledExecutorService()) .setServiceConfigParser(mock(ServiceConfigParser.class)) .setChannelLogger(mock(ChannelLogger.class)) + .setMetricRecorder(new MetricRecorder() {}) .build(); - private final FakeClock fakeExecutor = new FakeClock(); - private final FakeBootstrapSetter fakeBootstrapSetter = new FakeBootstrapSetter(); private final Resource fakeExecutorResource = new Resource() { @Override public Executor create() { @@ -101,34 +102,18 @@ public void close(Executor instance) {} @Mock private NameResolver.Listener2 mockListener; - private Random random = new Random(1); @Captor private ArgumentCaptor errorCaptor; private boolean originalIsOnGcp; - private boolean originalXdsBootstrapProvided; private GoogleCloudToProdNameResolver resolver; + private String responseToIpV6 = "1:1:1"; @Before public void setUp() { nsRegistry.register(new FakeNsProvider("dns")); nsRegistry.register(new FakeNsProvider("xds")); originalIsOnGcp = GoogleCloudToProdNameResolver.isOnGcp; - originalXdsBootstrapProvided = GoogleCloudToProdNameResolver.xdsBootstrapProvided; - } - - @After - public void tearDown() { - GoogleCloudToProdNameResolver.isOnGcp = originalIsOnGcp; - GoogleCloudToProdNameResolver.xdsBootstrapProvided = originalXdsBootstrapProvided; - resolver.shutdown(); - verify(Iterables.getOnlyElement(delegatedResolver.values())).shutdown(); - } - - private void createResolver() { - createResolver("1:1:1"); - } - private void createResolver(String responseToIpV6) { HttpConnectionProvider httpConnections = new HttpConnectionProvider() { @Override public HttpURLConnection createConnection(String url) throws IOException { @@ -148,10 +133,24 @@ public HttpURLConnection createConnection(String url) throws IOException { throw new AssertionError("Unknown http query"); } }; + GoogleCloudToProdNameResolver.setHttpConnectionProvider(httpConnections); + + GoogleCloudToProdNameResolver.setC2pId(new Random(1).nextInt()); + } + + @After + public void tearDown() { + GoogleCloudToProdNameResolver.isOnGcp = originalIsOnGcp; + GoogleCloudToProdNameResolver.setHttpConnectionProvider(null); + if (resolver != null) { + resolver.shutdown(); + verify(Iterables.getOnlyElement(delegatedResolver.values())).shutdown(); + } + } + + private void createResolver() { resolver = new GoogleCloudToProdNameResolver( - TARGET_URI, args, fakeExecutorResource, random, fakeBootstrapSetter, - nsRegistry.asFactory()); - resolver.setHttpConnectionProvider(httpConnections); + TARGET_URI, args, fakeExecutorResource, nsRegistry.asFactory()); } @Test @@ -164,27 +163,19 @@ public void notOnGcp_DelegateToDns() { } @Test - public void hasProvidedBootstrap_DelegateToDns() { + public void onGcpAndNoProvidedBootstrap_DelegateToXds() { GoogleCloudToProdNameResolver.isOnGcp = true; - GoogleCloudToProdNameResolver.xdsBootstrapProvided = true; - GoogleCloudToProdNameResolver.enableFederation = false; createResolver(); resolver.start(mockListener); - assertThat(delegatedResolver.keySet()).containsExactly("dns"); + fakeExecutor.runDueTasks(); + assertThat(delegatedResolver.keySet()).containsExactly("xds"); verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); } @SuppressWarnings("unchecked") @Test - public void onGcpAndNoProvidedBootstrap_DelegateToXds() { - GoogleCloudToProdNameResolver.isOnGcp = true; - GoogleCloudToProdNameResolver.xdsBootstrapProvided = false; - createResolver(); - resolver.start(mockListener); - fakeExecutor.runDueTasks(); - assertThat(delegatedResolver.keySet()).containsExactly("xds"); - verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); - Map bootstrap = fakeBootstrapSetter.bootstrapRef.get(); + public void generateBootstrap_ipv6() throws IOException { + Map bootstrap = GoogleCloudToProdNameResolver.generateBootstrap(); Map node = (Map) bootstrap.get("node"); assertThat(node).containsExactly( "id", "C2P-991614323", @@ -204,15 +195,9 @@ public void onGcpAndNoProvidedBootstrap_DelegateToXds() { @SuppressWarnings("unchecked") @Test - public void onGcpAndNoProvidedBootstrap_DelegateToXds_noIpV6() { - GoogleCloudToProdNameResolver.isOnGcp = true; - GoogleCloudToProdNameResolver.xdsBootstrapProvided = false; - createResolver(null); - resolver.start(mockListener); - fakeExecutor.runDueTasks(); - assertThat(delegatedResolver.keySet()).containsExactly("xds"); - verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); - Map bootstrap = fakeBootstrapSetter.bootstrapRef.get(); + public void generateBootstrap_noIpV6() throws IOException { + responseToIpV6 = null; + Map bootstrap = GoogleCloudToProdNameResolver.generateBootstrap(); Map node = (Map) bootstrap.get("node"); assertThat(node).containsExactly( "id", "C2P-991614323", @@ -231,70 +216,18 @@ public void onGcpAndNoProvidedBootstrap_DelegateToXds_noIpV6() { @SuppressWarnings("unchecked") @Test - public void emptyResolverMeetadataValue() { - GoogleCloudToProdNameResolver.isOnGcp = true; - GoogleCloudToProdNameResolver.xdsBootstrapProvided = false; - createResolver(""); - resolver.start(mockListener); - fakeExecutor.runDueTasks(); - assertThat(delegatedResolver.keySet()).containsExactly("xds"); - verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); - Map bootstrap = fakeBootstrapSetter.bootstrapRef.get(); + public void emptyResolverMeetadataValue() throws IOException { + responseToIpV6 = ""; + Map bootstrap = GoogleCloudToProdNameResolver.generateBootstrap(); Map node = (Map) bootstrap.get("node"); assertThat(node).containsExactly( "id", "C2P-991614323", "locality", ImmutableMap.of("zone", ZONE)); } - @SuppressWarnings("unchecked") - @Test - public void onGcpAndNoProvidedBootstrapAndFederationEnabled_DelegateToXds() { - GoogleCloudToProdNameResolver.isOnGcp = true; - GoogleCloudToProdNameResolver.xdsBootstrapProvided = false; - GoogleCloudToProdNameResolver.enableFederation = true; - createResolver(); - resolver.start(mockListener); - fakeExecutor.runDueTasks(); - assertThat(delegatedResolver.keySet()).containsExactly("xds"); - verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); - // check bootstrap - Map bootstrap = fakeBootstrapSetter.bootstrapRef.get(); - Map node = (Map) bootstrap.get("node"); - assertThat(node).containsExactly( - "id", "C2P-991614323", - "locality", ImmutableMap.of("zone", ZONE), - "metadata", ImmutableMap.of("TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE", true)); - Map server = Iterables.getOnlyElement( - (List>) bootstrap.get("xds_servers")); - assertThat(server).containsExactly( - "server_uri", "directpath-pa.googleapis.com", - "channel_creds", ImmutableList.of(ImmutableMap.of("type", "google_default")), - "server_features", ImmutableList.of("xds_v3", "ignore_resource_deletion")); - Map authorities = (Map) bootstrap.get("authorities"); - assertThat(authorities).containsExactly( - "traffic-director-c2p.xds.googleapis.com", - ImmutableMap.of("xds_servers", ImmutableList.of(server))); - } - - @SuppressWarnings("unchecked") - @Test - public void onGcpAndProvidedBootstrapAndFederationEnabled_DontDelegateToXds() { - GoogleCloudToProdNameResolver.isOnGcp = true; - GoogleCloudToProdNameResolver.xdsBootstrapProvided = true; - GoogleCloudToProdNameResolver.enableFederation = true; - createResolver(); - resolver.start(mockListener); - fakeExecutor.runDueTasks(); - assertThat(delegatedResolver.keySet()).containsExactly("xds"); - verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); - // Bootstrapper should not have been set, since there was no user provided config. - assertThat(fakeBootstrapSetter.bootstrapRef.get()).isNull(); - } - @Test public void failToQueryMetadata() { GoogleCloudToProdNameResolver.isOnGcp = true; - GoogleCloudToProdNameResolver.xdsBootstrapProvided = false; createResolver(); HttpConnectionProvider httpConnections = new HttpConnectionProvider() { @Override @@ -304,7 +237,7 @@ public HttpURLConnection createConnection(String url) throws IOException { return con; } }; - resolver.setHttpConnectionProvider(httpConnections); + GoogleCloudToProdNameResolver.setHttpConnectionProvider(httpConnections); resolver.start(mockListener); fakeExecutor.runDueTasks(); verify(mockListener).onError(errorCaptor.capture()); @@ -344,14 +277,4 @@ public String getDefaultScheme() { return scheme; } } - - private static final class FakeBootstrapSetter - implements GoogleCloudToProdNameResolver.BootstrapSetter { - private final AtomicReference> bootstrapRef = new AtomicReference<>(); - - @Override - public void setBootstrap(Map bootstrap) { - bootstrapRef.set(bootstrap); - } - } } diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index 00fe736761b..ad5dff3cea1 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -37,7 +37,6 @@ import io.grpc.Status; import io.grpc.internal.ForwardingClientStreamTracer; import io.grpc.internal.GrpcUtil; -import io.grpc.internal.ObjectPool; import io.grpc.services.MetricReport; import io.grpc.util.ForwardingLoadBalancerHelper; import io.grpc.util.ForwardingSubchannel; @@ -96,7 +95,6 @@ final class ClusterImplLoadBalancer extends LoadBalancer { private String cluster; @Nullable private String edsServiceName; - private ObjectPool xdsClientPool; private XdsClient xdsClient; private CallCounterProvider callCounterProvider; private ClusterDropStats dropStats; @@ -119,10 +117,8 @@ final class ClusterImplLoadBalancer extends LoadBalancer { public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses); Attributes attributes = resolvedAddresses.getAttributes(); - if (xdsClientPool == null) { - xdsClientPool = attributes.get(io.grpc.xds.XdsAttributes.XDS_CLIENT_POOL); - assert xdsClientPool != null; - xdsClient = xdsClientPool.getObject(); + if (xdsClient == null) { + xdsClient = checkNotNull(attributes.get(io.grpc.xds.XdsAttributes.XDS_CLIENT), "xdsClient"); } if (callCounterProvider == null) { callCounterProvider = attributes.get(io.grpc.xds.XdsAttributes.CALL_COUNTER_PROVIDER); @@ -192,9 +188,7 @@ public void shutdown() { childLbHelper = null; } } - if (xdsClient != null) { - xdsClient = xdsClientPool.returnObject(xdsClient); - } + xdsClient = null; } /** diff --git a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java index f61fab42cae..fdcf3a972b5 100644 --- a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java @@ -18,6 +18,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.ChannelCredentials; import io.grpc.internal.JsonUtil; import io.grpc.xds.client.BootstrapperImpl; @@ -48,7 +49,11 @@ class GrpcBootstrapperImpl extends BootstrapperImpl { @Override public BootstrapInfo bootstrap(Map rawData) throws XdsInitializationException { - return super.bootstrap(rawData); + BootstrapInfo info = super.bootstrap(rawData); + if (info.servers().isEmpty()) { + throw new XdsInitializationException("Invalid bootstrap: 'xds_servers' is empty"); + } + return info; } /** @@ -95,6 +100,16 @@ protected Object getImplSpecificConfig(Map serverConfig, String serve return getChannelCredentials(serverConfig, serverUri); } + @GuardedBy("GrpcBootstrapperImpl.class") + private static BootstrapInfo defaultBootstrap; + + static synchronized BootstrapInfo defaultBootstrap() throws XdsInitializationException { + if (defaultBootstrap == null) { + defaultBootstrap = new GrpcBootstrapperImpl().bootstrap(); + } + return defaultBootstrap; + } + private static ChannelCredentials getChannelCredentials(Map serverConfig, String serverUri) throws XdsInitializationException { diff --git a/xds/src/main/java/io/grpc/xds/InternalGrpcBootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/InternalGrpcBootstrapperImpl.java index 929619c11d7..6a852a2fbb4 100644 --- a/xds/src/main/java/io/grpc/xds/InternalGrpcBootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/InternalGrpcBootstrapperImpl.java @@ -17,8 +17,10 @@ package io.grpc.xds; import io.grpc.Internal; +import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.XdsInitializationException; import java.io.IOException; +import java.util.Map; /** * Internal accessors for GrpcBootstrapperImpl. @@ -30,4 +32,9 @@ private InternalGrpcBootstrapperImpl() {} // prevent instantiation public static String getJsonContent() throws XdsInitializationException, IOException { return new GrpcBootstrapperImpl().getJsonContent(); } + + public static BootstrapInfo parseBootstrap(Map bootstrap) + throws XdsInitializationException { + return new GrpcBootstrapperImpl().bootstrap(bootstrap); + } } diff --git a/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java index 9c98bba93cf..5eb36a498cf 100644 --- a/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java +++ b/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java @@ -20,6 +20,7 @@ import io.grpc.Internal; import io.grpc.MetricRecorder; import io.grpc.internal.ObjectPool; +import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsInitializationException; import java.util.Map; @@ -32,24 +33,79 @@ public final class InternalSharedXdsClientPoolProvider { // Prevent instantiation private InternalSharedXdsClientPoolProvider() {} + /** + * Override the global bootstrap. + * + * @deprecated Use InternalGrpcBootstrapperImpl.parseBootstrap() and pass the result to + * getOrCreate(). + */ + @Deprecated public static void setDefaultProviderBootstrapOverride(Map bootstrap) { SharedXdsClientPoolProvider.getDefaultProvider().setBootstrapOverride(bootstrap); } + /** + * Get an XdsClient pool. + * + * @deprecated Use InternalGrpcBootstrapperImpl.parseBootstrap() and pass the result to the other + * getOrCreate(). + */ + @Deprecated public static ObjectPool getOrCreate(String target) throws XdsInitializationException { return getOrCreate(target, new MetricRecorder() {}); } + /** + * Get an XdsClient pool. + * + * @deprecated Use InternalGrpcBootstrapperImpl.parseBootstrap() and pass the result to the other + * getOrCreate(). + */ + @Deprecated public static ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) throws XdsInitializationException { return getOrCreate(target, metricRecorder, null); } + /** + * Get an XdsClient pool. + * + * @deprecated Use InternalGrpcBootstrapperImpl.parseBootstrap() and pass the result to the other + * getOrCreate(). + */ + @Deprecated public static ObjectPool getOrCreate( String target, MetricRecorder metricRecorder, CallCredentials transportCallCredentials) throws XdsInitializationException { return SharedXdsClientPoolProvider.getDefaultProvider() .getOrCreate(target, metricRecorder, transportCallCredentials); } + + public static XdsClientResult getOrCreate( + String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder, + CallCredentials transportCallCredentials) { + return new XdsClientResult(SharedXdsClientPoolProvider.getDefaultProvider() + .getOrCreate(target, bootstrapInfo, metricRecorder, transportCallCredentials)); + } + + /** + * An ObjectPool, except without exposing io.grpc.internal, which must not be used for + * cross-package APIs. + */ + public static final class XdsClientResult { + private final ObjectPool xdsClientPool; + + XdsClientResult(ObjectPool xdsClientPool) { + this.xdsClientPool = xdsClientPool; + } + + public XdsClient getObject() { + return xdsClientPool.getObject(); + } + + public XdsClient returnObject(XdsClient xdsClient) { + return xdsClientPool.returnObject(xdsClient); + } + } } diff --git a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java index 5302880d48c..29b6870ab97 100644 --- a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java +++ b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java @@ -73,7 +73,7 @@ static SharedXdsClientPoolProvider getDefaultProvider() { return SharedXdsClientPoolProviderHolder.instance; } - @Override + @Deprecated public void setBootstrapOverride(Map bootstrap) { bootstrapOverride.set(bootstrap); } @@ -84,30 +84,36 @@ public ObjectPool get(String target) { return targetToXdsClientMap.get(target); } - @Override - public ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) + @Deprecated + public ObjectPool getOrCreate( + String target, MetricRecorder metricRecorder, CallCredentials transportCallCredentials) throws XdsInitializationException { - return getOrCreate(target, metricRecorder, null); + BootstrapInfo bootstrapInfo; + Map rawBootstrap = bootstrapOverride.get(); + if (rawBootstrap != null) { + bootstrapInfo = bootstrapper.bootstrap(rawBootstrap); + } else { + bootstrapInfo = bootstrapper.bootstrap(); + } + return getOrCreate(target, bootstrapInfo, metricRecorder, transportCallCredentials); } + @Override public ObjectPool getOrCreate( - String target, MetricRecorder metricRecorder, CallCredentials transportCallCredentials) - throws XdsInitializationException { + String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder) { + return getOrCreate(target, bootstrapInfo, metricRecorder, null); + } + + public ObjectPool getOrCreate( + String target, + BootstrapInfo bootstrapInfo, + MetricRecorder metricRecorder, + CallCredentials transportCallCredentials) { ObjectPool ref = targetToXdsClientMap.get(target); if (ref == null) { synchronized (lock) { ref = targetToXdsClientMap.get(target); if (ref == null) { - BootstrapInfo bootstrapInfo; - Map rawBootstrap = bootstrapOverride.get(); - if (rawBootstrap != null) { - bootstrapInfo = bootstrapper.bootstrap(rawBootstrap); - } else { - bootstrapInfo = bootstrapper.bootstrap(); - } - if (bootstrapInfo.servers().isEmpty()) { - throw new XdsInitializationException("No xDS server provided"); - } ref = new RefCountedXdsClientObjectPool( bootstrapInfo, target, metricRecorder, transportCallCredentials); @@ -157,9 +163,9 @@ class RefCountedXdsClientObjectPool implements ObjectPool { String target, MetricRecorder metricRecorder, CallCredentials transportCallCredentials) { - this.bootstrapInfo = checkNotNull(bootstrapInfo); + this.bootstrapInfo = checkNotNull(bootstrapInfo, "bootstrapInfo"); this.target = target; - this.metricRecorder = metricRecorder; + this.metricRecorder = checkNotNull(metricRecorder, "metricRecorder"); this.transportCallCredentials = transportCallCredentials; } diff --git a/xds/src/main/java/io/grpc/xds/XdsAttributes.java b/xds/src/main/java/io/grpc/xds/XdsAttributes.java index 0e770173219..5647b25e418 100644 --- a/xds/src/main/java/io/grpc/xds/XdsAttributes.java +++ b/xds/src/main/java/io/grpc/xds/XdsAttributes.java @@ -20,7 +20,6 @@ import io.grpc.EquivalentAddressGroup; import io.grpc.Grpc; import io.grpc.NameResolver; -import io.grpc.internal.ObjectPool; import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider; import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsClient; @@ -33,8 +32,8 @@ final class XdsAttributes { * Attribute key for passing around the XdsClient object pool across NameResolver/LoadBalancers. */ @NameResolver.ResolutionResultAttr - static final Attributes.Key> XDS_CLIENT_POOL = - Attributes.Key.create("io.grpc.xds.XdsAttributes.xdsClientPool"); + static final Attributes.Key XDS_CLIENT = + Attributes.Key.create("io.grpc.xds.XdsAttributes.xdsClient"); /** * Attribute key for passing around the latest XdsConfig across NameResolver/LoadBalancers. diff --git a/xds/src/main/java/io/grpc/xds/XdsClientPoolFactory.java b/xds/src/main/java/io/grpc/xds/XdsClientPoolFactory.java index f10d6504d79..6df8d566a7a 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClientPoolFactory.java +++ b/xds/src/main/java/io/grpc/xds/XdsClientPoolFactory.java @@ -18,20 +18,17 @@ import io.grpc.MetricRecorder; import io.grpc.internal.ObjectPool; +import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.XdsClient; -import io.grpc.xds.client.XdsInitializationException; import java.util.List; -import java.util.Map; import javax.annotation.Nullable; interface XdsClientPoolFactory { - void setBootstrapOverride(Map bootstrap); - @Nullable ObjectPool get(String target); - ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) - throws XdsInitializationException; + ObjectPool getOrCreate( + String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder); List getTargets(); } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 20001b6558d..196d51fb5a6 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -64,6 +64,7 @@ import io.grpc.xds.client.Bootstrapper.AuthorityInfo; import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.XdsClient; +import io.grpc.xds.client.XdsInitializationException; import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger.XdsLogLevel; import java.net.URI; @@ -80,6 +81,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import javax.annotation.Nullable; /** @@ -91,7 +93,6 @@ * @see XdsNameResolverProvider */ final class XdsNameResolver extends NameResolver { - static final CallOptions.Key CLUSTER_SELECTION_KEY = CallOptions.Key.create("io.grpc.xds.CLUSTER_SELECTION_KEY"); static final CallOptions.Key XDS_CONFIG_CALL_OPTION_KEY = @@ -118,7 +119,7 @@ final class XdsNameResolver extends NameResolver { private final ServiceConfigParser serviceConfigParser; private final SynchronizationContext syncContext; private final ScheduledExecutorService scheduler; - private final XdsClientPoolFactory xdsClientPoolFactory; + private final XdsClientPool xdsClientPool; private final ThreadSafeRandom random; private final FilterRegistry filterRegistry; private final XxHash64 hashFunc = XxHash64.INSTANCE; @@ -127,7 +128,6 @@ final class XdsNameResolver extends NameResolver { private final ConcurrentMap clusterRefs = new ConcurrentHashMap<>(); private final ConfigSelector configSelector = new ConfigSelector(); private final long randomChannelId; - private final MetricRecorder metricRecorder; private final Args nameResolverArgs; // Must be accessed in syncContext. // Filter instances are unique per channel, and per filter (name+typeUrl). @@ -136,7 +136,6 @@ final class XdsNameResolver extends NameResolver { private volatile RoutingConfig routingConfig; private Listener2 listener; - private ObjectPool xdsClientPool; private XdsClient xdsClient; private CallCounterProvider callCounterProvider; private ResolveState resolveState; @@ -148,7 +147,10 @@ final class XdsNameResolver extends NameResolver { @Nullable Map bootstrapOverride, MetricRecorder metricRecorder, Args nameResolverArgs) { this(targetUri, targetUri.getAuthority(), name, overrideAuthority, serviceConfigParser, - syncContext, scheduler, SharedXdsClientPoolProvider.getDefaultProvider(), + syncContext, scheduler, + bootstrapOverride == null + ? SharedXdsClientPoolProvider.getDefaultProvider() + : new SharedXdsClientPoolProvider(), ThreadSafeRandomImpl.instance, FilterRegistry.getDefaultRegistry(), bootstrapOverride, metricRecorder, nameResolverArgs); } @@ -173,12 +175,17 @@ final class XdsNameResolver extends NameResolver { this.serviceConfigParser = checkNotNull(serviceConfigParser, "serviceConfigParser"); this.syncContext = checkNotNull(syncContext, "syncContext"); this.scheduler = checkNotNull(scheduler, "scheduler"); - this.xdsClientPoolFactory = bootstrapOverride == null ? checkNotNull(xdsClientPoolFactory, - "xdsClientPoolFactory") : new SharedXdsClientPoolProvider(); - this.xdsClientPoolFactory.setBootstrapOverride(bootstrapOverride); + Supplier xdsClientSupplierArg = + nameResolverArgs.getArg(XdsNameResolverProvider.XDS_CLIENT_SUPPLIER); + if (xdsClientSupplierArg != null) { + this.xdsClientPool = new SupplierXdsClientPool(xdsClientSupplierArg); + } else { + checkNotNull(xdsClientPoolFactory, "xdsClientPoolFactory"); + this.xdsClientPool = new BootstrappingXdsClientPool( + xdsClientPoolFactory, target, bootstrapOverride, metricRecorder); + } this.random = checkNotNull(random, "random"); this.filterRegistry = checkNotNull(filterRegistry, "filterRegistry"); - this.metricRecorder = metricRecorder; this.nameResolverArgs = checkNotNull(nameResolverArgs, "nameResolverArgs"); randomChannelId = random.nextLong(); @@ -196,13 +203,12 @@ public String getServiceAuthority() { public void start(Listener2 listener) { this.listener = checkNotNull(listener, "listener"); try { - xdsClientPool = xdsClientPoolFactory.getOrCreate(target, metricRecorder); + xdsClient = xdsClientPool.getObject(); } catch (Exception e) { listener.onError( Status.UNAVAILABLE.withDescription("Failed to initialize xDS").withCause(e)); return; } - xdsClient = xdsClientPool.getObject(); BootstrapInfo bootstrapInfo = xdsClient.getBootstrapInfo(); String listenerNameTemplate; if (targetAuthority == null) { @@ -319,7 +325,7 @@ private void updateResolutionResult(XdsConfig xdsConfig) { ConfigOrError parsedServiceConfig = serviceConfigParser.parseServiceConfig(rawServiceConfig); Attributes attrs = Attributes.newBuilder() - .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) + .set(XdsAttributes.XDS_CLIENT, xdsClient) .set(XdsAttributes.XDS_CONFIG, xdsConfig) .set(XdsAttributes.XDS_CLUSTER_SUBSCRIPT_REGISTRY, resolveState.xdsDependencyManager) .set(XdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider) @@ -1034,4 +1040,72 @@ static ClusterRefState forRlsPlugin( return new ClusterRefState(refCount, null, rlsPluginConfig, null); } } + + /** An ObjectPool, except it can throw an exception. */ + private interface XdsClientPool { + XdsClient getObject() throws XdsInitializationException; + + XdsClient returnObject(XdsClient xdsClient); + } + + private static final class BootstrappingXdsClientPool implements XdsClientPool { + private final XdsClientPoolFactory xdsClientPoolFactory; + private final String target; + private final @Nullable Map bootstrapOverride; + private final @Nullable MetricRecorder metricRecorder; + private ObjectPool xdsClientPool; + + BootstrappingXdsClientPool( + XdsClientPoolFactory xdsClientPoolFactory, + String target, + @Nullable Map bootstrapOverride, + @Nullable MetricRecorder metricRecorder) { + this.xdsClientPoolFactory = checkNotNull(xdsClientPoolFactory, "xdsClientPoolFactory"); + this.target = checkNotNull(target, "target"); + this.bootstrapOverride = bootstrapOverride; + this.metricRecorder = metricRecorder; + } + + @Override + public XdsClient getObject() throws XdsInitializationException { + if (xdsClientPool == null) { + BootstrapInfo bootstrapInfo; + if (bootstrapOverride == null) { + bootstrapInfo = GrpcBootstrapperImpl.defaultBootstrap(); + } else { + bootstrapInfo = new GrpcBootstrapperImpl().bootstrap(bootstrapOverride); + } + this.xdsClientPool = + xdsClientPoolFactory.getOrCreate(target, bootstrapInfo, metricRecorder); + } + return xdsClientPool.getObject(); + } + + @Override + public XdsClient returnObject(XdsClient xdsClient) { + return xdsClientPool.returnObject(xdsClient); + } + } + + private static final class SupplierXdsClientPool implements XdsClientPool { + private final Supplier xdsClientSupplier; + + SupplierXdsClientPool(Supplier xdsClientSupplier) { + this.xdsClientSupplier = checkNotNull(xdsClientSupplier, "xdsClientSupplier"); + } + + @Override + public XdsClient getObject() throws XdsInitializationException { + XdsClient xdsClient = xdsClientSupplier.get(); + if (xdsClient == null) { + throw new XdsInitializationException("Caller failed to initialize XDS_CLIENT_SUPPLIER"); + } + return xdsClient; + } + + @Override + public XdsClient returnObject(XdsClient xdsClient) { + return null; + } + } } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java b/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java index eb3887396a0..e3462276b17 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java @@ -22,6 +22,7 @@ import io.grpc.Internal; import io.grpc.NameResolver.Args; import io.grpc.NameResolverProvider; +import io.grpc.xds.client.XdsClient; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; @@ -29,6 +30,7 @@ import java.util.Collections; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; import javax.annotation.Nullable; /** @@ -43,6 +45,13 @@ */ @Internal public final class XdsNameResolverProvider extends NameResolverProvider { + /** + * If provided, the suppler must return non-null when lb.start() is called (which implies not + * throwing), and the XdsClient must remain alive until lb.shutdown() returns. It may only be + * called from the synchronization context. + */ + public static final Args.Key> XDS_CLIENT_SUPPLIER = + Args.Key.create("io.grpc.xds.XdsNameResolverProvider.XDS_CLIENT_SUPPLIER"); private static final String SCHEME = "xds"; private final String scheme; diff --git a/xds/src/main/java/io/grpc/xds/XdsServerBuilder.java b/xds/src/main/java/io/grpc/xds/XdsServerBuilder.java index 928f22c4d5e..4a4fb71aa84 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerBuilder.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerBuilder.java @@ -55,6 +55,7 @@ public final class XdsServerBuilder extends ForwardingServerBuilder bootstrapOverride; private long drainGraceTime = 10; private TimeUnit drainGraceTimeUnit = TimeUnit.MINUTES; @@ -127,7 +128,7 @@ public Server build() { } InternalNettyServerBuilder.eagAttributes(delegate, builder.build()); return new XdsServerWrapper("0.0.0.0:" + port, delegate, xdsServingStatusListener, - filterChainSelectorManager, xdsClientPoolFactory, filterRegistry); + filterChainSelectorManager, xdsClientPoolFactory, bootstrapOverride, filterRegistry); } @VisibleForTesting @@ -140,11 +141,10 @@ XdsServerBuilder xdsClientPoolFactory(XdsClientPoolFactory xdsClientPoolFactory) * Allows providing bootstrap override, useful for testing. */ public XdsServerBuilder overrideBootstrapForTest(Map bootstrapOverride) { - checkNotNull(bootstrapOverride, "bootstrapOverride"); + this.bootstrapOverride = checkNotNull(bootstrapOverride, "bootstrapOverride"); if (this.xdsClientPoolFactory == SharedXdsClientPoolProvider.getDefaultProvider()) { this.xdsClientPoolFactory = new SharedXdsClientPoolProvider(); } - this.xdsClientPoolFactory.setBootstrapOverride(bootstrapOverride); return this; } diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index be64b56eef5..f54c4af5cff 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -56,6 +56,7 @@ import io.grpc.xds.XdsListenerResource.LdsUpdate; import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import io.grpc.xds.XdsServerBuilder.XdsServingStatusListener; +import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsClient.ResourceWatcher; import io.grpc.xds.internal.security.SslContextProviderSupplier; @@ -103,6 +104,7 @@ public void uncaughtException(Thread t, Throwable e) { private final FilterRegistry filterRegistry; private final ThreadSafeRandom random = ThreadSafeRandomImpl.instance; private final XdsClientPoolFactory xdsClientPoolFactory; + private final @Nullable Map bootstrapOverride; private final XdsServingStatusListener listener; private final FilterChainSelectorManager filterChainSelectorManager; private final AtomicBoolean started = new AtomicBoolean(false); @@ -131,9 +133,17 @@ public void uncaughtException(Thread t, Throwable e) { XdsServingStatusListener listener, FilterChainSelectorManager filterChainSelectorManager, XdsClientPoolFactory xdsClientPoolFactory, + @Nullable Map bootstrapOverride, FilterRegistry filterRegistry) { - this(listenerAddress, delegateBuilder, listener, filterChainSelectorManager, - xdsClientPoolFactory, filterRegistry, SharedResourceHolder.get(GrpcUtil.TIMER_SERVICE)); + this( + listenerAddress, + delegateBuilder, + listener, + filterChainSelectorManager, + xdsClientPoolFactory, + bootstrapOverride, + filterRegistry, + SharedResourceHolder.get(GrpcUtil.TIMER_SERVICE)); sharedTimeService = true; } @@ -144,6 +154,7 @@ public void uncaughtException(Thread t, Throwable e) { XdsServingStatusListener listener, FilterChainSelectorManager filterChainSelectorManager, XdsClientPoolFactory xdsClientPoolFactory, + @Nullable Map bootstrapOverride, FilterRegistry filterRegistry, ScheduledExecutorService timeService) { this.listenerAddress = checkNotNull(listenerAddress, "listenerAddress"); @@ -153,6 +164,7 @@ public void uncaughtException(Thread t, Throwable e) { this.filterChainSelectorManager = checkNotNull(filterChainSelectorManager, "filterChainSelectorManager"); this.xdsClientPoolFactory = checkNotNull(xdsClientPoolFactory, "xdsClientPoolFactory"); + this.bootstrapOverride = bootstrapOverride; this.timeService = checkNotNull(timeService, "timeService"); this.filterRegistry = checkNotNull(filterRegistry,"filterRegistry"); this.delegate = delegateBuilder.build(); @@ -182,7 +194,14 @@ public void run() { private void internalStart() { try { - xdsClientPool = xdsClientPoolFactory.getOrCreate("#server", new MetricRecorder() {}); + BootstrapInfo bootstrapInfo; + if (bootstrapOverride == null) { + bootstrapInfo = GrpcBootstrapperImpl.defaultBootstrap(); + } else { + bootstrapInfo = new GrpcBootstrapperImpl().bootstrap(bootstrapOverride); + } + xdsClientPool = xdsClientPoolFactory.getOrCreate( + "#server", bootstrapInfo, new MetricRecorder() {}); } catch (Exception e) { StatusException statusException = Status.UNAVAILABLE.withDescription( "Failed to initialize xDS").withCause(e).asException(); diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index 50138cb8e5a..af618beda6a 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -55,7 +55,6 @@ import io.grpc.Status.Code; import io.grpc.SynchronizationContext; import io.grpc.internal.FakeClock; -import io.grpc.internal.ObjectPool; import io.grpc.internal.PickFirstLoadBalancerProvider; import io.grpc.internal.PickSubchannelArgsImpl; import io.grpc.protobuf.ProtoUtils; @@ -140,19 +139,6 @@ public void uncaughtException(Thread t, Throwable e) { private final LoadStatsManager2 loadStatsManager = new LoadStatsManager2(fakeClock.getStopwatchSupplier()); private final FakeXdsClient xdsClient = new FakeXdsClient(); - private final ObjectPool xdsClientPool = new ObjectPool() { - @Override - public XdsClient getObject() { - xdsClientRefs++; - return xdsClient; - } - - @Override - public XdsClient returnObject(Object object) { - xdsClientRefs--; - return null; - } - }; private final CallCounterProvider callCounterProvider = new CallCounterProvider() { @Override public AtomicLong getOrCreate(String cluster, @Nullable String edsServiceName) { @@ -199,8 +185,8 @@ public void handleResolvedAddresses_propagateToChildPolicy() { FakeLoadBalancer childBalancer = Iterables.getOnlyElement(downstreamBalancers); assertThat(Iterables.getOnlyElement(childBalancer.addresses)).isEqualTo(endpoint); assertThat(childBalancer.config).isSameInstanceAs(weightedTargetConfig); - assertThat(childBalancer.attributes.get(io.grpc.xds.XdsAttributes.XDS_CLIENT_POOL)) - .isSameInstanceAs(xdsClientPool); + assertThat(childBalancer.attributes.get(io.grpc.xds.XdsAttributes.XDS_CLIENT)) + .isSameInstanceAs(xdsClient); assertThat(childBalancer.attributes.get(NameResolver.ATTR_BACKEND_SERVICE)).isEqualTo(CLUSTER); } @@ -673,7 +659,7 @@ public void dropRpcsWithRespectToLbConfigDropCategories() { .setAddresses(Collections.singletonList(endpoint)) .setAttributes( Attributes.newBuilder() - .set(io.grpc.xds.XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) + .set(io.grpc.xds.XdsAttributes.XDS_CLIENT, xdsClient) .build()) .setLoadBalancingPolicyConfig(config) .build()); @@ -1070,7 +1056,7 @@ private void deliverAddressesAndConfig(List addresses, .setAddresses(addresses) .setAttributes( Attributes.newBuilder() - .set(io.grpc.xds.XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) + .set(io.grpc.xds.XdsAttributes.XDS_CLIENT, xdsClient) .set(io.grpc.xds.XdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider) .build()) .setLoadBalancingPolicyConfig(config) diff --git a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java index 573aef7ca1e..e8bd7461736 100644 --- a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java +++ b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java @@ -513,13 +513,8 @@ public List getTargets() { } @Override - public void setBootstrapOverride(Map bootstrap) { - throw new UnsupportedOperationException("Should not be called"); - } - - - @Override - public ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) { + public ObjectPool getOrCreate( + String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder) { throw new UnsupportedOperationException("Should not be called"); } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java index 3f93cc6f191..2b7bd53d5ef 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -84,6 +84,19 @@ public void restoreEnvironment() { CommonBootstrapperTestUtils.setEnableXdsFallback(originalExperimentalXdsFallbackFlag); } + @Test + public void parseBootstrap_emptyServers_throws() { + String rawData = "{\n" + + " \"xds_servers\": [\n" + + " ]\n" + + "}"; + + bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); + XdsInitializationException e = Assert.assertThrows(XdsInitializationException.class, + bootstrapper::bootstrap); + assertThat(e).hasMessageThat().isEqualTo("Invalid bootstrap: 'xds_servers' is empty"); + } + @Test public void parseBootstrap_singleXdsServer() throws XdsInitializationException { String rawData = "{\n" @@ -352,7 +365,14 @@ public void parseBootstrap_serverWithoutServerUri() throws XdsInitializationExce public void parseBootstrap_certProviderInstances() throws XdsInitializationException { String rawData = "{\n" - + " \"xds_servers\": [],\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\"type\": \"insecure\"}\n" + + " ]\n" + + " }\n" + + " ],\n" + " \"certificate_providers\": {\n" + " \"gcp_id\": {\n" + " \"plugin_name\": \"meshca\",\n" @@ -389,7 +409,6 @@ public void parseBootstrap_certProviderInstances() throws XdsInitializationExcep bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); BootstrapInfo info = bootstrapper.bootstrap(); - assertThat(info.servers()).isEmpty(); assertThat(info.node()).isEqualTo(getNodeBuilder().build()); Map certProviders = info.certProviders(); assertThat(certProviders).isNotNull(); @@ -556,7 +575,14 @@ public void parseBootstrap_missingPluginName() { @Test public void parseBootstrap_grpcServerResourceId() throws XdsInitializationException { String rawData = "{\n" - + " \"xds_servers\": [],\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\"type\": \"insecure\"}\n" + + " ]\n" + + " }\n" + + " ],\n" + " \"server_listener_resource_name_template\": \"grpc/serverx=%s\"\n" + "}"; @@ -779,6 +805,12 @@ public void fallbackToConfigFromSysProp() throws XdsInitializationException { public void parseClientDefaultListenerResourceNameTemplate() throws Exception { String rawData = "{\n" + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\"type\": \"insecure\"}\n" + + " ]\n" + + " }\n" + " ]\n" + "}"; bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); @@ -788,6 +820,12 @@ public void parseClientDefaultListenerResourceNameTemplate() throws Exception { rawData = "{\n" + " \"client_default_listener_resource_name_template\": \"xdstp://a.com/faketype/%s\",\n" + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\"type\": \"insecure\"}\n" + + " ]\n" + + " }\n" + " ]\n" + "}"; bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); diff --git a/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java b/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java index 24f1750d5a8..29b149f166f 100644 --- a/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java @@ -19,7 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; -import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -74,16 +74,24 @@ public class SharedXdsClientPoolProviderTest { private GrpcBootstrapperImpl bootstrapper; @Mock private ResourceWatcher ldsResourceWatcher; + @Deprecated @Test - public void noServer() throws XdsInitializationException { + public void sharedXdsClientObjectPool_deprecated() throws XdsInitializationException { + ServerInfo server = ServerInfo.create(SERVER_URI, InsecureChannelCredentials.create()); BootstrapInfo bootstrapInfo = - BootstrapInfo.builder().servers(Collections.emptyList()).node(node).build(); + BootstrapInfo.builder().servers(Collections.singletonList(server)).node(node).build(); when(bootstrapper.bootstrap()).thenReturn(bootstrapInfo); + SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); - XdsInitializationException e = assertThrows(XdsInitializationException.class, - () -> provider.getOrCreate(DUMMY_TARGET, metricRecorder)); - assertThat(e).hasMessageThat().isEqualTo("No xDS server provided"); assertThat(provider.get(DUMMY_TARGET)).isNull(); + ObjectPool xdsClientPool = + provider.getOrCreate(DUMMY_TARGET, metricRecorder, null); + verify(bootstrapper).bootstrap(); + assertThat(provider.getOrCreate(DUMMY_TARGET, bootstrapInfo, metricRecorder)) + .isSameInstanceAs(xdsClientPool); + assertThat(provider.get(DUMMY_TARGET)).isNotNull(); + assertThat(provider.get(DUMMY_TARGET)).isSameInstanceAs(xdsClientPool); + verifyNoMoreInteractions(bootstrapper); } @Test @@ -91,13 +99,14 @@ public void sharedXdsClientObjectPool() throws XdsInitializationException { ServerInfo server = ServerInfo.create(SERVER_URI, InsecureChannelCredentials.create()); BootstrapInfo bootstrapInfo = BootstrapInfo.builder().servers(Collections.singletonList(server)).node(node).build(); - when(bootstrapper.bootstrap()).thenReturn(bootstrapInfo); SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); assertThat(provider.get(DUMMY_TARGET)).isNull(); - ObjectPool xdsClientPool = provider.getOrCreate(DUMMY_TARGET, metricRecorder); - verify(bootstrapper).bootstrap(); - assertThat(provider.getOrCreate(DUMMY_TARGET, metricRecorder)).isSameInstanceAs(xdsClientPool); + ObjectPool xdsClientPool = + provider.getOrCreate(DUMMY_TARGET, bootstrapInfo, metricRecorder); + verify(bootstrapper, never()).bootstrap(); + assertThat(provider.getOrCreate(DUMMY_TARGET, bootstrapInfo, metricRecorder)) + .isSameInstanceAs(xdsClientPool); assertThat(provider.get(DUMMY_TARGET)).isNotNull(); assertThat(provider.get(DUMMY_TARGET)).isSameInstanceAs(xdsClientPool); verifyNoMoreInteractions(bootstrapper); @@ -108,7 +117,7 @@ public void refCountedXdsClientObjectPool_delayedCreation() { ServerInfo server = ServerInfo.create(SERVER_URI, InsecureChannelCredentials.create()); BootstrapInfo bootstrapInfo = BootstrapInfo.builder().servers(Collections.singletonList(server)).node(node).build(); - SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); + SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(); RefCountedXdsClientObjectPool xdsClientPool = provider.new RefCountedXdsClientObjectPool(bootstrapInfo, DUMMY_TARGET, metricRecorder); assertThat(xdsClientPool.getXdsClientForTest()).isNull(); @@ -122,7 +131,7 @@ public void refCountedXdsClientObjectPool_refCounted() { ServerInfo server = ServerInfo.create(SERVER_URI, InsecureChannelCredentials.create()); BootstrapInfo bootstrapInfo = BootstrapInfo.builder().servers(Collections.singletonList(server)).node(node).build(); - SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); + SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(); RefCountedXdsClientObjectPool xdsClientPool = provider.new RefCountedXdsClientObjectPool(bootstrapInfo, DUMMY_TARGET, metricRecorder); // getObject once @@ -143,7 +152,7 @@ public void refCountedXdsClientObjectPool_getObjectCreatesNewInstanceIfAlreadySh ServerInfo server = ServerInfo.create(SERVER_URI, InsecureChannelCredentials.create()); BootstrapInfo bootstrapInfo = BootstrapInfo.builder().servers(Collections.singletonList(server)).node(node).build(); - SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); + SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(); RefCountedXdsClientObjectPool xdsClientPool = provider.new RefCountedXdsClientObjectPool(bootstrapInfo, DUMMY_TARGET, metricRecorder); XdsClient xdsClient1 = xdsClientPool.getObject(); @@ -189,8 +198,7 @@ public void xdsClient_usesCallCredentials() throws Exception { ServerInfo server = ServerInfo.create(xdsServerUri, InsecureChannelCredentials.create()); BootstrapInfo bootstrapInfo = BootstrapInfo.builder().servers(Collections.singletonList(server)).node(node).build(); - when(bootstrapper.bootstrap()).thenReturn(bootstrapInfo); - SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); + SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(); // Create custom xDS transport CallCredentials CallCredentials sampleCreds = @@ -199,7 +207,7 @@ public void xdsClient_usesCallCredentials() throws Exception { // Create xDS client that uses the CallCredentials on the transport ObjectPool xdsClientPool = - provider.getOrCreate("target", metricRecorder, sampleCreds); + provider.getOrCreate("target", bootstrapInfo, metricRecorder, sampleCreds); XdsClient xdsClient = xdsClientPool.getObject(); xdsClient.watchXdsResource( XdsListenerResource.getInstance(), "someLDSresource", ldsResourceWatcher); diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java index 1e7ce6dc2a2..09b4c5d8637 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java @@ -178,8 +178,10 @@ public void setUp() throws XdsInitializationException { setAdsConfig(fallbackServer, FALLBACK_SERVER); SharedXdsClientPoolProvider clientPoolProvider = new SharedXdsClientPoolProvider(); - clientPoolProvider.setBootstrapOverride(defaultBootstrapOverride()); - xdsClientPool = clientPoolProvider.getOrCreate(DUMMY_TARGET, metricRecorder); + xdsClientPool = clientPoolProvider.getOrCreate( + DUMMY_TARGET, + new GrpcBootstrapperImpl().bootstrap(defaultBootstrapOverride()), + metricRecorder); } @After diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java index ee32c4490af..24ef60c4685 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java @@ -79,8 +79,10 @@ public class XdsClientFederationTest { @Before public void setUp() throws XdsInitializationException { SharedXdsClientPoolProvider clientPoolProvider = new SharedXdsClientPoolProvider(); - clientPoolProvider.setBootstrapOverride(defaultBootstrapOverride()); - xdsClientPool = clientPoolProvider.getOrCreate(DUMMY_TARGET, metricRecorder); + xdsClientPool = clientPoolProvider.getOrCreate( + DUMMY_TARGET, + new GrpcBootstrapperImpl().bootstrap(defaultBootstrapOverride()), + metricRecorder); xdsClient = xdsClientPool.getObject(); } diff --git a/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java b/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java index f997f583898..53dcd15dd3b 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java @@ -120,7 +120,8 @@ public void setUp() { when(mockBuilder.build()).thenReturn(mockServer); when(mockServer.isShutdown()).thenReturn(false); xdsServerWrapper = new XdsServerWrapper("0.0.0.0:" + PORT, mockBuilder, listener, - selectorManager, new FakeXdsClientPoolFactory(xdsClient), FilterRegistry.newRegistry()); + selectorManager, new FakeXdsClientPoolFactory(xdsClient), + XdsServerTestHelper.RAW_BOOTSTRAP, FilterRegistry.newRegistry()); } @Test diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 6edd98f923e..d090687a072 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -101,7 +101,6 @@ import io.grpc.xds.client.Bootstrapper.ServerInfo; import io.grpc.xds.client.EnvoyProtoData.Node; import io.grpc.xds.client.XdsClient; -import io.grpc.xds.client.XdsInitializationException; import io.grpc.xds.client.XdsResourceType; import java.io.IOException; import java.net.URI; @@ -174,6 +173,15 @@ public ConfigOrError parseServiceConfig(Map rawServiceConfig) { private final CallInfo call2 = new CallInfo("GreetService", "bye"); private final TestChannel channel = new TestChannel(); private final MetricRecorder metricRecorder = new MetricRecorder() {}; + private final Map rawBootstrap = ImmutableMap.of( + "xds_servers", ImmutableList.of( + ImmutableMap.of( + "server_uri", "td.googleapis.com", + "channel_creds", ImmutableList.of( + ImmutableMap.of( + "type", "insecure"))) + )); + private BootstrapInfo bootstrapInfo = BootstrapInfo.builder() .servers(ImmutableList.of(ServerInfo.create( "td.googleapis.com", InsecureChannelCredentials.create()))) @@ -230,7 +238,8 @@ public void setUp() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, filterRegistry, null, metricRecorder, nameResolverArgs); + xdsClientPoolFactory, mockRandom, filterRegistry, rawBootstrap, metricRecorder, + nameResolverArgs); } @After @@ -250,46 +259,23 @@ public void tearDown() { @Test public void resolving_failToCreateXdsClientPool() { - XdsClientPoolFactory xdsClientPoolFactory = new XdsClientPoolFactory() { - @Override - public void setBootstrapOverride(Map bootstrap) { - } - - @Override - @Nullable - public ObjectPool get(String target) { - throw new UnsupportedOperationException("Should not be called"); - } - - @Override - public ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) - throws XdsInitializationException { - throw new XdsInitializationException("Fail to read bootstrap file"); - } - - @Override - public List getTargets() { - return null; - } - }; - resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder, nameResolverArgs); + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), + Collections.emptyMap(), metricRecorder, nameResolverArgs); resolver.start(mockListener); verify(mockListener).onError(errorCaptor.capture()); Status error = errorCaptor.getValue(); assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(error.getDescription()).isEqualTo("Failed to initialize xDS"); - assertThat(error.getCause()).hasMessageThat().isEqualTo("Fail to read bootstrap file"); + assertThat(error.getCause()).hasMessageThat().contains("Invalid bootstrap"); } @Test public void resolving_withTargetAuthorityNotFound() { resolver = new XdsNameResolver(targetUri, "notfound.google.com", AUTHORITY, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), rawBootstrap, metricRecorder, nameResolverArgs); resolver.start(mockListener); verify(mockListener).onError(errorCaptor.capture()); @@ -312,7 +298,8 @@ public void resolving_noTargetAuthority_templateWithoutXdstp() { resolver = new XdsNameResolver( targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, - mockRandom, FilterRegistry.getDefaultRegistry(), null, metricRecorder, nameResolverArgs); + mockRandom, FilterRegistry.getDefaultRegistry(), rawBootstrap, metricRecorder, + nameResolverArgs); resolver.start(mockListener); verify(mockListener, never()).onError(any(Status.class)); } @@ -332,7 +319,7 @@ public void resolving_noTargetAuthority_templateWithXdstp() { + "%5B::FFFF:129.144.52.38%5D:80?id=1"; resolver = new XdsNameResolver( targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), rawBootstrap, metricRecorder, nameResolverArgs); resolver.start(mockListener); verify(mockListener, never()).onError(any(Status.class)); @@ -353,7 +340,7 @@ public void resolving_noTargetAuthority_xdstpWithMultipleSlashes() { + "path/to/service?id=1"; resolver = new XdsNameResolver( targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), rawBootstrap, metricRecorder, nameResolverArgs); @@ -382,7 +369,7 @@ public void resolving_targetAuthorityInAuthoritiesMap() { + "%5B::FFFF:129.144.52.38%5D:80?bar=2&foo=1"; // query param canonified resolver = new XdsNameResolver(targetUri, "xds.authority.com", serviceAuthority, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), rawBootstrap, metricRecorder, nameResolverArgs); resolver.start(mockListener); verify(mockListener, never()).onError(any(Status.class)); @@ -415,7 +402,7 @@ public void resolving_ldsResourceUpdateRdsName() { .build(); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), rawBootstrap, metricRecorder, nameResolverArgs); // use different ldsResourceName and service authority. The virtualhost lookup should use // service authority. @@ -617,7 +604,7 @@ public void resolving_matchingVirtualHostNotFound_matchingOverrideAuthority() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, "random", serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), rawBootstrap, metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); @@ -642,7 +629,7 @@ public void resolving_matchingVirtualHostNotFound_notMatchingOverrideAuthority() resolver = new XdsNameResolver(targetUri, null, AUTHORITY, "random", serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), rawBootstrap, metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); @@ -655,7 +642,7 @@ public void resolving_matchingVirtualHostNotFound_notMatchingOverrideAuthority() public void resolving_matchingVirtualHostNotFoundForOverrideAuthority() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, AUTHORITY, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), rawBootstrap, metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); @@ -740,8 +727,8 @@ public void retryPolicyInPerMethodConfigGeneratedByResolverIsValid() { ServiceConfigParser realParser = new ScParser( true, 5, 5, new AutoConfiguredLoadBalancerFactory("pick-first")); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, realParser, syncContext, - scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, - metricRecorder, nameResolverArgs); + scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), + rawBootstrap, metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); RetryPolicy retryPolicy = RetryPolicy.create( @@ -822,7 +809,7 @@ public void resolved_simpleCallFailedToRoute_routeWithNonForwardingAction() { assertServiceConfigForLoadBalancingConfig( Collections.singletonList(cluster2), (Map) result.getServiceConfig().getConfig()); - assertThat(result.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL)).isNotNull(); + assertThat(result.getAttributes().get(XdsAttributes.XDS_CLIENT)).isNotNull(); assertThat(result.getAttributes().get(XdsAttributes.CALL_COUNTER_PROVIDER)).isNotNull(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); // Simulates making a call1 RPC. @@ -952,7 +939,7 @@ public void resolved_rpcHashingByChannelId() { when(mockRandom.nextLong()).thenReturn(123L); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, - xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, + xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), rawBootstrap, metricRecorder, nameResolverArgs); resolver.start(mockListener); xdsClient = (FakeXdsClient) resolver.getXdsClient(); @@ -986,7 +973,7 @@ public void resolved_rpcHashingByChannelId() { public void resolved_routeActionHasAutoHostRewrite_emitsCallOptionForTheSame() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, - FilterRegistry.getDefaultRegistry(), null, metricRecorder, nameResolverArgs); + FilterRegistry.getDefaultRegistry(), rawBootstrap, metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( @@ -1017,7 +1004,7 @@ public void resolved_routeActionHasAutoHostRewrite_emitsCallOptionForTheSame() { public void resolved_routeActionNoAutoHostRewrite_doesntEmitCallOptionForTheSame() { resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, - FilterRegistry.getDefaultRegistry(), null, metricRecorder, nameResolverArgs); + FilterRegistry.getDefaultRegistry(), rawBootstrap, metricRecorder, nameResolverArgs); resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( @@ -1235,7 +1222,7 @@ public void resolved_simpleCallSucceeds_routeToWeightedCluster() { assertThat(result.getAddressesOrError().getValue()).isEmpty(); assertServiceConfigForLoadBalancingConfig( Arrays.asList(cluster1, cluster2), (Map) result.getServiceConfig().getConfig()); - assertThat(result.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL)).isNotNull(); + assertThat(result.getAttributes().get(XdsAttributes.XDS_CLIENT)).isNotNull(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); assertCallSelectClusterResult(call1, configSelector, cluster2, 20.0); assertCallSelectClusterResult(call1, configSelector, cluster1, 20.0); @@ -1300,7 +1287,7 @@ public void resolved_simpleCallSucceeds_routeToRls() { ImmutableList.of(ImmutableMap.of("rls_experimental", expectedRlsLbConfig))))); assertThat(clusterManagerLbConfig).isEqualTo(expectedClusterManagerLbConfig); - assertThat(result.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL)).isNotNull(); + assertThat(result.getAttributes().get(XdsAttributes.XDS_CLIENT)).isNotNull(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); assertCallSelectRlsPluginResult( call1, configSelector, "rls-plugin-foo", 20.0); @@ -1489,7 +1476,7 @@ public void filterState_specialCase_sameNameDifferentTypeUrl() { FilterRegistry filterRegistry = FilterRegistry.newRegistry() .register(statefulFilterProvider, altStatefulFilterProvider, ROUTER_FILTER_PROVIDER); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, - syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, null, + syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, rawBootstrap, metricRecorder, nameResolverArgs); resolver.start(mockListener); @@ -1627,7 +1614,7 @@ private StatefulFilter.Provider filterStateTestSetupResolver() { FilterRegistry filterRegistry = FilterRegistry.newRegistry() .register(statefulFilterProvider, ROUTER_FILTER_PROVIDER); resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, - syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, null, + syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, rawBootstrap, metricRecorder, nameResolverArgs); resolver.start(mockListener); return statefulFilterProvider; @@ -1761,7 +1748,7 @@ private InternalConfigSelector resolveToClusters() { assertThat(result.getAddressesOrError().getValue()).isEmpty(); assertServiceConfigForLoadBalancingConfig( Arrays.asList(cluster1, cluster2), (Map) result.getServiceConfig().getConfig()); - assertThat(result.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL)).isNotNull(); + assertThat(result.getAttributes().get(XdsAttributes.XDS_CLIENT)).isNotNull(); assertThat(result.getAttributes().get(XdsAttributes.CALL_COUNTER_PROVIDER)).isNotNull(); return result.getAttributes().get(InternalConfigSelector.KEY); } @@ -2423,9 +2410,6 @@ private final class FakeXdsClientPoolFactory implements XdsClientPoolFactory { Set targets = new HashSet<>(); XdsClient xdsClient = new FakeXdsClient(); - @Override - public void setBootstrapOverride(Map bootstrap) {} - @Override @Nullable public ObjectPool get(String target) { @@ -2433,8 +2417,8 @@ public ObjectPool get(String target) { } @Override - public ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) - throws XdsInitializationException { + public ObjectPool getOrCreate( + String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder) { targets.add(target); return new ObjectPool() { @Override diff --git a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java index 6e4d243e904..9141147702f 100644 --- a/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsSecurityClientServerTest.java @@ -753,6 +753,7 @@ private void buildServerWithFallbackServerCredentials( ServerCredentials xdsCredentials = XdsServerCredentials.create(fallbackCredentials); XdsServerBuilder builder = XdsServerBuilder.forPort(0, xdsCredentials) .xdsClientPoolFactory(fakePoolFactory) + .overrideBootstrapForTest(XdsServerTestHelper.RAW_BOOTSTRAP) .addService(new SimpleServiceImpl()); buildServer(builder, downstreamTlsContext); } @@ -872,7 +873,18 @@ public void run() { } } }); - xdsClient.ldsResource.get(8000, TimeUnit.MILLISECONDS); + try { + xdsClient.ldsResource.get(8000, TimeUnit.MILLISECONDS); + } catch (Exception ex) { + // start() probably failed, so throw its exception + if (settableFuture.isDone()) { + Throwable t = settableFuture.get(); + if (t != null) { + throw new Exception(t); + } + } + throw ex; + } return settableFuture; } diff --git a/xds/src/test/java/io/grpc/xds/XdsServerBuilderTest.java b/xds/src/test/java/io/grpc/xds/XdsServerBuilderTest.java index 2c168c65869..40225d3860d 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerBuilderTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerBuilderTest.java @@ -43,7 +43,6 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.SocketAddress; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -84,6 +83,7 @@ private void buildBuilder(XdsServerBuilder.XdsServingStatusListener xdsServingSt XdsServerBuilder.forPort( port, XdsServerCredentials.create(InsecureServerCredentials.create())); builder.xdsClientPoolFactory(xdsClientPoolFactory); + builder.overrideBootstrapForTest(XdsServerTestHelper.RAW_BOOTSTRAP); if (xdsServingStatusListener != null) { builder.xdsServingStatusListener(xdsServingStatusListener); } @@ -138,7 +138,18 @@ public void run() { } } }); - xdsClient.ldsResource.get(5000, TimeUnit.MILLISECONDS); + try { + xdsClient.ldsResource.get(5000, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + // start() probably failed, so throw its exception + if (settableFuture.isDone()) { + Throwable t = settableFuture.get(); + if (t != null) { + throw new ExecutionException(t); + } + } + throw ex; + } return settableFuture; } @@ -304,9 +315,12 @@ public void drainGraceTime_negativeThrows() throws IOException { @Test public void testOverrideBootstrap() throws Exception { - Map b = new HashMap<>(); + Map b = XdsServerTestHelper.RAW_BOOTSTRAP; buildBuilder(null); builder.overrideBootstrapForTest(b); - assertThat(xdsClientPoolFactory.savedBootstrap).isEqualTo(b); + xdsServer = cleanupRule.register((XdsServerWrapper) builder.build()); + Future unused = startServerAsync(); + assertThat(xdsClientPoolFactory.savedBootstrapInfo.node().getId()) + .isEqualTo(XdsServerTestHelper.BOOTSTRAP_INFO.node().getId()); } } diff --git a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java index b0472b4729d..a9236ac77ca 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java @@ -37,7 +37,6 @@ import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.EnvoyProtoData; import io.grpc.xds.client.XdsClient; -import io.grpc.xds.client.XdsInitializationException; import io.grpc.xds.client.XdsResourceType; import java.time.Duration; import java.util.ArrayList; @@ -63,6 +62,17 @@ public class XdsServerTestHelper { "projects/42/networks/default/nodes/5c85b298-6f5b-4722-b74a-f7d1f0ccf5ad"; private static final EnvoyProtoData.Node BOOTSTRAP_NODE = EnvoyProtoData.Node.newBuilder().setId(NODE_ID).build(); + static final Map RAW_BOOTSTRAP = ImmutableMap.of( + "node", ImmutableMap.of( + "id", NODE_ID), + "server_listener_resource_name_template", "grpc/server?udpa.resource.listening_address=%s", + "xds_servers", ImmutableList.of( + ImmutableMap.of( + "server_uri", SERVER_URI, + "channel_creds", ImmutableList.of( + ImmutableMap.of( + "type", "insecure"))) + )); static final Bootstrapper.BootstrapInfo BOOTSTRAP_INFO = Bootstrapper.BootstrapInfo.builder() .servers(Arrays.asList( @@ -140,17 +150,12 @@ static final class FakeXdsClientPoolFactory implements XdsClientPoolFactory { private XdsClient xdsClient; - Map savedBootstrap; + BootstrapInfo savedBootstrapInfo; FakeXdsClientPoolFactory(XdsClient xdsClient) { this.xdsClient = xdsClient; } - @Override - public void setBootstrapOverride(Map bootstrap) { - this.savedBootstrap = bootstrap; - } - @Override @Nullable public ObjectPool get(String target) { @@ -158,8 +163,9 @@ public ObjectPool get(String target) { } @Override - public ObjectPool getOrCreate(String target, MetricRecorder metricRecorder) - throws XdsInitializationException { + public ObjectPool getOrCreate( + String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder) { + this.savedBootstrapInfo = bootstrapInfo; return new ObjectPool() { @Override public XdsClient getObject() { diff --git a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java index ce1c6b94a35..99e9907f7d0 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java @@ -135,6 +135,7 @@ public void setup() { when(mockBuilder.build()).thenReturn(mockServer); xdsServerWrapper = new XdsServerWrapper("0.0.0.0:1", mockBuilder, listener, selectorManager, new FakeXdsClientPoolFactory(xdsClient), + XdsServerTestHelper.RAW_BOOTSTRAP, filterRegistry, executor.getScheduledExecutorService()); } @@ -157,7 +158,8 @@ public void testBootstrap() throws Exception { XdsListenerResource listenerResource = XdsListenerResource.getInstance(); when(xdsClient.getBootstrapInfo()).thenReturn(b); xdsServerWrapper = new XdsServerWrapper("[::FFFF:129.144.52.38]:80", mockBuilder, listener, - selectorManager, new FakeXdsClientPoolFactory(xdsClient), filterRegistry); + selectorManager, new FakeXdsClientPoolFactory(xdsClient), + XdsServerTestHelper.RAW_BOOTSTRAP, filterRegistry); Executors.newSingleThreadExecutor().execute(new Runnable() { @Override public void run() { @@ -190,7 +192,8 @@ private void verifyBootstrapFail(Bootstrapper.BootstrapInfo b) throws Exception XdsClient xdsClient = mock(XdsClient.class); when(xdsClient.getBootstrapInfo()).thenReturn(b); xdsServerWrapper = new XdsServerWrapper("0.0.0.0:1", mockBuilder, listener, - selectorManager, new FakeXdsClientPoolFactory(xdsClient), filterRegistry); + selectorManager, new FakeXdsClientPoolFactory(xdsClient), + XdsServerTestHelper.RAW_BOOTSTRAP, filterRegistry); final SettableFuture start = SettableFuture.create(); Executors.newSingleThreadExecutor().execute(new Runnable() { @Override @@ -229,7 +232,8 @@ public void testBootstrap_templateWithXdstp() throws Exception { XdsListenerResource listenerResource = XdsListenerResource.getInstance(); when(xdsClient.getBootstrapInfo()).thenReturn(b); xdsServerWrapper = new XdsServerWrapper("[::FFFF:129.144.52.38]:80", mockBuilder, listener, - selectorManager, new FakeXdsClientPoolFactory(xdsClient), filterRegistry); + selectorManager, new FakeXdsClientPoolFactory(xdsClient), + XdsServerTestHelper.RAW_BOOTSTRAP, filterRegistry); Executors.newSingleThreadExecutor().execute(new Runnable() { @Override public void run() { @@ -546,6 +550,7 @@ public void onChanged_listenerIsNull() throws ExecutionException, InterruptedException, TimeoutException { xdsServerWrapper = new XdsServerWrapper("10.1.2.3:1", mockBuilder, listener, selectorManager, new FakeXdsClientPoolFactory(xdsClient), + XdsServerTestHelper.RAW_BOOTSTRAP, filterRegistry, executor.getScheduledExecutorService()); final SettableFuture start = SettableFuture.create(); Executors.newSingleThreadExecutor().execute(new Runnable() { @@ -575,6 +580,7 @@ public void onChanged_listenerAddressMissingPort() throws ExecutionException, InterruptedException, TimeoutException { xdsServerWrapper = new XdsServerWrapper("10.1.2.3:1", mockBuilder, listener, selectorManager, new FakeXdsClientPoolFactory(xdsClient), + XdsServerTestHelper.RAW_BOOTSTRAP, filterRegistry, executor.getScheduledExecutorService()); final SettableFuture start = SettableFuture.create(); Executors.newSingleThreadExecutor().execute(new Runnable() { @@ -612,6 +618,7 @@ public void onChanged_listenerAddressMismatch() throws ExecutionException, InterruptedException, TimeoutException { xdsServerWrapper = new XdsServerWrapper("10.1.2.3:1", mockBuilder, listener, selectorManager, new FakeXdsClientPoolFactory(xdsClient), + XdsServerTestHelper.RAW_BOOTSTRAP, filterRegistry, executor.getScheduledExecutorService()); final SettableFuture start = SettableFuture.create(); Executors.newSingleThreadExecutor().execute(new Runnable() { @@ -649,6 +656,7 @@ public void onChanged_listenerAddressPortMismatch() throws ExecutionException, InterruptedException, TimeoutException { xdsServerWrapper = new XdsServerWrapper("10.1.2.3:1", mockBuilder, listener, selectorManager, new FakeXdsClientPoolFactory(xdsClient), + XdsServerTestHelper.RAW_BOOTSTRAP, filterRegistry, executor.getScheduledExecutorService()); final SettableFuture start = SettableFuture.create(); Executors.newSingleThreadExecutor().execute(new Runnable() { @@ -1842,7 +1850,8 @@ private FilterRegistry filterStateTestFilterRegistry( private SettableFuture filterStateTestStartServer(FilterRegistry filterRegistry) { xdsServerWrapper = new XdsServerWrapper("0.0.0.0:1", mockBuilder, listener, - selectorManager, new FakeXdsClientPoolFactory(xdsClient), filterRegistry); + selectorManager, new FakeXdsClientPoolFactory(xdsClient), + XdsServerTestHelper.RAW_BOOTSTRAP, filterRegistry); SettableFuture serverStart = SettableFuture.create(); scheduleServerStart(xdsServerWrapper, serverStart); return serverStart; From 86e8b56170a12b45dcf243a40db1670de595b3c0 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 7 Oct 2025 15:16:40 -0700 Subject: [PATCH 423/591] Include causal status details in higher-level statuses When an operation fails and we want to produce a new status at a higher level, we commonly are turning the first status into an exception to attach to the new exception. We should instead prefer to keep as much information in the status description itself, as cause is not as reliable to be logged/propagated. I do expect long-term we'll want to expose an API in grpc-api for this, but for the moment let's keep it internal. In particular, we'd have to figure out its name. I could also believe we might want different formatting, which becomes a clearer discussion when we can see the usages. I'm pretty certain there are some other places that could benefit from this utility, as I remember really wishing I had these functions a month or two ago. But these are the places I could immediately find. OutlierDetectionLoadBalancerConfig had its status code changed from INTERNAL to UNAVAILABLE because the value comes externally, and so isn't a gRPC bug or such. I didn't change the xds policies in the same way because it's murkier as the configuration for those is largely generated within xds itself. --- .../main/java/io/grpc/internal/GrpcUtil.java | 25 +++++++++++++++++++ .../io/grpc/internal/RetriableStream.java | 5 ++-- .../OpenTelemetryTracingModule.java | 11 ++------ .../OutlierDetectionLoadBalancerProvider.java | 8 +++--- .../WeightedTargetLoadBalancerProvider.java | 8 +++--- .../xds/WrrLocalityLoadBalancerProvider.java | 8 +++--- 6 files changed, 44 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index a512fff6af2..048812ac1de 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -821,6 +821,31 @@ public static Status replaceInappropriateControlPlaneStatus(Status status) { + status.getDescription()).withCause(status.getCause()) : status; } + /** + * Returns a "clean" representation of a status code and description (not cause) like + * "UNAVAILABLE: The description". Should be similar to Status.formatThrowableMessage(). + */ + public static String statusToPrettyString(Status status) { + if (status.getDescription() == null) { + return status.getCode().toString(); + } else { + return status.getCode() + ": " + status.getDescription(); + } + } + + /** + * Create a status with contextual information, propagating details from a non-null status that + * contributed to the failure. For example, if UNAVAILABLE, "Couldn't load bar", and status + * "FAILED_PRECONDITION: Foo missing" were passed as arguments, then this method would produce the + * status "UNAVAILABLE: Couldn't load bar: FAILED_PRECONDITION: Foo missing" with a cause if the + * passed status had a cause. + */ + public static Status statusWithDetails(Status.Code code, String description, Status causeStatus) { + return code.toStatus() + .withDescription(description + ": " + statusToPrettyString(causeStatus)) + .withCause(causeStatus.getCause()); + } + /** * Checks whether the given item exists in the iterable. This is copied from Guava Collect's * {@code Iterables.contains()} because Guava Collect is not Android-friendly thus core can't diff --git a/core/src/main/java/io/grpc/internal/RetriableStream.java b/core/src/main/java/io/grpc/internal/RetriableStream.java index 9fe8ab4ffff..96816eb6f15 100644 --- a/core/src/main/java/io/grpc/internal/RetriableStream.java +++ b/core/src/main/java/io/grpc/internal/RetriableStream.java @@ -938,9 +938,8 @@ public void run() { && localOnlyTransparentRetries.incrementAndGet() > 1_000) { commitAndRun(substream); if (state.winningSubstream == substream) { - Status tooManyTransparentRetries = Status.INTERNAL - .withDescription("Too many transparent retries. Might be a bug in gRPC") - .withCause(status.asRuntimeException()); + Status tooManyTransparentRetries = GrpcUtil.statusWithDetails( + Status.Code.INTERNAL, "Too many transparent retries. Might be a bug in gRPC", status); safeCloseMasterListener(tooManyTransparentRetries, rpcProgress, trailers); } return; diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java index 8c42a189ac2..4fa4f802dc0 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java @@ -36,6 +36,7 @@ import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.ServerStreamTracer; +import io.grpc.internal.GrpcUtil; import io.grpc.opentelemetry.internal.OpenTelemetryConstants; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributesBuilder; @@ -463,19 +464,11 @@ private void recordInboundMessageSize(Span span, int seqNo, long bytes) { span.addEvent("Inbound message", attributesBuilder.build()); } - private String generateErrorStatusDescription(io.grpc.Status status) { - if (status.getDescription() != null) { - return status.getCode() + ": " + status.getDescription(); - } else { - return status.getCode().toString(); - } - } - private void endSpanWithStatus(Span span, io.grpc.Status status) { if (status.isOk()) { span.setStatus(StatusCode.OK); } else { - span.setStatus(StatusCode.ERROR, generateErrorStatusDescription(status)); + span.setStatus(StatusCode.ERROR, GrpcUtil.statusToPrettyString(status)); } span.end(); } diff --git a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java index c76d68a03de..084898bc38f 100644 --- a/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java +++ b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java @@ -23,6 +23,7 @@ import io.grpc.LoadBalancerProvider; import io.grpc.NameResolver.ConfigOrError; import io.grpc.Status; +import io.grpc.internal.GrpcUtil; import io.grpc.internal.JsonUtil; import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig; import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig.FailurePercentageEjection; @@ -148,9 +149,10 @@ private ConfigOrError parseLoadBalancingPolicyConfigInternal(Map rawC ConfigOrError childConfig = GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig( JsonUtil.getListOfObjects(rawConfig, "childPolicy")); if (childConfig.getError() != null) { - return ConfigOrError.fromError(Status.INTERNAL - .withDescription("Failed to parse child in outlier_detection_experimental: " + rawConfig) - .withCause(childConfig.getError().asRuntimeException())); + return ConfigOrError.fromError(GrpcUtil.statusWithDetails( + Status.Code.UNAVAILABLE, + "Failed to parse child in outlier_detection_experimental", + childConfig.getError())); } configBuilder.setChildConfig(childConfig.getConfig()); diff --git a/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancerProvider.java index 55f33fb11aa..15318693aca 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancerProvider.java @@ -25,6 +25,7 @@ import io.grpc.LoadBalancerRegistry; import io.grpc.NameResolver.ConfigOrError; import io.grpc.Status; +import io.grpc.internal.GrpcUtil; import io.grpc.internal.JsonUtil; import io.grpc.util.GracefulSwitchLoadBalancer; import java.util.LinkedHashMap; @@ -99,9 +100,10 @@ public ConfigOrError parseLoadBalancingPolicyConfig(Map rawConfig) { ConfigOrError childConfig = GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig( JsonUtil.getListOfObjects(rawWeightedTarget, "childPolicy"), lbRegistry); if (childConfig.getError() != null) { - return ConfigOrError.fromError(Status.INTERNAL - .withDescription("Could not parse weighted_target's child policy:" + name) - .withCause(childConfig.getError().asRuntimeException())); + return ConfigOrError.fromError(GrpcUtil.statusWithDetails( + Status.Code.INTERNAL, + "Could not parse weighted_target's child policy: " + name, + childConfig.getError())); } parsedChildConfigs.put(name, new WeightedPolicySelection(weight, childConfig.getConfig())); } diff --git a/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancerProvider.java index 384831b8a05..3693df9208a 100644 --- a/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancerProvider.java @@ -23,6 +23,7 @@ import io.grpc.LoadBalancerRegistry; import io.grpc.NameResolver.ConfigOrError; import io.grpc.Status; +import io.grpc.internal.GrpcUtil; import io.grpc.internal.JsonUtil; import io.grpc.util.GracefulSwitchLoadBalancer; import io.grpc.xds.WrrLocalityLoadBalancer.WrrLocalityConfig; @@ -62,9 +63,10 @@ public ConfigOrError parseLoadBalancingPolicyConfig(Map rawConfig) { ConfigOrError childConfig = GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig( JsonUtil.getListOfObjects(rawConfig, "childPolicy")); if (childConfig.getError() != null) { - return ConfigOrError.fromError(Status.INTERNAL - .withDescription("Failed to parse child policy in wrr_locality LB policy: " + rawConfig) - .withCause(childConfig.getError().asRuntimeException())); + return ConfigOrError.fromError(GrpcUtil.statusWithDetails( + Status.Code.INTERNAL, + "Failed to parse child policy in wrr_locality LB policy", + childConfig.getError())); } return ConfigOrError.fromConfig(new WrrLocalityConfig(childConfig.getConfig())); } catch (RuntimeException e) { From 827558077861daf444638e4f432e6562cb5cb3b5 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Wed, 29 Oct 2025 10:26:02 -0700 Subject: [PATCH 424/591] Flatten the BinderTransports using the "guard clause" pattern (#12449) `Binder[Client/Server]Transport` uses the following "single exit point" style/pattern of control flow all over the place: ``` if (inState(ExpectedState)) { if (!doStep1ThatCanFail()) { // error handling } else if (!doStep2ThatCanFail()) { // error handling } else if (...) { ... } else { // Happy case. } } ``` This has the advantage of a single exit point which, in C-like languages has historically been useful for unconditional cleanup like releasing locks and closing resources. However, in Java with exceptions, unconditional cleanup has to happen in a finally block and so we don't really benefit. On the other hand the single exit point pattern creates a lot of nesting: - The happy case is always nested at least two levels deep in an `else`. - Inserting a new no-fail step in the sequence (say between 1 and 2) is awkward. You have to either create an empty error handling block or create a new "else" clause, increasing the indent/nesting level by one. In this PR, I reduce nesting and make life easier for future PRs by converting all BinderTransport methods to use the guard clause pattern instead: https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html https://testing.googleblog.com/2017/06/code-health-reduce-nesting-reduce.html --- .../internal/BinderClientTransport.java | 183 ++++++++++-------- .../internal/BinderServerTransport.java | 29 +-- 2 files changed, 117 insertions(+), 95 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index c6aa195544e..54d65936fab 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -148,31 +148,33 @@ public synchronized void onUnbound(Status reason) { @Override public synchronized Runnable start(Listener clientTransportListener) { this.clientTransportListener = checkNotNull(clientTransportListener); - return () -> { - synchronized (BinderClientTransport.this) { - if (inState(TransportState.NOT_STARTED)) { - setState(TransportState.SETUP); - try { - if (preAuthorizeServer) { - preAuthorize(serviceBinding.resolve()); - } else { - serviceBinding.bind(); - } - } catch (StatusException e) { - shutdownInternal(e.getStatus(), true); - return; - } - if (readyTimeoutMillis >= 0) { - readyTimeoutFuture = - getScheduledExecutorService() - .schedule( - BinderClientTransport.this::onReadyTimeout, - readyTimeoutMillis, - MILLISECONDS); - } - } + return this::postStartRunnable; + } + + private synchronized void postStartRunnable() { + if (!inState(TransportState.NOT_STARTED)) { + return; + } + + setState(TransportState.SETUP); + + try { + if (preAuthorizeServer) { + preAuthorize(serviceBinding.resolve()); + } else { + serviceBinding.bind(); } - }; + } catch (StatusException e) { + shutdownInternal(e.getStatus(), true); + return; + } + + if (readyTimeoutMillis >= 0) { + readyTimeoutFuture = + getScheduledExecutorService() + .schedule( + BinderClientTransport.this::onReadyTimeout, readyTimeoutMillis, MILLISECONDS); + } } @GuardedBy("this") @@ -204,13 +206,16 @@ public void onFailure(Throwable t) { } private synchronized void handlePreAuthResult(Status authorization) { - if (inState(TransportState.SETUP)) { - if (!authorization.isOk()) { - shutdownInternal(authorization, true); - } else { - serviceBinding.bind(); - } + if (!inState(TransportState.SETUP)) { + return; + } + + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + return; } + + serviceBinding.bind(); } private synchronized void onReadyTimeout() { @@ -252,17 +257,17 @@ public synchronized ClientStream newStream( Status failure = Status.INTERNAL.withDescription("Clashing call IDs"); shutdownInternal(failure, true); return newFailingClientStream(failure, attributes, headers, tracers); + } + + if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) { + clientTransportListener.transportInUse(true); + } + Outbound.ClientOutbound outbound = + new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext); + if (method.getType().clientSendsOneMessage()) { + return new SingleMessageClientStream(inbound, outbound, attributes); } else { - if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) { - clientTransportListener.transportInUse(true); - } - Outbound.ClientOutbound outbound = - new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext); - if (method.getType().clientSendsOneMessage()) { - return new SingleMessageClientStream(inbound, outbound, attributes); - } else { - return new MultiMessageClientStream(inbound, outbound, attributes); - } + return new MultiMessageClientStream(inbound, outbound, attributes); } } @@ -314,39 +319,46 @@ void notifyTerminated() { @Override @GuardedBy("this") protected void handleSetupTransport(Parcel parcel) { - int remoteUid = Binder.getCallingUid(); - if (inState(TransportState.SETUP)) { - int version = parcel.readInt(); - IBinder binder = parcel.readStrongBinder(); - if (version != WIRE_FORMAT_VERSION) { - shutdownInternal(Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true); - } else if (binder == null) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); - } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); - } else { - restrictIncomingBinderToCallsFrom(remoteUid); - attributes = setSecurityAttrs(attributes, remoteUid); - ListenableFuture authResultFuture = - register(checkServerAuthorizationAsync(remoteUid)); - Futures.addCallback( - authResultFuture, - new FutureCallback() { - @Override - public void onSuccess(Status result) { - handleAuthResult(result); - } - - @Override - public void onFailure(Throwable t) { - handleAuthResult(t); - } - }, - offloadExecutor); - } + if (!inState(TransportState.SETUP)) { + return; + } + + int version = parcel.readInt(); + if (version != WIRE_FORMAT_VERSION) { + shutdownInternal(Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true); + return; + } + + IBinder binder = parcel.readStrongBinder(); + if (binder == null) { + shutdownInternal(Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); + return; + } + + if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + return; } + + int remoteUid = Binder.getCallingUid(); + restrictIncomingBinderToCallsFrom(remoteUid); + attributes = setSecurityAttrs(attributes, remoteUid); + ListenableFuture authResultFuture = register(checkServerAuthorizationAsync(remoteUid)); + Futures.addCallback( + authResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + handleAuthResult(result); + } + + @Override + public void onFailure(Throwable t) { + handleAuthResult(t); + } + }, + offloadExecutor); } private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { @@ -356,18 +368,21 @@ private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { } private synchronized void handleAuthResult(Status authorization) { - if (inState(TransportState.SETUP)) { - if (!authorization.isOk()) { - shutdownInternal(authorization, true); - } else { - setState(TransportState.READY); - attributes = clientTransportListener.filterTransport(attributes); - clientTransportListener.transportReady(); - if (readyTimeoutFuture != null) { - readyTimeoutFuture.cancel(false); - readyTimeoutFuture = null; - } - } + if (!inState(TransportState.SETUP)) { + return; + } + + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + return; + } + + setState(TransportState.READY); + attributes = clientTransportListener.filterTransport(attributes); + clientTransportListener.transportReady(); + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; } } diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java index cbe64149e15..44909f9c0bc 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java @@ -69,15 +69,22 @@ public BinderServerTransport( */ public synchronized void start(ServerTransportListener serverTransportListener) { this.listenerPromise.set(serverTransportListener); - if (!isShutdown()) { - sendSetupTransaction(); - // Check we're not shutdown again, since a failure inside sendSetupTransaction (or a callback - // it triggers), could have shut us down. - if (!isShutdown()) { - setState(TransportState.READY); - attributes = serverTransportListener.transportReady(attributes); - } + if (isShutdown()) { + // It's unlikely, but we could be shutdown externally between construction and start(). One + // possible cause is an extremely short handshake timeout. + return; } + + sendSetupTransaction(); + + // Check we're not shutdown again, since a failure inside sendSetupTransaction (or a callback + // it triggers), could have shut us down. + if (isShutdown()) { + return; + } + + setState(TransportState.READY); + attributes = serverTransportListener.transportReady(attributes); } StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) { @@ -92,10 +99,10 @@ StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) { synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) { if (isShutdown()) { return Status.UNAVAILABLE.withDescription("transport is shutdown"); - } else { - listenerPromise.get().streamCreated(stream, methodName, headers); - return Status.OK; } + + listenerPromise.get().streamCreated(stream, methodName, headers); + return Status.OK; } @Override From 06d734469fa7170f9f3c890723027364b3bad2b0 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 29 Oct 2025 13:09:07 -0700 Subject: [PATCH 425/591] binder: Move FakeDeadBinder to internal It is only used by internal and isn't for general consumption. --- .../java/io/grpc/binder/internal/BinderClientTransportTest.java | 2 +- .../java/io/grpc/binder/{ => internal}/FakeDeadBinder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename binder/src/testFixtures/java/io/grpc/binder/{ => internal}/FakeDeadBinder.java (98%) diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java index ee28f132f79..17b83a090fe 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java @@ -39,9 +39,9 @@ import io.grpc.Status.Code; import io.grpc.binder.AndroidComponentAddress; import io.grpc.binder.BinderServerBuilder; -import io.grpc.binder.FakeDeadBinder; import io.grpc.binder.HostServices; import io.grpc.binder.SecurityPolicy; +import io.grpc.binder.internal.FakeDeadBinder; import io.grpc.binder.internal.OneWayBinderProxies.BlackHoleOneWayBinderProxy; import io.grpc.binder.internal.OneWayBinderProxies.BlockingBinderDecorator; import io.grpc.binder.internal.OneWayBinderProxies.ThrowingOneWayBinderProxy; diff --git a/binder/src/testFixtures/java/io/grpc/binder/FakeDeadBinder.java b/binder/src/testFixtures/java/io/grpc/binder/internal/FakeDeadBinder.java similarity index 98% rename from binder/src/testFixtures/java/io/grpc/binder/FakeDeadBinder.java rename to binder/src/testFixtures/java/io/grpc/binder/internal/FakeDeadBinder.java index b1658c13812..5bce7498c4b 100644 --- a/binder/src/testFixtures/java/io/grpc/binder/FakeDeadBinder.java +++ b/binder/src/testFixtures/java/io/grpc/binder/internal/FakeDeadBinder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.binder; +package io.grpc.binder.internal; import android.os.DeadObjectException; import android.os.IBinder; From e25c414c9cf1016e5845139ae6fe03e0f35d4cfe Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 29 Oct 2025 13:16:41 -0700 Subject: [PATCH 426/591] Start 1.78.0 development cycle --- MODULE.bazel | 2 +- build.gradle | 2 +- .../src/test/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/test/golden/TestService.java.txt | 2 +- core/src/main/java/io/grpc/internal/GrpcUtil.java | 2 +- examples/MODULE.bazel | 2 +- examples/android/clientcache/app/build.gradle | 10 +++++----- examples/android/helloworld/app/build.gradle | 8 ++++---- examples/android/routeguide/app/build.gradle | 8 ++++---- examples/android/strictmode/app/build.gradle | 8 ++++---- examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 4 ++-- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 4 ++-- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 4 ++-- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 4 ++-- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 4 ++-- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- 34 files changed, 55 insertions(+), 55 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 9e83053fb0b..36db31dbd71 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "grpc-java", compatibility_level = 0, repo_name = "io_grpc_grpc_java", - version = "1.77.0-SNAPSHOT", # CURRENT_GRPC_VERSION + version = "1.78.0-SNAPSHOT", # CURRENT_GRPC_VERSION ) # GRPC_DEPS_START diff --git a/build.gradle b/build.gradle index 35f7161d4bf..38ee6220169 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.77.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.78.0-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index f42922c76b2..3451d0cad09 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.77.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.78.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index 2e8d702a877..e86ad0b8f08 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.77.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.78.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index 048812ac1de..4a9da1d19fe 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -219,7 +219,7 @@ public byte[] parseAsciiString(byte[] serialized) { public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); - public static final String IMPLEMENTATION_VERSION = "1.77.0-SNAPSHOT"; // CURRENT_GRPC_VERSION + public static final String IMPLEMENTATION_VERSION = "1.78.0-SNAPSHOT"; // CURRENT_GRPC_VERSION /** * The default timeout in nanos for a keepalive ping request. diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index 7f639476b84..67c97bbf690 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,4 +1,4 @@ -bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.77.0-SNAPSHOT") # CURRENT_GRPC_VERSION +bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.78.0-SNAPSHOT") # CURRENT_GRPC_VERSION bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") bazel_dep(name = "rules_jvm_external", version = "6.0") diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index d9b3f50a941..02389619418 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -34,7 +34,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -54,11 +54,11 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.5' - testImplementation 'io.grpc:grpc-testing:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + testImplementation 'io.grpc:grpc-testing:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index 81ea9235155..60d32f0360d 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,7 +52,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index a0aef26cb61..c232389ff3d 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,7 +52,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index e1e6b8badc4..6c8209621a9 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -33,7 +33,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -53,7 +53,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/build.gradle b/examples/build.gradle index f0965338b8f..1cd8eea75fc 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index 970e287f609..4796123ef23 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index b9baf5f3817..a21ece5686f 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' dependencies { diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index 4bb8b33f9ab..ddfedaa2e39 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -6,13 +6,13 @@ jar - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT example-debug https://github.com/grpc/grpc-java UTF-8 - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index 8c32559a6dc..affd18996e6 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' dependencies { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index 3561254ed0c..66e6b481da4 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -6,13 +6,13 @@ jar - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT example-dualstack https://github.com/grpc/grpc-java UTF-8 - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 3c3978603c7..fef81b77697 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index 70d7ea56a5a..afae8ee6ec4 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,13 +6,13 @@ jar - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index a1e767d02df..9b714b0a88f 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' def openTelemetryVersion = '1.52.0' def openTelemetryPrometheusVersion = '1.52.0-alpha' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 605c93ea75f..9ece32e2b78 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 19fe0816580..671de65d2ff 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' dependencies { diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index 3d9225d72da..d228ee3e7c5 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,13 +6,13 @@ jar - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index e1509f08b79..c2ff9f7a7f2 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index 2a097386637..5deb3583561 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,13 +7,13 @@ jar - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT 3.25.8 3.25.8 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 38f105ec37f..36e45958b84 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index 30c0608c24a..e81f5e50190 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -7,13 +7,13 @@ jar - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT example-oauth https://github.com/grpc/grpc-java UTF-8 - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT 3.25.8 3.25.8 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index 46591b42c80..188e5ff0a00 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' def openTelemetryVersion = '1.52.0' def openTelemetryPrometheusVersion = '1.52.0-alpha' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index 3e3d141b5a1..e188b4079ef 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index 9c49388a946..5148eaf7938 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index ee891209000..3ee7bb2a592 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -15,7 +15,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index e55a7ce7f33..b36d21f1b41 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index 202ad17d896..34819f52a45 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,13 +6,13 @@ jar - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT example-tls https://github.com/grpc/grpc-java UTF-8 - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index bd040511b3e..f5945320080 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.77.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/pom.xml b/examples/pom.xml index 2a6817a56a0..63921323fb9 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,13 +6,13 @@ jar - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT examples https://github.com/grpc/grpc-java UTF-8 - 1.77.0-SNAPSHOT + 1.78.0-SNAPSHOT 3.25.8 3.25.8 From 4843256afde4fd7a0fda0791def5d7c834472cae Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:58:34 +0200 Subject: [PATCH 427/591] core: simplify DnsNameResolver.resolveAddresses() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `resolveAddresses()` is a private method, called only once. There is no need to handle exceptions in multiple places. The reason for creating this PR: I have noticed an exception like this in the logs ``` 2025-10-16 13:09:33.141 WARN  [grpc-default-executor-222]    ManagedChannelImpl            [Channel<47>: (x.y.com:443)] Failed to resolve name. status=Status{code=UNAVAILABLE, description=Unable to resolve host x.y.com, cause=java.lang.RuntimeException: java.net.UnknownHostException: x.y.com: nodename nor servname provided, or not known ... Caused by: java.net.UnknownHostException: x.y.com: nodename nor servname provided, or not known ... } ``` --- .../java/io/grpc/internal/DnsNameResolver.java | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/DnsNameResolver.java b/core/src/main/java/io/grpc/internal/DnsNameResolver.java index 6f1cf4cd900..6c48389b95b 100644 --- a/core/src/main/java/io/grpc/internal/DnsNameResolver.java +++ b/core/src/main/java/io/grpc/internal/DnsNameResolver.java @@ -23,7 +23,6 @@ import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; -import com.google.common.base.Throwables; import com.google.common.base.Verify; import com.google.common.base.VerifyException; import io.grpc.Attributes; @@ -211,20 +210,8 @@ public void refresh() { resolve(); } - private List resolveAddresses() { - List addresses; - Exception addressesException = null; - try { - addresses = addressResolver.resolveAddress(host); - } catch (Exception e) { - addressesException = e; - Throwables.throwIfUnchecked(e); - throw new RuntimeException(e); - } finally { - if (addressesException != null) { - logger.log(Level.FINE, "Address resolution failure", addressesException); - } - } + private List resolveAddresses() throws Exception { + List addresses = addressResolver.resolveAddress(host); // Each address forms an EAG List servers = new ArrayList<>(addresses.size()); for (InetAddress inetAddr : addresses) { @@ -280,6 +267,7 @@ protected InternalResolutionResult doResolve(boolean forceTxt) { try { result.addresses = resolveAddresses(); } catch (Exception e) { + logger.log(Level.FINE, "Address resolution failure", e); if (!forceTxt) { result.error = Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e); From d89628c3c5035d4b8e4221c702492a31fe9303c1 Mon Sep 17 00:00:00 2001 From: srkethireddy <73372452+srkethireddy@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:22:42 -0700 Subject: [PATCH 428/591] alts: Override metadata server address with env variable Adding an option to override "metadata.google.internal.:8080" by setting a value for GCE_METADATA_HOST environment variable. b/451639946 --- .../main/java/io/grpc/alts/HandshakerServiceChannel.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java b/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java index fa1d4f7fa1c..f03cc6f8c94 100644 --- a/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java +++ b/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java @@ -16,6 +16,7 @@ package io.grpc.alts; +import com.google.common.base.MoreObjects; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; @@ -39,7 +40,10 @@ final class HandshakerServiceChannel { static final Resource SHARED_HANDSHAKER_CHANNEL = - new ChannelResource("metadata.google.internal.:8080"); + new ChannelResource( + MoreObjects.firstNonNull( + System.getenv("GCE_METADATA_HOST"), "metadata.google.internal.:8080")); + /** Returns a resource of handshaker service channel for testing only. */ static Resource getHandshakerChannelForTesting(String handshakerAddress) { From 9e58ea8ab8e66a8f7fc4d0fcafdce5f90a789427 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Fri, 31 Oct 2025 22:19:46 +0530 Subject: [PATCH 429/591] buildscripts: Increase macos.cfg timeout to 1h We are seeing frequent Kokoro VM timeouts with the 45 minute setting. Even successful builds take 38 mins which is close, so increasing the timeout to 1 hour. --- buildscripts/kokoro/macos.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildscripts/kokoro/macos.cfg b/buildscripts/kokoro/macos.cfg index a58691a7102..4c79743692e 100644 --- a/buildscripts/kokoro/macos.cfg +++ b/buildscripts/kokoro/macos.cfg @@ -2,7 +2,7 @@ # Location of the continuous shell script in repository. build_file: "grpc-java/buildscripts/kokoro/macos.sh" -timeout_mins: 45 +timeout_mins: 60 # We always build mvn artifacts. action { From 89d77e0620108588bbc76c8cfdc029073db82953 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Fri, 31 Oct 2025 10:10:41 -0700 Subject: [PATCH 430/591] Stop leaking `this` from BinderServerTransport's ctor (#12453) Accomplish this with a new factory method. Document why ignoring errors from setOutgoingBinder() is ok. Add a test for a dead-on-arrival client binder. --- .../io/grpc/binder/internal/BinderServer.java | 2 +- .../internal/BinderServerTransport.java | 22 +++++++++++++++---- .../grpc/binder/internal/BinderTransport.java | 9 ++++++++ .../internal/BinderServerTransportTest.java | 15 ++++++++++--- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderServer.java b/binder/src/main/java/io/grpc/binder/internal/BinderServer.java index b1ae344d5f9..96685a2f8bd 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderServer.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderServer.java @@ -179,7 +179,7 @@ public synchronized boolean handleTransaction(int code, Parcel parcel) { checkNotNull(executor, "Not started?")); // Create a new transport and let our listener know about it. BinderServerTransport transport = - new BinderServerTransport( + BinderServerTransport.create( executorServicePool, attrsBuilder.build(), streamTracerFactories, diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java index 44909f9c0bc..a57f89e72ac 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java @@ -42,21 +42,35 @@ public final class BinderServerTransport extends BinderTransport implements Serv @GuardedBy("this") private final SimplePromise listenerPromise = new SimplePromise<>(); + private BinderServerTransport( + ObjectPool executorServicePool, + Attributes attributes, + List streamTracerFactories, + OneWayBinderProxy.Decorator binderDecorator) { + super(executorServicePool, attributes, binderDecorator, buildLogId(attributes)); + this.streamTracerFactories = streamTracerFactories; + } + /** * Constructs a new transport instance. * * @param binderDecorator used to decorate 'callbackBinder', for fault injection. */ - public BinderServerTransport( + public static BinderServerTransport create( ObjectPool executorServicePool, Attributes attributes, List streamTracerFactories, OneWayBinderProxy.Decorator binderDecorator, IBinder callbackBinder) { - super(executorServicePool, attributes, binderDecorator, buildLogId(attributes)); - this.streamTracerFactories = streamTracerFactories; + BinderServerTransport transport = + new BinderServerTransport( + executorServicePool, attributes, streamTracerFactories, binderDecorator); // TODO(jdcormie): Plumb in the Server's executor() and use it here instead. - setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService())); + // No need to handle failure here because if 'callbackBinder' is already dead, we'll notice it + // again in start() when we send the first transaction. + transport.setOutgoingBinder( + OneWayBinderProxy.wrap(callbackBinder, transport.getScheduledExecutorService())); + return transport; } /** diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index a5b8d94ba0e..1592f6977df 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -159,6 +159,7 @@ protected enum TransportState { private final ObjectPool executorServicePool; private final ScheduledExecutorService scheduledExecutorService; private final InternalLogId logId; + @GuardedBy("this") private final LeakSafeOneWayBinder incomingBinder; @@ -277,6 +278,14 @@ final void setState(TransportState newState) { transportState = newState; } + /** + * Sets the binder to use for sending subsequent transactions to our peer. + * + *

    Subclasses should call this as early as possible but not from a constructor. + * + *

    Returns true for success, false if the process hosting 'binder' is already dead. Callers are + * responsible for handling this. + */ @GuardedBy("this") protected boolean setOutgoingBinder(OneWayBinderProxy binder) { binder = binderDecorator.decorate(binder); diff --git a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java index 12416922fc9..d261ce43c8c 100644 --- a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java @@ -21,10 +21,8 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; -import android.os.DeadObjectException; import android.os.IBinder; import android.os.Looper; import android.os.Parcel; @@ -96,6 +94,17 @@ public void testSetupTransactionFailureReportsMultipleTerminations_b153460678() assertThat(transportListener.isTerminated()).isTrue(); } + @Test + public void testClientBinderIsDeadOnArrival() throws Exception { + transport = newBinderServerTransportBuilder() + .setCallbackBinder(new FakeDeadBinder()) + .build(); + transport.start(transportListener); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(transportListener.isTerminated()).isTrue(); + } + @Test public void testStartAfterShutdownAndIdle() throws Exception { transport = newBinderServerTransportBuilder().build(); @@ -125,7 +134,7 @@ static class BinderServerTransportBuilder { IBinder callbackBinder; public BinderServerTransport build() { - return new BinderServerTransport( + return BinderServerTransport.create( executorServicePool, attributes, streamTracerFactories, binderDecorator, callbackBinder); } From 589df4e3016dcbd6169eb09662d7ce42aa787810 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 31 Oct 2025 11:31:55 -0700 Subject: [PATCH 431/591] xds: Avoid default bootstrap when global override in XdsNameResolver This fixes a regression with an internal API from 27d150890 where overridding the global bootstrap didn't impact parsing the default bootstrap. So if no global bootstrap was available XdsNameResolver would fail to start even though an override was in place in SharedXdsClientPoolProvider. Instead of dealing with the override in SharedXdsClientPoolProvider, do it in GrpcBootstrapperImpl so XdsNameResolver is ignorant of the source of the default bootstrap. We want all of this to go away in favor of XDS_CLIENT_SUPPLIER injection, but there needs to be some overlap for migration. cl/826085025 --- .../io/grpc/xds/GrpcBootstrapperImpl.java | 12 ++++++++++- .../InternalSharedXdsClientPoolProvider.java | 2 +- .../grpc/xds/SharedXdsClientPoolProvider.java | 21 +++++++------------ 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java index fdcf3a972b5..494e95a58f6 100644 --- a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java @@ -100,12 +100,22 @@ protected Object getImplSpecificConfig(Map serverConfig, String serve return getChannelCredentials(serverConfig, serverUri); } + @GuardedBy("GrpcBootstrapperImpl.class") + private static Map defaultBootstrapOverride; @GuardedBy("GrpcBootstrapperImpl.class") private static BootstrapInfo defaultBootstrap; + static synchronized void setDefaultBootstrapOverride(Map rawBootstrap) { + defaultBootstrapOverride = rawBootstrap; + } + static synchronized BootstrapInfo defaultBootstrap() throws XdsInitializationException { if (defaultBootstrap == null) { - defaultBootstrap = new GrpcBootstrapperImpl().bootstrap(); + if (defaultBootstrapOverride == null) { + defaultBootstrap = new GrpcBootstrapperImpl().bootstrap(); + } else { + defaultBootstrap = new GrpcBootstrapperImpl().bootstrap(defaultBootstrapOverride); + } } return defaultBootstrap; } diff --git a/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java index 5eb36a498cf..cc5ff128274 100644 --- a/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java +++ b/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java @@ -41,7 +41,7 @@ private InternalSharedXdsClientPoolProvider() {} */ @Deprecated public static void setDefaultProviderBootstrapOverride(Map bootstrap) { - SharedXdsClientPoolProvider.getDefaultProvider().setBootstrapOverride(bootstrap); + GrpcBootstrapperImpl.setDefaultBootstrapOverride(bootstrap); } /** diff --git a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java index 29b6870ab97..4cafbd1a762 100644 --- a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java +++ b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java @@ -37,7 +37,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -55,29 +54,24 @@ final class SharedXdsClientPoolProvider implements XdsClientPoolFactory { private static final ExponentialBackoffPolicy.Provider BACKOFF_POLICY_PROVIDER = new ExponentialBackoffPolicy.Provider(); + @Nullable private final Bootstrapper bootstrapper; private final Object lock = new Object(); - private final AtomicReference> bootstrapOverride = new AtomicReference<>(); private final Map> targetToXdsClientMap = new ConcurrentHashMap<>(); SharedXdsClientPoolProvider() { - this(new GrpcBootstrapperImpl()); + this(null); } @VisibleForTesting - SharedXdsClientPoolProvider(Bootstrapper bootstrapper) { - this.bootstrapper = checkNotNull(bootstrapper, "bootstrapper"); + SharedXdsClientPoolProvider(@Nullable Bootstrapper bootstrapper) { + this.bootstrapper = bootstrapper; } static SharedXdsClientPoolProvider getDefaultProvider() { return SharedXdsClientPoolProviderHolder.instance; } - @Deprecated - public void setBootstrapOverride(Map bootstrap) { - bootstrapOverride.set(bootstrap); - } - @Override @Nullable public ObjectPool get(String target) { @@ -89,11 +83,10 @@ public ObjectPool getOrCreate( String target, MetricRecorder metricRecorder, CallCredentials transportCallCredentials) throws XdsInitializationException { BootstrapInfo bootstrapInfo; - Map rawBootstrap = bootstrapOverride.get(); - if (rawBootstrap != null) { - bootstrapInfo = bootstrapper.bootstrap(rawBootstrap); - } else { + if (bootstrapper != null) { bootstrapInfo = bootstrapper.bootstrap(); + } else { + bootstrapInfo = GrpcBootstrapperImpl.defaultBootstrap(); } return getOrCreate(target, bootstrapInfo, metricRecorder, transportCallCredentials); } From a40d549ced82becbb12ca6f6b0e007cd430555a6 Mon Sep 17 00:00:00 2001 From: vimanikag Date: Mon, 3 Nov 2025 14:49:04 +0530 Subject: [PATCH 432/591] core: Report marshaller error for uncompressed size too large back to the client Report a status code of RESOURCE_EXHAUSTED instead of UNKNOWN, when the marshaller throws error for uncompressed message size being too large. Fixes : #11246 --- .../java/io/grpc/internal/ServerCallImpl.java | 16 +++++--- .../io/grpc/internal/ServerCallImplTest.java | 41 +++++++++++++++++++ .../integration/AbstractInteropTest.java | 2 +- .../integration/TransportCompressionTest.java | 29 ++++++++++++- 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/ServerCallImpl.java b/core/src/main/java/io/grpc/internal/ServerCallImpl.java index e224384ce8f..22d5912050a 100644 --- a/core/src/main/java/io/grpc/internal/ServerCallImpl.java +++ b/core/src/main/java/io/grpc/internal/ServerCallImpl.java @@ -327,16 +327,20 @@ private void messagesAvailableInternal(final MessageProducer producer) { return; } - InputStream message; try { + InputStream message; while ((message = producer.next()) != null) { - try { - listener.onMessage(call.method.parseRequest(message)); - } catch (Throwable t) { + ReqT parsedMessage; + try (InputStream ignored = message) { + parsedMessage = call.method.parseRequest(message); + } catch (StatusRuntimeException e) { GrpcUtil.closeQuietly(message); - throw t; + GrpcUtil.closeQuietly(producer); + call.cancelled = true; + call.close(e.getStatus(), new Metadata()); + return; } - message.close(); + listener.onMessage(parsedMessage); } } catch (Throwable t) { GrpcUtil.closeQuietly(producer); diff --git a/core/src/test/java/io/grpc/internal/ServerCallImplTest.java b/core/src/test/java/io/grpc/internal/ServerCallImplTest.java index 7394c83eab2..abe8fb0ee56 100644 --- a/core/src/test/java/io/grpc/internal/ServerCallImplTest.java +++ b/core/src/test/java/io/grpc/internal/ServerCallImplTest.java @@ -48,9 +48,11 @@ import io.grpc.SecurityLevel; import io.grpc.ServerCall; import io.grpc.Status; +import io.grpc.StatusRuntimeException; import io.grpc.internal.ServerCallImpl.ServerStreamListenerImpl; import io.perfmark.PerfMark; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import org.junit.Before; @@ -69,6 +71,8 @@ public class ServerCallImplTest { @Mock private ServerStream stream; @Mock private ServerCall.Listener callListener; + @Mock private StreamListener.MessageProducer messageProducer; + @Mock private InputStream message; private final CallTracer serverCallTracer = CallTracer.getDefaultFactory().create(); private ServerCallImpl call; @@ -493,6 +497,43 @@ public void streamListener_unexpectedRuntimeException() { assertThat(e).hasMessageThat().isEqualTo("unexpected exception"); } + @Test + public void streamListener_statusRuntimeException() throws IOException { + MethodDescriptor failingParseMethod = MethodDescriptor.newBuilder() + .setType(MethodType.UNARY) + .setFullMethodName("service/method") + .setRequestMarshaller(new LongMarshaller() { + @Override + public Long parse(InputStream stream) { + throw new StatusRuntimeException(Status.RESOURCE_EXHAUSTED + .withDescription("Decompressed gRPC message exceeds maximum size")); + } + }) + .setResponseMarshaller(new LongMarshaller()) + .build(); + + call = new ServerCallImpl<>(stream, failingParseMethod, requestHeaders, context, + DecompressorRegistry.getDefaultInstance(), CompressorRegistry.getDefaultInstance(), + serverCallTracer, PerfMark.createTag()); + + ServerStreamListenerImpl streamListener = + new ServerCallImpl.ServerStreamListenerImpl<>(call, callListener, context); + + when(messageProducer.next()).thenReturn(message, (InputStream) null); + streamListener.messagesAvailable(messageProducer); + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(Metadata.class); + + verify(stream).close(statusCaptor.capture(), metadataCaptor.capture()); + Status status = statusCaptor.getValue(); + assertEquals(Status.RESOURCE_EXHAUSTED.getCode(), status.getCode()); + assertEquals("Decompressed gRPC message exceeds maximum size", status.getDescription()); + + streamListener.halfClosed(); + verify(callListener, never()).onHalfClose(); + verify(callListener, never()).onMessage(any()); + } + private static class LongMarshaller implements Marshaller { @Override public InputStream stream(Long value) { diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java index 11455790497..843019433aa 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java @@ -2030,7 +2030,7 @@ private void assertPayload(Payload expected, Payload actual) { } } - private static void assertCodeEquals(Status.Code expected, Status actual) { + protected static void assertCodeEquals(Status.Code expected, Status actual) { assertWithMessage("Unexpected status: %s", actual).that(actual.getCode()).isEqualTo(expected); } diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java index b9692383254..33cd624aebb 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java @@ -17,6 +17,7 @@ package io.grpc.testing.integration; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import com.google.protobuf.ByteString; @@ -37,6 +38,8 @@ import io.grpc.ServerCall.Listener; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; +import io.grpc.Status.Code; +import io.grpc.StatusRuntimeException; import io.grpc.internal.GrpcUtil; import io.grpc.netty.InternalNettyChannelBuilder; import io.grpc.netty.InternalNettyServerBuilder; @@ -53,7 +56,9 @@ import java.io.OutputStream; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -84,10 +89,16 @@ public static void registerCompressors() { compressors.register(Codec.Identity.NONE); } + @Rule + public final TestName currentTest = new TestName(); + @Override protected ServerBuilder getServerBuilder() { NettyServerBuilder builder = NettyServerBuilder.forPort(0, InsecureServerCredentials.create()) - .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE) + .maxInboundMessageSize( + DECOMPRESSED_MESSAGE_TOO_LONG_METHOD_NAME.equals(currentTest.getMethodName()) + ? 1000 + : AbstractInteropTest.MAX_MESSAGE_SIZE) .compressorRegistry(compressors) .decompressorRegistry(decompressors) .intercept(new ServerInterceptor() { @@ -126,6 +137,22 @@ public void compresses() { assertTrue(FZIPPER.anyWritten); } + private static final String DECOMPRESSED_MESSAGE_TOO_LONG_METHOD_NAME = + "decompressedMessageTooLong"; + + @Test + public void decompressedMessageTooLong() { + assertEquals(DECOMPRESSED_MESSAGE_TOO_LONG_METHOD_NAME, currentTest.getMethodName()); + final SimpleRequest bigRequest = SimpleRequest.newBuilder() + .setPayload(Payload.newBuilder().setBody(ByteString.copyFrom(new byte[10_000]))) + .build(); + StatusRuntimeException e = assertThrows(StatusRuntimeException.class, + () -> blockingStub.withCompression("gzip").unaryCall(bigRequest)); + assertCodeEquals(Code.RESOURCE_EXHAUSTED, e.getStatus()); + assertEquals("Decompressed gRPC message exceeds maximum size 1000", + e.getStatus().getDescription()); + } + @Override protected NettyChannelBuilder createChannelBuilder() { NettyChannelBuilder builder = NettyChannelBuilder.forAddress(getListenAddress()) From 881996a0c6657c6981d850bd66b8527048b7eaca Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 31 Oct 2025 15:24:57 -0700 Subject: [PATCH 433/591] xds: Detect negative ref count for xds client If the refcount goes negative, then the next getObject() will return null. This was noticed during code inspection when investigating a NullPointerException in b/454396128, although it is unclear if this is actually happening. --- .../main/java/io/grpc/xds/SharedXdsClientPoolProvider.java | 4 ++++ xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java index 4cafbd1a762..45c379244af 100644 --- a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java +++ b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java @@ -202,6 +202,10 @@ public XdsClient returnObject(Object object) { metricReporter = null; targetToXdsClientMap.remove(target); scheduler = SharedResourceHolder.release(GrpcUtil.TIMER_SERVICE, scheduler); + } else if (refCount < 0) { + assert false; // We want our tests to fail + log.log(Level.SEVERE, "Negative reference count. File a bug", new Exception()); + refCount = 0; } return null; } diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java index 09b4c5d8637..b2b4e2c14cf 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java @@ -186,8 +186,8 @@ public void setUp() throws XdsInitializationException { @After public void cleanUp() { - if (xdsClientPool != null) { - xdsClientPool.returnObject(xdsClient); + if (xdsClient != null) { + xdsClient = xdsClientPool.returnObject(xdsClient); } CommonBootstrapperTestUtils.setEnableXdsFallback(originalEnableXdsFallback); } From 76a1f2a8edff4615f62270baebbef008505a3a67 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 4 Nov 2025 07:26:04 +0000 Subject: [PATCH 434/591] rls: Add route lookup reason to request whether it is due to a cache miss or stale cache entry (#12442) b/348690462 --- .../java/io/grpc/rls/CachingRlsLbClient.java | 134 +++++++------ .../java/io/grpc/rls/RlsProtoConverters.java | 5 +- .../main/java/io/grpc/rls/RlsProtoData.java | 30 ++- .../java/io/grpc/rls/RlsRequestFactory.java | 12 +- .../io/grpc/rls/CachingRlsLbClientTest.java | 181 +++++++++++------- .../java/io/grpc/rls/RlsLoadBalancerTest.java | 12 +- .../io/grpc/rls/RlsProtoConvertersTest.java | 4 +- .../io/grpc/rls/RlsRequestFactoryTest.java | 26 +-- 8 files changed, 246 insertions(+), 158 deletions(-) diff --git a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java index cc3ac9f516e..2c26d29f14d 100644 --- a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java +++ b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java @@ -61,6 +61,7 @@ import io.grpc.rls.RlsProtoConverters.RouteLookupResponseConverter; import io.grpc.rls.RlsProtoData.RouteLookupConfig; import io.grpc.rls.RlsProtoData.RouteLookupRequest; +import io.grpc.rls.RlsProtoData.RouteLookupRequestKey; import io.grpc.rls.RlsProtoData.RouteLookupResponse; import io.grpc.stub.StreamObserver; import io.grpc.util.ForwardingLoadBalancerHelper; @@ -112,7 +113,7 @@ final class CachingRlsLbClient { private final Future periodicCleaner; // any RPC on the fly will cached in this map @GuardedBy("lock") - private final Map pendingCallCache = new HashMap<>(); + private final Map pendingCallCache = new HashMap<>(); private final ScheduledExecutorService scheduledExecutorService; private final Ticker ticker; @@ -292,18 +293,22 @@ private void periodicClean() { /** Populates async cache entry for new request. */ @GuardedBy("lock") private CachedRouteLookupResponse asyncRlsCall( - RouteLookupRequest request, @Nullable BackoffPolicy backoffPolicy) { + RouteLookupRequestKey routeLookupRequestKey, @Nullable BackoffPolicy backoffPolicy, + RouteLookupRequest.Reason routeLookupReason) { if (throttler.shouldThrottle()) { - logger.log(ChannelLogLevel.DEBUG, "[RLS Entry {0}] Throttled RouteLookup", request); + logger.log(ChannelLogLevel.DEBUG, "[RLS Entry {0}] Throttled RouteLookup", + routeLookupRequestKey); // Cache updated, but no need to call updateBalancingState because no RPCs were queued waiting // on this result return CachedRouteLookupResponse.backoffEntry(createBackOffEntry( - request, Status.RESOURCE_EXHAUSTED.withDescription("RLS throttled"), backoffPolicy)); + routeLookupRequestKey, Status.RESOURCE_EXHAUSTED.withDescription("RLS throttled"), + backoffPolicy)); } final SettableFuture response = SettableFuture.create(); - io.grpc.lookup.v1.RouteLookupRequest routeLookupRequest = REQUEST_CONVERTER.convert(request); + io.grpc.lookup.v1.RouteLookupRequest routeLookupRequest = REQUEST_CONVERTER.convert( + RouteLookupRequest.create(routeLookupRequestKey.keyMap(), routeLookupReason)); logger.log(ChannelLogLevel.DEBUG, - "[RLS Entry {0}] Starting RouteLookup: {1}", request, routeLookupRequest); + "[RLS Entry {0}] Starting RouteLookup: {1}", routeLookupRequestKey, routeLookupRequest); rlsStub.withDeadlineAfter(callTimeoutNanos, TimeUnit.NANOSECONDS) .routeLookup( routeLookupRequest, @@ -311,14 +316,14 @@ private CachedRouteLookupResponse asyncRlsCall( @Override public void onNext(io.grpc.lookup.v1.RouteLookupResponse value) { logger.log(ChannelLogLevel.DEBUG, - "[RLS Entry {0}] RouteLookup succeeded: {1}", request, value); + "[RLS Entry {0}] RouteLookup succeeded: {1}", routeLookupRequestKey, value); response.set(RESPONSE_CONVERTER.reverse().convert(value)); } @Override public void onError(Throwable t) { logger.log(ChannelLogLevel.DEBUG, - "[RLS Entry {0}] RouteLookup failed: {1}", request, t); + "[RLS Entry {0}] RouteLookup failed: {1}", routeLookupRequestKey, t); response.setException(t); throttler.registerBackendResponse(true); } @@ -329,7 +334,7 @@ public void onCompleted() { } }); return CachedRouteLookupResponse.pendingResponse( - createPendingEntry(request, response, backoffPolicy)); + createPendingEntry(routeLookupRequestKey, response, backoffPolicy)); } /** @@ -338,16 +343,17 @@ public void onCompleted() { * changed after the return. */ @CheckReturnValue - final CachedRouteLookupResponse get(final RouteLookupRequest request) { + final CachedRouteLookupResponse get(final RouteLookupRequestKey routeLookupRequestKey) { synchronized (lock) { final CacheEntry cacheEntry; - cacheEntry = linkedHashLruCache.read(request); + cacheEntry = linkedHashLruCache.read(routeLookupRequestKey); if (cacheEntry == null) { - PendingCacheEntry pendingEntry = pendingCallCache.get(request); + PendingCacheEntry pendingEntry = pendingCallCache.get(routeLookupRequestKey); if (pendingEntry != null) { return CachedRouteLookupResponse.pendingResponse(pendingEntry); } - return asyncRlsCall(request, /* backoffPolicy= */ null); + return asyncRlsCall(routeLookupRequestKey, /* backoffPolicy= */ null, + RouteLookupRequest.Reason.REASON_MISS); } if (cacheEntry instanceof DataCacheEntry) { @@ -383,13 +389,14 @@ void requestConnection() { @GuardedBy("lock") private PendingCacheEntry createPendingEntry( - RouteLookupRequest request, + RouteLookupRequestKey routeLookupRequestKey, ListenableFuture pendingCall, @Nullable BackoffPolicy backoffPolicy) { - PendingCacheEntry entry = new PendingCacheEntry(request, pendingCall, backoffPolicy); + PendingCacheEntry entry = new PendingCacheEntry(routeLookupRequestKey, pendingCall, + backoffPolicy); // Add the entry to the map before adding the Listener, because the listener removes the // entry from the map - pendingCallCache.put(request, entry); + pendingCallCache.put(routeLookupRequestKey, entry); // Beware that the listener can run immediately on the current thread pendingCall.addListener(() -> pendingRpcComplete(entry), MoreExecutors.directExecutor()); return entry; @@ -397,17 +404,18 @@ private PendingCacheEntry createPendingEntry( private void pendingRpcComplete(PendingCacheEntry entry) { synchronized (lock) { - boolean clientClosed = pendingCallCache.remove(entry.request) == null; + boolean clientClosed = pendingCallCache.remove(entry.routeLookupRequestKey) == null; if (clientClosed) { return; } try { - createDataEntry(entry.request, Futures.getDone(entry.pendingCall)); + createDataEntry(entry.routeLookupRequestKey, Futures.getDone(entry.pendingCall)); // Cache updated. DataCacheEntry constructor indirectly calls updateBalancingState() to // reattempt picks when the child LB is done connecting } catch (Exception e) { - createBackOffEntry(entry.request, Status.fromThrowable(e), entry.backoffPolicy); + createBackOffEntry(entry.routeLookupRequestKey, Status.fromThrowable(e), + entry.backoffPolicy); // Cache updated. updateBalancingState() to reattempt picks helper.triggerPendingRpcProcessing(); } @@ -416,21 +424,21 @@ private void pendingRpcComplete(PendingCacheEntry entry) { @GuardedBy("lock") private DataCacheEntry createDataEntry( - RouteLookupRequest request, RouteLookupResponse routeLookupResponse) { + RouteLookupRequestKey routeLookupRequestKey, RouteLookupResponse routeLookupResponse) { logger.log( ChannelLogLevel.DEBUG, "[RLS Entry {0}] Transition to data cache: routeLookupResponse={1}", - request, routeLookupResponse); - DataCacheEntry entry = new DataCacheEntry(request, routeLookupResponse); + routeLookupRequestKey, routeLookupResponse); + DataCacheEntry entry = new DataCacheEntry(routeLookupRequestKey, routeLookupResponse); // Constructor for DataCacheEntry causes updateBalancingState, but the picks can't happen until // this cache update because the lock is held - linkedHashLruCache.cacheAndClean(request, entry); + linkedHashLruCache.cacheAndClean(routeLookupRequestKey, entry); return entry; } @GuardedBy("lock") - private BackoffCacheEntry createBackOffEntry( - RouteLookupRequest request, Status status, @Nullable BackoffPolicy backoffPolicy) { + private BackoffCacheEntry createBackOffEntry(RouteLookupRequestKey routeLookupRequestKey, + Status status, @Nullable BackoffPolicy backoffPolicy) { if (backoffPolicy == null) { backoffPolicy = backoffProvider.get(); } @@ -438,12 +446,12 @@ private BackoffCacheEntry createBackOffEntry( logger.log( ChannelLogLevel.DEBUG, "[RLS Entry {0}] Transition to back off: status={1}, delayNanos={2}", - request, status, delayNanos); - BackoffCacheEntry entry = new BackoffCacheEntry(request, status, backoffPolicy); + routeLookupRequestKey, status, delayNanos); + BackoffCacheEntry entry = new BackoffCacheEntry(routeLookupRequestKey, status, backoffPolicy); // Lock is held, so the task can't execute before the assignment entry.scheduledFuture = scheduledExecutorService.schedule( () -> refreshBackoffEntry(entry), delayNanos, TimeUnit.NANOSECONDS); - linkedHashLruCache.cacheAndClean(request, entry); + linkedHashLruCache.cacheAndClean(routeLookupRequestKey, entry); return entry; } @@ -455,9 +463,10 @@ private void refreshBackoffEntry(BackoffCacheEntry entry) { return; } logger.log(ChannelLogLevel.DEBUG, - "[RLS Entry {0}] Calling RLS for transition to pending", entry.request); - linkedHashLruCache.invalidate(entry.request); - asyncRlsCall(entry.request, entry.backoffPolicy); + "[RLS Entry {0}] Calling RLS for transition to pending", entry.routeLookupRequestKey); + linkedHashLruCache.invalidate(entry.routeLookupRequestKey); + asyncRlsCall(entry.routeLookupRequestKey, entry.backoffPolicy, + RouteLookupRequest.Reason.REASON_MISS); } } @@ -590,15 +599,15 @@ public String toString() { /** A pending cache entry when the async RouteLookup RPC is still on the fly. */ static final class PendingCacheEntry { private final ListenableFuture pendingCall; - private final RouteLookupRequest request; + private final RouteLookupRequestKey routeLookupRequestKey; @Nullable private final BackoffPolicy backoffPolicy; PendingCacheEntry( - RouteLookupRequest request, + RouteLookupRequestKey routeLookupRequestKey, ListenableFuture pendingCall, @Nullable BackoffPolicy backoffPolicy) { - this.request = checkNotNull(request, "request"); + this.routeLookupRequestKey = checkNotNull(routeLookupRequestKey, "request"); this.pendingCall = checkNotNull(pendingCall, "pendingCall"); this.backoffPolicy = backoffPolicy; } @@ -606,7 +615,7 @@ static final class PendingCacheEntry { @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("request", request) + .add("routeLookupRequestKey", routeLookupRequestKey) .toString(); } } @@ -614,10 +623,10 @@ public String toString() { /** Common cache entry data for {@link RlsAsyncLruCache}. */ abstract static class CacheEntry { - protected final RouteLookupRequest request; + protected final RouteLookupRequestKey routeLookupRequestKey; - CacheEntry(RouteLookupRequest request) { - this.request = checkNotNull(request, "request"); + CacheEntry(RouteLookupRequestKey routeLookupRequestKey) { + this.routeLookupRequestKey = checkNotNull(routeLookupRequestKey, "request"); } abstract int getSizeBytes(); @@ -640,8 +649,9 @@ final class DataCacheEntry extends CacheEntry { private final List childPolicyWrappers; // GuardedBy CachingRlsLbClient.lock - DataCacheEntry(RouteLookupRequest request, final RouteLookupResponse response) { - super(request); + DataCacheEntry(RouteLookupRequestKey routeLookupRequestKey, + final RouteLookupResponse response) { + super(routeLookupRequestKey); this.response = checkNotNull(response, "response"); checkState(!response.targets().isEmpty(), "No targets returned by RLS"); childPolicyWrappers = @@ -669,13 +679,14 @@ final class DataCacheEntry extends CacheEntry { */ void maybeRefresh() { synchronized (lock) { // Lock is already held, but ErrorProne can't tell - if (pendingCallCache.containsKey(request)) { + if (pendingCallCache.containsKey(routeLookupRequestKey)) { // pending already requested return; } logger.log(ChannelLogLevel.DEBUG, - "[RLS Entry {0}] Cache entry is stale, refreshing", request); - asyncRlsCall(request, /* backoffPolicy= */ null); + "[RLS Entry {0}] Cache entry is stale, refreshing", routeLookupRequestKey); + asyncRlsCall(routeLookupRequestKey, /* backoffPolicy= */ null, + RouteLookupRequest.Reason.REASON_STALE); } } @@ -745,7 +756,7 @@ void cleanup() { @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("request", request) + .add("request", routeLookupRequestKey) .add("response", response) .add("expireTime", expireTime) .add("staleTime", staleTime) @@ -764,8 +775,9 @@ private static final class BackoffCacheEntry extends CacheEntry { private final BackoffPolicy backoffPolicy; private Future scheduledFuture; - BackoffCacheEntry(RouteLookupRequest request, Status status, BackoffPolicy backoffPolicy) { - super(request); + BackoffCacheEntry(RouteLookupRequestKey routeLookupRequestKey, Status status, + BackoffPolicy backoffPolicy) { + super(routeLookupRequestKey); this.status = checkNotNull(status, "status"); this.backoffPolicy = checkNotNull(backoffPolicy, "backoffPolicy"); } @@ -792,7 +804,7 @@ void cleanup() { @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("request", request) + .add("request", routeLookupRequestKey) .add("status", status) .toString(); } @@ -811,7 +823,7 @@ static final class Builder { private Throttler throttler = new HappyThrottler(); private ResolvedAddressFactory resolvedAddressFactory; private Ticker ticker = Ticker.systemTicker(); - private EvictionListener evictionListener; + private EvictionListener evictionListener; private BackoffPolicy.Provider backoffProvider = new ExponentialBackoffPolicy.Provider(); Builder setHelper(Helper helper) { @@ -845,7 +857,7 @@ Builder setTicker(Ticker ticker) { } Builder setEvictionListener( - @Nullable EvictionListener evictionListener) { + @Nullable EvictionListener evictionListener) { this.evictionListener = evictionListener; return this; } @@ -867,17 +879,17 @@ CachingRlsLbClient build() { * CacheEntry#cleanup()} after original {@link EvictionListener} is finished. */ private static final class AutoCleaningEvictionListener - implements EvictionListener { + implements EvictionListener { - private final EvictionListener delegate; + private final EvictionListener delegate; AutoCleaningEvictionListener( - @Nullable EvictionListener delegate) { + @Nullable EvictionListener delegate) { this.delegate = delegate; } @Override - public void onEviction(RouteLookupRequest key, CacheEntry value, EvictionType cause) { + public void onEviction(RouteLookupRequestKey key, CacheEntry value, EvictionType cause) { if (delegate != null) { delegate.onEviction(key, value, cause); } @@ -902,29 +914,29 @@ public void registerBackendResponse(boolean throttled) { /** Implementation of {@link LinkedHashLruCache} for RLS. */ private static final class RlsAsyncLruCache - extends LinkedHashLruCache { + extends LinkedHashLruCache { private final RlsLbHelper helper; RlsAsyncLruCache(long maxEstimatedSizeBytes, - @Nullable EvictionListener evictionListener, + @Nullable EvictionListener evictionListener, Ticker ticker, RlsLbHelper helper) { super(maxEstimatedSizeBytes, evictionListener, ticker); this.helper = checkNotNull(helper, "helper"); } @Override - protected boolean isExpired(RouteLookupRequest key, CacheEntry value, long nowNanos) { + protected boolean isExpired(RouteLookupRequestKey key, CacheEntry value, long nowNanos) { return value.isExpired(nowNanos); } @Override - protected int estimateSizeOf(RouteLookupRequest key, CacheEntry value) { + protected int estimateSizeOf(RouteLookupRequestKey key, CacheEntry value) { return value.getSizeBytes(); } @Override protected boolean shouldInvalidateEldestEntry( - RouteLookupRequest eldestKey, CacheEntry eldestValue, long now) { + RouteLookupRequestKey eldestKey, CacheEntry eldestValue, long now) { if (!eldestValue.isOldEnoughToBeEvicted(now)) { return false; } @@ -933,7 +945,7 @@ protected boolean shouldInvalidateEldestEntry( return this.estimatedSizeBytes() > this.estimatedMaxSizeBytes(); } - public CacheEntry cacheAndClean(RouteLookupRequest key, CacheEntry value) { + public CacheEntry cacheAndClean(RouteLookupRequestKey key, CacheEntry value) { CacheEntry newEntry = cache(key, value); // force cleanup if new entry pushed cache over max size (in bytes) @@ -989,9 +1001,9 @@ final class RlsPicker extends SubchannelPicker { public PickResult pickSubchannel(PickSubchannelArgs args) { String serviceName = args.getMethodDescriptor().getServiceName(); String methodName = args.getMethodDescriptor().getBareMethodName(); - RouteLookupRequest request = + RlsProtoData.RouteLookupRequestKey lookupRequestKey = requestFactory.create(serviceName, methodName, args.getHeaders()); - final CachedRouteLookupResponse response = CachingRlsLbClient.this.get(request); + final CachedRouteLookupResponse response = CachingRlsLbClient.this.get(lookupRequestKey); if (response.getHeaderData() != null && !response.getHeaderData().isEmpty()) { Metadata headers = args.getHeaders(); diff --git a/rls/src/main/java/io/grpc/rls/RlsProtoConverters.java b/rls/src/main/java/io/grpc/rls/RlsProtoConverters.java index aa5147449c4..70f9fb4d891 100644 --- a/rls/src/main/java/io/grpc/rls/RlsProtoConverters.java +++ b/rls/src/main/java/io/grpc/rls/RlsProtoConverters.java @@ -64,7 +64,9 @@ static final class RouteLookupRequestConverter @Override protected RlsProtoData.RouteLookupRequest doForward(RouteLookupRequest routeLookupRequest) { return RlsProtoData.RouteLookupRequest.create( - ImmutableMap.copyOf(routeLookupRequest.getKeyMapMap())); + ImmutableMap.copyOf(routeLookupRequest.getKeyMapMap()), + RlsProtoData.RouteLookupRequest.Reason.valueOf(routeLookupRequest.getReason().name()) + ); } @Override @@ -72,6 +74,7 @@ protected RouteLookupRequest doBackward(RlsProtoData.RouteLookupRequest routeLoo return RouteLookupRequest.newBuilder() .setTargetType("grpc") + .setReason(RouteLookupRequest.Reason.valueOf(routeLookupRequest.reason().name())) .putAllKeyMap(routeLookupRequest.keyMap()) .build(); } diff --git a/rls/src/main/java/io/grpc/rls/RlsProtoData.java b/rls/src/main/java/io/grpc/rls/RlsProtoData.java index 49f32c6b6e3..39c404870f9 100644 --- a/rls/src/main/java/io/grpc/rls/RlsProtoData.java +++ b/rls/src/main/java/io/grpc/rls/RlsProtoData.java @@ -27,16 +27,42 @@ final class RlsProtoData { private RlsProtoData() {} + /** A key object for the Rls route lookup data cache. */ + @AutoValue + @Immutable + abstract static class RouteLookupRequestKey { + + /** Returns a map of key values extracted via key builders for the gRPC or HTTP request. */ + abstract ImmutableMap keyMap(); + + static RouteLookupRequestKey create(ImmutableMap keyMap) { + return new AutoValue_RlsProtoData_RouteLookupRequestKey(keyMap); + } + } + /** A request object sent to route lookup service. */ @AutoValue @Immutable abstract static class RouteLookupRequest { + /** Names should match those in {@link io.grpc.lookup.v1.RouteLookupRequest.Reason}. */ + enum Reason { + /** Unused. */ + REASON_UNKNOWN, + /** No data available in local cache. */ + REASON_MISS, + /** Data in local cache is stale. */ + REASON_STALE; + } + + /** Reason for making this request. */ + abstract Reason reason(); + /** Returns a map of key values extracted via key builders for the gRPC or HTTP request. */ abstract ImmutableMap keyMap(); - static RouteLookupRequest create(ImmutableMap keyMap) { - return new AutoValue_RlsProtoData_RouteLookupRequest(keyMap); + static RouteLookupRequest create(ImmutableMap keyMap, Reason reason) { + return new AutoValue_RlsProtoData_RouteLookupRequest(reason, keyMap); } } diff --git a/rls/src/main/java/io/grpc/rls/RlsRequestFactory.java b/rls/src/main/java/io/grpc/rls/RlsRequestFactory.java index e26e49979e1..1fed78f4df3 100644 --- a/rls/src/main/java/io/grpc/rls/RlsRequestFactory.java +++ b/rls/src/main/java/io/grpc/rls/RlsRequestFactory.java @@ -27,13 +27,13 @@ import io.grpc.rls.RlsProtoData.GrpcKeyBuilder.Name; import io.grpc.rls.RlsProtoData.NameMatcher; import io.grpc.rls.RlsProtoData.RouteLookupConfig; -import io.grpc.rls.RlsProtoData.RouteLookupRequest; +import io.grpc.rls.RlsProtoData.RouteLookupRequestKey; import java.util.HashMap; import java.util.List; import java.util.Map; /** - * A RlsRequestFactory creates {@link RouteLookupRequest} using key builder map from {@link + * A RlsRequestFactory creates {@link RouteLookupRequestKey} using key builder map from {@link * RouteLookupConfig}. */ final class RlsRequestFactory { @@ -61,9 +61,9 @@ private static Map createKeyBuilderTable( return table; } - /** Creates a {@link RouteLookupRequest} for given request's metadata. */ + /** Creates a {@link RouteLookupRequestKey} for the given request lookup metadata. */ @CheckReturnValue - RouteLookupRequest create(String service, String method, Metadata metadata) { + RouteLookupRequestKey create(String service, String method, Metadata metadata) { checkNotNull(service, "service"); checkNotNull(method, "method"); String path = "/" + service + "/" + method; @@ -73,7 +73,7 @@ RouteLookupRequest create(String service, String method, Metadata metadata) { grpcKeyBuilder = keyBuilderTable.get("/" + service + "/*"); } if (grpcKeyBuilder == null) { - return RouteLookupRequest.create(ImmutableMap.of()); + return RouteLookupRequestKey.create(ImmutableMap.of()); } ImmutableMap.Builder rlsRequestHeaders = createRequestHeaders(metadata, grpcKeyBuilder.headers()); @@ -89,7 +89,7 @@ RouteLookupRequest create(String service, String method, Metadata metadata) { rlsRequestHeaders.put(extraKeys.method(), method); } rlsRequestHeaders.putAll(constantKeys); - return RouteLookupRequest.create(rlsRequestHeaders.buildOrThrow()); + return RouteLookupRequestKey.create(rlsRequestHeaders.buildOrThrow()); } private ImmutableMap.Builder createRequestHeaders( diff --git a/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java b/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java index 4f086abc4a2..93c7d0f00ff 100644 --- a/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java +++ b/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java @@ -128,7 +128,7 @@ public class CachingRlsLbClientTest { public final GrpcCleanupRule grpcCleanupRule = new GrpcCleanupRule(); @Mock - private EvictionListener evictionListener; + private EvictionListener evictionListener; @Mock private SocketAddress socketAddress; @Mock @@ -200,14 +200,14 @@ public void tearDown() throws Exception { } private CachedRouteLookupResponse getInSyncContext( - final RouteLookupRequest request) + final RlsProtoData.RouteLookupRequestKey routeLookupRequestKey) throws ExecutionException, InterruptedException, TimeoutException { final SettableFuture responseSettableFuture = SettableFuture.create(); syncContext.execute(new Runnable() { @Override public void run() { - responseSettableFuture.set(rlsLbClient.get(request)); + responseSettableFuture.set(rlsLbClient.get(routeLookupRequestKey)); } }); return responseSettableFuture.get(5, TimeUnit.SECONDS); @@ -217,48 +217,53 @@ public void run() { public void get_noError_lifeCycle() throws Exception { setUpRlsLbClient(); InOrder inOrder = inOrder(evictionListener); - RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( - "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create( + ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); rlsServerImpl.setLookupTable( ImmutableMap.of( - routeLookupRequest, + routeLookupRequestKey, RouteLookupResponse.create(ImmutableList.of("target"), "header"))); // initial request - CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequest); + CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.isPending()).isTrue(); // server response fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); - resp = getInSyncContext(routeLookupRequest); + resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.hasData()).isTrue(); // cache hit for staled entry fakeClock.forwardTime(ROUTE_LOOKUP_CONFIG.staleAgeInNanos(), TimeUnit.NANOSECONDS); - resp = getInSyncContext(routeLookupRequest); + rlsServerImpl.routeLookupReason = null; + resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.hasData()).isTrue(); // async refresh finishes fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); inOrder .verify(evictionListener) - .onEviction(eq(routeLookupRequest), any(CacheEntry.class), eq(EvictionType.REPLACED)); + .onEviction(eq(routeLookupRequestKey), any(CacheEntry.class), eq(EvictionType.REPLACED)); - resp = getInSyncContext(routeLookupRequest); + resp = getInSyncContext(routeLookupRequestKey); + assertThat(rlsServerImpl.routeLookupReason).isEqualTo( + io.grpc.lookup.v1.RouteLookupRequest.Reason.REASON_STALE); assertThat(resp.hasData()).isTrue(); // existing cache expired fakeClock.forwardTime(ROUTE_LOOKUP_CONFIG.maxAgeInNanos(), TimeUnit.NANOSECONDS); - resp = getInSyncContext(routeLookupRequest); + resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.isPending()).isTrue(); inOrder .verify(evictionListener) - .onEviction(eq(routeLookupRequest), any(CacheEntry.class), eq(EvictionType.EXPIRED)); + .onEviction(eq(routeLookupRequestKey), any(CacheEntry.class), eq(EvictionType.EXPIRED)); inOrder.verifyNoMoreInteractions(); } @@ -287,22 +292,27 @@ public void rls_withCustomRlsChannelServiceConfig() throws Exception { .setThrottler(fakeThrottler) .setTicker(fakeClock.getTicker()) .build(); - RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( - "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create( + ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); rlsServerImpl.setLookupTable( ImmutableMap.of( - routeLookupRequest, + routeLookupRequestKey, RouteLookupResponse.create(ImmutableList.of("target"), "header"))); + rlsServerImpl.routeLookupReason = null; // initial request - CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequest); + CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.isPending()).isTrue(); // server response fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); - resp = getInSyncContext(routeLookupRequest); + resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.hasData()).isTrue(); + assertThat(rlsServerImpl.routeLookupReason).isEqualTo( + io.grpc.lookup.v1.RouteLookupRequest.Reason.REASON_MISS); assertThat(rlsChannelOverriddenAuthority).isEqualTo("bigtable.googleapis.com:443"); assertThat(rlsChannelServiceConfig).isEqualTo(routeLookupChannelServiceConfig); @@ -311,26 +321,28 @@ public void rls_withCustomRlsChannelServiceConfig() throws Exception { @Test public void get_throttledAndRecover() throws Exception { setUpRlsLbClient(); - RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( - "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create( + ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); rlsServerImpl.setLookupTable( ImmutableMap.of( - routeLookupRequest, + routeLookupRequestKey, RouteLookupResponse.create(ImmutableList.of("target"), "header"))); fakeThrottler.nextResult = true; fakeBackoffProvider.nextPolicy = createBackoffPolicy(10, TimeUnit.MILLISECONDS); - CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequest); + CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.hasError()).isTrue(); fakeClock.forwardTime(10, TimeUnit.MILLISECONDS); // initially backed off entry is backed off again verify(evictionListener) - .onEviction(eq(routeLookupRequest), any(CacheEntry.class), eq(EvictionType.EXPLICIT)); + .onEviction(eq(routeLookupRequestKey), any(CacheEntry.class), eq(EvictionType.EXPLICIT)); - resp = getInSyncContext(routeLookupRequest); + resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.hasError()).isTrue(); @@ -338,14 +350,17 @@ public void get_throttledAndRecover() throws Exception { fakeThrottler.nextResult = false; fakeClock.forwardTime(10, TimeUnit.MILLISECONDS); - resp = getInSyncContext(routeLookupRequest); + resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.isPending()).isTrue(); + rlsServerImpl.routeLookupReason = null; // server responses fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); + assertThat(rlsServerImpl.routeLookupReason).isEqualTo( + io.grpc.lookup.v1.RouteLookupRequest.Reason.REASON_MISS); - resp = getInSyncContext(routeLookupRequest); + resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.hasData()).isTrue(); } @@ -354,21 +369,24 @@ public void get_throttledAndRecover() throws Exception { public void get_updatesLbState() throws Exception { setUpRlsLbClient(); InOrder inOrder = inOrder(helper); - RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( - "server", "bigtable.googleapis.com", "service-key", "service1", "method-key", "create")); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create( + ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "service1", + "method-key", "create")); rlsServerImpl.setLookupTable( ImmutableMap.of( - routeLookupRequest, + routeLookupRequestKey, RouteLookupResponse.create( ImmutableList.of("primary.cloudbigtable.googleapis.com"), "header-rls-data-value"))); // valid channel - CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequest); + CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.isPending()).isTrue(); fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); - resp = getInSyncContext(routeLookupRequest); + resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.hasData()).isTrue(); ArgumentCaptor pickerCaptor = ArgumentCaptor.forClass(SubchannelPicker.class); @@ -393,13 +411,13 @@ public void get_updatesLbState() throws Exception { // move backoff further back to only test error behavior fakeBackoffProvider.nextPolicy = createBackoffPolicy(100, TimeUnit.MILLISECONDS); // try to get invalid - RouteLookupRequest invalidRouteLookupRequest = - RouteLookupRequest.create(ImmutableMap.of()); - CachedRouteLookupResponse errorResp = getInSyncContext(invalidRouteLookupRequest); + RlsProtoData.RouteLookupRequestKey invalidRouteLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create(ImmutableMap.of()); + CachedRouteLookupResponse errorResp = getInSyncContext(invalidRouteLookupRequestKey); assertThat(errorResp.isPending()).isTrue(); fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); - errorResp = getInSyncContext(invalidRouteLookupRequest); + errorResp = getInSyncContext(invalidRouteLookupRequestKey); assertThat(errorResp.hasError()).isTrue(); // Channel is still READY because the subchannel for method /service1/create is still READY. @@ -423,21 +441,24 @@ public void get_updatesLbState() throws Exception { @Test public void timeout_not_changing_picked_subchannel() throws Exception { setUpRlsLbClient(); - RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( - "server", "bigtable.googleapis.com", "service-key", "service1", "method-key", "create")); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create( + ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "service1", + "method-key", "create")); rlsServerImpl.setLookupTable( ImmutableMap.of( - routeLookupRequest, + routeLookupRequestKey, RouteLookupResponse.create( ImmutableList.of("primary.cloudbigtable.googleapis.com", "target2", "target3"), "header-rls-data-value"))); // valid channel - CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequest); + CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.hasData()).isFalse(); fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); - resp = getInSyncContext(routeLookupRequest); + resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.hasData()).isTrue(); ArgumentCaptor pickerCaptor = ArgumentCaptor.forClass(SubchannelPicker.class); @@ -493,21 +514,24 @@ public void get_withAdaptiveThrottler() throws Exception { .setTicker(fakeClock.getTicker()) .build(); InOrder inOrder = inOrder(helper); - RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( - "server", "bigtable.googleapis.com", "service-key", "service1", "method-key", "create")); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create( + ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "service1", + "method-key", "create")); rlsServerImpl.setLookupTable( ImmutableMap.of( - routeLookupRequest, + routeLookupRequestKey, RouteLookupResponse.create( ImmutableList.of("primary.cloudbigtable.googleapis.com"), "header-rls-data-value"))); // valid channel - CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequest); + CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.isPending()).isTrue(); fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); - resp = getInSyncContext(routeLookupRequest); + resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.hasData()).isTrue(); ArgumentCaptor pickerCaptor = ArgumentCaptor.forClass(SubchannelPicker.class); @@ -524,13 +548,13 @@ public void get_withAdaptiveThrottler() throws Exception { // move backoff further back to only test error behavior fakeBackoffProvider.nextPolicy = createBackoffPolicy(100, TimeUnit.MILLISECONDS); // try to get invalid - RouteLookupRequest invalidRouteLookupRequest = - RouteLookupRequest.create(ImmutableMap.of()); - CachedRouteLookupResponse errorResp = getInSyncContext(invalidRouteLookupRequest); + RlsProtoData.RouteLookupRequestKey invalidRouteLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create(ImmutableMap.of()); + CachedRouteLookupResponse errorResp = getInSyncContext(invalidRouteLookupRequestKey); assertThat(errorResp.isPending()).isTrue(); fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); - errorResp = getInSyncContext(invalidRouteLookupRequest); + errorResp = getInSyncContext(invalidRouteLookupRequestKey); assertThat(errorResp.hasError()).isTrue(); // Channel is still READY because the subchannel for method /service1/create is still READY. @@ -560,22 +584,26 @@ private PickSubchannelArgsImpl getInvalidArgs(Metadata headers) { @Test public void get_childPolicyWrapper_reusedForSameTarget() throws Exception { setUpRlsLbClient(); - RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( - "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); - RouteLookupRequest routeLookupRequest2 = RouteLookupRequest.create(ImmutableMap.of( - "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "baz")); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create( + ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey2 = + RlsProtoData.RouteLookupRequestKey.create( + ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "baz")); rlsServerImpl.setLookupTable( ImmutableMap.of( - routeLookupRequest, + routeLookupRequestKey, RouteLookupResponse.create(ImmutableList.of("target"), "header"), - routeLookupRequest2, + routeLookupRequestKey2, RouteLookupResponse.create(ImmutableList.of("target"), "header2"))); - CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequest); + CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.isPending()).isTrue(); fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); - resp = getInSyncContext(routeLookupRequest); + resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.hasData()).isTrue(); assertThat(resp.getHeaderData()).isEqualTo("header"); @@ -585,11 +613,11 @@ public void get_childPolicyWrapper_reusedForSameTarget() throws Exception { assertThat(childPolicyWrapper.getPicker()).isNotInstanceOf(RlsPicker.class); // request2 has same target, it should reuse childPolicyWrapper - CachedRouteLookupResponse resp2 = getInSyncContext(routeLookupRequest2); + CachedRouteLookupResponse resp2 = getInSyncContext(routeLookupRequestKey2); assertThat(resp2.isPending()).isTrue(); fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); - resp2 = getInSyncContext(routeLookupRequest2); + resp2 = getInSyncContext(routeLookupRequestKey2); assertThat(resp2.hasData()).isTrue(); assertThat(resp2.getHeaderData()).isEqualTo("header2"); assertThat(resp2.getChildPolicyWrapper()).isEqualTo(resp.getChildPolicyWrapper()); @@ -598,20 +626,22 @@ public void get_childPolicyWrapper_reusedForSameTarget() throws Exception { @Test public void get_childPolicyWrapper_multiTarget() throws Exception { setUpRlsLbClient(); - RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( - "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create( + ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); rlsServerImpl.setLookupTable( ImmutableMap.of( - routeLookupRequest, + routeLookupRequestKey, RouteLookupResponse.create( ImmutableList.of("target1", "target2", "target3"), "header"))); - CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequest); + CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.isPending()).isTrue(); fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); - resp = getInSyncContext(routeLookupRequest); + resp = getInSyncContext(routeLookupRequestKey); assertThat(resp.hasData()).isTrue(); List policyWrappers = new ArrayList<>(); @@ -680,14 +710,15 @@ public void metricGauges() throws ExecutionException, InterruptedException, Time .recordLongGauge(argThat(new LongGaugeInstrumentArgumentMatcher("grpc.lb.rls.cache_size")), eq(0L), any(), any()); - RouteLookupRequest routeLookupRequest = RouteLookupRequest.create( - ImmutableMap.of("server", "bigtable.googleapis.com", "service-key", "foo", "method-key", - "bar")); - rlsServerImpl.setLookupTable(ImmutableMap.of(routeLookupRequest, + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create( + ImmutableMap.of("server", "bigtable.googleapis.com", "service-key", "foo", "method-key", + "bar")); + rlsServerImpl.setLookupTable(ImmutableMap.of(routeLookupRequestKey, RouteLookupResponse.create(ImmutableList.of("target"), "header"))); // Make a request that will populate the cache with an entry - getInSyncContext(routeLookupRequest); + getInSyncContext(routeLookupRequestKey); fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); // Gauge values should reflect the new cache entry. @@ -857,7 +888,9 @@ private static final class StaticFixedDelayRlsServerImpl private final long responseDelayNano; private final ScheduledExecutorService scheduledExecutorService; - private Map lookupTable = ImmutableMap.of(); + private Map lookupTable = + ImmutableMap.of(); + io.grpc.lookup.v1.RouteLookupRequest.Reason routeLookupReason; public StaticFixedDelayRlsServerImpl( long responseDelayNano, ScheduledExecutorService scheduledExecutorService) { @@ -867,7 +900,8 @@ public StaticFixedDelayRlsServerImpl( checkNotNull(scheduledExecutorService, "scheduledExecutorService"); } - private void setLookupTable(Map lookupTable) { + private void setLookupTable(Map lookupTable) { this.lookupTable = checkNotNull(lookupTable, "lookupTable"); } @@ -879,8 +913,11 @@ public void routeLookup(final io.grpc.lookup.v1.RouteLookupRequest request, new Runnable() { @Override public void run() { + routeLookupReason = request.getReason(); RouteLookupResponse response = - lookupTable.get(REQUEST_CONVERTER.convert(request)); + lookupTable.get( + RlsProtoData.RouteLookupRequestKey.create( + REQUEST_CONVERTER.convert(request).keyMap())); if (response == null) { responseObserver.onError(new RuntimeException("not found")); } else { diff --git a/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java b/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java index 354466f3caf..c180935b153 100644 --- a/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java +++ b/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java @@ -166,15 +166,19 @@ public void setUp() { .build(); fakeRlsServerImpl.setLookupTable( ImmutableMap.of( - RouteLookupRequest.create(ImmutableMap.of( + RouteLookupRequest.create( + ImmutableMap.of( "server", "fake-bigtable.googleapis.com", "service-key", "com.google", - "method-key", "Search")), + "method-key", "Search"), + RouteLookupRequest.Reason.REASON_MISS), RouteLookupResponse.create(ImmutableList.of("wilderness"), "where are you?"), - RouteLookupRequest.create(ImmutableMap.of( + RouteLookupRequest.create( + ImmutableMap.of( "server", "fake-bigtable.googleapis.com", "service-key", "com.google", - "method-key", "Rescue")), + "method-key", "Rescue"), + RouteLookupRequest.Reason.REASON_MISS), RouteLookupResponse.create(ImmutableList.of("civilization"), "you are safe"))); rlsLb = (RlsLoadBalancer) provider.newLoadBalancer(helper); diff --git a/rls/src/test/java/io/grpc/rls/RlsProtoConvertersTest.java b/rls/src/test/java/io/grpc/rls/RlsProtoConvertersTest.java index fc5fdb59f21..ad1ce8c363e 100644 --- a/rls/src/test/java/io/grpc/rls/RlsProtoConvertersTest.java +++ b/rls/src/test/java/io/grpc/rls/RlsProtoConvertersTest.java @@ -61,12 +61,14 @@ public void convert_toRequestObject() { Converter converter = new RouteLookupRequestConverter().reverse(); RlsProtoData.RouteLookupRequest requestObject = - RlsProtoData.RouteLookupRequest.create(ImmutableMap.of("key1", "val1")); + RlsProtoData.RouteLookupRequest.create(ImmutableMap.of("key1", "val1"), + RlsProtoData.RouteLookupRequest.Reason.REASON_MISS); RouteLookupRequest proto = converter.convert(requestObject); assertThat(proto.getTargetType()).isEqualTo("grpc"); assertThat(proto.getKeyMapMap()).containsExactly("key1", "val1"); + assertThat(proto.getReason()).isEqualTo(RouteLookupRequest.Reason.REASON_MISS); } @Test diff --git a/rls/src/test/java/io/grpc/rls/RlsRequestFactoryTest.java b/rls/src/test/java/io/grpc/rls/RlsRequestFactoryTest.java index 6ee2c01af8a..2b900994ed9 100644 --- a/rls/src/test/java/io/grpc/rls/RlsRequestFactoryTest.java +++ b/rls/src/test/java/io/grpc/rls/RlsRequestFactoryTest.java @@ -26,7 +26,6 @@ import io.grpc.rls.RlsProtoData.GrpcKeyBuilder.Name; import io.grpc.rls.RlsProtoData.NameMatcher; import io.grpc.rls.RlsProtoData.RouteLookupConfig; -import io.grpc.rls.RlsProtoData.RouteLookupRequest; import java.util.concurrent.TimeUnit; import org.junit.Test; import org.junit.runner.RunWith; @@ -82,8 +81,9 @@ public void create_pathMatches() { metadata.put(Metadata.Key.of("X-Google-Id", Metadata.ASCII_STRING_MARSHALLER), "123"); metadata.put(Metadata.Key.of("foo", Metadata.ASCII_STRING_MARSHALLER), "bar"); - RouteLookupRequest request = factory.create("com.google.service1", "Create", metadata); - assertThat(request.keyMap()).containsExactly( + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + factory.create("com.google.service1", "Create", metadata); + assertThat(routeLookupRequestKey.keyMap()).containsExactly( "user", "test", "id", "123", "server-1", "bigtable.googleapis.com", @@ -97,9 +97,10 @@ public void create_pathFallbackMatches() { metadata.put(Metadata.Key.of("Password", Metadata.ASCII_STRING_MARSHALLER), "hunter2"); metadata.put(Metadata.Key.of("foo", Metadata.ASCII_STRING_MARSHALLER), "bar"); - RouteLookupRequest request = factory.create("com.google.service1" , "Update", metadata); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + factory.create("com.google.service1" , "Update", metadata); - assertThat(request.keyMap()).containsExactly( + assertThat(routeLookupRequestKey.keyMap()).containsExactly( "user", "test", "password", "hunter2", "service-2", "com.google.service1", @@ -113,9 +114,10 @@ public void create_pathFallbackMatches_optionalHeaderMissing() { metadata.put(Metadata.Key.of("X-Google-Id", Metadata.ASCII_STRING_MARSHALLER), "123"); metadata.put(Metadata.Key.of("foo", Metadata.ASCII_STRING_MARSHALLER), "bar"); - RouteLookupRequest request = factory.create("com.google.service1", "Update", metadata); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + factory.create("com.google.service1", "Update", metadata); - assertThat(request.keyMap()).containsExactly( + assertThat(routeLookupRequestKey.keyMap()).containsExactly( "user", "test", "service-2", "com.google.service1", "const-key-2", "const-value-2"); @@ -128,8 +130,9 @@ public void create_unknownPath() { metadata.put(Metadata.Key.of("X-Google-Id", Metadata.ASCII_STRING_MARSHALLER), "123"); metadata.put(Metadata.Key.of("foo", Metadata.ASCII_STRING_MARSHALLER), "bar"); - RouteLookupRequest request = factory.create("abc.def.service999", "Update", metadata); - assertThat(request.keyMap()).isEmpty(); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + factory.create("abc.def.service999", "Update", metadata); + assertThat(routeLookupRequestKey.keyMap()).isEmpty(); } @Test @@ -139,9 +142,10 @@ public void create_noMethodInRlsConfig() { metadata.put(Metadata.Key.of("X-Google-Id", Metadata.ASCII_STRING_MARSHALLER), "123"); metadata.put(Metadata.Key.of("foo", Metadata.ASCII_STRING_MARSHALLER), "bar"); - RouteLookupRequest request = factory.create("com.google.service3", "Update", metadata); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + factory.create("com.google.service3", "Update", metadata); - assertThat(request.keyMap()).containsExactly( + assertThat(routeLookupRequestKey.keyMap()).containsExactly( "user", "test", "const-key-4", "const-value-4"); } } From 9313e87dfe55f00a5b73028b33e6d6866cbd6330 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Fri, 17 Oct 2025 22:59:17 -0700 Subject: [PATCH 435/591] Pre-factor out the guts of the BinderClientTransport handshake. Moves the existing handshake logic behind an interface using the strategy pattern. No functional change. --- .../internal/BinderClientTransport.java | 65 +++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 54d65936fab..b456b60de34 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -25,6 +25,8 @@ import android.os.IBinder; import android.os.Parcel; import android.os.Process; +import androidx.annotation.BinderThread; +import androidx.annotation.MainThread; import com.google.common.base.Ticker; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -72,6 +74,9 @@ public final class BinderClientTransport extends BinderTransport private final SecurityPolicy securityPolicy; private final Bindable serviceBinding; + @GuardedBy("this") + private final ClientHandshake handshake; + /** Number of ongoing calls which keep this transport "in-use". */ private final AtomicInteger numInUseStreams; @@ -114,6 +119,7 @@ public BinderClientTransport( Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE); this.preAuthorizeServer = preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; + this.handshake = new LegacyClientHandshake(); numInUseStreams = new AtomicInteger(); pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); @@ -136,7 +142,7 @@ void releaseExecutors() { @Override public synchronized void onBound(IBinder binder) { - sendSetupTransaction(binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + handshake.onBound(binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); } @Override @@ -340,10 +346,11 @@ protected void handleSetupTransport(Parcel parcel) { Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); return; } + handshake.handleSetupTransport(); + } - int remoteUid = Binder.getCallingUid(); - restrictIncomingBinderToCallsFrom(remoteUid); - attributes = setSecurityAttrs(attributes, remoteUid); + @GuardedBy("this") + private void checkServerAuthorization(int remoteUid) { ListenableFuture authResultFuture = register(checkServerAuthorizationAsync(remoteUid)); Futures.addCallback( authResultFuture, @@ -376,7 +383,11 @@ private synchronized void handleAuthResult(Status authorization) { shutdownInternal(authorization, true); return; } + handshake.onServerAuthorizationOk(); + } + @GuardedBy("this") + private void onHandshakeComplete() { setState(TransportState.READY); attributes = clientTransportListener.filterTransport(attributes); clientTransportListener.transportReady(); @@ -397,6 +408,52 @@ protected void handlePingResponse(Parcel parcel) { pingTracker.onPingResponse(parcel.readInt()); } + /** + * An abstract implementation of the client's connection handshake. + * + *

    Supports a clean migration away from the legacy approach, one client at a time. + */ + private interface ClientHandshake { + /** + * Notifies the implementation that the binding has succeeded and we are now connected to the + * server's "endpoint" which can be reached at 'endpointBinder'. + */ + @MainThread + void onBound(OneWayBinderProxy endpointBinder); + + /** Notifies the implementation that we've received a valid SETUP_TRANSPORT transaction. */ + @BinderThread + void handleSetupTransport(); + + /** Notifies the implementation that the SecurityPolicy check of the server succeeded. */ + void onServerAuthorizationOk(); + } + + private final class LegacyClientHandshake implements ClientHandshake { + @Override + @MainThread + @GuardedBy("BinderClientTransport.this") // By way of @GuardedBy("this") `handshake` member. + public void onBound(OneWayBinderProxy binder) { + sendSetupTransaction(binder); + } + + @Override + @BinderThread + @GuardedBy("BinderClientTransport.this") // By way of @GuardedBy("this") `handshake` member. + public void handleSetupTransport() { + int remoteUid = Binder.getCallingUid(); + restrictIncomingBinderToCallsFrom(remoteUid); + attributes = setSecurityAttrs(attributes, remoteUid); + checkServerAuthorization(remoteUid); + } + + @Override + @GuardedBy("BinderClientTransport.this") // By way of @GuardedBy("this") `handshake` member. + public void onServerAuthorizationOk() { + onHandshakeComplete(); + } + } + private static ClientStream newFailingClientStream( Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) { StatsTraceContext statsTraceContext = From d9710725d708e64b0455b8a184614b91c003d372 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Fri, 17 Oct 2025 23:29:34 -0700 Subject: [PATCH 436/591] binder: Introduce server authorization strategy v2 Adds support for android:isolatedProcess Services (fixing #12293) and moves all peer authorization checks to the handshake, allowing subsequent transactions to go unchecked. --- .../grpc/binder/BinderChannelSmokeTest.java | 29 ++++++--- .../io/grpc/binder/BinderChannelBuilder.java | 61 +++++++++++++++++++ .../io/grpc/binder/internal/Bindable.java | 22 ++++++- .../internal/BinderClientTransport.java | 54 +++++++++++++++- .../BinderClientTransportFactory.java | 9 +++ .../grpc/binder/internal/ServiceBinding.java | 24 ++++++++ .../RobolectricBinderTransportTest.java | 43 ++++++++++--- .../binder/internal/ServiceBindingTest.java | 31 ++++++++++ 8 files changed, 252 insertions(+), 21 deletions(-) diff --git a/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java b/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java index e3a8c58bf88..4e3cfcf0d05 100644 --- a/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java @@ -97,6 +97,7 @@ public final class BinderChannelSmokeTest { .setType(MethodDescriptor.MethodType.BIDI_STREAMING) .build(); + AndroidComponentAddress serverAddress; ManagedChannel channel; AtomicReference headersCapture = new AtomicReference<>(); AtomicReference clientUidCapture = new AtomicReference<>(); @@ -134,7 +135,7 @@ public void setUp() throws Exception { TestUtils.recordRequestHeadersInterceptor(headersCapture), PeerUids.newPeerIdentifyingServerInterceptor()); - AndroidComponentAddress serverAddress = HostServices.allocateService(appContext); + serverAddress = HostServices.allocateService(appContext); HostServices.configureService( serverAddress, HostServices.serviceParamsBuilder() @@ -149,13 +150,15 @@ public void setUp() throws Exception { .build()) .build()); - channel = - BinderChannelBuilder.forAddress(serverAddress, appContext) + channel = newBinderChannelBuilder().build(); + } + + BinderChannelBuilder newBinderChannelBuilder() { + return BinderChannelBuilder.forAddress(serverAddress, appContext) .inboundParcelablePolicy( - InboundParcelablePolicy.newBuilder() - .setAcceptParcelableMetadataValues(true) - .build()) - .build(); + InboundParcelablePolicy.newBuilder() + .setAcceptParcelableMetadataValues(true) + .build()); } @After @@ -185,6 +188,18 @@ public void testBasicCall() throws Exception { assertThat(doCall("Hello").get()).isEqualTo("Hello"); } + @Test + public void testBasicCallWithLegacyAuthStrategy() throws Exception { + channel = newBinderChannelBuilder().useLegacyAuthStrategy().build(); + assertThat(doCall("Hello").get()).isEqualTo("Hello"); + } + + @Test + public void testBasicCallWithV2AuthStrategy() throws Exception { + channel = newBinderChannelBuilder().useV2AuthStrategy().build(); + assertThat(doCall("Hello").get()).isEqualTo("Hello"); + } + @Test public void testPeerUidIsRecorded() throws Exception { assertThat(doCall("Hello").get()).isEqualTo("Hello"); diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java index 9a20d35ddb5..a241634dd22 100644 --- a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java +++ b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java @@ -280,6 +280,67 @@ public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) { return this; } + /** + * Specifies how and when to authorize a server against this Channel's {@link SecurityPolicy}. + * + *

    This method selects the original "legacy" authorization strategy, which is no longer + * preferred for two reasons: First, the legacy strategy considers the UID of the server *process* + * we connect to. This is problematic for services using the `android:isolatedProcess` attribute, + * which runs them under a different "ephemeral" UID. This UID lacks all the privileges of the + * hosting app -- any non-trivial SecurityPolicy would fail to authorize it. Second, the legacy + * authorization strategy performs SecurityPolicy checks later in the connection handshake, which + * means the calling UID must be rechecked on every subsequent RPC. For these reasons, prefer + * {@link #useV2AuthStrategy} instead. + * + *

    The server does not know which authorization strategy a client is using. Both strategies + * work with all versions of the grpc-binder server. + * + *

    Callers need not specify an authorization strategy, but the default is unspecified and will + * eventually become {@link #useV2AuthStrategy()}. Clients that require the legacy strategy should + * configure it explicitly using this method. Eventually, however, legacy support will be + * deprecated and removed. + * + * @return this + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12397") + public BinderChannelBuilder useLegacyAuthStrategy() { + transportFactoryBuilder.setUseLegacyAuthStrategy(true); + return this; + } + + /** + * Specifies how and when to authorize a server against this Channel's {@link SecurityPolicy}. + * + *

    This method selects the v2 authorization strategy. It improves on the original strategy + * ({@link #useLegacyAuthStrategy}), by considering the UID of the server *app* we connect to, + * rather than the server *process*. This allows clients to connect to services configured with + * the `android:isolatedProcess` attribute, which run with the same authority as the hosting app, + * but under a different "ephemeral" UID that any non-trivial SecurityPolicy would fail to + * authorize. + * + *

    Furthermore, the v2 authorization strategy performs SecurityPolicy checks earlier in the + * connection handshake, which allows subsequent RPCs over that connection to proceed securely + * without further UID checks. For these reasons, clients should prefer the v2 strategy. + * + *

    The server does not know which authorization strategy a client is using. Both strategies + * work with all versions of the grpc-binder server. + * + *

    Callers need not specify an authorization strategy, but the default is unspecified and can + * change over time. Clients that require the v2 strategy should configure it explicitly using + * this method. Eventually, this strategy will become the default and legacy support will be + * removed. + * + *

    If moving to the new authorization strategy causes a robolectric test to fail, ensure your + * fake Service component is registered with `ShadowPackageManager` using `addOrUpdateService()`. + * + * @return this + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12397") + public BinderChannelBuilder useV2AuthStrategy() { + transportFactoryBuilder.setUseLegacyAuthStrategy(false); + return this; + } + @Override public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) { checkState( diff --git a/binder/src/main/java/io/grpc/binder/internal/Bindable.java b/binder/src/main/java/io/grpc/binder/internal/Bindable.java index ae0c7284faf..59a2502de2b 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Bindable.java +++ b/binder/src/main/java/io/grpc/binder/internal/Bindable.java @@ -54,8 +54,11 @@ interface Observer { * before giving them a chance to run. However, note that the identity/existence of the resolved * Service can change between the time this method returns and the time you actually bind/connect * to it. For example, suppose the target package gets uninstalled or upgraded right after this - * method returns. In {@link Observer#onBound}, you should verify that the server you resolved is - * the same one you connected to. + * method returns. + * + *

    Compare with {@link #getConnectedServiceInfo()}, which can only be called after {@link + * Observer#onBound(IBinder)} but can be used to learn about the service you actually connected + * to. */ @AnyThread ServiceInfo resolve() throws StatusException; @@ -68,6 +71,21 @@ interface Observer { @AnyThread void bind(); + /** + * Asks PackageManager for details about the remote Service we *actually* connected to. + * + *

    Can only be called after {@link Observer#onBound}. + * + *

    Compare with {@link #resolve()}, which reports which service would be selected as of now but + * *without* connecting. + * + * @throws StatusException UNIMPLEMENTED if the connected service isn't found (an {@link + * Observer#onUnbound} callback has likely already happened or is on its way!) + * @throws IllegalStateException if {@link Observer#onBound} has not "happened-before" this call + */ + @AnyThread + ServiceInfo getConnectedServiceInfo() throws StatusException; + /** * Unbind from the remote service if connected. * diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index b456b60de34..00437956a51 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -119,10 +119,10 @@ public BinderClientTransport( Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE); this.preAuthorizeServer = preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; - this.handshake = new LegacyClientHandshake(); + this.handshake = + factory.useLegacyAuthStrategy ? new LegacyClientHandshake() : new V2ClientHandshake(); numInUseStreams = new AtomicInteger(); pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); - serviceBinding = new ServiceBinding( factory.mainThreadExecutor, @@ -386,6 +386,56 @@ private synchronized void handleAuthResult(Status authorization) { handshake.onServerAuthorizationOk(); } + private final class V2ClientHandshake implements ClientHandshake { + + private OneWayBinderProxy endpointBinder; + + @Override + @GuardedBy("BinderClientTransport.this") // By way of @GuardedBy("this") `handshake` member. + public void onBound(OneWayBinderProxy endpointBinder) { + this.endpointBinder = endpointBinder; + Futures.addCallback( + Futures.submit(serviceBinding::getConnectedServiceInfo, offloadExecutor), + new FutureCallback() { + @Override + public void onSuccess(ServiceInfo result) { + synchronized (BinderClientTransport.this) { + onConnectedServiceInfo(result); + } + } + + @Override + public void onFailure(Throwable t) { + synchronized (BinderClientTransport.this) { + shutdownInternal(Status.fromThrowable(t), true); + } + } + }, + offloadExecutor); + } + + @GuardedBy("BinderClientTransport.this") + private void onConnectedServiceInfo(ServiceInfo serviceInfo) { + if (!inState(TransportState.SETUP)) { + return; + } + attributes = setSecurityAttrs(attributes, serviceInfo.applicationInfo.uid); + checkServerAuthorization(serviceInfo.applicationInfo.uid); + } + + @Override + @GuardedBy("BinderClientTransport.this") + public void onServerAuthorizationOk() { + sendSetupTransaction(endpointBinder); + } + + @Override + @GuardedBy("BinderClientTransport.this") // By way of @GuardedBy("this") `handshake` member. + public void handleSetupTransport() { + onHandshakeComplete(); + } + } + @GuardedBy("this") private void onHandshakeComplete() { setState(TransportState.READY); diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java index e156a6a7b92..459e064ad9b 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java @@ -53,6 +53,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor final OneWayBinderProxy.Decorator binderDecorator; final long readyTimeoutMillis; final boolean preAuthorizeServers; // TODO(jdcormie): Default to true. + final boolean useLegacyAuthStrategy; ScheduledExecutorService executorService; Executor offloadExecutor; @@ -73,6 +74,7 @@ private BinderClientTransportFactory(Builder builder) { binderDecorator = checkNotNull(builder.binderDecorator); readyTimeoutMillis = builder.readyTimeoutMillis; preAuthorizeServers = builder.preAuthorizeServers; + useLegacyAuthStrategy = builder.useLegacyAuthStrategy; executorService = scheduledExecutorPool.getObject(); offloadExecutor = offloadExecutorPool.getObject(); @@ -126,6 +128,7 @@ public static final class Builder implements ClientTransportFactoryBuilder { OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR; long readyTimeoutMillis = 60_000; boolean preAuthorizeServers; + boolean useLegacyAuthStrategy = true; // TODO(jdcormie): Default to false. @Override public BinderClientTransportFactory buildClientTransportFactory() { @@ -219,5 +222,11 @@ public Builder setPreAuthorizeServers(boolean preAuthorizeServers) { this.preAuthorizeServers = preAuthorizeServers; return this; } + + /** Specifies which version of the client handshake to use. */ + public Builder setUseLegacyAuthStrategy(boolean useLegacyAuthStrategy) { + this.useLegacyAuthStrategy = useLegacyAuthStrategy; + return this; + } } } diff --git a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java index 3351736108e..4b6bf7d06fb 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -102,6 +102,9 @@ public String methodName() { private State reportedState; // Only used on the main thread. + @GuardedBy("this") + private ComponentName connectedServiceName; + @AnyThread ServiceBinding( Executor mainThreadExecutor, @@ -305,6 +308,26 @@ private void clearReferences() { sourceContext = null; } + @AnyThread + @Override + public ServiceInfo getConnectedServiceInfo() throws StatusException { + try { + return getContextForTargetUser("cross-user v2 handshake") + .getPackageManager() + .getServiceInfo(getConnectedServiceName(), /* flags= */ 0); + } catch (PackageManager.NameNotFoundException e) { + throw Status.UNIMPLEMENTED + .withCause(e) + .withDescription("connected remote service was uninstalled/disabled during handshake") + .asException(); + } + } + + private synchronized ComponentName getConnectedServiceName() { + checkState(connectedServiceName != null, "onBound() not yet called!"); + return connectedServiceName; + } + @Override @MainThread public void onServiceConnected(ComponentName className, IBinder binder) { @@ -312,6 +335,7 @@ public void onServiceConnected(ComponentName className, IBinder binder) { synchronized (this) { if (state == State.BINDING) { state = State.BOUND; + connectedServiceName = className; bound = true; } } diff --git a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java index 6beb92c1691..737c5651131 100644 --- a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java @@ -25,6 +25,7 @@ import static io.grpc.binder.internal.BinderTransport.SHUTDOWN_TRANSPORT; import static io.grpc.binder.internal.BinderTransport.WIRE_FORMAT_VERSION; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -99,6 +100,9 @@ @LooperMode(Mode.INSTRUMENTATION_TEST) public final class RobolectricBinderTransportTest extends AbstractTransportTest { + static final int SERVER_APP_UID = 11111; + static final int EPHEMERAL_SERVER_UID = 22222; // UID of isolated server process. + private final Application application = ApplicationProvider.getApplicationContext(); private final ObjectPool executorServicePool = SharedResourcePool.forResource(GrpcUtil.TIMER_SERVICE); @@ -111,8 +115,7 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest @Mock AsyncSecurityPolicy mockClientSecurityPolicy; - @Captor - ArgumentCaptor statusCaptor; + @Captor ArgumentCaptor statusCaptor; ApplicationInfo serverAppInfo; PackageInfo serverPkgInfo; @@ -120,11 +123,19 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest private int nextServerAddress; - @Parameter public boolean preAuthServersParam; + @Parameter(value = 0) + public boolean preAuthServersParam; + + @Parameter(value = 1) + public boolean useLegacyAuthStrategy; - @Parameters(name = "preAuthServersParam={0}") - public static ImmutableList data() { - return ImmutableList.of(true, false); + @Parameters(name = "preAuthServersParam={0};useLegacyAuthStrategy={1}") + public static ImmutableList data() { + return ImmutableList.of( + new Object[] {false, false}, + new Object[] {false, true}, + new Object[] {true, false}, + new Object[] {true, true}); } @Override @@ -190,6 +201,7 @@ protected InternalServer newServer( BinderClientTransportFactory.Builder newClientTransportFactoryBuilder() { return new BinderClientTransportFactory.Builder() .setPreAuthorizeServers(preAuthServersParam) + .setUseLegacyAuthStrategy(useLegacyAuthStrategy) .setSourceContext(application) .setScheduledExecutorPool(executorServicePool) .setOffloadExecutorPool(offloadExecutorPool); @@ -224,9 +236,9 @@ public void clientAuthorizesServerUidsInOrder() throws Exception { // lets us fake value this *globally*. So the ShadowBinder#setCallingUid() here unrealistically // affects the server's view of the client's uid too. For now this doesn't matter because this // test never exercises server SecurityPolicy. - ShadowBinder.setCallingUid(11111); // UID of the server *process*. + ShadowBinder.setCallingUid(EPHEMERAL_SERVER_UID); - serverPkgInfo.applicationInfo.uid = 22222; // UID of the server *app*, which can be different. + serverPkgInfo.applicationInfo.uid = SERVER_APP_UID; shadowOf(application.getPackageManager()).installPackage(serverPkgInfo); shadowOf(application.getPackageManager()).addOrUpdateService(serviceInfo); server = newServer(ImmutableList.of()); @@ -244,13 +256,17 @@ public void clientAuthorizesServerUidsInOrder() throws Exception { if (preAuthServersParam) { AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS); - assertThat(preAuthRequest.uid).isEqualTo(22222); + assertThat(preAuthRequest.uid).isEqualTo(SERVER_APP_UID); verify(mockClientTransportListener, never()).transportReady(); preAuthRequest.setResult(Status.OK); } AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS); - assertThat(authRequest.uid).isEqualTo(11111); + if (useLegacyAuthStrategy) { + assertThat(authRequest.uid).isEqualTo(EPHEMERAL_SERVER_UID); + } else { + assertThat(authRequest.uid).isEqualTo(SERVER_APP_UID); + } verify(mockClientTransportListener, never()).transportReady(); authRequest.setResult(Status.OK); @@ -321,6 +337,10 @@ public void clientIgnoresDuplicateSetupTransaction() throws Exception { @Test public void clientIgnoresTransactionFromNonServerUids() throws Exception { server.start(serverListener); + + // This test is not applicable to the new auth strategy which keeps the client Binder a secret. + assumeTrue(useLegacyAuthStrategy); + client = newClientTransport(server); startTransport(client, mockClientTransportListener); @@ -369,7 +389,10 @@ public void clientReportsAuthzErrorToServer() throws Exception { .transportShutdown(statusCaptor.capture()); assertThat(statusCaptor.getValue().getCode()).isEqualTo(Status.Code.PERMISSION_DENIED); + // Client doesn't tell the server in this case by design -- we don't even want to start it! TruthJUnit.assume().that(preAuthServersParam).isFalse(); + // Similar story here. The client won't send a setup transaction to an unauthorized server. + TruthJUnit.assume().that(useLegacyAuthStrategy).isTrue(); MockServerTransportListener serverTransportListener = serverListener.takeListenerOrFail(TIMEOUT_MS, MILLISECONDS); diff --git a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java index 0acd7a75f22..0f57b6f8a30 100644 --- a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java @@ -115,6 +115,32 @@ public void testBind() throws Exception { assertThat(binding.isSourceContextCleared()).isFalse(); } + @Test + public void testGetConnectedServiceInfo() throws Exception { + binding = newBuilder().setTargetComponent(serviceComponent).build(); + binding.bind(); + shadowOf(getMainLooper()).idle(); + + assertThat(observer.gotBoundEvent).isTrue(); + + ServiceInfo serviceInfo = binding.getConnectedServiceInfo(); + assertThat(serviceInfo.name).isEqualTo(serviceComponent.getClassName()); + assertThat(serviceInfo.packageName).isEqualTo(serviceComponent.getPackageName()); + } + + @Test + public void testGetConnectedServiceInfoThrows() throws Exception { + binding = newBuilder().setTargetComponent(serviceComponent).build(); + binding.bind(); + shadowOf(getMainLooper()).idle(); + + assertThat(observer.gotBoundEvent).isTrue(); + shadowOf(appContext.getPackageManager()).removeService(serviceComponent); + + StatusException se = assertThrows(StatusException.class, binding::getConnectedServiceInfo); + assertThat(se.getStatus().getCode()).isEqualTo(Code.UNIMPLEMENTED); + } + @Test public void testBindingIntent() throws Exception { shadowApplication.setComponentNameAndServiceForBindService(null, null); @@ -389,6 +415,7 @@ public void testBindWithDeviceAdmin() throws Exception { binding = newBuilder() .setTargetUserHandle(user0) + .setTargetComponent(serviceComponent) .setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent)) .build(); shadowOf(getMainLooper()).idle(); @@ -401,6 +428,10 @@ public void testBindWithDeviceAdmin() throws Exception { assertThat(observer.binder).isSameInstanceAs(mockBinder); assertThat(observer.gotUnboundEvent).isFalse(); assertThat(binding.isSourceContextCleared()).isFalse(); + + ServiceInfo serviceInfo = binding.getConnectedServiceInfo(); + assertThat(serviceInfo.name).isEqualTo(serviceComponent.getClassName()); + assertThat(serviceInfo.packageName).isEqualTo(serviceComponent.getPackageName()); } private void assertNoLockHeld() { From 1ef5ee25905b540a64424d36ea735ac431e117fa Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 3 Nov 2025 13:12:50 -0800 Subject: [PATCH 437/591] core: Fix NPE during address update with Happy Eyeballs Fixes #12168 --- .../internal/PickFirstLeafLoadBalancer.java | 8 +++- .../PickFirstLeafLoadBalancerTest.java | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java index ebe329ca591..2689d7d2308 100644 --- a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java +++ b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java @@ -137,9 +137,13 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { final ImmutableList newImmutableAddressGroups = ImmutableList.builder().addAll(cleanServers).build(); - if (rawConnectivityState == READY || rawConnectivityState == CONNECTING) { + if (rawConnectivityState == READY + || (rawConnectivityState == CONNECTING + && (!enableHappyEyeballs || addressIndex.isValid()))) { // If the previous ready (or connecting) subchannel exists in new address list, - // keep this connection and don't create new subchannels + // keep this connection and don't create new subchannels. Happy Eyeballs is excluded when + // connecting, because it allows multiple attempts simultaneously, thus is fine to start at + // the beginning. SocketAddress previousAddress = addressIndex.getCurrentAddress(); addressIndex.updateGroups(newImmutableAddressGroups); if (addressIndex.seekTo(previousAddress)) { diff --git a/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java b/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java index 0e902bfdd56..8b09fce2aa2 100644 --- a/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/internal/PickFirstLeafLoadBalancerTest.java @@ -1872,6 +1872,45 @@ public void updateAddresses_identical_transient_failure() { assertEquals(PickResult.withSubchannel(mockSubchannel1), picker.pickSubchannel(mockArgs)); } + @Test + public void updateAddresses_identicalSingleAddress_connecting() { + // Creating first set of endpoints/addresses + List oldServers = Lists.newArrayList(servers.get(0)); + + // Accept Addresses and verify proper connection flow + assertEquals(IDLE, loadBalancer.getConcludedConnectivityState()); + loadBalancer.acceptResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(oldServers).setAttributes(affinity).build()); + verify(mockSubchannel1).start(stateListenerCaptor.capture()); + SubchannelStateListener stateListener = stateListenerCaptor.getValue(); + assertEquals(CONNECTING, loadBalancer.getConcludedConnectivityState()); + + // First connection attempt is successful + stateListener.onSubchannelState(ConnectivityStateInfo.forNonError(CONNECTING)); + assertEquals(CONNECTING, loadBalancer.getConcludedConnectivityState()); + fakeClock.forwardTime(CONNECTION_DELAY_INTERVAL_MS, TimeUnit.MILLISECONDS); + + // verify that picker returns no subchannel + verify(mockHelper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); + SubchannelPicker picker = pickerCaptor.getValue(); + assertEquals(PickResult.withNoResult(), picker.pickSubchannel(mockArgs)); + + // Accept same resolved addresses to update + reset(mockHelper); + loadBalancer.acceptResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(oldServers).setAttributes(affinity).build()); + fakeClock.forwardTime(CONNECTION_DELAY_INTERVAL_MS, TimeUnit.MILLISECONDS); + + // Verify that no new subchannels were created or started + verify(mockSubchannel2, never()).start(any()); + assertEquals(CONNECTING, loadBalancer.getConcludedConnectivityState()); + + // verify that picker hasn't changed via checking mock helper's interactions + verify(mockHelper, atLeast(0)).getSynchronizationContext(); // Don't care + verify(mockHelper, atLeast(0)).getScheduledExecutorService(); + verifyNoMoreInteractions(mockHelper); + } + @Test public void twoAddressesSeriallyConnect() { // Starting first connection attempt From 236077138220ce125861e4d014a6d7d92dfb5d71 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 4 Nov 2025 12:44:13 -0800 Subject: [PATCH 438/591] binder: Hold lock when calling setOutgoingBinder() setOutgoingBinder() has `@GuardedBy` for the transport. ``` binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java:71: error: [GuardedBy] This access should be guarded by 'transport', which is not currently held transport.setOutgoingBinder( ^ ``` --- .../java/io/grpc/binder/internal/BinderServerTransport.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java index a57f89e72ac..b8ab5e9f843 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java @@ -68,8 +68,10 @@ public static BinderServerTransport create( // TODO(jdcormie): Plumb in the Server's executor() and use it here instead. // No need to handle failure here because if 'callbackBinder' is already dead, we'll notice it // again in start() when we send the first transaction. - transport.setOutgoingBinder( - OneWayBinderProxy.wrap(callbackBinder, transport.getScheduledExecutorService())); + synchronized (transport) { + transport.setOutgoingBinder( + OneWayBinderProxy.wrap(callbackBinder, transport.getScheduledExecutorService())); + } return transport; } From a76ab792121d6aecc70351d35ba4047220322aac Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 4 Nov 2025 14:18:23 -0800 Subject: [PATCH 439/591] interop-testing: timeoutOnSleepingServer should fail on exception This is ancient code, but our API should not throw IllegalStateException in the case of races. This essentially reverts 0958fd407. The stream code has changed a lot since this code was introduced, but at the very least the replacement of AbstractStream that culminated in b661ac7d7 means the bug is almost certainly gone. I ran Http2Test 1000 times and there were no failures. --- .../grpc/testing/integration/AbstractInteropTest.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java index 843019433aa..f5cd111a5b1 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java @@ -1625,7 +1625,6 @@ public void unimplementedService() { } /** Start a fullDuplexCall which the server will not respond, and verify the deadline expires. */ - @SuppressWarnings("MissingFail") @Test public void timeoutOnSleepingServer() throws Exception { TestServiceGrpc.TestServiceStub stub = @@ -1635,15 +1634,10 @@ public void timeoutOnSleepingServer() throws Exception { StreamObserver requestObserver = stub.fullDuplexCall(responseObserver); - StreamingOutputCallRequest request = StreamingOutputCallRequest.newBuilder() + requestObserver.onNext(StreamingOutputCallRequest.newBuilder() .setPayload(Payload.newBuilder() .setBody(ByteString.copyFrom(new byte[27182]))) - .build(); - try { - requestObserver.onNext(request); - } catch (IllegalStateException expected) { - // This can happen if the stream has already been terminated due to deadline exceeded. - } + .build()); assertTrue(responseObserver.awaitCompletion(operationTimeoutMillis(), TimeUnit.MILLISECONDS)); assertEquals(0, responseObserver.getValues().size()); From 7eab160b70cf9da0ea32647f7c51e485af6b6fc6 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 5 Nov 2025 07:19:24 -0800 Subject: [PATCH 440/591] Revert "core: Report marshaller error for uncompressed size too large back to the client " This reverts commit a40d549ced82becbb12ca6f6b0e007cd430555a6. A user has noticed it caused test failures. cl/828098798 ``` java.lang.AssertionError: Failed executing read operation at io.grpc.internal.CompositeReadableBuffer.execute(CompositeReadableBuffer.java:328) at io.grpc.internal.CompositeReadableBuffer.executeNoThrow(CompositeReadableBuffer.java:336) at io.grpc.internal.CompositeReadableBuffer.readBytes(CompositeReadableBuffer.java:151) at io.grpc.internal.ReadableBuffers$BufferInputStream.read(ReadableBuffers.java:377) at io.grpc.protobuf.lite.ProtoLiteUtils$MessageMarshaller.parse(ProtoLiteUtils.java:205) at io.grpc.protobuf.lite.ProtoLiteUtils$MessageMarshaller.parse(ProtoLiteUtils.java:133) at io.grpc.MethodDescriptor.parseRequest(MethodDescriptor.java:307) at io.grpc.ServerInterceptors$2$2.onMessage(ServerInterceptors.java:324) ``` --- .../java/io/grpc/internal/ServerCallImpl.java | 16 +++----- .../io/grpc/internal/ServerCallImplTest.java | 41 ------------------- .../integration/AbstractInteropTest.java | 2 +- .../integration/TransportCompressionTest.java | 29 +------------ 4 files changed, 8 insertions(+), 80 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/ServerCallImpl.java b/core/src/main/java/io/grpc/internal/ServerCallImpl.java index 22d5912050a..e224384ce8f 100644 --- a/core/src/main/java/io/grpc/internal/ServerCallImpl.java +++ b/core/src/main/java/io/grpc/internal/ServerCallImpl.java @@ -327,20 +327,16 @@ private void messagesAvailableInternal(final MessageProducer producer) { return; } + InputStream message; try { - InputStream message; while ((message = producer.next()) != null) { - ReqT parsedMessage; - try (InputStream ignored = message) { - parsedMessage = call.method.parseRequest(message); - } catch (StatusRuntimeException e) { + try { + listener.onMessage(call.method.parseRequest(message)); + } catch (Throwable t) { GrpcUtil.closeQuietly(message); - GrpcUtil.closeQuietly(producer); - call.cancelled = true; - call.close(e.getStatus(), new Metadata()); - return; + throw t; } - listener.onMessage(parsedMessage); + message.close(); } } catch (Throwable t) { GrpcUtil.closeQuietly(producer); diff --git a/core/src/test/java/io/grpc/internal/ServerCallImplTest.java b/core/src/test/java/io/grpc/internal/ServerCallImplTest.java index abe8fb0ee56..7394c83eab2 100644 --- a/core/src/test/java/io/grpc/internal/ServerCallImplTest.java +++ b/core/src/test/java/io/grpc/internal/ServerCallImplTest.java @@ -48,11 +48,9 @@ import io.grpc.SecurityLevel; import io.grpc.ServerCall; import io.grpc.Status; -import io.grpc.StatusRuntimeException; import io.grpc.internal.ServerCallImpl.ServerStreamListenerImpl; import io.perfmark.PerfMark; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import org.junit.Before; @@ -71,8 +69,6 @@ public class ServerCallImplTest { @Mock private ServerStream stream; @Mock private ServerCall.Listener callListener; - @Mock private StreamListener.MessageProducer messageProducer; - @Mock private InputStream message; private final CallTracer serverCallTracer = CallTracer.getDefaultFactory().create(); private ServerCallImpl call; @@ -497,43 +493,6 @@ public void streamListener_unexpectedRuntimeException() { assertThat(e).hasMessageThat().isEqualTo("unexpected exception"); } - @Test - public void streamListener_statusRuntimeException() throws IOException { - MethodDescriptor failingParseMethod = MethodDescriptor.newBuilder() - .setType(MethodType.UNARY) - .setFullMethodName("service/method") - .setRequestMarshaller(new LongMarshaller() { - @Override - public Long parse(InputStream stream) { - throw new StatusRuntimeException(Status.RESOURCE_EXHAUSTED - .withDescription("Decompressed gRPC message exceeds maximum size")); - } - }) - .setResponseMarshaller(new LongMarshaller()) - .build(); - - call = new ServerCallImpl<>(stream, failingParseMethod, requestHeaders, context, - DecompressorRegistry.getDefaultInstance(), CompressorRegistry.getDefaultInstance(), - serverCallTracer, PerfMark.createTag()); - - ServerStreamListenerImpl streamListener = - new ServerCallImpl.ServerStreamListenerImpl<>(call, callListener, context); - - when(messageProducer.next()).thenReturn(message, (InputStream) null); - streamListener.messagesAvailable(messageProducer); - ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(Metadata.class); - - verify(stream).close(statusCaptor.capture(), metadataCaptor.capture()); - Status status = statusCaptor.getValue(); - assertEquals(Status.RESOURCE_EXHAUSTED.getCode(), status.getCode()); - assertEquals("Decompressed gRPC message exceeds maximum size", status.getDescription()); - - streamListener.halfClosed(); - verify(callListener, never()).onHalfClose(); - verify(callListener, never()).onMessage(any()); - } - private static class LongMarshaller implements Marshaller { @Override public InputStream stream(Long value) { diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java index f5cd111a5b1..51295281a90 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java @@ -2024,7 +2024,7 @@ private void assertPayload(Payload expected, Payload actual) { } } - protected static void assertCodeEquals(Status.Code expected, Status actual) { + private static void assertCodeEquals(Status.Code expected, Status actual) { assertWithMessage("Unexpected status: %s", actual).that(actual.getCode()).isEqualTo(expected); } diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java index 33cd624aebb..b9692383254 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java @@ -17,7 +17,6 @@ package io.grpc.testing.integration; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import com.google.protobuf.ByteString; @@ -38,8 +37,6 @@ import io.grpc.ServerCall.Listener; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; -import io.grpc.Status.Code; -import io.grpc.StatusRuntimeException; import io.grpc.internal.GrpcUtil; import io.grpc.netty.InternalNettyChannelBuilder; import io.grpc.netty.InternalNettyServerBuilder; @@ -56,9 +53,7 @@ import java.io.OutputStream; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -89,16 +84,10 @@ public static void registerCompressors() { compressors.register(Codec.Identity.NONE); } - @Rule - public final TestName currentTest = new TestName(); - @Override protected ServerBuilder getServerBuilder() { NettyServerBuilder builder = NettyServerBuilder.forPort(0, InsecureServerCredentials.create()) - .maxInboundMessageSize( - DECOMPRESSED_MESSAGE_TOO_LONG_METHOD_NAME.equals(currentTest.getMethodName()) - ? 1000 - : AbstractInteropTest.MAX_MESSAGE_SIZE) + .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE) .compressorRegistry(compressors) .decompressorRegistry(decompressors) .intercept(new ServerInterceptor() { @@ -137,22 +126,6 @@ public void compresses() { assertTrue(FZIPPER.anyWritten); } - private static final String DECOMPRESSED_MESSAGE_TOO_LONG_METHOD_NAME = - "decompressedMessageTooLong"; - - @Test - public void decompressedMessageTooLong() { - assertEquals(DECOMPRESSED_MESSAGE_TOO_LONG_METHOD_NAME, currentTest.getMethodName()); - final SimpleRequest bigRequest = SimpleRequest.newBuilder() - .setPayload(Payload.newBuilder().setBody(ByteString.copyFrom(new byte[10_000]))) - .build(); - StatusRuntimeException e = assertThrows(StatusRuntimeException.class, - () -> blockingStub.withCompression("gzip").unaryCall(bigRequest)); - assertCodeEquals(Code.RESOURCE_EXHAUSTED, e.getStatus()); - assertEquals("Decompressed gRPC message exceeds maximum size 1000", - e.getStatus().getDescription()); - } - @Override protected NettyChannelBuilder createChannelBuilder() { NettyChannelBuilder builder = NettyChannelBuilder.forAddress(getListenAddress()) From 48a42889d5f8c9aca019d2cfe64400ff28a1a271 Mon Sep 17 00:00:00 2001 From: Zgoda91 Date: Wed, 5 Nov 2025 21:37:54 +0000 Subject: [PATCH 441/591] util: Add A68 random subsetting LB implementing [gRFC A68](https://github.com/grpc/proposal/blob/master/A68-random-subsetting.md) This change contains: 1. Usage of `murmur3_128` hashing algorithm from Guava library. 2. Implementation of `RandomSubsettingLoadBalancer` and `RandomSubsettingLoadBalancerProvider` classes and integration into the `util` project. Since envoy extensions does not support `random_subsetting` LB policy yet, xDS related changes will be introduced later. Envoy PR [here](https://github.com/envoyproxy/envoy/pull/41758). --- .../io/grpc/LoadBalancerRegistryTest.java | 8 +- util/build.gradle | 1 + .../grpc/util/GracefulSwitchLoadBalancer.java | 4 +- .../util/RandomSubsettingLoadBalancer.java | 161 +++++++++ .../RandomSubsettingLoadBalancerProvider.java | 86 +++++ .../services/io.grpc.LoadBalancerProvider | 1 + ...domSubsettingLoadBalancerProviderTest.java | 135 +++++++ .../RandomSubsettingLoadBalancerTest.java | 330 ++++++++++++++++++ 8 files changed, 723 insertions(+), 3 deletions(-) create mode 100644 util/src/main/java/io/grpc/util/RandomSubsettingLoadBalancer.java create mode 100644 util/src/main/java/io/grpc/util/RandomSubsettingLoadBalancerProvider.java create mode 100644 util/src/test/java/io/grpc/util/RandomSubsettingLoadBalancerProviderTest.java create mode 100644 util/src/test/java/io/grpc/util/RandomSubsettingLoadBalancerTest.java diff --git a/api/src/test/java/io/grpc/LoadBalancerRegistryTest.java b/api/src/test/java/io/grpc/LoadBalancerRegistryTest.java index 5b348b7adab..690db6622e0 100644 --- a/api/src/test/java/io/grpc/LoadBalancerRegistryTest.java +++ b/api/src/test/java/io/grpc/LoadBalancerRegistryTest.java @@ -40,7 +40,7 @@ public void getClassesViaHardcoded_classesPresent() throws Exception { @Test public void stockProviders() { LoadBalancerRegistry defaultRegistry = LoadBalancerRegistry.getDefaultRegistry(); - assertThat(defaultRegistry.providers()).hasSize(3); + assertThat(defaultRegistry.providers()).hasSize(4); LoadBalancerProvider pickFirst = defaultRegistry.getProvider("pick_first"); assertThat(pickFirst).isInstanceOf(PickFirstLoadBalancerProvider.class); @@ -56,6 +56,12 @@ public void stockProviders() { assertThat(outlierDetection.getClass().getName()).isEqualTo( "io.grpc.util.OutlierDetectionLoadBalancerProvider"); assertThat(roundRobin.getPriority()).isEqualTo(5); + + LoadBalancerProvider randomSubsetting = defaultRegistry.getProvider( + "random_subsetting_experimental"); + assertThat(randomSubsetting.getClass().getName()).isEqualTo( + "io.grpc.util.RandomSubsettingLoadBalancerProvider"); + assertThat(randomSubsetting.getPriority()).isEqualTo(5); } @Test diff --git a/util/build.gradle b/util/build.gradle index 6fbd6925c00..846b110b106 100644 --- a/util/build.gradle +++ b/util/build.gradle @@ -58,6 +58,7 @@ animalsniffer { tasks.named("javadoc").configure { exclude 'io/grpc/util/MultiChildLoadBalancer.java' exclude 'io/grpc/util/OutlierDetectionLoadBalancer*' + exclude 'io/grpc/util/RandomSubsettingLoadBalancer*' exclude 'io/grpc/util/RoundRobinLoadBalancer*' } diff --git a/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java b/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java index 1dc4fb6750a..dc296a7293e 100644 --- a/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java @@ -207,14 +207,14 @@ public static ConfigOrError parseLoadBalancingPolicyConfig( ServiceConfigUtil.unwrapLoadBalancingConfigList(loadBalancingConfigs); if (childConfigCandidates == null || childConfigCandidates.isEmpty()) { return ConfigOrError.fromError( - Status.INTERNAL.withDescription("No child LB config specified")); + Status.UNAVAILABLE.withDescription("No child LB config specified")); } ConfigOrError selectedConfig = ServiceConfigUtil.selectLbPolicyFromList(childConfigCandidates, lbRegistry); if (selectedConfig.getError() != null) { Status error = selectedConfig.getError(); return ConfigOrError.fromError( - Status.INTERNAL + Status.UNAVAILABLE .withCause(error.getCause()) .withDescription(error.getDescription()) .augmentDescription("Failed to select child config")); diff --git a/util/src/main/java/io/grpc/util/RandomSubsettingLoadBalancer.java b/util/src/main/java/io/grpc/util/RandomSubsettingLoadBalancer.java new file mode 100644 index 00000000000..ad4de9e8921 --- /dev/null +++ b/util/src/main/java/io/grpc/util/RandomSubsettingLoadBalancer.java @@ -0,0 +1,161 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.util; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.hash.HashCode; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import com.google.common.primitives.Ints; +import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; +import io.grpc.Status; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Random; + + +/** + * Wraps a child {@code LoadBalancer}, separating the total set of backends into smaller subsets for + * the child balancer to balance across. + * + *

    This implements random subsetting gRFC: + * https://https://github.com/grpc/proposal/blob/master/A68-random-subsetting.md + */ +final class RandomSubsettingLoadBalancer extends LoadBalancer { + private final GracefulSwitchLoadBalancer switchLb; + private final HashFunction hashFunc; + + public RandomSubsettingLoadBalancer(Helper helper) { + this(helper, new Random().nextInt()); + } + + @VisibleForTesting + RandomSubsettingLoadBalancer(Helper helper, int seed) { + switchLb = new GracefulSwitchLoadBalancer(checkNotNull(helper, "helper")); + hashFunc = Hashing.murmur3_128(seed); + } + + @Override + public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + RandomSubsettingLoadBalancerConfig config = + (RandomSubsettingLoadBalancerConfig) + resolvedAddresses.getLoadBalancingPolicyConfig(); + + ResolvedAddresses subsetAddresses = filterEndpoints(resolvedAddresses, config.subsetSize); + + return switchLb.acceptResolvedAddresses( + subsetAddresses.toBuilder() + .setLoadBalancingPolicyConfig(config.childConfig) + .build()); + } + + // implements the subsetting algorithm, as described in A68: + // https://github.com/grpc/proposal/pull/423 + private ResolvedAddresses filterEndpoints(ResolvedAddresses resolvedAddresses, int subsetSize) { + if (subsetSize >= resolvedAddresses.getAddresses().size()) { + return resolvedAddresses; + } + + ArrayList endpointWithHashList = + new ArrayList<>(resolvedAddresses.getAddresses().size()); + + for (EquivalentAddressGroup addressGroup : resolvedAddresses.getAddresses()) { + HashCode hashCode = hashFunc.hashString( + addressGroup.getAddresses().get(0).toString(), + StandardCharsets.UTF_8); + endpointWithHashList.add(new EndpointWithHash(addressGroup, hashCode.asLong())); + } + + Collections.sort(endpointWithHashList, new HashAddressComparator()); + + ArrayList addressGroups = new ArrayList<>(subsetSize); + + for (int idx = 0; idx < subsetSize; ++idx) { + addressGroups.add(endpointWithHashList.get(idx).addressGroup); + } + + return resolvedAddresses.toBuilder().setAddresses(addressGroups).build(); + } + + @Override + public void handleNameResolutionError(Status error) { + switchLb.handleNameResolutionError(error); + } + + @Override + public void shutdown() { + switchLb.shutdown(); + } + + private static final class EndpointWithHash { + public final EquivalentAddressGroup addressGroup; + public final long hashCode; + + public EndpointWithHash(EquivalentAddressGroup addressGroup, long hashCode) { + this.addressGroup = addressGroup; + this.hashCode = hashCode; + } + } + + private static final class HashAddressComparator implements Comparator { + @Override + public int compare(EndpointWithHash lhs, EndpointWithHash rhs) { + return Long.compare(lhs.hashCode, rhs.hashCode); + } + } + + public static final class RandomSubsettingLoadBalancerConfig { + public final int subsetSize; + public final Object childConfig; + + private RandomSubsettingLoadBalancerConfig(int subsetSize, Object childConfig) { + this.subsetSize = subsetSize; + this.childConfig = childConfig; + } + + public static class Builder { + int subsetSize; + Object childConfig; + + public Builder setSubsetSize(long subsetSize) { + checkArgument(subsetSize > 0L, "Subset size must be greater than 0"); + // clamping subset size to Integer.MAX_VALUE due to collection indexing limitations in JVM + this.subsetSize = Ints.saturatedCast(subsetSize); + return this; + } + + public Builder setChildConfig(Object childConfig) { + this.childConfig = checkNotNull(childConfig, "childConfig"); + return this; + } + + public RandomSubsettingLoadBalancerConfig build() { + checkState(subsetSize != 0L, "Subset size must be set before building the config"); + return new RandomSubsettingLoadBalancerConfig( + subsetSize, + checkNotNull(childConfig, "childConfig")); + } + } + } +} diff --git a/util/src/main/java/io/grpc/util/RandomSubsettingLoadBalancerProvider.java b/util/src/main/java/io/grpc/util/RandomSubsettingLoadBalancerProvider.java new file mode 100644 index 00000000000..edcbf48a201 --- /dev/null +++ b/util/src/main/java/io/grpc/util/RandomSubsettingLoadBalancerProvider.java @@ -0,0 +1,86 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.util; + +import io.grpc.Internal; +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancerProvider; +import io.grpc.NameResolver.ConfigOrError; +import io.grpc.Status; +import io.grpc.internal.JsonUtil; +import java.util.Map; + +@Internal +public final class RandomSubsettingLoadBalancerProvider extends LoadBalancerProvider { + private static final String POLICY_NAME = "random_subsetting_experimental"; + + @Override + public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) { + return new RandomSubsettingLoadBalancer(helper); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public int getPriority() { + return 5; + } + + @Override + public String getPolicyName() { + return POLICY_NAME; + } + + @Override + public ConfigOrError parseLoadBalancingPolicyConfig(Map rawConfig) { + try { + return parseLoadBalancingPolicyConfigInternal(rawConfig); + } catch (RuntimeException e) { + return ConfigOrError.fromError( + Status.UNAVAILABLE + .withCause(e) + .withDescription("Failed parsing configuration for " + getPolicyName())); + } + } + + private ConfigOrError parseLoadBalancingPolicyConfigInternal(Map rawConfig) { + Long subsetSize = JsonUtil.getNumberAsLong(rawConfig, "subsetSize"); + if (subsetSize == null) { + return ConfigOrError.fromError( + Status.UNAVAILABLE.withDescription( + "Subset size missing in " + getPolicyName() + ", LB policy config=" + rawConfig)); + } + + ConfigOrError childConfig = GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig( + JsonUtil.getListOfObjects(rawConfig, "childPolicy")); + if (childConfig.getError() != null) { + return ConfigOrError.fromError(Status.UNAVAILABLE + .withDescription( + "Failed to parse child in " + getPolicyName() + ", LB policy config=" + rawConfig) + .withCause(childConfig.getError().asRuntimeException())); + } + + return ConfigOrError.fromConfig( + new RandomSubsettingLoadBalancer.RandomSubsettingLoadBalancerConfig.Builder() + .setSubsetSize(subsetSize) + .setChildConfig(childConfig.getConfig()) + .build()); + } +} diff --git a/util/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider b/util/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider index 1fdd69cb00b..d973a6f6728 100644 --- a/util/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider +++ b/util/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider @@ -1,2 +1,3 @@ io.grpc.util.SecretRoundRobinLoadBalancerProvider$Provider io.grpc.util.OutlierDetectionLoadBalancerProvider +io.grpc.util.RandomSubsettingLoadBalancerProvider diff --git a/util/src/test/java/io/grpc/util/RandomSubsettingLoadBalancerProviderTest.java b/util/src/test/java/io/grpc/util/RandomSubsettingLoadBalancerProviderTest.java new file mode 100644 index 00000000000..18a0766d4b2 --- /dev/null +++ b/util/src/test/java/io/grpc/util/RandomSubsettingLoadBalancerProviderTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.util; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +import io.grpc.InternalServiceProviders; +import io.grpc.LoadBalancer.Helper; +import io.grpc.LoadBalancerProvider; +import io.grpc.NameResolver.ConfigOrError; +import io.grpc.Status; +import io.grpc.internal.JsonParser; +import io.grpc.util.RandomSubsettingLoadBalancer.RandomSubsettingLoadBalancerConfig; +import java.io.IOException; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class RandomSubsettingLoadBalancerProviderTest { + private final RandomSubsettingLoadBalancerProvider provider = + new RandomSubsettingLoadBalancerProvider(); + + @Test + public void registered() { + for (LoadBalancerProvider current : + InternalServiceProviders.getCandidatesViaServiceLoader( + LoadBalancerProvider.class, getClass().getClassLoader())) { + if (current instanceof RandomSubsettingLoadBalancerProvider) { + return; + } + } + fail("RandomSubsettingLoadBalancerProvider not registered"); + } + + @Test + public void providesLoadBalancer() { + Helper helper = mock(Helper.class); + assertThat(provider.newLoadBalancer(helper)) + .isInstanceOf(RandomSubsettingLoadBalancer.class); + } + + @Test + public void parseConfigRequiresSubsetSize() throws IOException { + String emptyConfig = "{}"; + + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(emptyConfig)); + assertThat(configOrError.getError()).isNotNull(); + assertThat(configOrError.getError().toString()) + .isEqualTo( + Status.UNAVAILABLE + .withDescription( + "Subset size missing in random_subsetting_experimental, LB policy config={}") + .toString()); + } + + @Test + public void parseConfigReturnsErrorWhenChildPolicyMissing() throws IOException { + String missingChildPolicyConfig = "{\"subsetSize\": 3}"; + + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(missingChildPolicyConfig)); + assertThat(configOrError.getError()).isNotNull(); + + Status error = configOrError.getError(); + assertThat(error.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(error.getDescription()).isEqualTo( + "Failed to parse child in random_subsetting_experimental" + + ", LB policy config={subsetSize=3.0}"); + assertThat(error.getCause().getMessage()).isEqualTo( + "UNAVAILABLE: No child LB config specified"); + } + + @Test + public void parseConfigReturnsErrorWhenChildPolicyInvalid() throws IOException { + String invalidChildPolicyConfig = + "{" + + "\"subsetSize\": 3, " + + "\"childPolicy\" : [{\"random_policy\" : {}}]" + + "}"; + + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(invalidChildPolicyConfig)); + assertThat(configOrError.getError()).isNotNull(); + + Status error = configOrError.getError(); + assertThat(error.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(error.getDescription()).isEqualTo( + "Failed to parse child in random_subsetting_experimental, LB policy config=" + + "{subsetSize=3.0, childPolicy=[{random_policy={}}]}"); + assertThat(error.getCause().getMessage()).contains( + "UNAVAILABLE: None of [random_policy] specified by Service Config are available."); + } + + @Test + public void parseValidConfig() throws IOException { + String validConfig = + "{" + + "\"subsetSize\": 3, " + + "\"childPolicy\" : [{\"round_robin\" : {}}]" + + "}"; + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(validConfig)); + assertThat(configOrError.getConfig()).isNotNull(); + + RandomSubsettingLoadBalancerConfig actualConfig = + (RandomSubsettingLoadBalancerConfig) configOrError.getConfig(); + assertThat(GracefulSwitchLoadBalancerAccessor.getChildProvider( + actualConfig.childConfig).getPolicyName()).isEqualTo("round_robin"); + assertThat(actualConfig.subsetSize).isEqualTo(3); + } + + @SuppressWarnings("unchecked") + private static Map parseJsonObject(String json) throws IOException { + return (Map) JsonParser.parse(json); + } +} diff --git a/util/src/test/java/io/grpc/util/RandomSubsettingLoadBalancerTest.java b/util/src/test/java/io/grpc/util/RandomSubsettingLoadBalancerTest.java new file mode 100644 index 00000000000..91dde6ba19d --- /dev/null +++ b/util/src/test/java/io/grpc/util/RandomSubsettingLoadBalancerTest.java @@ -0,0 +1,330 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.util; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import io.grpc.ConnectivityState; +import io.grpc.ConnectivityStateInfo; +import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancer.CreateSubchannelArgs; +import io.grpc.LoadBalancer.ResolvedAddresses; +import io.grpc.LoadBalancer.Subchannel; +import io.grpc.LoadBalancer.SubchannelStateListener; +import io.grpc.LoadBalancerProvider; +import io.grpc.Status; +import io.grpc.internal.TestUtils; +import io.grpc.util.RandomSubsettingLoadBalancer.RandomSubsettingLoadBalancerConfig; +import java.net.SocketAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; + +public class RandomSubsettingLoadBalancerTest { + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private LoadBalancer.Helper mockHelper; + @Mock + private LoadBalancer mockChildLb; + @Mock + private SocketAddress mockSocketAddress; + + @Captor + private ArgumentCaptor resolvedAddrCaptor; + + private BackendDetails backendDetails; + + private RandomSubsettingLoadBalancer loadBalancer; + + private final LoadBalancerProvider mockChildLbProvider = + new TestUtils.StandardLoadBalancerProvider("foo_policy") { + @Override + public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) { + return mockChildLb; + } + }; + + private final LoadBalancerProvider roundRobinLbProvider = + new TestUtils.StandardLoadBalancerProvider("round_robin") { + @Override + public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) { + return new RoundRobinLoadBalancer(helper); + } + }; + + private Object newChildConfig(LoadBalancerProvider provider, Object config) { + return GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig(provider, config); + } + + private RandomSubsettingLoadBalancerConfig createRandomSubsettingLbConfig( + int subsetSize, LoadBalancerProvider childLbProvider, Object childConfig) { + return new RandomSubsettingLoadBalancer.RandomSubsettingLoadBalancerConfig.Builder() + .setSubsetSize(subsetSize) + .setChildConfig(newChildConfig(childLbProvider, childConfig)) + .build(); + } + + private BackendDetails setupBackends(int backendCount) { + List servers = Lists.newArrayList(); + Map, Subchannel> subchannels = Maps.newLinkedHashMap(); + + for (int i = 0; i < backendCount; i++) { + SocketAddress addr = new FakeSocketAddress("server" + i); + EquivalentAddressGroup addressGroup = new EquivalentAddressGroup(addr); + servers.add(addressGroup); + Subchannel subchannel = mock(Subchannel.class); + subchannels.put(Arrays.asList(addressGroup), subchannel); + } + + return new BackendDetails(servers, subchannels); + } + + @Before + public void setUp() { + int seed = 0; + loadBalancer = new RandomSubsettingLoadBalancer(mockHelper, seed); + + int backendSize = 5; + backendDetails = setupBackends(backendSize); + } + + @Test + public void handleNameResolutionError() { + int subsetSize = 2; + Object childConfig = "someConfig"; + + RandomSubsettingLoadBalancerConfig config = createRandomSubsettingLbConfig( + subsetSize, mockChildLbProvider, childConfig); + + loadBalancer.acceptResolvedAddresses( + ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.of(new EquivalentAddressGroup(mockSocketAddress))) + .setLoadBalancingPolicyConfig(config) + .build()); + + loadBalancer.handleNameResolutionError(Status.DEADLINE_EXCEEDED); + verify(mockChildLb).handleNameResolutionError(Status.DEADLINE_EXCEEDED); + } + + @Test + public void shutdown() { + int subsetSize = 2; + Object childConfig = "someConfig"; + + RandomSubsettingLoadBalancerConfig config = createRandomSubsettingLbConfig( + subsetSize, mockChildLbProvider, childConfig); + + loadBalancer.acceptResolvedAddresses( + ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.of(new EquivalentAddressGroup(mockSocketAddress))) + .setLoadBalancingPolicyConfig(config) + .build()); + + loadBalancer.shutdown(); + verify(mockChildLb).shutdown(); + } + + @Test + public void acceptResolvedAddresses_mockedChildLbPolicy() { + int subsetSize = 3; + Object childConfig = "someConfig"; + + RandomSubsettingLoadBalancerConfig config = createRandomSubsettingLbConfig( + subsetSize, mockChildLbProvider, childConfig); + + ResolvedAddresses resolvedAddresses = + ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.copyOf(backendDetails.servers)) + .setLoadBalancingPolicyConfig(config) + .build(); + + loadBalancer.acceptResolvedAddresses(resolvedAddresses); + + verify(mockChildLb).acceptResolvedAddresses(resolvedAddrCaptor.capture()); + assertThat(resolvedAddrCaptor.getValue().getAddresses().size()).isEqualTo(subsetSize); + assertThat(resolvedAddrCaptor.getValue().getLoadBalancingPolicyConfig()).isEqualTo(childConfig); + } + + @Test + public void acceptResolvedAddresses_roundRobinChildLbPolicy() { + int subsetSize = 3; + Object childConfig = null; + + RandomSubsettingLoadBalancerConfig config = createRandomSubsettingLbConfig( + subsetSize, roundRobinLbProvider, childConfig); + + ResolvedAddresses resolvedAddresses = + ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.copyOf(backendDetails.servers)) + .setLoadBalancingPolicyConfig(config) + .build(); + + loadBalancer.acceptResolvedAddresses(resolvedAddresses); + + int insubset = 0; + for (Subchannel subchannel : backendDetails.subchannels.values()) { + LoadBalancer.SubchannelStateListener ssl = + backendDetails.subchannelStateListeners.get(subchannel); + if (ssl != null) { // it might be null if it's not in the subset. + insubset += 1; + ssl.onSubchannelState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); + } + } + + assertThat(insubset).isEqualTo(subsetSize); + } + + // verifies: https://github.com/grpc/proposal/blob/master/A68_graphics/subsetting100-100-5.png + @Test + public void backendsCanBeDistributedEvenly_subsetting100_100_5() { + verifyConnectionsByServer(100, 100, 5, 15); + } + + // verifies https://github.com/grpc/proposal/blob/master/A68_graphics/subsetting100-100-25.png + @Test + public void backendsCanBeDistributedEvenly_subsetting100_100_25() { + verifyConnectionsByServer(100, 100, 25, 40); + } + + // verifies: https://github.com/grpc/proposal/blob/master/A68_graphics/subsetting100-10-5.png + @Test + public void backendsCanBeDistributedEvenly_subsetting100_10_5() { + verifyConnectionsByServer(100, 10, 5, 65); + } + + // verifies: https://github.com/grpc/proposal/blob/master/A68_graphics/subsetting500-10-5.png + @Test + public void backendsCanBeDistributedEvenly_subsetting500_10_5() { + verifyConnectionsByServer(500, 10, 5, 600); + } + + // verifies: https://github.com/grpc/proposal/blob/master/A68_graphics/subsetting2000-10-5.png + @Test + public void backendsCanBeDistributedEvenly_subsetting2000_100_5() { + verifyConnectionsByServer(2000, 10, 5, 1200); + } + + public void verifyConnectionsByServer( + int clientsCount, int serversCount, int subsetSize, int expectedMaxConnections) { + backendDetails = setupBackends(serversCount); + Object childConfig = "someConfig"; + + List configs = Lists.newArrayList(); + for (int i = 0; i < clientsCount; i++) { + configs.add(createRandomSubsettingLbConfig(subsetSize, mockChildLbProvider, childConfig)); + } + + Map connectionsByServer = Maps.newLinkedHashMap(); + + for (int i = 0; i < clientsCount; i++) { + RandomSubsettingLoadBalancerConfig config = configs.get(i); + + ResolvedAddresses resolvedAddresses = + ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.copyOf(backendDetails.servers)) + .setLoadBalancingPolicyConfig(config) + .build(); + + loadBalancer = new RandomSubsettingLoadBalancer(mockHelper, i); + loadBalancer.acceptResolvedAddresses(resolvedAddresses); + + verify(mockChildLb, atLeastOnce()).acceptResolvedAddresses(resolvedAddrCaptor.capture()); + // Verify ChildLB is only getting subsetSize ResolvedAddresses each time + assertThat(resolvedAddrCaptor.getValue().getAddresses().size()).isEqualTo(config.subsetSize); + + for (EquivalentAddressGroup eag : resolvedAddrCaptor.getValue().getAddresses()) { + for (SocketAddress addr : eag.getAddresses()) { + Integer prev = connectionsByServer.getOrDefault(addr, 0); + connectionsByServer.put(addr, prev + 1); + } + } + } + + int maxConnections = Collections.max(connectionsByServer.values()); + + assertThat(maxConnections).isAtMost(expectedMaxConnections); + } + + private class BackendDetails { + private final List servers; + private final Map, Subchannel> subchannels; + private final Map subchannelStateListeners; + + BackendDetails(List servers, + Map, Subchannel> subchannels) { + this.servers = servers; + this.subchannels = subchannels; + this.subchannelStateListeners = Maps.newLinkedHashMap(); + + when(mockHelper.createSubchannel(any(LoadBalancer.CreateSubchannelArgs.class))).then( + new Answer() { + @Override + public Subchannel answer(InvocationOnMock invocation) throws Throwable { + CreateSubchannelArgs args = (CreateSubchannelArgs) invocation.getArguments()[0]; + final Subchannel subchannel = backendDetails.subchannels.get(args.getAddresses()); + when(subchannel.getAllAddresses()).thenReturn(args.getAddresses()); + when(subchannel.getAttributes()).thenReturn(args.getAttributes()); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + subchannelStateListeners.put(subchannel, + (SubchannelStateListener) invocation.getArguments()[0]); + return null; + } + }).when(subchannel).start(any(SubchannelStateListener.class)); + return subchannel; + } + }); + } + } + + private static class FakeSocketAddress extends SocketAddress { + final String name; + + FakeSocketAddress(String name) { + this.name = name; + } + + @Override + public String toString() { + return "FakeSocketAddress-" + name; + } + } +} From 26c1c1341936640597f93a6a648e427a66d8dfe3 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Thu, 6 Nov 2025 10:27:19 +0000 Subject: [PATCH 442/591] rls: Control plane channel monitor state and back off handling (#12460) At the end of back off time, instead of firing a Rls RPC, just update the RLS picker, and RLS connectivity state change from TRANSIENT_FAILURE to READY deactivate all active backoffs. --- .../java/io/grpc/rls/CachingRlsLbClient.java | 88 ++++---- .../io/grpc/rls/LbPolicyConfiguration.java | 27 +-- .../io/grpc/rls/CachingRlsLbClientTest.java | 212 ++++++++++++++++-- .../grpc/rls/LbPolicyConfigurationTest.java | 6 +- .../java/io/grpc/rls/RlsLoadBalancerTest.java | 3 +- 5 files changed, 249 insertions(+), 87 deletions(-) diff --git a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java index 2c26d29f14d..83e9d482bc5 100644 --- a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java +++ b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java @@ -53,7 +53,6 @@ import io.grpc.lookup.v1.RouteLookupServiceGrpc; import io.grpc.lookup.v1.RouteLookupServiceGrpc.RouteLookupServiceStub; import io.grpc.rls.ChildLoadBalancerHelper.ChildLoadBalancerHelperProvider; -import io.grpc.rls.LbPolicyConfiguration.ChildLbStatusListener; import io.grpc.rls.LbPolicyConfiguration.ChildPolicyWrapper; import io.grpc.rls.LbPolicyConfiguration.RefCountedChildPolicyWrapperFactory; import io.grpc.rls.LruCache.EvictionListener; @@ -217,6 +216,35 @@ private CachingRlsLbClient(Builder builder) { rlsChannelBuilder.disableServiceConfigLookUp(); } rlsChannel = rlsChannelBuilder.build(); + Runnable rlsServerConnectivityStateChangeHandler = new Runnable() { + private boolean wasInTransientFailure; + @Override + public void run() { + ConnectivityState currentState = rlsChannel.getState(false); + if (currentState == ConnectivityState.TRANSIENT_FAILURE) { + wasInTransientFailure = true; + } else if (wasInTransientFailure && currentState == ConnectivityState.READY) { + wasInTransientFailure = false; + synchronized (lock) { + boolean anyBackoffsCanceled = false; + for (CacheEntry value : linkedHashLruCache.values()) { + if (value instanceof BackoffCacheEntry) { + if (((BackoffCacheEntry) value).scheduledFuture.cancel(false)) { + anyBackoffsCanceled = true; + } + } + } + if (anyBackoffsCanceled) { + // Cache updated. updateBalancingState() to reattempt picks + helper.triggerPendingRpcProcessing(); + } + } + } + rlsChannel.notifyWhenStateChanged(currentState, this); + } + }; + rlsChannel.notifyWhenStateChanged( + ConnectivityState.IDLE, rlsServerConnectivityStateChangeHandler); rlsStub = RouteLookupServiceGrpc.newStub(rlsChannel); childLbResolvedAddressFactory = checkNotNull(builder.resolvedAddressFactory, "resolvedAddressFactory"); @@ -226,8 +254,7 @@ private CachingRlsLbClient(Builder builder) { refCountedChildPolicyWrapperFactory = new RefCountedChildPolicyWrapperFactory( lbPolicyConfig.getLoadBalancingPolicy(), childLbResolvedAddressFactory, - childLbHelperProvider, - new BackoffRefreshListener()); + childLbHelperProvider); // TODO(creamsoup) wait until lb is ready String defaultTarget = lbPolicyConfig.getRouteLookupConfig().defaultTarget(); if (defaultTarget != null && !defaultTarget.isEmpty()) { @@ -347,12 +374,15 @@ final CachedRouteLookupResponse get(final RouteLookupRequestKey routeLookupReque synchronized (lock) { final CacheEntry cacheEntry; cacheEntry = linkedHashLruCache.read(routeLookupRequestKey); - if (cacheEntry == null) { + if (cacheEntry == null + || (cacheEntry instanceof BackoffCacheEntry + && !((BackoffCacheEntry) cacheEntry).isInBackoffPeriod())) { PendingCacheEntry pendingEntry = pendingCallCache.get(routeLookupRequestKey); if (pendingEntry != null) { return CachedRouteLookupResponse.pendingResponse(pendingEntry); } - return asyncRlsCall(routeLookupRequestKey, /* backoffPolicy= */ null, + return asyncRlsCall(routeLookupRequestKey, cacheEntry instanceof BackoffCacheEntry + ? ((BackoffCacheEntry) cacheEntry).backoffPolicy : null, RouteLookupRequest.Reason.REASON_MISS); } @@ -447,7 +477,8 @@ private BackoffCacheEntry createBackOffEntry(RouteLookupRequestKey routeLookupRe ChannelLogLevel.DEBUG, "[RLS Entry {0}] Transition to back off: status={1}, delayNanos={2}", routeLookupRequestKey, status, delayNanos); - BackoffCacheEntry entry = new BackoffCacheEntry(routeLookupRequestKey, status, backoffPolicy); + BackoffCacheEntry entry = new BackoffCacheEntry(routeLookupRequestKey, status, backoffPolicy, + ticker.read() + delayNanos * 2); // Lock is held, so the task can't execute before the assignment entry.scheduledFuture = scheduledExecutorService.schedule( () -> refreshBackoffEntry(entry), delayNanos, TimeUnit.NANOSECONDS); @@ -462,11 +493,8 @@ private void refreshBackoffEntry(BackoffCacheEntry entry) { // Future was previously cancelled return; } - logger.log(ChannelLogLevel.DEBUG, - "[RLS Entry {0}] Calling RLS for transition to pending", entry.routeLookupRequestKey); - linkedHashLruCache.invalidate(entry.routeLookupRequestKey); - asyncRlsCall(entry.routeLookupRequestKey, entry.backoffPolicy, - RouteLookupRequest.Reason.REASON_MISS); + // Cache updated. updateBalancingState() to reattempt picks + helper.triggerPendingRpcProcessing(); } } @@ -773,13 +801,15 @@ private static final class BackoffCacheEntry extends CacheEntry { private final Status status; private final BackoffPolicy backoffPolicy; + private final long expiryTimeNanos; private Future scheduledFuture; BackoffCacheEntry(RouteLookupRequestKey routeLookupRequestKey, Status status, - BackoffPolicy backoffPolicy) { + BackoffPolicy backoffPolicy, long expiryTimeNanos) { super(routeLookupRequestKey); this.status = checkNotNull(status, "status"); this.backoffPolicy = checkNotNull(backoffPolicy, "backoffPolicy"); + this.expiryTimeNanos = expiryTimeNanos; } Status getStatus() { @@ -791,9 +821,13 @@ int getSizeBytes() { return OBJ_OVERHEAD_B * 3 + Long.SIZE + 8; // 3 java objects, 1 long and a boolean } + boolean isInBackoffPeriod() { + return !scheduledFuture.isDone(); + } + @Override - boolean isExpired(long now) { - return scheduledFuture.isDone(); + boolean isExpired(long nowNanos) { + return nowNanos > expiryTimeNanos; } @Override @@ -956,32 +990,6 @@ public CacheEntry cacheAndClean(RouteLookupRequestKey key, CacheEntry value) { } } - /** - * LbStatusListener refreshes {@link BackoffCacheEntry} when lb state is changed to {@link - * ConnectivityState#READY} from {@link ConnectivityState#TRANSIENT_FAILURE}. - */ - private final class BackoffRefreshListener implements ChildLbStatusListener { - - @Nullable - private ConnectivityState prevState = null; - - @Override - public void onStatusChanged(ConnectivityState newState) { - if (prevState == ConnectivityState.TRANSIENT_FAILURE - && newState == ConnectivityState.READY) { - logger.log(ChannelLogLevel.DEBUG, "Transitioning from TRANSIENT_FAILURE to READY"); - synchronized (lock) { - for (CacheEntry value : linkedHashLruCache.values()) { - if (value instanceof BackoffCacheEntry) { - refreshBackoffEntry((BackoffCacheEntry) value); - } - } - } - } - prevState = newState; - } - } - /** A header will be added when RLS server respond with additional header data. */ @VisibleForTesting static final Metadata.Key RLS_DATA_KEY = diff --git a/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java b/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java index 226176d25ff..0fb10326f28 100644 --- a/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java +++ b/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java @@ -210,20 +210,17 @@ static final class RefCountedChildPolicyWrapperFactory { new HashMap<>(); private final ChildLoadBalancerHelperProvider childLbHelperProvider; - private final ChildLbStatusListener childLbStatusListener; private final ChildLoadBalancingPolicy childPolicy; private ResolvedAddressFactory childLbResolvedAddressFactory; public RefCountedChildPolicyWrapperFactory( ChildLoadBalancingPolicy childPolicy, ResolvedAddressFactory childLbResolvedAddressFactory, - ChildLoadBalancerHelperProvider childLbHelperProvider, - ChildLbStatusListener childLbStatusListener) { + ChildLoadBalancerHelperProvider childLbHelperProvider) { this.childPolicy = checkNotNull(childPolicy, "childPolicy"); this.childLbResolvedAddressFactory = checkNotNull(childLbResolvedAddressFactory, "childLbResolvedAddressFactory"); this.childLbHelperProvider = checkNotNull(childLbHelperProvider, "childLbHelperProvider"); - this.childLbStatusListener = checkNotNull(childLbStatusListener, "childLbStatusListener"); } void init() { @@ -248,8 +245,7 @@ ChildPolicyWrapper createOrGet(String target) { RefCountedChildPolicyWrapper pooledChildPolicyWrapper = childPolicyMap.get(target); if (pooledChildPolicyWrapper == null) { ChildPolicyWrapper childPolicyWrapper = new ChildPolicyWrapper( - target, childPolicy, childLbResolvedAddressFactory, childLbHelperProvider, - childLbStatusListener); + target, childPolicy, childLbResolvedAddressFactory, childLbHelperProvider); pooledChildPolicyWrapper = RefCountedChildPolicyWrapper.of(childPolicyWrapper); childPolicyMap.put(target, pooledChildPolicyWrapper); return pooledChildPolicyWrapper.getObject(); @@ -299,11 +295,9 @@ public ChildPolicyWrapper( String target, ChildLoadBalancingPolicy childPolicy, final ResolvedAddressFactory childLbResolvedAddressFactory, - ChildLoadBalancerHelperProvider childLbHelperProvider, - ChildLbStatusListener childLbStatusListener) { + ChildLoadBalancerHelperProvider childLbHelperProvider) { this.target = target; - this.helper = - new ChildPolicyReportingHelper(childLbHelperProvider, childLbStatusListener); + this.helper = new ChildPolicyReportingHelper(childLbHelperProvider); LoadBalancerProvider lbProvider = childPolicy.getEffectiveLbProvider(); final ConfigOrError lbConfig = lbProvider @@ -386,14 +380,11 @@ public String toString() { final class ChildPolicyReportingHelper extends ForwardingLoadBalancerHelper { private final ChildLoadBalancerHelper delegate; - private final ChildLbStatusListener listener; ChildPolicyReportingHelper( - ChildLoadBalancerHelperProvider childHelperProvider, - ChildLbStatusListener listener) { + ChildLoadBalancerHelperProvider childHelperProvider) { checkNotNull(childHelperProvider, "childHelperProvider"); this.delegate = childHelperProvider.forTarget(getTarget()); - this.listener = checkNotNull(listener, "listener"); } @Override @@ -406,18 +397,10 @@ public void updateBalancingState(ConnectivityState newState, SubchannelPicker ne picker = newPicker; state = newState; super.updateBalancingState(newState, newPicker); - listener.onStatusChanged(newState); } } } - /** Listener for child lb status change events. */ - interface ChildLbStatusListener { - - /** Notifies when child lb status changes. */ - void onStatusChanged(ConnectivityState newState); - } - private static final class RefCountedChildPolicyWrapper implements ObjectPool { diff --git a/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java b/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java index 93c7d0f00ff..12483d60794 100644 --- a/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java +++ b/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java @@ -59,6 +59,7 @@ import io.grpc.MetricRecorder.BatchRecorder; import io.grpc.MetricRecorder.Registration; import io.grpc.NameResolver.ConfigOrError; +import io.grpc.Server; import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.SynchronizationContext; @@ -66,7 +67,10 @@ import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.internal.BackoffPolicy; import io.grpc.internal.FakeClock; +import io.grpc.internal.GrpcUtil; +import io.grpc.internal.ObjectPool; import io.grpc.internal.PickSubchannelArgsImpl; +import io.grpc.internal.SharedResourcePool; import io.grpc.lookup.v1.RouteLookupServiceGrpc; import io.grpc.rls.CachingRlsLbClient.CacheEntry; import io.grpc.rls.CachingRlsLbClient.CachedRouteLookupResponse; @@ -96,10 +100,13 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nonnull; import org.junit.After; import org.junit.Before; @@ -160,8 +167,9 @@ public void uncaughtException(Thread t, Throwable e) { fakeClock.getScheduledExecutorService()); private final ChildLoadBalancingPolicy childLbPolicy = new ChildLoadBalancingPolicy("target", Collections.emptyMap(), lbProvider); + private final FakeHelper fakeHelper = new FakeHelper(); private final Helper helper = - mock(Helper.class, delegatesTo(new FakeHelper())); + mock(Helper.class, delegatesTo(fakeHelper)); private final FakeThrottler fakeThrottler = new FakeThrottler(); private final LbPolicyConfiguration lbPolicyConfiguration = new LbPolicyConfiguration(ROUTE_LOOKUP_CONFIG, null, childLbPolicy); @@ -319,7 +327,44 @@ public void rls_withCustomRlsChannelServiceConfig() throws Exception { } @Test - public void get_throttledAndRecover() throws Exception { + public void backoffTimerEnd_updatesPicker() throws Exception { + setUpRlsLbClient(); + InOrder inOrder = inOrder(helper); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create( + ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); + rlsServerImpl.setLookupTable( + ImmutableMap.of( + routeLookupRequestKey, + RouteLookupResponse.create(ImmutableList.of("target"), "header"))); + + fakeThrottler.nextResult = true; + fakeBackoffProvider.nextPolicy = createBackoffPolicy(10, TimeUnit.MILLISECONDS); + + CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequestKey); + assertThat(resp.hasError()).isTrue(); + + fakeClock.forwardTime(10, TimeUnit.MILLISECONDS); + // Assert that Rls LB policy picker was updated which picks the fallback target + ArgumentCaptor pickerCaptor = ArgumentCaptor.forClass(SubchannelPicker.class); + ArgumentCaptor stateCaptor = + ArgumentCaptor.forClass(ConnectivityState.class); + + inOrder.verify(helper, times(3)) + .updateBalancingState(stateCaptor.capture(), pickerCaptor.capture()); + assertThat(new HashSet<>(pickerCaptor.getAllValues())).hasSize(1); + assertThat(stateCaptor.getAllValues()) + .containsExactly(ConnectivityState.TRANSIENT_FAILURE, ConnectivityState.CONNECTING, + ConnectivityState.CONNECTING); + Metadata headers = new Metadata(); + PickResult pickResult = getPickResultForCreate(pickerCaptor, headers); + assertThat(pickResult.getStatus().getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(pickResult.getStatus().getDescription()).isEqualTo("fallback not available"); + } + + @Test + public void get_throttledTwice_usesSameBackoffpolicy() throws Exception { setUpRlsLbClient(); RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = RlsProtoData.RouteLookupRequestKey.create( @@ -338,20 +383,59 @@ public void get_throttledAndRecover() throws Exception { assertThat(resp.hasError()).isTrue(); fakeClock.forwardTime(10, TimeUnit.MILLISECONDS); - // initially backed off entry is backed off again - verify(evictionListener) - .onEviction(eq(routeLookupRequestKey), any(CacheEntry.class), eq(EvictionType.EXPLICIT)); + // Assert that the same backoff policy is still in effect for the cache entry. + // The below provider should not get used, so the back off time will still be set to 10ms. + fakeBackoffProvider.nextPolicy = createBackoffPolicy(20, TimeUnit.MILLISECONDS); + // let it be throttled again resp = getInSyncContext(routeLookupRequestKey); - assertThat(resp.hasError()).isTrue(); - // let it pass throttler - fakeThrottler.nextResult = false; fakeClock.forwardTime(10, TimeUnit.MILLISECONDS); + // Backoff entry's backoff timer has gone off, so next rpc should not be backed off. + fakeThrottler.nextResult = false; resp = getInSyncContext(routeLookupRequestKey); + assertThat(resp.isPending()).isTrue(); + rlsServerImpl.routeLookupReason = null; + // server responses + fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); + assertThat(rlsServerImpl.routeLookupReason).isEqualTo( + io.grpc.lookup.v1.RouteLookupRequest.Reason.REASON_MISS); + } + + @Test + public void get_errorResponseTwice_usesSameBackoffPolicy() throws Exception { + setUpRlsLbClient(); + RlsProtoData.RouteLookupRequestKey invalidRouteLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create(ImmutableMap.of()); + CachedRouteLookupResponse resp = getInSyncContext(invalidRouteLookupRequestKey); + assertThat(resp.isPending()).isTrue(); + fakeBackoffProvider.nextPolicy = createBackoffPolicy(10, TimeUnit.MILLISECONDS); + fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); + assertThat(rlsServerImpl.routeLookupReason).isEqualTo( + io.grpc.lookup.v1.RouteLookupRequest.Reason.REASON_MISS); + + resp = getInSyncContext(invalidRouteLookupRequestKey); + assertThat(resp.hasError()).isTrue(); + + // Backoff time expiry + fakeClock.forwardTime(10, TimeUnit.MILLISECONDS); + resp = getInSyncContext(invalidRouteLookupRequestKey); + assertThat(resp.isPending()).isTrue(); + // Assert that the same backoff policy is still in effect for the cache entry. + // The below provider should not get used, so the back off time will still be set to 10ms. + fakeBackoffProvider.nextPolicy = createBackoffPolicy(20, TimeUnit.MILLISECONDS); + // Gets error again and backed off again + fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); + + resp = getInSyncContext(invalidRouteLookupRequestKey); + assertThat(resp.hasError()).isTrue(); + + // Backoff time expiry + fakeClock.forwardTime(10, TimeUnit.MILLISECONDS); + resp = getInSyncContext(invalidRouteLookupRequestKey); assertThat(resp.isPending()).isTrue(); rlsServerImpl.routeLookupReason = null; @@ -359,10 +443,95 @@ public void get_throttledAndRecover() throws Exception { fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); assertThat(rlsServerImpl.routeLookupReason).isEqualTo( io.grpc.lookup.v1.RouteLookupRequest.Reason.REASON_MISS); + } - resp = getInSyncContext(routeLookupRequestKey); + @Test + public void controlPlaneTransientToReady_backOffEntriesRemovedAndPickerUpdated() + throws Exception { + setUpRlsLbClient(); + InOrder inOrder = inOrder(helper); + final ConnectivityState[] rlsChannelState = new ConnectivityState[1]; + Runnable channelStateListener = new Runnable() { + @Override + public void run() { + rlsChannelState[0] = fakeHelper.oobChannel.getState(false); + fakeHelper.oobChannel.notifyWhenStateChanged(rlsChannelState[0], this); + synchronized (this) { + notify(); + } + } + }; + fakeHelper.oobChannel.notifyWhenStateChanged(fakeHelper.oobChannel.getState(false), + channelStateListener); + + fakeHelper.server.shutdown(); + // Channel goes to IDLE state from the shutdown listener handling. + try { + if (!fakeHelper.server.awaitTermination(10, TimeUnit.SECONDS)) { + fakeHelper.server.shutdownNow(); // Forceful shutdown if graceful timeout expires + } + } catch (InterruptedException e) { + fakeHelper.server.shutdownNow(); + } + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey = + RlsProtoData.RouteLookupRequestKey.create(ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); + // Rls channel will go to TRANSIENT_FAILURE (connection back-off). + CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequestKey); + assertThat(resp.isPending()).isTrue(); + assertThat(rlsChannelState[0]).isEqualTo(ConnectivityState.TRANSIENT_FAILURE); + // Throttle the next rpc call. + fakeThrottler.nextResult = true; + fakeBackoffProvider.nextPolicy = createBackoffPolicy(10, TimeUnit.MILLISECONDS); - assertThat(resp.hasData()).isTrue(); + // Cause two cache misses by using new request keys. This will create back-off Rls cache + // entries. RLS control plane state transitioning to READY should reset both back-offs but + // update picker only once. + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey2 = + RlsProtoData.RouteLookupRequestKey.create(ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "foo2", "method-key", "bar")); + resp = getInSyncContext(routeLookupRequestKey2); + assertThat(resp.hasError()).isTrue(); + RlsProtoData.RouteLookupRequestKey routeLookupRequestKey3 = + RlsProtoData.RouteLookupRequestKey.create(ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "foo3", "method-key", "bar")); + resp = getInSyncContext(routeLookupRequestKey3); + assertThat(resp.hasError()).isTrue(); + + fakeHelper.createServerAndRegister("service1"); + // Wait for Rls control plane channel back-off expiry and its moving to READY + synchronized (channelStateListener) { + channelStateListener.wait(2000); + } + assertThat(rlsChannelState[0]).isEqualTo(ConnectivityState.READY); + final ObjectPool defaultExecutorPool = + SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR); + AtomicBoolean isSuccess = new AtomicBoolean(false); + ((ExecutorService) defaultExecutorPool.getObject()).submit(() -> { + // Assert that Rls LB policy picker was updated which picks the fallback target + ArgumentCaptor pickerCaptor = + ArgumentCaptor.forClass(SubchannelPicker.class); + ArgumentCaptor stateCaptor = + ArgumentCaptor.forClass(ConnectivityState.class); + + inOrder.verify(helper, times(4)) + .updateBalancingState(stateCaptor.capture(), pickerCaptor.capture()); + assertThat(new HashSet<>(pickerCaptor.getAllValues())).hasSize(1); + assertThat(stateCaptor.getAllValues()) + .containsExactly(ConnectivityState.TRANSIENT_FAILURE, ConnectivityState.CONNECTING, + ConnectivityState.CONNECTING, ConnectivityState.CONNECTING); + Metadata headers = new Metadata(); + PickResult pickResult = getPickResultForCreate(pickerCaptor, headers); + assertThat(pickResult.getStatus().getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(pickResult.getStatus().getDescription()).isEqualTo("fallback not available"); + isSuccess.set(true); + }).get(); + assertThat(isSuccess.get()).isTrue(); + + fakeThrottler.nextResult = false; + // Rpcs are not backed off now. + assertThat(getInSyncContext(routeLookupRequestKey2).isPending()).isTrue(); + assertThat(getInSyncContext(routeLookupRequestKey3).isPending()).isTrue(); } @Test @@ -931,16 +1100,23 @@ public void run() { private final class FakeHelper extends Helper { + Server server; + ManagedChannel oobChannel; + + void createServerAndRegister(String target) throws IOException { + server = InProcessServerBuilder.forName(target) + .addService(rlsServerImpl) + .directExecutor() + .build() + .start(); + grpcCleanupRule.register(server); + } + @Override public ManagedChannelBuilder createResolvingOobChannelBuilder( String target, ChannelCredentials creds) { try { - grpcCleanupRule.register( - InProcessServerBuilder.forName(target) - .addService(rlsServerImpl) - .directExecutor() - .build() - .start()); + createServerAndRegister(target); } catch (IOException e) { throw new RuntimeException("cannot create server: " + target, e); } @@ -956,7 +1132,8 @@ protected ManagedChannelBuilder delegate() { @Override public ManagedChannel build() { - return grpcCleanupRule.register(super.build()); + oobChannel = super.build(); + return grpcCleanupRule.register(oobChannel); } @Override @@ -985,7 +1162,6 @@ public ManagedChannel createOobChannel(EquivalentAddressGroup eag, String author @Override public void updateBalancingState( @Nonnull ConnectivityState newState, @Nonnull SubchannelPicker newPicker) { - // no-op } @Override diff --git a/rls/src/test/java/io/grpc/rls/LbPolicyConfigurationTest.java b/rls/src/test/java/io/grpc/rls/LbPolicyConfigurationTest.java index d6025d5bad4..fc48e6a5405 100644 --- a/rls/src/test/java/io/grpc/rls/LbPolicyConfigurationTest.java +++ b/rls/src/test/java/io/grpc/rls/LbPolicyConfigurationTest.java @@ -39,7 +39,6 @@ import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.rls.ChildLoadBalancerHelper.ChildLoadBalancerHelperProvider; -import io.grpc.rls.LbPolicyConfiguration.ChildLbStatusListener; import io.grpc.rls.LbPolicyConfiguration.ChildLoadBalancingPolicy; import io.grpc.rls.LbPolicyConfiguration.ChildPolicyWrapper; import io.grpc.rls.LbPolicyConfiguration.ChildPolicyWrapper.ChildPolicyReportingHelper; @@ -61,7 +60,6 @@ public class LbPolicyConfigurationTest { private final LoadBalancer lb = mock(LoadBalancer.class); private final SubchannelStateManager subchannelStateManager = new SubchannelStateManagerImpl(); private final SubchannelPicker picker = mock(SubchannelPicker.class); - private final ChildLbStatusListener childLbStatusListener = mock(ChildLbStatusListener.class); private final ResolvedAddressFactory resolvedAddressFactory = new ResolvedAddressFactory() { @Override @@ -78,8 +76,7 @@ public ResolvedAddresses create(Object childLbConfig) { ImmutableMap.of("foo", "bar"), lbProvider), resolvedAddressFactory, - new ChildLoadBalancerHelperProvider(helper, subchannelStateManager, picker), - childLbStatusListener); + new ChildLoadBalancerHelperProvider(helper, subchannelStateManager, picker)); @Before public void setUp() { @@ -185,7 +182,6 @@ public void updateBalancingState_triggersListener() { childPolicyReportingHelper.updateBalancingState(ConnectivityState.READY, childPicker); - verify(childLbStatusListener).onStatusChanged(ConnectivityState.READY); assertThat(childPolicyWrapper.getPicker()).isEqualTo(childPicker); // picker governs childPickers will be reported to parent LB verify(helper).updateBalancingState(ConnectivityState.READY, picker); diff --git a/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java b/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java index c180935b153..8d16d1bd74c 100644 --- a/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java +++ b/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java @@ -456,8 +456,7 @@ public void lb_working_withDefaultTarget_noRlsResponse() throws Exception { (FakeSubchannel) markReadyAndGetPickResult(inOrder, searchSubchannelArgs).getSubchannel(); assertThat(searchSubchannel).isNotNull(); assertThat(searchSubchannel).isNotSameInstanceAs(fallbackSubchannel); - times = PickFirstLoadBalancerProvider.isEnabledNewPickFirst() ? 1 : 2; - verifyLongCounterAdd("grpc.lb.rls.target_picks", times, 1, "wilderness", "complete"); + verifyLongCounterAdd("grpc.lb.rls.target_picks", 1, 1, "wilderness", "complete"); // create rescue subchannel picker.pickSubchannel(rescueSubchannelArgs); From 97953ca9075ccf3f99068ee80463e361ab3258e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Nam=20Kh=C3=A1nh?= <55955273+khanhkhanhlele@users.noreply.github.com> Date: Thu, 6 Nov 2025 23:18:38 +0700 Subject: [PATCH 443/591] chore: fix typos in some files (#12478) This PR fixes typos in the file file using codespell. --- .../main/java/io/grpc/alts/internal/AltsTsiHandshaker.java | 2 +- services/src/main/proto/grpc/binlog/v1/binarylog.proto | 2 +- .../src/main/proto/grpc/reflection/v1alpha/reflection.proto | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsTsiHandshaker.java b/alts/src/main/java/io/grpc/alts/internal/AltsTsiHandshaker.java index 007db9e1eed..2d6c322c1b1 100644 --- a/alts/src/main/java/io/grpc/alts/internal/AltsTsiHandshaker.java +++ b/alts/src/main/java/io/grpc/alts/internal/AltsTsiHandshaker.java @@ -80,7 +80,7 @@ public boolean processBytesFromPeer(ByteBuffer bytes) throws GeneralSecurityExce return true; } int remaining = bytes.remaining(); - // Call handshaker service to proceess the bytes. + // Call handshaker service to process the bytes. if (outputFrame == null) { checkState(!isClient, "Client handshaker should not process any frame at the beginning."); outputFrame = handshaker.startServerHandshake(bytes); diff --git a/services/src/main/proto/grpc/binlog/v1/binarylog.proto b/services/src/main/proto/grpc/binlog/v1/binarylog.proto index 9ed1733e2d8..b18bd88ddc9 100644 --- a/services/src/main/proto/grpc/binlog/v1/binarylog.proto +++ b/services/src/main/proto/grpc/binlog/v1/binarylog.proto @@ -120,7 +120,7 @@ message ClientHeader { // A single process may be used to run multiple virtual // servers with different identities. - // The authority is the name of such a server identitiy. + // The authority is the name of such a server identity. // It is typically a portion of the URI in the form of // or : . string authority = 3; diff --git a/services/src/main/proto/grpc/reflection/v1alpha/reflection.proto b/services/src/main/proto/grpc/reflection/v1alpha/reflection.proto index 8c5e06fe148..a3984b55c2d 100644 --- a/services/src/main/proto/grpc/reflection/v1alpha/reflection.proto +++ b/services/src/main/proto/grpc/reflection/v1alpha/reflection.proto @@ -80,7 +80,7 @@ message ExtensionRequest { message ServerReflectionResponse { string valid_host = 1; ServerReflectionRequest original_request = 2; - // The server set one of the following fields accroding to the message_request + // The server set one of the following fields according to the message_request // in the request. oneof message_response { // This message is used to answer file_by_filename, file_containing_symbol, @@ -91,7 +91,7 @@ message ServerReflectionResponse { // that were previously sent in response to earlier requests in the stream. FileDescriptorResponse file_descriptor_response = 4; - // This message is used to answer all_extension_numbers_of_type requst. + // This message is used to answer all_extension_numbers_of_type request. ExtensionNumberResponse all_extension_numbers_response = 5; // This message is used to answer list_services request. From 14087f841c0bef87cffd9da45278c19c04da4a35 Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Thu, 6 Nov 2025 18:19:22 +0200 Subject: [PATCH 444/591] core: Report marshaller error for uncompressed size too large back to the client 2 (#12477) Code mostly as in #12360 but trying to use `handleInternalError()` / `cancel()` as suggested by @ejona86 Fixes #11246 --- .../grpc/internal/CloseWithHeadersMarker.java | 32 ++++++++++++++ .../java/io/grpc/internal/ServerCallImpl.java | 24 +++++++++-- .../io/grpc/internal/ServerCallImplTest.java | 42 +++++++++++++++++++ .../integration/AbstractInteropTest.java | 2 +- .../integration/TransportCompressionTest.java | 29 ++++++++++++- .../java/io/grpc/netty/NettyServerStream.java | 7 +++- 6 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 core/src/main/java/io/grpc/internal/CloseWithHeadersMarker.java diff --git a/core/src/main/java/io/grpc/internal/CloseWithHeadersMarker.java b/core/src/main/java/io/grpc/internal/CloseWithHeadersMarker.java new file mode 100644 index 00000000000..376b9edb614 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/CloseWithHeadersMarker.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import io.grpc.Status; + +/** + * Marker to be used for Status sent to {@link ServerStream#cancel(Status)} to signal that stream + * should be closed by sending headers. + */ +public class CloseWithHeadersMarker extends Throwable { + private static final long serialVersionUID = 0L; + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } +} diff --git a/core/src/main/java/io/grpc/internal/ServerCallImpl.java b/core/src/main/java/io/grpc/internal/ServerCallImpl.java index e224384ce8f..1c1f76cbb12 100644 --- a/core/src/main/java/io/grpc/internal/ServerCallImpl.java +++ b/core/src/main/java/io/grpc/internal/ServerCallImpl.java @@ -279,6 +279,17 @@ private void handleInternalError(Throwable internalError) { serverCallTracer.reportCallEnded(false); // error so always false } + /** + * Close the {@link ServerStream} because parsing request message failed. + * Similar to {@link #handleInternalError(Throwable)}. + */ + private void handleParseError(StatusRuntimeException parseError) { + cancelled = true; + log.log(Level.WARNING, "Cancelling the stream because of parse error", parseError); + stream.cancel(parseError.getStatus().withCause(new CloseWithHeadersMarker())); + serverCallTracer.reportCallEnded(false); // error so always false + } + /** * All of these callbacks are assumed to called on an application thread, and the caller is * responsible for handling thrown exceptions. @@ -327,18 +338,23 @@ private void messagesAvailableInternal(final MessageProducer producer) { return; } - InputStream message; + InputStream message = null; try { while ((message = producer.next()) != null) { + ReqT parsed; try { - listener.onMessage(call.method.parseRequest(message)); - } catch (Throwable t) { + parsed = call.method.parseRequest(message); + } catch (StatusRuntimeException e) { GrpcUtil.closeQuietly(message); - throw t; + GrpcUtil.closeQuietly(producer); + call.handleParseError(e); + return; } message.close(); + listener.onMessage(parsed); } } catch (Throwable t) { + GrpcUtil.closeQuietly(message); GrpcUtil.closeQuietly(producer); Throwables.throwIfUnchecked(t); throw new RuntimeException(t); diff --git a/core/src/test/java/io/grpc/internal/ServerCallImplTest.java b/core/src/test/java/io/grpc/internal/ServerCallImplTest.java index 7394c83eab2..028f1ac93cd 100644 --- a/core/src/test/java/io/grpc/internal/ServerCallImplTest.java +++ b/core/src/test/java/io/grpc/internal/ServerCallImplTest.java @@ -48,9 +48,11 @@ import io.grpc.SecurityLevel; import io.grpc.ServerCall; import io.grpc.Status; +import io.grpc.StatusRuntimeException; import io.grpc.internal.ServerCallImpl.ServerStreamListenerImpl; import io.perfmark.PerfMark; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import org.junit.Before; @@ -69,6 +71,8 @@ public class ServerCallImplTest { @Mock private ServerStream stream; @Mock private ServerCall.Listener callListener; + @Mock private StreamListener.MessageProducer messageProducer; + @Mock private InputStream message; private final CallTracer serverCallTracer = CallTracer.getDefaultFactory().create(); private ServerCallImpl call; @@ -493,6 +497,44 @@ public void streamListener_unexpectedRuntimeException() { assertThat(e).hasMessageThat().isEqualTo("unexpected exception"); } + @Test + public void streamListener_statusRuntimeException() throws IOException { + MethodDescriptor failingParseMethod = MethodDescriptor.newBuilder() + .setType(MethodType.UNARY) + .setFullMethodName("service/method") + .setRequestMarshaller(new LongMarshaller() { + @Override + public Long parse(InputStream stream) { + throw new StatusRuntimeException(Status.RESOURCE_EXHAUSTED + .withDescription("Decompressed gRPC message exceeds maximum size")); + } + }) + .setResponseMarshaller(new LongMarshaller()) + .build(); + + call = new ServerCallImpl<>(stream, failingParseMethod, requestHeaders, context, + DecompressorRegistry.getDefaultInstance(), CompressorRegistry.getDefaultInstance(), + serverCallTracer, PerfMark.createTag()); + + ServerStreamListenerImpl streamListener = + new ServerCallImpl.ServerStreamListenerImpl<>(call, callListener, context); + + when(messageProducer.next()).thenReturn(message, (InputStream) null); + streamListener.messagesAvailable(messageProducer); + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); + verify(stream).cancel(statusCaptor.capture()); + Status status = statusCaptor.getValue(); + assertEquals(Status.Code.RESOURCE_EXHAUSTED, status.getCode()); + assertEquals("Decompressed gRPC message exceeds maximum size", status.getDescription()); + + streamListener.halfClosed(); + verify(callListener, never()).onHalfClose(); + + when(messageProducer.next()).thenReturn(message, (InputStream) null); + streamListener.messagesAvailable(messageProducer); + verify(callListener, never()).onMessage(any()); + } + private static class LongMarshaller implements Marshaller { @Override public InputStream stream(Long value) { diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java index 51295281a90..f5cd111a5b1 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java @@ -2024,7 +2024,7 @@ private void assertPayload(Payload expected, Payload actual) { } } - private static void assertCodeEquals(Status.Code expected, Status actual) { + protected static void assertCodeEquals(Status.Code expected, Status actual) { assertWithMessage("Unexpected status: %s", actual).that(actual.getCode()).isEqualTo(expected); } diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java index b9692383254..33cd624aebb 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java @@ -17,6 +17,7 @@ package io.grpc.testing.integration; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import com.google.protobuf.ByteString; @@ -37,6 +38,8 @@ import io.grpc.ServerCall.Listener; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; +import io.grpc.Status.Code; +import io.grpc.StatusRuntimeException; import io.grpc.internal.GrpcUtil; import io.grpc.netty.InternalNettyChannelBuilder; import io.grpc.netty.InternalNettyServerBuilder; @@ -53,7 +56,9 @@ import java.io.OutputStream; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -84,10 +89,16 @@ public static void registerCompressors() { compressors.register(Codec.Identity.NONE); } + @Rule + public final TestName currentTest = new TestName(); + @Override protected ServerBuilder getServerBuilder() { NettyServerBuilder builder = NettyServerBuilder.forPort(0, InsecureServerCredentials.create()) - .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE) + .maxInboundMessageSize( + DECOMPRESSED_MESSAGE_TOO_LONG_METHOD_NAME.equals(currentTest.getMethodName()) + ? 1000 + : AbstractInteropTest.MAX_MESSAGE_SIZE) .compressorRegistry(compressors) .decompressorRegistry(decompressors) .intercept(new ServerInterceptor() { @@ -126,6 +137,22 @@ public void compresses() { assertTrue(FZIPPER.anyWritten); } + private static final String DECOMPRESSED_MESSAGE_TOO_LONG_METHOD_NAME = + "decompressedMessageTooLong"; + + @Test + public void decompressedMessageTooLong() { + assertEquals(DECOMPRESSED_MESSAGE_TOO_LONG_METHOD_NAME, currentTest.getMethodName()); + final SimpleRequest bigRequest = SimpleRequest.newBuilder() + .setPayload(Payload.newBuilder().setBody(ByteString.copyFrom(new byte[10_000]))) + .build(); + StatusRuntimeException e = assertThrows(StatusRuntimeException.class, + () -> blockingStub.withCompression("gzip").unaryCall(bigRequest)); + assertCodeEquals(Code.RESOURCE_EXHAUSTED, e.getStatus()); + assertEquals("Decompressed gRPC message exceeds maximum size 1000", + e.getStatus().getDescription()); + } + @Override protected NettyChannelBuilder createChannelBuilder() { NettyChannelBuilder builder = NettyChannelBuilder.forAddress(getListenAddress()) diff --git a/netty/src/main/java/io/grpc/netty/NettyServerStream.java b/netty/src/main/java/io/grpc/netty/NettyServerStream.java index 836f39ddf19..681e649d1ef 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerStream.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerStream.java @@ -23,6 +23,7 @@ import io.grpc.Metadata; import io.grpc.Status; import io.grpc.internal.AbstractServerStream; +import io.grpc.internal.CloseWithHeadersMarker; import io.grpc.internal.StatsTraceContext; import io.grpc.internal.TransportTracer; import io.grpc.internal.WritableBuffer; @@ -130,7 +131,11 @@ public void writeTrailers(Metadata trailers, boolean headersSent, Status status) @Override public void cancel(Status status) { try (TaskCloseable ignore = PerfMark.traceTask("NettyServerStream$Sink.cancel")) { - writeQueue.enqueue(CancelServerStreamCommand.withReset(transportState(), status), true); + CancelServerStreamCommand cmd = + status.getCause() instanceof CloseWithHeadersMarker + ? CancelServerStreamCommand.withReason(transportState(), status) + : CancelServerStreamCommand.withReset(transportState(), status); + writeQueue.enqueue(cmd, true); } } } From 7cb8b68c3aa8dea787bf0ac71e11aadf0f4eed85 Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:30:23 +0300 Subject: [PATCH 445/591] opentelemetry: propagate baggage to metrics for custom attributes, helps with b/406058193 (#12389) --- opentelemetry/build.gradle | 1 + .../grpc/opentelemetry/GrpcOpenTelemetry.java | 4 +- .../OpenTelemetryMetricsModule.java | 45 ++++-- .../OpenTelemetryTracingModule.java | 24 +++- .../internal/OpenTelemetryConstants.java | 6 + .../OpenTelemetryMetricsModuleTest.java | 132 ++++++++++++++++++ .../OpenTelemetryTracingModuleTest.java | 116 +++++++++++++++ 7 files changed, 309 insertions(+), 19 deletions(-) diff --git a/opentelemetry/build.gradle b/opentelemetry/build.gradle index b729f393e4b..c856ad8dcb9 100644 --- a/opentelemetry/build.gradle +++ b/opentelemetry/build.gradle @@ -15,6 +15,7 @@ dependencies { libraries.auto.value.annotations testImplementation project(':grpc-testing'), + project(':grpc-testing-proto'), project(':grpc-inprocess'), testFixtures(project(':grpc-core')), testFixtures(project(':grpc-api')), diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java index e0af0f80ed3..4341b27daa4 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java @@ -179,12 +179,14 @@ public void configureChannelBuilder(ManagedChannelBuilder builder) { * @param serverBuilder the server builder to configure */ public void configureServerBuilder(ServerBuilder serverBuilder) { - serverBuilder.addStreamTracerFactory(openTelemetryMetricsModule.getServerTracerFactory()); + /* To ensure baggage propagation to metrics, we need the tracing + tracers to be initialised before metrics */ if (ENABLE_OTEL_TRACING) { serverBuilder.addStreamTracerFactory( openTelemetryTracingModule.getServerTracerFactory()); serverBuilder.intercept(openTelemetryTracingModule.getServerSpanPropagationInterceptor()); } + serverBuilder.addStreamTracerFactory(openTelemetryMetricsModule.getServerTracerFactory()); } @VisibleForTesting diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java index 1d77f9ee3e4..3e5137e0034 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.BACKEND_SERVICE_KEY; +import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.BAGGAGE_KEY; import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.LOCALITY_KEY; import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.METHOD_KEY; import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.STATUS_KEY; @@ -44,7 +45,9 @@ import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.StreamTracer; +import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -94,8 +97,8 @@ final class OpenTelemetryMetricsModule { private final ImmutableList plugins; OpenTelemetryMetricsModule(Supplier stopwatchSupplier, - OpenTelemetryMetricsResource resource, Collection optionalLabels, - List plugins) { + OpenTelemetryMetricsResource resource, + Collection optionalLabels, List plugins) { this.resource = checkNotNull(resource, "resource"); this.stopwatchSupplier = checkNotNull(stopwatchSupplier, "stopwatchSupplier"); this.localityEnabled = optionalLabels.contains(LOCALITY_KEY.getKey()); @@ -128,6 +131,14 @@ static String recordMethodName(String fullMethodName, boolean isGeneratedMethod) return isGeneratedMethod ? fullMethodName : "other"; } + private static Context otelContextWithBaggage() { + Baggage baggage = BAGGAGE_KEY.get(); + if (baggage == null) { + return Context.current(); + } + return Context.current().with(baggage); + } + private static final class ClientTracer extends ClientStreamTracer { @Nullable private static final AtomicLongFieldUpdater outboundWireSizeUpdater; @Nullable private static final AtomicLongFieldUpdater inboundWireSizeUpdater; @@ -243,6 +254,7 @@ public void streamClosed(Status status) { } void recordFinishedAttempt() { + Context otelContext = otelContextWithBaggage(); AttributesBuilder builder = io.opentelemetry.api.common.Attributes.builder() .put(METHOD_KEY, fullMethodName) .put(TARGET_KEY, target) @@ -268,15 +280,15 @@ void recordFinishedAttempt() { if (module.resource.clientAttemptDurationCounter() != null ) { module.resource.clientAttemptDurationCounter() - .record(attemptNanos * SECONDS_PER_NANO, attribute); + .record(attemptNanos * SECONDS_PER_NANO, attribute, otelContext); } if (module.resource.clientTotalSentCompressedMessageSizeCounter() != null) { module.resource.clientTotalSentCompressedMessageSizeCounter() - .record(outboundWireSize, attribute); + .record(outboundWireSize, attribute, otelContext); } if (module.resource.clientTotalReceivedCompressedMessageSizeCounter() != null) { module.resource.clientTotalReceivedCompressedMessageSizeCounter() - .record(inboundWireSize, attribute); + .record(inboundWireSize, attribute, otelContext); } } } @@ -408,6 +420,7 @@ void callEnded(Status status) { } void recordFinishedCall() { + Context otelContext = otelContextWithBaggage(); if (attemptsPerCall.get() == 0) { ClientTracer tracer = newClientTracer(null); tracer.attemptNanos = attemptDelayStopwatch.elapsed(TimeUnit.NANOSECONDS); @@ -429,7 +442,8 @@ void recordFinishedCall() { callLatencyNanos * SECONDS_PER_NANO, baseAttributes.toBuilder() .put(STATUS_KEY, status.getCode().toString()) - .build() + .build(), + otelContext ); } @@ -437,7 +451,8 @@ void recordFinishedCall() { if (module.resource.clientCallRetriesCounter() != null) { long retriesPerCall = Math.max(attemptsPerCall.get() - 1, 0); if (retriesPerCall > 0) { - module.resource.clientCallRetriesCounter().record(retriesPerCall, baseAttributes); + module.resource.clientCallRetriesCounter() + .record(retriesPerCall, baseAttributes, otelContext); } } @@ -446,7 +461,7 @@ void recordFinishedCall() { long hedges = hedgedAttemptsPerCall.get(); if (hedges > 0) { module.resource.clientCallHedgesCounter() - .record(hedges, baseAttributes); + .record(hedges, baseAttributes, otelContext); } } @@ -454,8 +469,8 @@ void recordFinishedCall() { if (module.resource.clientCallTransparentRetriesCounter() != null) { long transparentRetries = transparentRetriesPerCall.get(); if (transparentRetries > 0) { - module.resource.clientCallTransparentRetriesCounter().record( - transparentRetries, baseAttributes); + module.resource.clientCallTransparentRetriesCounter() + .record(transparentRetries, baseAttributes, otelContext); } } @@ -463,7 +478,8 @@ void recordFinishedCall() { if (module.resource.clientCallRetryDelayCounter() != null) { module.resource.clientCallRetryDelayCounter().record( retryDelayNanos * SECONDS_PER_NANO, - baseAttributes + baseAttributes, + otelContext ); } } @@ -562,6 +578,7 @@ public void inboundWireSize(long bytes) { */ @Override public void streamClosed(Status status) { + Context otelContext = otelContextWithBaggage(); if (streamClosedUpdater != null) { if (streamClosedUpdater.getAndSet(this, 1) != 0) { return; @@ -584,15 +601,15 @@ public void streamClosed(Status status) { if (module.resource.serverCallDurationCounter() != null) { module.resource.serverCallDurationCounter() - .record(elapsedTimeNanos * SECONDS_PER_NANO, attributes); + .record(elapsedTimeNanos * SECONDS_PER_NANO, attributes, otelContext); } if (module.resource.serverTotalSentCompressedMessageSizeCounter() != null) { module.resource.serverTotalSentCompressedMessageSizeCounter() - .record(outboundWireSize, attributes); + .record(outboundWireSize, attributes, otelContext); } if (module.resource.serverTotalReceivedCompressedMessageSizeCounter() != null) { module.resource.serverTotalReceivedCompressedMessageSizeCounter() - .record(inboundWireSize, attributes); + .record(inboundWireSize, attributes, otelContext); } } } diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java index 4fa4f802dc0..d214e99bd75 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryTracingModule.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.ClientStreamTracer.NAME_RESOLUTION_DELAYED; import static io.grpc.internal.GrpcUtil.IMPLEMENTATION_VERSION; +import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.BAGGAGE_KEY; import com.google.common.annotations.VisibleForTesting; import io.grpc.Attributes; @@ -39,6 +40,7 @@ import io.grpc.internal.GrpcUtil; import io.grpc.opentelemetry.internal.OpenTelemetryConstants; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; @@ -59,6 +61,7 @@ final class OpenTelemetryTracingModule { @VisibleForTesting final io.grpc.Context.Key otelSpan = io.grpc.Context.key("opentelemetry-span-key"); + @Nullable private static final AtomicIntegerFieldUpdater callEndedUpdater; @Nullable @@ -243,13 +246,15 @@ private final class ServerTracer extends ServerStreamTracer { private final Span span; volatile int streamClosed; private int seqNo; + private Baggage baggage; - ServerTracer(String fullMethodName, @Nullable Span remoteSpan) { + ServerTracer(String fullMethodName, @Nullable Span remoteSpan, Baggage baggage) { checkNotNull(fullMethodName, "fullMethodName"); this.span = otelTracer.spanBuilder(generateTraceSpanName(true, fullMethodName)) .setParent(remoteSpan == null ? null : Context.current().with(remoteSpan)) .startSpan(); + this.baggage = baggage; } /** @@ -275,7 +280,9 @@ public void streamClosed(io.grpc.Status status) { @Override public io.grpc.Context filterContext(io.grpc.Context context) { - return context.withValue(otelSpan, span); + return context + .withValue(otelSpan, span) + .withValue(BAGGAGE_KEY, baggage); } @Override @@ -315,7 +322,8 @@ public ServerStreamTracer newServerStreamTracer(String fullMethodName, Metadata if (remoteSpan == Span.getInvalid()) { remoteSpan = null; } - return new ServerTracer(fullMethodName, remoteSpan); + Baggage baggage = Baggage.fromContext(context); + return new ServerTracer(fullMethodName, remoteSpan, baggage); } } @@ -330,7 +338,15 @@ public ServerCall.Listener interceptCall(ServerCall(next.startCall(call, headers), serverCallContext); } diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java index ff2b88acbfd..2c7123198c4 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java @@ -16,7 +16,9 @@ package io.grpc.opentelemetry.internal; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.common.AttributeKey; import java.util.List; @@ -42,6 +44,10 @@ public final class OpenTelemetryConstants { public static final AttributeKey SECURITY_LEVEL_KEY = AttributeKey.stringKey("grpc.security_level"); + @VisibleForTesting + public static final io.grpc.Context.Key BAGGAGE_KEY = + io.grpc.Context.key("opentelemetry-baggage-key"); + public static final List LATENCY_BUCKETS = ImmutableList.of( 0d, 0.00001d, 0.00005d, 0.0001d, 0.0003d, 0.0006d, 0.0008d, 0.001d, 0.002d, diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java index 6d1234497d6..58759294fca 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java @@ -27,6 +27,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.Mockito.verify; import com.google.common.collect.ImmutableMap; @@ -47,11 +48,21 @@ import io.grpc.ServerStreamTracer.ServerCallInfo; import io.grpc.Status; import io.grpc.Status.Code; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.internal.FakeClock; import io.grpc.opentelemetry.OpenTelemetryMetricsModule.CallAttemptsTracerFactory; import io.grpc.opentelemetry.internal.OpenTelemetryConstants; +import io.grpc.stub.MetadataUtils; +import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcServerRule; +import io.grpc.testing.protobuf.SimpleRequest; +import io.grpc.testing.protobuf.SimpleResponse; +import io.grpc.testing.protobuf.SimpleServiceGrpc; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.metrics.data.MetricData; @@ -65,6 +76,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -161,6 +173,14 @@ public String parse(InputStream stream) { private ServerCall.Listener mockServerCallListener; @Captor private ArgumentCaptor statusCaptor; + @Mock + private DoubleHistogram mockServerCallDurationHistogram; + @Captor + private ArgumentCaptor contextCaptor; + private io.grpc.Server server; + private io.grpc.ManagedChannel channel; + private OpenTelemetryMetricsResource resource; + private final String serverName = "E2ETestServer-" + Math.random(); private final FakeClock fakeClock = new FakeClock(); private final MethodDescriptor method = @@ -180,6 +200,19 @@ public String parse(InputStream stream) { public void setUp() throws Exception { testMeter = openTelemetryTesting.getOpenTelemetry() .getMeter(OpenTelemetryConstants.INSTRUMENTATION_SCOPE); + resource = OpenTelemetryMetricsResource.builder() + .serverCallDurationCounter(mockServerCallDurationHistogram) + .build(); + } + + @After + public void tearDown() { + if (channel != null) { + channel.shutdownNow(); + } + if (server != null) { + server.shutdownNow(); + } } @Test @@ -1595,6 +1628,45 @@ public void serverBasicMetrics() { } + @Test + public void serverBaggagePropagationToMetrics() { + // 1. Create module and tracer factory using the mock resource + OpenTelemetryMetricsModule module = new OpenTelemetryMetricsModule( + fakeClock.getStopwatchSupplier(), resource, emptyList(), emptyList()); + ServerStreamTracer.Factory tracerFactory = module.getServerTracerFactory(); + ServerStreamTracer tracer = + tracerFactory.newServerStreamTracer(method.getFullMethodName(), new Metadata()); + + // 2. Define the test baggage and gRPC context + Baggage testBaggage = Baggage.builder() + .put("user-id", "67") + .build(); + + // This simulates the context that the Tracing module would have created + io.grpc.Context grpcContext = io.grpc.Context.current() + .withValue(OpenTelemetryConstants.BAGGAGE_KEY, testBaggage); + + // 3. Attach the gRPC context, trigger metric recording, and detach + io.grpc.Context previousContext = grpcContext.attach(); + try { + tracer.streamClosed(Status.OK); + } finally { + grpcContext.detach(previousContext); + } + + // 4. Verify the record call and capture the OTel Context + verify(mockServerCallDurationHistogram).record( + anyDouble(), + any(io.opentelemetry.api.common.Attributes.class), + contextCaptor.capture()); + + // 5. Assert on the captured OTel Context + io.opentelemetry.context.Context capturedOtelContext = contextCaptor.getValue(); + Baggage capturedBaggage = Baggage.fromContext(capturedOtelContext); + + assertEquals("67", capturedBaggage.getEntryValue("user-id")); + } + private OpenTelemetryMetricsModule newOpenTelemetryMetricsModule( OpenTelemetryMetricsResource resource) { return new OpenTelemetryMetricsModule( @@ -1631,4 +1703,64 @@ public String getAuthority() { return authority; } } + + @Test + public void serverBaggagePropagation_EndToEnd() throws Exception { + // 1. Create Both Modules + OpenTelemetry otel = openTelemetryTesting.getOpenTelemetry(); + OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule(otel); + OpenTelemetryMetricsModule metricsModule = new OpenTelemetryMetricsModule( + fakeClock.getStopwatchSupplier(), resource, emptyList(), emptyList()); + + // 2. Create Server with *both* tracer factories + server = InProcessServerBuilder.forName(serverName) + .addService(new SimpleServiceImpl()) // <-- Uses the helper class below + .addStreamTracerFactory(tracingModule.getServerTracerFactory()) + .addStreamTracerFactory(metricsModule.getServerTracerFactory()) + .build() + .start(); + + // 3. Create Client Channel + channel = InProcessChannelBuilder.forName(serverName).directExecutor().build(); + + // 4. Manually create baggage headers + Metadata headers = new Metadata(); + headers.put(Metadata.Key.of("baggage", Metadata.ASCII_STRING_MARSHALLER), + "choice=red_pill_or_blue_pill"); + + // 5. Make the gRPC call with these headers + ClientInterceptor headerAttachingInterceptor = + MetadataUtils.newAttachHeadersInterceptor(headers); + + // Now, create the stub and apply that interceptor + SimpleServiceGrpc.SimpleServiceBlockingStub stub = + SimpleServiceGrpc.newBlockingStub(channel) + .withInterceptors(headerAttachingInterceptor); + + // Use the imported SimpleRequest + stub.unaryRpc(SimpleRequest.getDefaultInstance()); + + // 6. Verify the Mock + verify(mockServerCallDurationHistogram).record( + anyDouble(), + any(io.opentelemetry.api.common.Attributes.class), + contextCaptor.capture()); + + // 7. Assert on the captured Context + io.opentelemetry.context.Context capturedOtelContext = contextCaptor.getValue(); + Baggage capturedBaggage = Baggage.fromContext(capturedOtelContext); + + assertEquals("red_pill_or_blue_pill", capturedBaggage.getEntryValue("choice")); + } + + /** + * A simple service implementation for the E2E test. + */ + private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase { + @Override + public void unaryRpc(SimpleRequest request, StreamObserver responseObserver) { + responseObserver.onNext(SimpleResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + } } diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java index bca6be94b9f..e6759aadb1e 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryTracingModuleTest.java @@ -17,12 +17,15 @@ package io.grpc.opentelemetry; import static io.grpc.ClientStreamTracer.NAME_RESOLUTION_DELAYED; +import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.BAGGAGE_KEY; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -59,11 +62,15 @@ import io.grpc.testing.GrpcCleanupRule; import io.grpc.testing.GrpcServerRule; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanId; import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.api.trace.TracerBuilder; import io.opentelemetry.api.trace.TracerProvider; @@ -750,6 +757,115 @@ public void onComplete() { } } + /** + * Tests that baggage from the initial context is propagated + * to the context active during the next handler's execution. + */ + @Test + public void testBaggageIsPropagatedToHandlerContext() { + // 1. ARRANGE + OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule( + openTelemetryRule.getOpenTelemetry()); + ServerInterceptor interceptor = tracingModule.getServerSpanPropagationInterceptor(); + + // Create mocks for the gRPC call chain + @SuppressWarnings("unchecked") + ServerCallHandler mockHandler = mock(ServerCallHandler.class); + @SuppressWarnings("unchecked") + ServerCall.Listener mockListener = mock(ServerCall.Listener.class); + ServerCall mockCall = new NoopServerCall<>(); + Metadata mockHeaders = new Metadata(); + + // Create a non-null Span (required to pass the first 'if' check) + Span testSpan = Span.wrap( + SpanContext.create("time-period", "star-wars", + TraceFlags.getSampled(), TraceState.getDefault())); + + // Create the test Baggage + Baggage testBaggage = Baggage.builder().put("best-bot", "R2D2").build(); + + // Create the initial gRPC context that the interceptor will read from + io.grpc.Context initialGrpcContext = io.grpc.Context.current() + .withValue(tracingModule.otelSpan, testSpan) + .withValue(BAGGAGE_KEY, testBaggage); + + // This AtomicReference will capture the Baggage from *within* the handler + final AtomicReference capturedBaggage = new AtomicReference<>(); + + // Stub the handler to capture the *current* context when it's called + doAnswer(invocation -> { + // Baggage.current() gets baggage from io.opentelemetry.context.Context.current() + capturedBaggage.set(Baggage.current()); + return mockListener; + }).when(mockHandler).startCall(any(), any()); + + // 2. ACT + // Run the interceptCall method within the prepared context + io.grpc.Context previous = initialGrpcContext.attach(); + try { + interceptor.interceptCall(mockCall, mockHeaders, mockHandler); + } finally { + initialGrpcContext.detach(previous); + } + + // 3. ASSERT + // Verify the next handler was called + verify(mockHandler).startCall(same(mockCall), same(mockHeaders)); + + // Check the baggage that was captured + assertNotNull("Baggage should not be null in handler context", capturedBaggage.get()); + assertEquals("Baggage was not correctly propagated to the handler's context", + "R2D2", capturedBaggage.get().getEntryValue("best-bot")); + } + + /** + * Tests that the interceptor proceeds correctly if baggage is null or empty. + */ + @Test + public void testNullBaggageIsHandledGracefully() { + // 1. ARRANGE + OpenTelemetryTracingModule tracingModule = new OpenTelemetryTracingModule( + openTelemetryRule.getOpenTelemetry()); + ServerInterceptor interceptor = tracingModule.getServerSpanPropagationInterceptor(); + + @SuppressWarnings("unchecked") + ServerCallHandler mockHandler = mock(ServerCallHandler.class); + @SuppressWarnings("unchecked") + ServerCall.Listener mockListener = mock(ServerCall.Listener.class); + ServerCall mockCall = new NoopServerCall<>(); + Metadata mockHeaders = new Metadata(); + + Span testSpan = Span.getInvalid(); // A non-null span + + // No baggage is set in the context + io.grpc.Context initialGrpcContext = io.grpc.Context.current() + .withValue(tracingModule.otelSpan, testSpan); + + final AtomicReference capturedBaggage = new AtomicReference<>(); + + // Stub the handler to capture the *current* context when it's called + doAnswer(invocation -> { + // Baggage.current() gets baggage from io.opentelemetry.context.Context.current() + capturedBaggage.set(Baggage.current()); + return mockListener; + }).when(mockHandler).startCall(any(), any()); + + // 2. ACT + io.grpc.Context previous = initialGrpcContext.attach(); + try { + interceptor.interceptCall(mockCall, mockHeaders, mockHandler); + } finally { + initialGrpcContext.detach(previous); + } + + // 3. ASSERT + verify(mockHandler).startCall(same(mockCall), same(mockHeaders)); + + // Baggage should be null in the downstream context + assertEquals("Baggage should be empty when not provided", + Baggage.empty(), capturedBaggage.get()); + } + @Test public void generateTraceSpanName() { assertEquals( From d50098f80647f5737c53fd7110bf5f79b9d05255 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:23:03 -0800 Subject: [PATCH 446/591] s2a: Remove channel.awaitTermination() from S2A ChannelResource awaitTermination() was always timing out, because when the channel terminates it releases shared resources from a transport thread. Releasing the reference count was blocked on the same lock that the close() thread is holding. So it essentially deadlocked, except one thread would eventually give up. b/388769143 --- .../channel/S2AHandshakerServiceChannel.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannel.java b/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannel.java index b1ba88d1886..8453268efc0 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannel.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/channel/S2AHandshakerServiceChannel.java @@ -24,9 +24,6 @@ import io.grpc.ManagedChannel; import io.grpc.internal.SharedResourceHolder.Resource; import io.grpc.netty.NettyChannelBuilder; -import java.time.Duration; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.annotation.concurrent.ThreadSafe; /** @@ -50,9 +47,6 @@ */ @ThreadSafe public final class S2AHandshakerServiceChannel { - private static final Duration CHANNEL_SHUTDOWN_TIMEOUT = Duration.ofSeconds(10); - private static final Logger logger = - Logger.getLogger(S2AHandshakerServiceChannel.class.getName()); /** * Returns a {@link SharedResourceHolder.Resource} instance for managing channels to an S2A server @@ -101,13 +95,6 @@ public void close(Channel instanceChannel) { checkNotNull(instanceChannel); ManagedChannel channel = (ManagedChannel) instanceChannel; channel.shutdownNow(); - try { - channel.awaitTermination(CHANNEL_SHUTDOWN_TIMEOUT.getSeconds(), SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.log(Level.WARNING, "Channel to S2A was not shutdown."); - } - } @Override From da70387824ec77727397029f2bb1683050cb3531 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 10 Nov 2025 11:41:30 -0800 Subject: [PATCH 447/591] api: Document A18 TCP_USER_TIMEOUT handling for keepalive Fixes #12487 --- api/src/main/java/io/grpc/ManagedChannelBuilder.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/src/main/java/io/grpc/ManagedChannelBuilder.java b/api/src/main/java/io/grpc/ManagedChannelBuilder.java index df867d09ba1..3f370ab3003 100644 --- a/api/src/main/java/io/grpc/ManagedChannelBuilder.java +++ b/api/src/main/java/io/grpc/ManagedChannelBuilder.java @@ -374,9 +374,17 @@ public T maxInboundMetadataSize(int bytes) { * notice when they are causing excessive load. Clients are strongly encouraged to use only as * small of a value as necessary. * + *

    When the channel implementation supports TCP_USER_TIMEOUT, enabling keepalive will also + * enable TCP_USER_TIMEOUT for the connection. This requires all sent packets to receive + * a TCP acknowledgement before the keepalive timeout. The keepalive time is not used for + * TCP_USER_TIMEOUT, except as a signal to enable the feature. grpc-netty supports + * TCP_USER_TIMEOUT on Linux platforms supported by netty-transport-native-epoll. + * * @throws UnsupportedOperationException if unsupported * @see gRFC A8 * Client-side Keepalive + * @see gRFC A18 + * TCP User Timeout * @since 1.7.0 */ public T keepAliveTime(long keepAliveTime, TimeUnit timeUnit) { @@ -393,6 +401,8 @@ public T keepAliveTime(long keepAliveTime, TimeUnit timeUnit) { * @throws UnsupportedOperationException if unsupported * @see gRFC A8 * Client-side Keepalive + * @see gRFC A18 + * TCP User Timeout * @since 1.7.0 */ public T keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit) { From 51611bad102657dddbc9d897a40a78af83871a99 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 11 Nov 2025 16:39:31 +0000 Subject: [PATCH 448/591] xds: Enable flags for CSM Cloud run gRPC Java (#12499) Make default to true for the env vars GRPC_EXPERIMENTAL_XDS_SNI GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE GRPC_EXPERIMENTAL_XDS_SYSTEM_ROOT_CERTS and remove usage of the env var GRPC_EXPERIMENTAL_XDS_GCP_AUTHENTICATION_FILTER and make it enabled. --- .../main/java/io/grpc/xds/FilterRegistry.java | 10 ++-------- .../io/grpc/xds/GcpAuthenticationFilter.java | 5 ----- .../java/io/grpc/xds/XdsClusterResource.java | 2 +- .../grpc/xds/XdsRouteConfigureResource.java | 15 +++++++------- .../security/trust/CertificateUtils.java | 2 +- .../grpc/xds/GcpAuthenticationFilterTest.java | 3 ++- .../grpc/xds/GrpcXdsClientImplDataTest.java | 20 ++++++++----------- .../test/java/io/grpc/xds/XdsTestUtils.java | 10 +++++++--- 8 files changed, 28 insertions(+), 39 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/FilterRegistry.java b/xds/src/main/java/io/grpc/xds/FilterRegistry.java index 1fbccea8000..da3a59fe8c1 100644 --- a/xds/src/main/java/io/grpc/xds/FilterRegistry.java +++ b/xds/src/main/java/io/grpc/xds/FilterRegistry.java @@ -17,7 +17,6 @@ package io.grpc.xds; import com.google.common.annotations.VisibleForTesting; -import io.grpc.internal.GrpcUtil; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; @@ -33,18 +32,13 @@ final class FilterRegistry { private FilterRegistry() {} - static boolean isEnabledGcpAuthnFilter = - GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_GCP_AUTHENTICATION_FILTER", false); - static synchronized FilterRegistry getDefaultRegistry() { if (instance == null) { instance = newRegistry().register( new FaultFilter.Provider(), new RouterFilter.Provider(), - new RbacFilter.Provider()); - if (isEnabledGcpAuthnFilter) { - instance.register(new GcpAuthenticationFilter.Provider()); - } + new RbacFilter.Provider(), + new GcpAuthenticationFilter.Provider()); } return instance; } diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java index dc133eaaf1a..8ec02f4f809 100644 --- a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java +++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java @@ -17,7 +17,6 @@ package io.grpc.xds; import static com.google.common.base.Preconditions.checkNotNull; -import static io.grpc.xds.FilterRegistry.isEnabledGcpAuthnFilter; import static io.grpc.xds.XdsNameResolver.CLUSTER_SELECTION_KEY; import static io.grpc.xds.XdsNameResolver.XDS_CONFIG_CALL_OPTION_KEY; @@ -313,10 +312,6 @@ public String getTypeUrl() { public AudienceWrapper parse(Any any) throws ResourceInvalidException { Audience audience; try { - if (!isEnabledGcpAuthnFilter) { - throw new InvalidProtocolBufferException("Environment variable for GCP Authentication " - + "Filter is Not Set"); - } audience = any.unpack(Audience.class); } catch (InvalidProtocolBufferException ex) { throw new ResourceInvalidException("Invalid Resource in address proto", ex); diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index 54089491671..593aed4ef48 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -66,7 +66,7 @@ class XdsClusterResource extends XdsResourceType { System.getProperty("io.grpc.xds.experimentalEnableLeastRequest", "true")); @VisibleForTesting public static boolean enableSystemRootCerts = - GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_SYSTEM_ROOT_CERTS", false); + GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_SYSTEM_ROOT_CERTS", true); static boolean isEnabledXdsHttpConnect = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT", false); diff --git a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java index 2ee326435c4..24ec0659b42 100644 --- a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java @@ -69,8 +69,8 @@ class XdsRouteConfigureResource extends XdsResourceType { - private static final String GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE = - "GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE"; + private static final boolean isXdsAuthorityRewriteEnabled = GrpcUtil.getFlag( + "GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE", true); @VisibleForTesting static boolean enableRouteLookup = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_RLS_LB", true); @@ -475,8 +475,8 @@ static StructOrError parseRouteAction( case CLUSTER: return StructOrError.fromStruct(RouteAction.forCluster( proto.getCluster(), hashPolicies, timeoutNano, retryPolicy, - GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, false) - && args.getServerInfo().isTrustedXdsServer() && proto.getAutoHostRewrite().getValue())); + isXdsAuthorityRewriteEnabled && args.getServerInfo().isTrustedXdsServer() + && proto.getAutoHostRewrite().getValue())); case CLUSTER_HEADER: return null; case WEIGHTED_CLUSTERS: @@ -510,8 +510,8 @@ static StructOrError parseRouteAction( } return StructOrError.fromStruct(VirtualHost.Route.RouteAction.forWeightedClusters( weightedClusters, hashPolicies, timeoutNano, retryPolicy, - GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, false) - && args.getServerInfo().isTrustedXdsServer() && proto.getAutoHostRewrite().getValue())); + isXdsAuthorityRewriteEnabled && args.getServerInfo().isTrustedXdsServer() + && proto.getAutoHostRewrite().getValue())); case CLUSTER_SPECIFIER_PLUGIN: if (enableRouteLookup) { String pluginName = proto.getClusterSpecifierPlugin(); @@ -527,8 +527,7 @@ static StructOrError parseRouteAction( NamedPluginConfig namedPluginConfig = NamedPluginConfig.create(pluginName, pluginConfig); return StructOrError.fromStruct(VirtualHost.Route.RouteAction.forClusterSpecifierPlugin( namedPluginConfig, hashPolicies, timeoutNano, retryPolicy, - GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_AUTHORITY_REWRITE, false) - && args.getServerInfo().isTrustedXdsServer() + isXdsAuthorityRewriteEnabled && args.getServerInfo().isTrustedXdsServer() && proto.getAutoHostRewrite().getValue())); } else { return null; diff --git a/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java b/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java index 30535df836e..ad030143f62 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java @@ -30,7 +30,7 @@ * Contains certificate utility method(s). */ public final class CertificateUtils { - public static boolean isXdsSniEnabled = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_SNI", false); + public static boolean isXdsSniEnabled = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_SNI", true); public static boolean useChannelAuthorityIfNoSniApplicable = GrpcUtil.getFlag("GRPC_USE_CHANNEL_AUTHORITY_IF_NO_SNI_APPLICABLE", false); diff --git a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java index 7f203e036b7..f252c6f4ec1 100644 --- a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java @@ -468,7 +468,8 @@ private static LdsUpdate getLdsUpdate() { private static RdsUpdate getRdsUpdate() { RouteConfiguration routeConfiguration = buildRouteConfiguration("my-server", RDS_NAME, CLUSTER_NAME); - XdsResourceType.Args args = new XdsResourceType.Args(null, "0", "0", null, null, null); + XdsResourceType.Args args = new XdsResourceType.Args( + XdsTestUtils.EMPTY_BOOTSTRAPPER_SERVER_INFO, "0", "0", null, null, null); try { return XdsRouteConfigureResource.getInstance().doParse(args, routeConfiguration); } catch (ResourceInvalidException ex) { diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 2c75ee04d91..0af64755969 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -568,7 +568,7 @@ public void parseRouteAction_withCluster_flagDisabled_autoHostRewriteNotEnabled( assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct().cluster()).isEqualTo("cluster-foo"); assertThat(struct.getStruct().weightedClusters()).isNull(); - assertThat(struct.getStruct().autoHostRewrite()).isFalse(); + assertThat(struct.getStruct().autoHostRewrite()).isTrue(); } @Test @@ -656,7 +656,7 @@ public void parseRouteAction_withWeightedCluster_flagDisabled_autoHostRewriteDis assertThat(struct.getStruct().weightedClusters()).containsExactly( ClusterWeight.create("cluster-foo", 30, ImmutableMap.of()), ClusterWeight.create("cluster-bar", 70, ImmutableMap.of())); - assertThat(struct.getStruct().autoHostRewrite()).isFalse(); + assertThat(struct.getStruct().autoHostRewrite()).isTrue(); } @Test @@ -1038,7 +1038,7 @@ public void parseRouteAction_clusterSpecifier_flagDisabled_autoHostRewriteDisabl ImmutableMap.of("lookupService", "rls-cbt.googleapis.com"))), ImmutableSet.of(), getXdsResourceTypeArgs(true)); assertThat(struct.getStruct()).isNotNull(); - assertThat(struct.getStruct().autoHostRewrite()).isFalse(); + assertThat(struct.getStruct().autoHostRewrite()).isTrue(); } @Test @@ -2447,7 +2447,6 @@ public Object parse(Any value) { @Test public void processCluster_parsesAudienceMetadata() throws Exception { - FilterRegistry.isEnabledGcpAuthnFilter = true; MetadataRegistry.getInstance(); Audience audience = Audience.newBuilder() @@ -2491,14 +2490,11 @@ public void processCluster_parsesAudienceMetadata() throws Exception { "FILTER_METADATA", ImmutableMap.of( "key1", "value1", "key2", 42.0)); - try { - assertThat(update.parsedMetadata().get("FILTER_METADATA")) - .isEqualTo(expectedParsedMetadata.get("FILTER_METADATA")); - assertThat(update.parsedMetadata().get("AUDIENCE_METADATA")) - .isInstanceOf(AudienceWrapper.class); - } finally { - FilterRegistry.isEnabledGcpAuthnFilter = false; - } + + assertThat(update.parsedMetadata().get("FILTER_METADATA")) + .isEqualTo(expectedParsedMetadata.get("FILTER_METADATA")); + assertThat(update.parsedMetadata().get("AUDIENCE_METADATA")) + .isInstanceOf(AudienceWrapper.class); } @Test diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java index 63bd139c2cd..becfe00c79b 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -47,6 +47,7 @@ import io.grpc.BindableService; import io.grpc.Context; import io.grpc.Context.CancellationListener; +import io.grpc.InsecureChannelCredentials; import io.grpc.StatusOr; import io.grpc.internal.ExponentialBackoffPolicy; import io.grpc.internal.FakeClock; @@ -84,6 +85,9 @@ public class XdsTestUtils { static final String HTTP_CONNECTION_MANAGER_TYPE_URL = "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3" + ".HttpConnectionManager"; + static final Bootstrapper.ServerInfo EMPTY_BOOTSTRAPPER_SERVER_INFO = + Bootstrapper.ServerInfo.create( + "td.googleapis.com", InsecureChannelCredentials.create(), false, true, false); public static final String ENDPOINT_HOSTNAME = "data-host"; public static final int ENDPOINT_PORT = 1234; @@ -247,8 +251,8 @@ static XdsConfig getDefaultXdsConfig(String serverHostName) RouteConfiguration routeConfiguration = buildRouteConfiguration(serverHostName, RDS_NAME, CLUSTER_NAME); - Bootstrapper.ServerInfo serverInfo = null; - XdsResourceType.Args args = new XdsResourceType.Args(serverInfo, "0", "0", null, null, null); + XdsResourceType.Args args = new XdsResourceType.Args( + EMPTY_BOOTSTRAPPER_SERVER_INFO, "0", "0", null, null, null); XdsRouteConfigureResource.RdsUpdate rdsUpdate = XdsRouteConfigureResource.getInstance().doParse(args, routeConfiguration); @@ -268,7 +272,7 @@ static XdsConfig getDefaultXdsConfig(String serverHostName) XdsEndpointResource.EdsUpdate edsUpdate = new XdsEndpointResource.EdsUpdate( EDS_NAME, lbEndpointsMap, Collections.emptyList()); XdsClusterResource.CdsUpdate cdsUpdate = XdsClusterResource.CdsUpdate.forEds( - CLUSTER_NAME, EDS_NAME, serverInfo, null, null, null, false, null) + CLUSTER_NAME, EDS_NAME, null, null, null, null, false, null) .lbPolicyConfig(getWrrLbConfigAsMap()).build(); XdsConfig.XdsClusterConfig clusterConfig = new XdsConfig.XdsClusterConfig( CLUSTER_NAME, cdsUpdate, new EndpointConfig(StatusOr.fromValue(edsUpdate))); From 2f64092b8a5a07dad83ce710c12aede0fe84ccee Mon Sep 17 00:00:00 2001 From: Mike Kruskal <62662355+mkruskal-google@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:38:33 -0800 Subject: [PATCH 449/591] compiler: Update maximum supported edition to EDITION_2024 gRPC doesn't do anything sensitive to the new 2024 features, but it does need a new enough libprotoc. --- compiler/src/java_plugin/cpp/java_plugin.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/compiler/src/java_plugin/cpp/java_plugin.cpp b/compiler/src/java_plugin/cpp/java_plugin.cpp index a595a6a6896..4b02d6e9884 100644 --- a/compiler/src/java_plugin/cpp/java_plugin.cpp +++ b/compiler/src/java_plugin/cpp/java_plugin.cpp @@ -58,7 +58,11 @@ class JavaGrpcGenerator : public protobuf::compiler::CodeGenerator { return protobuf::Edition::EDITION_PROTO2; } protobuf::Edition GetMaximumEdition() const override { +#if GOOGLE_PROTOBUF_VERSION >= 6032000 + return protobuf::Edition::EDITION_2024; +#else return protobuf::Edition::EDITION_2023; +#endif } std::vector GetFeatureExtensions() const override { From e2d5bad905ae0f3673ca076a37c5f3cbb323e341 Mon Sep 17 00:00:00 2001 From: srkethireddy <73372452+srkethireddy@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:39:33 -0800 Subject: [PATCH 450/591] alts: Metadata server address modification to account for default port Fixing the utilization of the GCE Metadata host server address environment variable to account for the case where the user does not specify a port (defaults to port 8080) b/451639946 --- .../grpc/alts/HandshakerServiceChannel.java | 27 ++++++++++++++----- .../alts/HandshakerServiceChannelTest.java | 18 +++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java b/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java index f03cc6f8c94..5e32d22d901 100644 --- a/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java +++ b/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java @@ -16,7 +16,6 @@ package io.grpc.alts; -import com.google.common.base.MoreObjects; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; @@ -38,13 +37,29 @@ * application will have at most one connection to the handshaker service. */ final class HandshakerServiceChannel { + // Port 8080 is necessary for ALTS handshake. + private static final int ALTS_PORT = 8080; + private static final String DEFAULT_TARGET = "metadata.google.internal.:8080"; static final Resource SHARED_HANDSHAKER_CHANNEL = - new ChannelResource( - MoreObjects.firstNonNull( - System.getenv("GCE_METADATA_HOST"), "metadata.google.internal.:8080")); - - + new ChannelResource(getHandshakerTarget(System.getenv("GCE_METADATA_HOST"))); + + /** + * Returns handshaker target. When GCE_METADATA_HOST is provided, it might contain port which we + * will discard and use ALTS_PORT instead. + */ + static String getHandshakerTarget(String envValue) { + if (envValue == null || envValue.isEmpty()) { + return DEFAULT_TARGET; + } + String host = envValue; + int portIndex = host.lastIndexOf(':'); + if (portIndex != -1) { + host = host.substring(0, portIndex); // Discard port if specified + } + return host + ":" + ALTS_PORT; // Utilize ALTS port in all cases + } + /** Returns a resource of handshaker service channel for testing only. */ static Resource getHandshakerChannelForTesting(String handshakerAddress) { return new ChannelResource(handshakerAddress); diff --git a/alts/src/test/java/io/grpc/alts/HandshakerServiceChannelTest.java b/alts/src/test/java/io/grpc/alts/HandshakerServiceChannelTest.java index a3937904cd7..221001157f1 100644 --- a/alts/src/test/java/io/grpc/alts/HandshakerServiceChannelTest.java +++ b/alts/src/test/java/io/grpc/alts/HandshakerServiceChannelTest.java @@ -67,6 +67,24 @@ public void sharedChannel_authority() { } } + @Test + public void getHandshakerTarget_nullEnvVar() { + assertThat(HandshakerServiceChannel.getHandshakerTarget(null)) + .isEqualTo("metadata.google.internal.:8080"); + } + + @Test + public void getHandshakerTarget_envVarWithPort() { + assertThat(HandshakerServiceChannel.getHandshakerTarget("169.254.169.254:80")) + .isEqualTo("169.254.169.254:8080"); + } + + @Test + public void getHandshakerTarget_envVarWithHostOnly() { + assertThat(HandshakerServiceChannel.getHandshakerTarget("169.254.169.254")) + .isEqualTo("169.254.169.254:8080"); + } + @Test public void resource_works() { Channel channel = resource.create(); From cb73f217ee687106fb87cd2627bd5402e5acd5cc Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 12 Nov 2025 11:34:58 -0800 Subject: [PATCH 451/591] core: Release lock before closing shared resource If a resource has dependencies that also use SharedResourceHolder, and its close() blocks waiting for processing on another thread, then the threads could become deadlocked on the SharedResourceHolder lock. Our answer should be "don't block," but it's also good to avoid calling arbitrary code with a lock held. create() is still called with the lock held, but that seems less likely to do work on another thread, and it is harder to avoid the lock. close() is very easy to call without the lock. See d50098f80 and b/458736211 --- .../grpc/internal/SharedResourceHolder.java | 18 ++++---- .../internal/SharedResourceHolderTest.java | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/SharedResourceHolder.java b/core/src/main/java/io/grpc/internal/SharedResourceHolder.java index 67d1a98b545..1dfa1f90718 100644 --- a/core/src/main/java/io/grpc/internal/SharedResourceHolder.java +++ b/core/src/main/java/io/grpc/internal/SharedResourceHolder.java @@ -134,18 +134,16 @@ synchronized T releaseInternal(final Resource resource, final T instance) public void run() { synchronized (SharedResourceHolder.this) { // Refcount may have gone up since the task was scheduled. Re-check it. - if (cached.refcount == 0) { - try { - resource.close(instance); - } finally { - instances.remove(resource); - if (instances.isEmpty()) { - destroyer.shutdown(); - destroyer = null; - } - } + if (cached.refcount != 0) { + return; + } + instances.remove(resource); + if (instances.isEmpty()) { + destroyer.shutdown(); + destroyer = null; } } + resource.close(instance); } }), DESTROY_DELAY_SECONDS, TimeUnit.SECONDS); } diff --git a/core/src/test/java/io/grpc/internal/SharedResourceHolderTest.java b/core/src/test/java/io/grpc/internal/SharedResourceHolderTest.java index d27195e2490..692b22a0a68 100644 --- a/core/src/test/java/io/grpc/internal/SharedResourceHolderTest.java +++ b/core/src/test/java/io/grpc/internal/SharedResourceHolderTest.java @@ -30,7 +30,9 @@ import io.grpc.internal.SharedResourceHolder.Resource; import java.util.LinkedList; +import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Delayed; +import java.util.concurrent.FutureTask; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -201,6 +203,46 @@ public void close(ResourceInstance instance) { assertNotSame(instance, holder.getInternal(resource)); } + @Test(timeout = 5000) + public void closeRunsConcurrently() throws Exception { + CyclicBarrier barrier = new CyclicBarrier(2); + class SlowResource implements Resource { + @Override + public ResourceInstance create() { + return new ResourceInstance(); + } + + @Override + public void close(ResourceInstance instance) { + instance.closed = true; + try { + barrier.await(); + barrier.await(); + } catch (Exception ex) { + throw new AssertionError(ex); + } + } + } + + Resource resource = new SlowResource(); + ResourceInstance instance = holder.getInternal(resource); + holder.releaseInternal(resource, instance); + MockScheduledFuture scheduledDestroyTask = scheduledDestroyTasks.poll(); + FutureTask runTask = new FutureTask<>(scheduledDestroyTask::runTask, null); + Thread t = new Thread(runTask); + t.start(); + + barrier.await(); // Ensure the other thread has blocked + assertTrue(instance.closed); + instance = holder.getInternal(resource); + assertFalse(instance.closed); + holder.releaseInternal(resource, instance); + + barrier.await(); // Resume the other thread + t.join(); + runTask.get(); // Check for exception + } + private class MockExecutorFactory implements SharedResourceHolder.ScheduledExecutorFactory { @Override From bbc0aa369f4972f5aa639c6632c16a35a70cc576 Mon Sep 17 00:00:00 2001 From: devalkone Date: Wed, 5 Nov 2025 14:26:31 +0300 Subject: [PATCH 452/591] api,netty: Add custom header support for HTTP CONNECT proxy Allow users to specify custom HTTP headers when connecting through an HTTP CONNECT proxy. This extends HttpConnectProxiedSocketAddress with an optional headers field (Map), which is converted to Netty's HttpHeaders in the protocol negotiator. This change is fully backward-compatible. Existing code without headers continues to work as before. Fixes #9826 --- .../grpc/HttpConnectProxiedSocketAddress.java | 35 ++- .../HttpConnectProxiedSocketAddressTest.java | 248 ++++++++++++++++++ .../io/grpc/netty/NettyChannelBuilder.java | 1 + .../io/grpc/netty/ProtocolNegotiators.java | 32 ++- .../grpc/netty/ProtocolNegotiatorsTest.java | 83 +++++- 5 files changed, 388 insertions(+), 11 deletions(-) create mode 100644 api/src/test/java/io/grpc/HttpConnectProxiedSocketAddressTest.java diff --git a/api/src/main/java/io/grpc/HttpConnectProxiedSocketAddress.java b/api/src/main/java/io/grpc/HttpConnectProxiedSocketAddress.java index d59c53db1d1..0df8dc452c1 100644 --- a/api/src/main/java/io/grpc/HttpConnectProxiedSocketAddress.java +++ b/api/src/main/java/io/grpc/HttpConnectProxiedSocketAddress.java @@ -23,6 +23,9 @@ import com.google.common.base.Objects; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import javax.annotation.Nullable; /** @@ -33,6 +36,8 @@ public final class HttpConnectProxiedSocketAddress extends ProxiedSocketAddress private final SocketAddress proxyAddress; private final InetSocketAddress targetAddress; + @SuppressWarnings("serial") + private final Map headers; @Nullable private final String username; @Nullable @@ -41,6 +46,7 @@ public final class HttpConnectProxiedSocketAddress extends ProxiedSocketAddress private HttpConnectProxiedSocketAddress( SocketAddress proxyAddress, InetSocketAddress targetAddress, + Map headers, @Nullable String username, @Nullable String password) { checkNotNull(proxyAddress, "proxyAddress"); @@ -53,6 +59,7 @@ private HttpConnectProxiedSocketAddress( } this.proxyAddress = proxyAddress; this.targetAddress = targetAddress; + this.headers = headers; this.username = username; this.password = password; } @@ -87,6 +94,14 @@ public InetSocketAddress getTargetAddress() { return targetAddress; } + /** + * Returns the custom HTTP headers to be sent during the HTTP CONNECT handshake. + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12479") + public Map getHeaders() { + return headers; + } + @Override public boolean equals(Object o) { if (!(o instanceof HttpConnectProxiedSocketAddress)) { @@ -95,13 +110,14 @@ public boolean equals(Object o) { HttpConnectProxiedSocketAddress that = (HttpConnectProxiedSocketAddress) o; return Objects.equal(proxyAddress, that.proxyAddress) && Objects.equal(targetAddress, that.targetAddress) + && Objects.equal(headers, that.headers) && Objects.equal(username, that.username) && Objects.equal(password, that.password); } @Override public int hashCode() { - return Objects.hashCode(proxyAddress, targetAddress, username, password); + return Objects.hashCode(proxyAddress, targetAddress, username, password, headers); } @Override @@ -109,6 +125,7 @@ public String toString() { return MoreObjects.toStringHelper(this) .add("proxyAddr", proxyAddress) .add("targetAddr", targetAddress) + .add("headers", headers) .add("username", username) // Intentionally mask out password .add("hasPassword", password != null) @@ -129,6 +146,7 @@ public static final class Builder { private SocketAddress proxyAddress; private InetSocketAddress targetAddress; + private Map headers = Collections.emptyMap(); @Nullable private String username; @Nullable @@ -153,6 +171,18 @@ public Builder setTargetAddress(InetSocketAddress targetAddress) { return this; } + /** + * Sets custom HTTP headers to be sent during the HTTP CONNECT handshake. This is an optional + * field. The headers will be sent in addition to any authentication headers (if username and + * password are set). + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12479") + public Builder setHeaders(Map headers) { + this.headers = Collections.unmodifiableMap( + new HashMap<>(checkNotNull(headers, "headers"))); + return this; + } + /** * Sets the username used to connect to the proxy. This is an optional field and can be {@code * null}. @@ -175,7 +205,8 @@ public Builder setPassword(@Nullable String password) { * Creates an {@code HttpConnectProxiedSocketAddress}. */ public HttpConnectProxiedSocketAddress build() { - return new HttpConnectProxiedSocketAddress(proxyAddress, targetAddress, username, password); + return new HttpConnectProxiedSocketAddress( + proxyAddress, targetAddress, headers, username, password); } } } diff --git a/api/src/test/java/io/grpc/HttpConnectProxiedSocketAddressTest.java b/api/src/test/java/io/grpc/HttpConnectProxiedSocketAddressTest.java new file mode 100644 index 00000000000..6620a7d413a --- /dev/null +++ b/api/src/test/java/io/grpc/HttpConnectProxiedSocketAddressTest.java @@ -0,0 +1,248 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + +import com.google.common.testing.EqualsTester; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class HttpConnectProxiedSocketAddressTest { + + private final InetSocketAddress proxyAddress = + new InetSocketAddress(InetAddress.getLoopbackAddress(), 8080); + private final InetSocketAddress targetAddress = + InetSocketAddress.createUnresolved("example.com", 443); + + @Test + public void buildWithAllFields() { + Map headers = new HashMap<>(); + headers.put("X-Custom-Header", "custom-value"); + headers.put("Proxy-Authorization", "Bearer token"); + + HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .setHeaders(headers) + .setUsername("user") + .setPassword("pass") + .build(); + + assertThat(address.getProxyAddress()).isEqualTo(proxyAddress); + assertThat(address.getTargetAddress()).isEqualTo(targetAddress); + assertThat(address.getHeaders()).hasSize(2); + assertThat(address.getHeaders()).containsEntry("X-Custom-Header", "custom-value"); + assertThat(address.getHeaders()).containsEntry("Proxy-Authorization", "Bearer token"); + assertThat(address.getUsername()).isEqualTo("user"); + assertThat(address.getPassword()).isEqualTo("pass"); + } + + @Test + public void buildWithoutOptionalFields() { + HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .build(); + + assertThat(address.getProxyAddress()).isEqualTo(proxyAddress); + assertThat(address.getTargetAddress()).isEqualTo(targetAddress); + assertThat(address.getHeaders()).isEmpty(); + assertThat(address.getUsername()).isNull(); + assertThat(address.getPassword()).isNull(); + } + + @Test + public void buildWithEmptyHeaders() { + HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .setHeaders(Collections.emptyMap()) + .build(); + + assertThat(address.getHeaders()).isEmpty(); + } + + @Test + public void headersAreImmutable() { + Map headers = new HashMap<>(); + headers.put("key1", "value1"); + + HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .setHeaders(headers) + .build(); + + headers.put("key2", "value2"); + + assertThat(address.getHeaders()).hasSize(1); + assertThat(address.getHeaders()).containsEntry("key1", "value1"); + assertThat(address.getHeaders()).doesNotContainKey("key2"); + } + + @Test + public void returnedHeadersAreUnmodifiable() { + Map headers = new HashMap<>(); + headers.put("key", "value"); + + HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .setHeaders(headers) + .build(); + + assertThrows(UnsupportedOperationException.class, + () -> address.getHeaders().put("newKey", "newValue")); + } + + @Test + public void nullHeadersThrowsException() { + assertThrows(NullPointerException.class, + () -> HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .setHeaders(null) + .build()); + } + + @Test + public void equalsAndHashCode() { + Map headers1 = new HashMap<>(); + headers1.put("header", "value"); + + Map headers2 = new HashMap<>(); + headers2.put("header", "value"); + + Map differentHeaders = new HashMap<>(); + differentHeaders.put("different", "header"); + + new EqualsTester() + .addEqualityGroup( + HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .setHeaders(headers1) + .setUsername("user") + .setPassword("pass") + .build(), + HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .setHeaders(headers2) + .setUsername("user") + .setPassword("pass") + .build()) + .addEqualityGroup( + HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .setHeaders(differentHeaders) + .setUsername("user") + .setPassword("pass") + .build()) + .addEqualityGroup( + HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .build()) + .testEquals(); + } + + @Test + public void toStringContainsHeaders() { + Map headers = new HashMap<>(); + headers.put("X-Test", "test-value"); + + HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .setHeaders(headers) + .setUsername("user") + .setPassword("secret") + .build(); + + String toString = address.toString(); + assertThat(toString).contains("headers"); + assertThat(toString).contains("X-Test"); + assertThat(toString).contains("hasPassword=true"); + assertThat(toString).doesNotContain("secret"); + } + + @Test + public void toStringWithoutPassword() { + HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .build(); + + String toString = address.toString(); + assertThat(toString).contains("hasPassword=false"); + } + + @Test + public void hashCodeDependsOnHeaders() { + Map headers1 = new HashMap<>(); + headers1.put("header", "value1"); + + Map headers2 = new HashMap<>(); + headers2.put("header", "value2"); + + HttpConnectProxiedSocketAddress address1 = HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .setHeaders(headers1) + .build(); + + HttpConnectProxiedSocketAddress address2 = HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .setHeaders(headers2) + .build(); + + assertNotEquals(address1.hashCode(), address2.hashCode()); + } + + @Test + public void multipleHeadersSupported() { + Map headers = new HashMap<>(); + headers.put("X-Header-1", "value1"); + headers.put("X-Header-2", "value2"); + headers.put("X-Header-3", "value3"); + + HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder() + .setProxyAddress(proxyAddress) + .setTargetAddress(targetAddress) + .setHeaders(headers) + .build(); + + assertThat(address.getHeaders()).hasSize(3); + assertThat(address.getHeaders()).containsEntry("X-Header-1", "value1"); + assertThat(address.getHeaders()).containsEntry("X-Header-2", "value2"); + assertThat(address.getHeaders()).containsEntry("X-Header-3", "value3"); + } +} + diff --git a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java index 2db5ab20a91..258aa15b005 100644 --- a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java +++ b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java @@ -818,6 +818,7 @@ public ConnectionClientTransport newClientTransport( serverAddress = proxiedAddr.getTargetAddress(); localNegotiator = ProtocolNegotiators.httpProxy( proxiedAddr.getProxyAddress(), + proxiedAddr.getHeaders(), proxiedAddr.getUsername(), proxiedAddr.getPassword(), protocolNegotiator); diff --git a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java index d30a6292d38..9323c58aae1 100644 --- a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java @@ -51,10 +51,12 @@ import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpClientUpgradeHandler; import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http2.Http2ClientUpgradeCodec; @@ -77,6 +79,7 @@ import java.util.Arrays; import java.util.EnumSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import java.util.logging.Level; @@ -484,7 +487,8 @@ private void fireProtocolNegotiationEvent(ChannelHandlerContext ctx, SSLSession * Returns a {@link ProtocolNegotiator} that does HTTP CONNECT proxy negotiation. */ public static ProtocolNegotiator httpProxy(final SocketAddress proxyAddress, - final @Nullable String proxyUsername, final @Nullable String proxyPassword, + final @Nullable Map headers, final @Nullable String proxyUsername, + final @Nullable String proxyPassword, final ProtocolNegotiator negotiator) { Preconditions.checkNotNull(negotiator, "negotiator"); Preconditions.checkNotNull(proxyAddress, "proxyAddress"); @@ -494,8 +498,9 @@ class ProxyNegotiator implements ProtocolNegotiator { public ChannelHandler newHandler(GrpcHttp2ConnectionHandler http2Handler) { ChannelHandler protocolNegotiationHandler = negotiator.newHandler(http2Handler); ChannelLogger negotiationLogger = http2Handler.getNegotiationLogger(); + HttpHeaders httpHeaders = toHttpHeaders(headers); return new ProxyProtocolNegotiationHandler( - proxyAddress, proxyUsername, proxyPassword, protocolNegotiationHandler, + proxyAddress, httpHeaders, proxyUsername, proxyPassword, protocolNegotiationHandler, negotiationLogger); } @@ -515,6 +520,22 @@ public void close() { return new ProxyNegotiator(); } + /** + * Converts generic Map of headers to Netty's HttpHeaders. + * Returns null if the map is null or empty. + */ + @Nullable + private static HttpHeaders toHttpHeaders(@Nullable Map headers) { + if (headers == null || headers.isEmpty()) { + return null; + } + HttpHeaders httpHeaders = new DefaultHttpHeaders(); + for (Map.Entry entry : headers.entrySet()) { + httpHeaders.add(entry.getKey(), entry.getValue()); + } + return httpHeaders; + } + /** * A Proxy handler follows {@link ProtocolNegotiationHandler} pattern. Upon successful proxy * connection, this handler will install {@code next} handler which should be a handler from @@ -523,17 +544,20 @@ public void close() { static final class ProxyProtocolNegotiationHandler extends ProtocolNegotiationHandler { private final SocketAddress address; + @Nullable private final HttpHeaders httpHeaders; @Nullable private final String userName; @Nullable private final String password; public ProxyProtocolNegotiationHandler( SocketAddress address, + @Nullable HttpHeaders httpHeaders, @Nullable String userName, @Nullable String password, ChannelHandler next, ChannelLogger negotiationLogger) { super(next, negotiationLogger); this.address = Preconditions.checkNotNull(address, "address"); + this.httpHeaders = httpHeaders; this.userName = userName; this.password = password; } @@ -542,9 +566,9 @@ public ProxyProtocolNegotiationHandler( protected void protocolNegotiationEventTriggered(ChannelHandlerContext ctx) { HttpProxyHandler nettyProxyHandler; if (userName == null || password == null) { - nettyProxyHandler = new HttpProxyHandler(address); + nettyProxyHandler = new HttpProxyHandler(address, httpHeaders); } else { - nettyProxyHandler = new HttpProxyHandler(address, userName, password); + nettyProxyHandler = new HttpProxyHandler(address, userName, password, httpHeaders); } ctx.pipeline().addBefore(ctx.name(), /* name= */ null, nettyProxyHandler); } diff --git a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java index 9bb5a43d792..cde33139965 100644 --- a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java +++ b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java @@ -126,6 +126,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Queue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -1088,20 +1089,21 @@ public void tls_invalidHost() throws SSLException { @Test public void httpProxy_nullAddressNpe() { assertThrows(NullPointerException.class, - () -> ProtocolNegotiators.httpProxy(null, "user", "pass", ProtocolNegotiators.plaintext())); + () -> ProtocolNegotiators.httpProxy(null, null, "user", "pass", + ProtocolNegotiators.plaintext())); } @Test public void httpProxy_nullNegotiatorNpe() { assertThrows(NullPointerException.class, () -> ProtocolNegotiators.httpProxy( - InetSocketAddress.createUnresolved("localhost", 80), "user", "pass", null)); + InetSocketAddress.createUnresolved("localhost", 80), null, "user", "pass", null)); } @Test public void httpProxy_nullUserPassNoException() throws Exception { assertNotNull(ProtocolNegotiators.httpProxy( - InetSocketAddress.createUnresolved("localhost", 80), null, null, + InetSocketAddress.createUnresolved("localhost", 80), null, null, null, ProtocolNegotiators.plaintext())); } @@ -1119,7 +1121,7 @@ public void httpProxy_completes() throws Exception { .bind(proxy).sync().channel(); ProtocolNegotiator nego = - ProtocolNegotiators.httpProxy(proxy, null, null, ProtocolNegotiators.plaintext()); + ProtocolNegotiators.httpProxy(proxy, null, null, null, ProtocolNegotiators.plaintext()); // normally NettyClientTransport will add WBAEH which kick start the ProtocolNegotiation, // mocking the behavior using KickStartHandler. ChannelHandler handler = @@ -1182,7 +1184,7 @@ public void httpProxy_500() throws Exception { .bind(proxy).sync().channel(); ProtocolNegotiator nego = - ProtocolNegotiators.httpProxy(proxy, null, null, ProtocolNegotiators.plaintext()); + ProtocolNegotiators.httpProxy(proxy, null, null, null, ProtocolNegotiators.plaintext()); // normally NettyClientTransport will add WBAEH which kick start the ProtocolNegotiation, // mocking the behavior using KickStartHandler. ChannelHandler handler = @@ -1220,6 +1222,77 @@ public void httpProxy_500() throws Exception { } } + @Test + public void httpProxy_customHeaders() throws Exception { + DefaultEventLoopGroup elg = new DefaultEventLoopGroup(1); + // ProxyHandler is incompatible with EmbeddedChannel because when channelRegistered() is called + // the channel is already active. + LocalAddress proxy = new LocalAddress("httpProxy_customHeaders"); + SocketAddress host = InetSocketAddress.createUnresolved("example.com", 443); + + ChannelInboundHandler mockHandler = mock(ChannelInboundHandler.class); + Channel serverChannel = new ServerBootstrap().group(elg).channel(LocalServerChannel.class) + .childHandler(mockHandler) + .bind(proxy).sync().channel(); + + Map headers = new java.util.HashMap<>(); + headers.put("X-Custom-Header", "custom-value"); + headers.put("Proxy-Authorization", "Bearer token123"); + + ProtocolNegotiator nego = ProtocolNegotiators.httpProxy( + proxy, headers, null, null, ProtocolNegotiators.plaintext()); + // normally NettyClientTransport will add WBAEH which kick start the ProtocolNegotiation, + // mocking the behavior using KickStartHandler. + ChannelHandler handler = + new KickStartHandler(nego.newHandler(FakeGrpcHttp2ConnectionHandler.noopHandler())); + Channel channel = new Bootstrap().group(elg).channel(LocalChannel.class).handler(handler) + .register().sync().channel(); + pipeline = channel.pipeline(); + // Wait for initialization to complete + channel.eventLoop().submit(NOOP_RUNNABLE).sync(); + channel.connect(host).sync(); + serverChannel.close(); + ArgumentCaptor contextCaptor = + ArgumentCaptor.forClass(ChannelHandlerContext.class); + Mockito.verify(mockHandler).channelActive(contextCaptor.capture()); + ChannelHandlerContext serverContext = contextCaptor.getValue(); + + final String golden = "testData"; + ChannelFuture negotiationFuture = channel.writeAndFlush(bb(golden, channel)); + + // Wait for sending initial request to complete + channel.eventLoop().submit(NOOP_RUNNABLE).sync(); + ArgumentCaptor objectCaptor = ArgumentCaptor.forClass(Object.class); + Mockito.verify(mockHandler) + .channelRead(ArgumentMatchers.any(), objectCaptor.capture()); + ByteBuf b = (ByteBuf) objectCaptor.getValue(); + String request = b.toString(UTF_8); + b.release(); + + // Verify custom headers are present in the CONNECT request + assertTrue("No trailing newline: " + request, request.endsWith("\r\n\r\n")); + assertTrue("No CONNECT: " + request, request.startsWith("CONNECT example.com:443 ")); + assertTrue("No custom header: " + request, + request.contains("X-Custom-Header: custom-value")); + assertTrue("No proxy authorization: " + request, + request.contains("Proxy-Authorization: Bearer token123")); + + assertFalse(negotiationFuture.isDone()); + serverContext.writeAndFlush(bb("HTTP/1.1 200 OK\r\n\r\n", serverContext.channel())).sync(); + negotiationFuture.sync(); + + channel.eventLoop().submit(NOOP_RUNNABLE).sync(); + objectCaptor = ArgumentCaptor.forClass(Object.class); + Mockito.verify(mockHandler, times(2)) + .channelRead(ArgumentMatchers.any(), objectCaptor.capture()); + b = (ByteBuf) objectCaptor.getAllValues().get(1); + String preface = b.toString(UTF_8); + b.release(); + assertEquals(golden, preface); + + channel.close(); + } + @Test public void waitUntilActiveHandler_firesNegotiation() throws Exception { EventLoopGroup elg = new DefaultEventLoopGroup(1); From 6dab2ceab57763265adacf8de9a5bc4bc8d1f1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20D=C4=85browski?= <3007876+marcindabrowski@users.noreply.github.com> Date: Wed, 12 Nov 2025 22:12:58 +0100 Subject: [PATCH 453/591] Upgrade gson to 2.12.1 Since error_prone_annotations are in version 2.36.0, there is not reason why gson couldn't be bumped to v2.12.1. Since v2.12.0 gson do not support Java 7, but it has been dropped in https://github.com/grpc/grpc-java/pull/8828. --- MODULE.bazel | 2 +- gradle/libs.versions.toml | 4 ++-- repositories.bzl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 36db31dbd71..418a78275b6 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -14,7 +14,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.auto.value:auto-value-annotations:1.11.0", "com.google.auto.value:auto-value:1.11.0", "com.google.code.findbugs:jsr305:3.0.2", - "com.google.code.gson:gson:2.11.0", + "com.google.code.gson:gson:2.12.1", "com.google.errorprone:error_prone_annotations:2.36.0", "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:33.4.8-android", diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf03cc6e18e..3d923af7d40 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,8 +41,8 @@ google-auth-credentials = "com.google.auth:google-auth-library-credentials:1.24. google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.24.1" # Release notes: https://cloud.google.com/logging/docs/release-notes google-cloud-logging = "com.google.cloud:google-cloud-logging:3.23.1" -# 2.12.1 requires error_prone_annotations:2.36.0 but we are stuck with 2.30.0 -gson = "com.google.code.gson:gson:2.11.0" +# 2.13.0 requires error_prone_annotations:2.37.0, but we are stuck with 2.36.0 +gson = "com.google.code.gson:gson:2.12.1" # 33.4.8 requires com.google.errorprone:error_prone_annotations:2.36.0 guava = "com.google.guava:guava:33.4.8-android" guava-betaChecker = "com.google.guava:guava-beta-checker:1.0" diff --git a/repositories.bzl b/repositories.bzl index 2c9b0d46bb6..a635b59a425 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -18,7 +18,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.auto.value:auto-value-annotations:1.11.0", "com.google.auto.value:auto-value:1.11.0", "com.google.code.findbugs:jsr305:3.0.2", - "com.google.code.gson:gson:2.11.0", + "com.google.code.gson:gson:2.12.1", "com.google.errorprone:error_prone_annotations:2.36.0", "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:33.4.8-android", From 8fa2c2c3c1454d411b1b029f8e83debf02c9bf11 Mon Sep 17 00:00:00 2001 From: Kim Sumin Date: Wed, 12 Nov 2025 14:55:35 +0900 Subject: [PATCH 454/591] okhttp: Fix bidirectional keep-alive causing spurious GO_AWAY When bidirectional keep-alive is enabled (both client and server sending keep-alive pings), the server incorrectly validates ping acknowledgments (ACKs) sent in response to server-initiated pings. This causes the KeepAliveEnforcer strike counter to increment for legitimate protocol responses, eventually triggering a GO_AWAY with ENHANCE_YOUR_CALM. Move the keepAliveEnforcer.pingAcceptable() check inside the !ack conditional block, so only client-initiated ping requests are validated. Ping ACKs now bypass enforcement entirely, enabling bidirectional keep-alive to work correctly. Fixes #12417 --- .../io/grpc/okhttp/OkHttpServerTransport.java | 10 ++++---- .../okhttp/OkHttpServerTransportTest.java | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java index b744bca3116..7d192b16943 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java @@ -951,13 +951,13 @@ public void settings(boolean clearPrevious, Settings settings) { @Override public void ping(boolean ack, int payload1, int payload2) { - if (!keepAliveEnforcer.pingAcceptable()) { - abruptShutdown(ErrorCode.ENHANCE_YOUR_CALM, "too_many_pings", - Status.RESOURCE_EXHAUSTED.withDescription("Too many pings from client"), false); - return; - } long payload = (((long) payload1) << 32) | (payload2 & 0xffffffffL); if (!ack) { + if (!keepAliveEnforcer.pingAcceptable()) { + abruptShutdown(ErrorCode.ENHANCE_YOUR_CALM, "too_many_pings", + Status.RESOURCE_EXHAUSTED.withDescription("Too many pings from client"), false); + return; + } frameLogger.logPing(OkHttpFrameLogger.Direction.INBOUND, payload); synchronized (lock) { frameWriter.ping(true, payload1, payload2); diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java index 4d2744dc9c7..00db6e1d339 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java @@ -1268,6 +1268,31 @@ public void keepAliveEnforcer_noticesActive() throws Exception { eq(ByteString.encodeString("too_many_pings", GrpcUtil.US_ASCII))); } + @Test + public void keepAliveEnforcer_doesNotEnforcePingAcks() throws Exception { + serverBuilder.permitKeepAliveTime(1, TimeUnit.HOURS) + .permitKeepAliveWithoutCalls(true); + initTransport(); + handshake(); + + for (int i = 0; i < KeepAliveEnforcer.MAX_PING_STRIKES + 2; i++) { + int serverPingId = 0xDEAD + i; + clientFrameWriter.ping(true, serverPingId, 0); + clientFrameWriter.flush(); + } + + for (int i = 0; i < KeepAliveEnforcer.MAX_PING_STRIKES; i++) { + pingPong(); + } + + pingPongId++; + clientFrameWriter.ping(false, pingPongId, 0); + clientFrameWriter.flush(); + assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); + verify(clientFramesRead).goAway(0, ErrorCode.ENHANCE_YOUR_CALM, + ByteString.encodeString("too_many_pings", GrpcUtil.US_ASCII)); + } + @Test public void maxConcurrentCallsPerConnection_failsWithRst() throws Exception { int maxConcurrentCallsPerConnection = 1; From 3fc026c29bd09c6ca87b72fd69eff843b88456a3 Mon Sep 17 00:00:00 2001 From: adam lazur Date: Thu, 13 Nov 2025 07:15:10 +0900 Subject: [PATCH 455/591] xds: Support deprecated xDS TLS fields for Istio compat (#12435) ## Problem When using xDS with Istio's grpc-agent in proxyless mode, Java gRPC fails with: ``` LDS response Listener validation error: tls_certificate_provider_instance is required in downstream-tls-context ``` **Root Cause:** Istio sends deprecated certificate provider fields for backward compatibility with older Envoy versions. Java gRPC currently only reads the current fields, causing validation failures. Specifically, Istio uses these deprecated fields: 1. **Field 11**: `tls_certificate_certificate_provider_instance` (deprecated) instead of field 14 (`tls_certificate_provider_instance`) 2. **Field 4**: `validation_context_certificate_provider_instance` in `CombinedValidationContext` (deprecated) instead of `ca_certificate_provider_instance` in `default_validation_context` ## Fix Istio is adding support for the new fields in https://github.com/istio/istio/pull/58257. Add fallback logic to support deprecated certificate provider fields before that is rolled out: **For identity certificates:** 1. Try current field 14 (`tls_certificate_provider_instance`) first 2. Fall back to deprecated field 11 (`tls_certificate_certificate_provider_instance`) **For validation context in CombinedValidationContext:** 1. Try `ca_certificate_provider_instance` in `default_validation_context` first 2. Fall back to deprecated field 4 (`validation_context_certificate_provider_instance`) This matches the behavior of [grpc-cpp](https://github.com/grpc/grpc/blob/master/src/core/xds/grpc/xds_common_types_parser.cc#L435-L474) and [grpc-go](https://github.com/grpc/grpc-go/blob/master/internal/xds/xdsclient/xdsresource/unmarshal_cds.go#L310-L344) implementations. ## Testing * Added new tests for both deprecated field paths (field 11 and field 4) * All existing tests pass * Manual local testing with Istio in proxyless mode verified the compatibility fix works --------- Co-authored-by: Amp --- .../java/io/grpc/xds/XdsClusterResource.java | 17 +++++- .../security/CommonTlsContextUtil.java | 19 +++++-- .../CertProviderSslContextProvider.java | 8 ++- .../grpc/xds/GrpcXdsClientImplDataTest.java | 30 +++++++++++ .../security/CommonTlsContextTestsUtil.java | 27 ++++++++++ ...tProviderClientSslContextProviderTest.java | 52 +++++++++++++++++++ 6 files changed, 148 insertions(+), 5 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index 593aed4ef48..d9848d97017 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -541,7 +541,12 @@ private static String getIdentityCertInstanceName(CommonTlsContext commonTlsCont if (commonTlsContext.hasTlsCertificateProviderInstance()) { return commonTlsContext.getTlsCertificateProviderInstance().getInstanceName(); } - return null; + // Fall back to deprecated field (field 11) for backward compatibility with Istio + @SuppressWarnings("deprecation") + String instanceName = commonTlsContext.hasTlsCertificateCertificateProviderInstance() + ? commonTlsContext.getTlsCertificateCertificateProviderInstance().getInstanceName() + : null; + return instanceName; } private static String getRootCertInstanceName(CommonTlsContext commonTlsContext) { @@ -559,6 +564,16 @@ private static String getRootCertInstanceName(CommonTlsContext commonTlsContext) return combinedCertificateValidationContext.getDefaultValidationContext() .getCaCertificateProviderInstance().getInstanceName(); } + // Fall back to deprecated field (field 4) in CombinedValidationContext + @SuppressWarnings("deprecation") + String instanceName = combinedCertificateValidationContext + .hasValidationContextCertificateProviderInstance() + ? combinedCertificateValidationContext.getValidationContextCertificateProviderInstance() + .getInstanceName() + : null; + if (instanceName != null) { + return instanceName; + } } return null; } diff --git a/xds/src/main/java/io/grpc/xds/internal/security/CommonTlsContextUtil.java b/xds/src/main/java/io/grpc/xds/internal/security/CommonTlsContextUtil.java index 50fa18b64f9..bd8a423e683 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/CommonTlsContextUtil.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/CommonTlsContextUtil.java @@ -28,7 +28,10 @@ public static boolean hasCertProviderInstance(CommonTlsContext commonTlsContext) if (commonTlsContext == null) { return false; } + @SuppressWarnings("deprecation") + boolean hasDeprecatedField = commonTlsContext.hasTlsCertificateCertificateProviderInstance(); return commonTlsContext.hasTlsCertificateProviderInstance() + || hasDeprecatedField || hasValidationProviderInstance(commonTlsContext); } @@ -37,9 +40,19 @@ private static boolean hasValidationProviderInstance(CommonTlsContext commonTlsC .hasCaCertificateProviderInstance()) { return true; } - return commonTlsContext.hasCombinedValidationContext() - && commonTlsContext.getCombinedValidationContext().getDefaultValidationContext() - .hasCaCertificateProviderInstance(); + if (commonTlsContext.hasCombinedValidationContext()) { + CommonTlsContext.CombinedCertificateValidationContext combined = + commonTlsContext.getCombinedValidationContext(); + if (combined.hasDefaultValidationContext() + && combined.getDefaultValidationContext().hasCaCertificateProviderInstance()) { + return true; + } + // Check deprecated field (field 4) in CombinedValidationContext + @SuppressWarnings("deprecation") + boolean hasDeprecatedField = combined.hasValidationContextCertificateProviderInstance(); + return hasDeprecatedField; + } + return false; } /** diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java index 2570dcb731b..cb99ca6ad95 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertProviderSslContextProvider.java @@ -113,7 +113,13 @@ protected static CertificateProviderInstance getCertProviderInstance( if (commonTlsContext.hasTlsCertificateProviderInstance()) { return CommonTlsContextUtil.convert(commonTlsContext.getTlsCertificateProviderInstance()); } - return null; + // Fall back to deprecated field for backward compatibility with Istio + @SuppressWarnings("deprecation") + CertificateProviderInstance deprecatedInstance = + commonTlsContext.hasTlsCertificateCertificateProviderInstance() + ? commonTlsContext.getTlsCertificateCertificateProviderInstance() + : null; + return deprecatedInstance; } @Nullable diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 0af64755969..975570d8205 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -3134,6 +3134,18 @@ public void validateCommonTlsContext_tlsNewCertificateProviderInstance() .validateCommonTlsContext(commonTlsContext, ImmutableSet.of("name1", "name2"), true); } + @Test + @SuppressWarnings("deprecation") + public void validateCommonTlsContext_tlsDeprecatedCertificateProviderInstance() + throws ResourceInvalidException { + CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() + .setTlsCertificateCertificateProviderInstance( + CommonTlsContext.CertificateProviderInstance.newBuilder().setInstanceName("name1")) + .build(); + XdsClusterResource + .validateCommonTlsContext(commonTlsContext, ImmutableSet.of("name1", "name2"), true); + } + @Test public void validateCommonTlsContext_tlsCertificateProviderInstance() throws ResourceInvalidException { @@ -3218,6 +3230,24 @@ public void validateCommonTlsContext_combinedValidationContextSystemRootCerts() .validateCommonTlsContext(commonTlsContext, ImmutableSet.of(), false); } + @Test + @SuppressWarnings("deprecation") + public void validateCommonTlsContext_combinedValidationContextDeprecatedCertProvider() + throws ResourceInvalidException { + CommonTlsContext commonTlsContext = CommonTlsContext.newBuilder() + .setTlsCertificateProviderInstance( + CertificateProviderPluginInstance.newBuilder().setInstanceName("cert1")) + .setCombinedValidationContext( + CommonTlsContext.CombinedCertificateValidationContext.newBuilder() + .setValidationContextCertificateProviderInstance( + CommonTlsContext.CertificateProviderInstance.newBuilder() + .setInstanceName("root1")) + .build()) + .build(); + XdsClusterResource + .validateCommonTlsContext(commonTlsContext, ImmutableSet.of("cert1", "root1"), true); + } + @Test public void validateCommonTlsContext_validationContextSystemRootCerts_envVarNotSet_throws() { XdsClusterResource.enableSystemRootCerts = false; diff --git a/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java b/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java index 80a8083fb27..abacd2038f8 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/CommonTlsContextTestsUtil.java @@ -232,6 +232,33 @@ private static CommonTlsContext buildCommonTlsContextForCertProviderInstance( return builder.build(); } + /** Helper method to build CommonTlsContext using deprecated certificate provider field. */ + @SuppressWarnings("deprecation") + public static CommonTlsContext buildCommonTlsContextWithDeprecatedCertProviderInstance( + String certInstanceName, + String certName, + String rootInstanceName, + String rootCertName, + Iterable alpnProtocols, + CertificateValidationContext staticCertValidationContext) { + CommonTlsContext.Builder builder = CommonTlsContext.newBuilder(); + if (certInstanceName != null) { + // Use deprecated field (field 11) instead of current field (field 14) + builder = + builder.setTlsCertificateCertificateProviderInstance( + CommonTlsContext.CertificateProviderInstance.newBuilder() + .setInstanceName(certInstanceName) + .setCertificateName(certName)); + } + builder = + addCertificateValidationContext( + builder, rootInstanceName, rootCertName, staticCertValidationContext); + if (alpnProtocols != null) { + builder.addAllAlpnProtocols(alpnProtocols); + } + return builder.build(); + } + private static CommonTlsContext buildNewCommonTlsContextForCertProviderInstance( String certInstanceName, String certName, diff --git a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java index 875a57b8f3d..91f02863ca4 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/certprovider/CertProviderClientSslContextProviderTest.java @@ -470,6 +470,58 @@ public void testProviderForClient_rootInstanceNull_but_isUsingSystemRootCerts_va .build(), false); } + @Test + public void testProviderForClient_deprecatedCertProviderField() throws Exception { + final CertificateProvider.DistributorWatcher[] watcherCaptor = + new CertificateProvider.DistributorWatcher[1]; + TestCertificateProvider.createAndRegisterProviderProvider( + certificateProviderRegistry, watcherCaptor, "testca", 0); + + // Build UpstreamTlsContext using deprecated field + EnvoyServerProtoData.UpstreamTlsContext upstreamTlsContext = + new EnvoyServerProtoData.UpstreamTlsContext( + CommonTlsContextTestsUtil.buildCommonTlsContextWithDeprecatedCertProviderInstance( + "gcp_id", + "cert-default", + "gcp_id", + "root-default", + /* alpnProtocols= */ null, + /* staticCertValidationContext= */ null)); + + Bootstrapper.BootstrapInfo bootstrapInfo = CommonBootstrapperTestUtils.getTestBootstrapInfo(); + CertProviderClientSslContextProvider provider = + (CertProviderClientSslContextProvider) + certProviderClientSslContextProviderFactory.getProvider( + upstreamTlsContext, + bootstrapInfo.node().toEnvoyProtoNode(), + bootstrapInfo.certProviders()); + + assertThat(provider.savedKey).isNull(); + assertThat(provider.savedCertChain).isNull(); + assertThat(provider.savedTrustedRoots).isNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); + + // Generate cert update + watcherCaptor[0].updateCertificate( + CommonCertProviderTestUtils.getPrivateKey(CLIENT_KEY_FILE), + ImmutableList.of(getCertFromResourceName(CLIENT_PEM_FILE))); + assertThat(provider.savedKey).isNotNull(); + assertThat(provider.savedCertChain).isNotNull(); + assertThat(provider.getSslContextAndTrustManager()).isNull(); + + // Generate root cert update + watcherCaptor[0].updateTrustedRoots(ImmutableList.of(getCertFromResourceName(CA_PEM_FILE))); + assertThat(provider.getSslContextAndTrustManager()).isNotNull(); + assertThat(provider.savedKey).isNull(); + assertThat(provider.savedCertChain).isNull(); + assertThat(provider.savedTrustedRoots).isNull(); + + TestCallback testCallback = + CommonTlsContextTestsUtil.getValueThruCallback(provider); + + doChecksOnSslContext(false, testCallback.updatedSslContext, /* expectedApnProtos= */ null); + } + static class QueuedExecutor implements Executor { /** A list of Runnables to be run in order. */ @VisibleForTesting final Queue runQueue = new ConcurrentLinkedQueue<>(); From 6a61a38eb4bbd9f226ada2a651429308811ded03 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 12 Nov 2025 13:27:16 -0800 Subject: [PATCH 456/591] util: Add RunWith to RandomSubsettingLoadBalancerTest In some environments the test won't run without the annotation. --- .../java/io/grpc/util/RandomSubsettingLoadBalancerTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/util/src/test/java/io/grpc/util/RandomSubsettingLoadBalancerTest.java b/util/src/test/java/io/grpc/util/RandomSubsettingLoadBalancerTest.java index 91dde6ba19d..2c43e8f4c3a 100644 --- a/util/src/test/java/io/grpc/util/RandomSubsettingLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/RandomSubsettingLoadBalancerTest.java @@ -47,6 +47,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -55,6 +57,7 @@ import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; +@RunWith(JUnit4.class) public class RandomSubsettingLoadBalancerTest { @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); From 40155b1002aac9b720215615e5900b405d6aa3c3 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 12 Nov 2025 08:10:21 -0800 Subject: [PATCH 457/591] interop-testing: Increase timeout for StressTestClientTest gaugesShouldBeExported with TSAN doesn't finish in less than 6 seconds and sometimes takes 12 seconds. --- .../java/io/grpc/testing/integration/StressTestClientTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/StressTestClientTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/StressTestClientTest.java index c09a0cfeab9..95ef5379120 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/StressTestClientTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/StressTestClientTest.java @@ -44,7 +44,7 @@ public class StressTestClientTest { @Rule - public final Timeout globalTimeout = Timeout.seconds(10); + public final Timeout globalTimeout = Timeout.seconds(15); @Test public void ipv6AddressesShouldBeSupported() { From 55314c3ca5dfc7bba5953f1c73ec70c3f9ce0dfd Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 12 Nov 2025 15:29:05 -0800 Subject: [PATCH 458/591] servlet: Ignore timeoutOnSleepingServer for Tomcat tomcat10Test is frequently failing on Windows because Tomcat doesn't support trailers-only. I removed the FIXMEs because we won't workaround Tomcat in grpc-servlet because: 1. An interceptor that converts trailers-only into headers+trailers would handle most cases, as it could run directly next to the application. That is semantically very safe. But an interceptor doesn't help deadline handling and other transport-caused closures, as those trailers-only are produced by the transport 2. Having the transport convert trailers-only would work for deadline handling, but is not semantically safe 3. The other languages didn't find Tomcat to be important enough to support HEADERS+empty DATA(END_STREAM) as trailers-only on the client-side ``` Unexpected status: Status{code=INTERNAL, description=Received unexpected EOS on empty DATA frame from server, cause=null} value of: getCode() expected: DEADLINE_EXCEEDED but was : INTERNAL at app//io.grpc.testing.integration.AbstractInteropTest.assertCodeEquals(AbstractInteropTest.java:2028) at app//io.grpc.testing.integration.AbstractInteropTest.timeoutOnSleepingServer(AbstractInteropTest.java:1644) ``` --- .../java/io/grpc/servlet/TomcatInteropTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatInteropTest.java b/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatInteropTest.java index 1422b5388fd..d072fea93a1 100644 --- a/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatInteropTest.java +++ b/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatInteropTest.java @@ -113,27 +113,28 @@ protected boolean metricsExpected() { @Test public void gracefulShutdown() {} - // FIXME @Override @Ignore("Tomcat is not able to send trailer only") @Test public void specialStatusMessage() {} - // FIXME @Override @Ignore("Tomcat is not able to send trailer only") @Test public void unimplementedMethod() {} - // FIXME @Override @Ignore("Tomcat is not able to send trailer only") @Test public void statusCodeAndMessage() {} - // FIXME @Override @Ignore("Tomcat is not able to send trailer only") @Test public void emptyStream() {} + + @Override + @Ignore("Tomcat is not able to send trailer only") + @Test + public void timeoutOnSleepingServer() {} } From 731d85bc911f51c22f50df78745c098c46fc23c1 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 29 Oct 2025 09:49:00 -0700 Subject: [PATCH 459/591] xds: Remove InternalGrpcBootstrapperImpl.getJsonContent() It has been unused since 84d30afad. --- .../main/java/io/grpc/xds/InternalGrpcBootstrapperImpl.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/InternalGrpcBootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/InternalGrpcBootstrapperImpl.java index 6a852a2fbb4..7bbc2a6dfca 100644 --- a/xds/src/main/java/io/grpc/xds/InternalGrpcBootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/InternalGrpcBootstrapperImpl.java @@ -19,7 +19,6 @@ import io.grpc.Internal; import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.XdsInitializationException; -import java.io.IOException; import java.util.Map; /** @@ -29,10 +28,6 @@ public final class InternalGrpcBootstrapperImpl { private InternalGrpcBootstrapperImpl() {} // prevent instantiation - public static String getJsonContent() throws XdsInitializationException, IOException { - return new GrpcBootstrapperImpl().getJsonContent(); - } - public static BootstrapInfo parseBootstrap(Map bootstrap) throws XdsInitializationException { return new GrpcBootstrapperImpl().bootstrap(bootstrap); From 28a6130e8026033a5906ff0d14f7eaedd3f066e9 Mon Sep 17 00:00:00 2001 From: Marius Volkhart Date: Thu, 13 Nov 2025 12:47:16 -0500 Subject: [PATCH 460/591] core: Fix AbstractClientStream Javadoc Subclasses also need to implement setAuthority(String) and getAttributes() which come in from the ClientStream interface. --- .../main/java/io/grpc/internal/AbstractClientStream.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/AbstractClientStream.java b/core/src/main/java/io/grpc/internal/AbstractClientStream.java index 14fd5888147..bce1820b482 100644 --- a/core/src/main/java/io/grpc/internal/AbstractClientStream.java +++ b/core/src/main/java/io/grpc/internal/AbstractClientStream.java @@ -43,9 +43,9 @@ import javax.annotation.Nullable; /** - * The abstract base class for {@link ClientStream} implementations. Extending classes only need to - * implement {@link #transportState()} and {@link #abstractClientStreamSink()}. Must only be called - * from the sending application thread. + * The abstract base class for {@link ClientStream} implementations. + * + *

    Must only be called from the sending application thread. */ public abstract class AbstractClientStream extends AbstractStream implements ClientStream, MessageFramer.Sink { From 322e488a4e9c9d68cbc2d4293e6514a5cec63f66 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 13 Nov 2025 07:11:52 -0800 Subject: [PATCH 461/591] Revert "core: Report marshaller error for uncompressed size too large back to the client 2 (#12477)" This reverts commit 14087f841c0bef87cffd9da45278c19c04da4a35. It caused the same failures as the previous attempt that was reverted in 7eab160. cl/831837315 ``` java.lang.AssertionError: Failed executing read operation at io.grpc.internal.CompositeReadableBuffer.execute(CompositeReadableBuffer.java:328) at io.grpc.internal.CompositeReadableBuffer.executeNoThrow(CompositeReadableBuffer.java:336) at io.grpc.internal.CompositeReadableBuffer.readBytes(CompositeReadableBuffer.java:151) at io.grpc.internal.ReadableBuffers$BufferInputStream.read(ReadableBuffers.java:377) at io.grpc.protobuf.lite.ProtoLiteUtils$MessageMarshaller.parse(ProtoLiteUtils.java:205) at io.grpc.protobuf.lite.ProtoLiteUtils$MessageMarshaller.parse(ProtoLiteUtils.java:133) at io.grpc.MethodDescriptor.parseRequest(MethodDescriptor.java:307) ``` --- .../grpc/internal/CloseWithHeadersMarker.java | 32 -------------- .../java/io/grpc/internal/ServerCallImpl.java | 24 ++--------- .../io/grpc/internal/ServerCallImplTest.java | 42 ------------------- .../integration/AbstractInteropTest.java | 2 +- .../integration/TransportCompressionTest.java | 29 +------------ .../java/io/grpc/netty/NettyServerStream.java | 7 +--- 6 files changed, 7 insertions(+), 129 deletions(-) delete mode 100644 core/src/main/java/io/grpc/internal/CloseWithHeadersMarker.java diff --git a/core/src/main/java/io/grpc/internal/CloseWithHeadersMarker.java b/core/src/main/java/io/grpc/internal/CloseWithHeadersMarker.java deleted file mode 100644 index 376b9edb614..00000000000 --- a/core/src/main/java/io/grpc/internal/CloseWithHeadersMarker.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2025 The gRPC Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.grpc.internal; - -import io.grpc.Status; - -/** - * Marker to be used for Status sent to {@link ServerStream#cancel(Status)} to signal that stream - * should be closed by sending headers. - */ -public class CloseWithHeadersMarker extends Throwable { - private static final long serialVersionUID = 0L; - - @Override - public synchronized Throwable fillInStackTrace() { - return this; - } -} diff --git a/core/src/main/java/io/grpc/internal/ServerCallImpl.java b/core/src/main/java/io/grpc/internal/ServerCallImpl.java index 1c1f76cbb12..e224384ce8f 100644 --- a/core/src/main/java/io/grpc/internal/ServerCallImpl.java +++ b/core/src/main/java/io/grpc/internal/ServerCallImpl.java @@ -279,17 +279,6 @@ private void handleInternalError(Throwable internalError) { serverCallTracer.reportCallEnded(false); // error so always false } - /** - * Close the {@link ServerStream} because parsing request message failed. - * Similar to {@link #handleInternalError(Throwable)}. - */ - private void handleParseError(StatusRuntimeException parseError) { - cancelled = true; - log.log(Level.WARNING, "Cancelling the stream because of parse error", parseError); - stream.cancel(parseError.getStatus().withCause(new CloseWithHeadersMarker())); - serverCallTracer.reportCallEnded(false); // error so always false - } - /** * All of these callbacks are assumed to called on an application thread, and the caller is * responsible for handling thrown exceptions. @@ -338,23 +327,18 @@ private void messagesAvailableInternal(final MessageProducer producer) { return; } - InputStream message = null; + InputStream message; try { while ((message = producer.next()) != null) { - ReqT parsed; try { - parsed = call.method.parseRequest(message); - } catch (StatusRuntimeException e) { + listener.onMessage(call.method.parseRequest(message)); + } catch (Throwable t) { GrpcUtil.closeQuietly(message); - GrpcUtil.closeQuietly(producer); - call.handleParseError(e); - return; + throw t; } message.close(); - listener.onMessage(parsed); } } catch (Throwable t) { - GrpcUtil.closeQuietly(message); GrpcUtil.closeQuietly(producer); Throwables.throwIfUnchecked(t); throw new RuntimeException(t); diff --git a/core/src/test/java/io/grpc/internal/ServerCallImplTest.java b/core/src/test/java/io/grpc/internal/ServerCallImplTest.java index 028f1ac93cd..7394c83eab2 100644 --- a/core/src/test/java/io/grpc/internal/ServerCallImplTest.java +++ b/core/src/test/java/io/grpc/internal/ServerCallImplTest.java @@ -48,11 +48,9 @@ import io.grpc.SecurityLevel; import io.grpc.ServerCall; import io.grpc.Status; -import io.grpc.StatusRuntimeException; import io.grpc.internal.ServerCallImpl.ServerStreamListenerImpl; import io.perfmark.PerfMark; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import org.junit.Before; @@ -71,8 +69,6 @@ public class ServerCallImplTest { @Mock private ServerStream stream; @Mock private ServerCall.Listener callListener; - @Mock private StreamListener.MessageProducer messageProducer; - @Mock private InputStream message; private final CallTracer serverCallTracer = CallTracer.getDefaultFactory().create(); private ServerCallImpl call; @@ -497,44 +493,6 @@ public void streamListener_unexpectedRuntimeException() { assertThat(e).hasMessageThat().isEqualTo("unexpected exception"); } - @Test - public void streamListener_statusRuntimeException() throws IOException { - MethodDescriptor failingParseMethod = MethodDescriptor.newBuilder() - .setType(MethodType.UNARY) - .setFullMethodName("service/method") - .setRequestMarshaller(new LongMarshaller() { - @Override - public Long parse(InputStream stream) { - throw new StatusRuntimeException(Status.RESOURCE_EXHAUSTED - .withDescription("Decompressed gRPC message exceeds maximum size")); - } - }) - .setResponseMarshaller(new LongMarshaller()) - .build(); - - call = new ServerCallImpl<>(stream, failingParseMethod, requestHeaders, context, - DecompressorRegistry.getDefaultInstance(), CompressorRegistry.getDefaultInstance(), - serverCallTracer, PerfMark.createTag()); - - ServerStreamListenerImpl streamListener = - new ServerCallImpl.ServerStreamListenerImpl<>(call, callListener, context); - - when(messageProducer.next()).thenReturn(message, (InputStream) null); - streamListener.messagesAvailable(messageProducer); - ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); - verify(stream).cancel(statusCaptor.capture()); - Status status = statusCaptor.getValue(); - assertEquals(Status.Code.RESOURCE_EXHAUSTED, status.getCode()); - assertEquals("Decompressed gRPC message exceeds maximum size", status.getDescription()); - - streamListener.halfClosed(); - verify(callListener, never()).onHalfClose(); - - when(messageProducer.next()).thenReturn(message, (InputStream) null); - streamListener.messagesAvailable(messageProducer); - verify(callListener, never()).onMessage(any()); - } - private static class LongMarshaller implements Marshaller { @Override public InputStream stream(Long value) { diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java index f5cd111a5b1..51295281a90 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java @@ -2024,7 +2024,7 @@ private void assertPayload(Payload expected, Payload actual) { } } - protected static void assertCodeEquals(Status.Code expected, Status actual) { + private static void assertCodeEquals(Status.Code expected, Status actual) { assertWithMessage("Unexpected status: %s", actual).that(actual.getCode()).isEqualTo(expected); } diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java index 33cd624aebb..b9692383254 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java @@ -17,7 +17,6 @@ package io.grpc.testing.integration; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import com.google.protobuf.ByteString; @@ -38,8 +37,6 @@ import io.grpc.ServerCall.Listener; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; -import io.grpc.Status.Code; -import io.grpc.StatusRuntimeException; import io.grpc.internal.GrpcUtil; import io.grpc.netty.InternalNettyChannelBuilder; import io.grpc.netty.InternalNettyServerBuilder; @@ -56,9 +53,7 @@ import java.io.OutputStream; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -89,16 +84,10 @@ public static void registerCompressors() { compressors.register(Codec.Identity.NONE); } - @Rule - public final TestName currentTest = new TestName(); - @Override protected ServerBuilder getServerBuilder() { NettyServerBuilder builder = NettyServerBuilder.forPort(0, InsecureServerCredentials.create()) - .maxInboundMessageSize( - DECOMPRESSED_MESSAGE_TOO_LONG_METHOD_NAME.equals(currentTest.getMethodName()) - ? 1000 - : AbstractInteropTest.MAX_MESSAGE_SIZE) + .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE) .compressorRegistry(compressors) .decompressorRegistry(decompressors) .intercept(new ServerInterceptor() { @@ -137,22 +126,6 @@ public void compresses() { assertTrue(FZIPPER.anyWritten); } - private static final String DECOMPRESSED_MESSAGE_TOO_LONG_METHOD_NAME = - "decompressedMessageTooLong"; - - @Test - public void decompressedMessageTooLong() { - assertEquals(DECOMPRESSED_MESSAGE_TOO_LONG_METHOD_NAME, currentTest.getMethodName()); - final SimpleRequest bigRequest = SimpleRequest.newBuilder() - .setPayload(Payload.newBuilder().setBody(ByteString.copyFrom(new byte[10_000]))) - .build(); - StatusRuntimeException e = assertThrows(StatusRuntimeException.class, - () -> blockingStub.withCompression("gzip").unaryCall(bigRequest)); - assertCodeEquals(Code.RESOURCE_EXHAUSTED, e.getStatus()); - assertEquals("Decompressed gRPC message exceeds maximum size 1000", - e.getStatus().getDescription()); - } - @Override protected NettyChannelBuilder createChannelBuilder() { NettyChannelBuilder builder = NettyChannelBuilder.forAddress(getListenAddress()) diff --git a/netty/src/main/java/io/grpc/netty/NettyServerStream.java b/netty/src/main/java/io/grpc/netty/NettyServerStream.java index 681e649d1ef..836f39ddf19 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerStream.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerStream.java @@ -23,7 +23,6 @@ import io.grpc.Metadata; import io.grpc.Status; import io.grpc.internal.AbstractServerStream; -import io.grpc.internal.CloseWithHeadersMarker; import io.grpc.internal.StatsTraceContext; import io.grpc.internal.TransportTracer; import io.grpc.internal.WritableBuffer; @@ -131,11 +130,7 @@ public void writeTrailers(Metadata trailers, boolean headersSent, Status status) @Override public void cancel(Status status) { try (TaskCloseable ignore = PerfMark.traceTask("NettyServerStream$Sink.cancel")) { - CancelServerStreamCommand cmd = - status.getCause() instanceof CloseWithHeadersMarker - ? CancelServerStreamCommand.withReason(transportState(), status) - : CancelServerStreamCommand.withReset(transportState(), status); - writeQueue.enqueue(cmd, true); + writeQueue.enqueue(CancelServerStreamCommand.withReset(transportState(), status), true); } } } From a57c373973f01524cde6d02161d87e77f5b638d4 Mon Sep 17 00:00:00 2001 From: Saurav Date: Fri, 14 Nov 2025 21:58:38 +0530 Subject: [PATCH 462/591] xds: Update Envoy proto definitions and add ExtAuthz gRPC service This commit updates the Envoy proto definitions to a newer version and adds the generated gRPC code for the `envoy.service.auth.v3.Authorization` service. The updated proto definitions include changes to the `ext_authz` filter, `GrpcService` configuration, and other related components. This also includes new proto files for gRPC credentials and header mutation rules. The generated `AuthorizationGrpc.java` file provides the gRPC stub that will be used to communicate with the external authorization service. --- .gitignore | 3 + .../service/auth/v3/AuthorizationGrpc.java | 377 +++++++++++++ xds/third_party/envoy/import.sh | 14 +- .../envoy/config/bootstrap/v3/bootstrap.proto | 10 +- .../envoy/config/cluster/v3/cluster.proto | 17 +- .../mutation_rules/v3/mutation_rules.proto | 113 ++++ .../proto/envoy/config/core/v3/address.proto | 3 - .../envoy/config/core/v3/config_source.proto | 5 +- .../envoy/config/core/v3/grpc_service.proto | 23 +- .../envoy/config/core/v3/health_check.proto | 6 +- .../proto/envoy/config/core/v3/protocol.proto | 27 +- .../envoy/config/endpoint/v3/endpoint.proto | 5 +- .../config/endpoint/v3/load_report.proto | 6 +- .../listener/v3/listener_components.proto | 2 +- .../proto/envoy/config/metrics/v3/stats.proto | 4 +- .../envoy/config/overload/v3/overload.proto | 22 +- .../config/route/v3/route_components.proto | 94 +++- .../envoy/config/trace/v3/opentelemetry.proto | 9 +- .../proto/envoy/config/trace/v3/zipkin.proto | 85 ++- .../filters/http/ext_authz/v3/ext_authz.proto | 529 ++++++++++++++++++ .../v3/http_connection_manager.proto | 34 +- .../v3/access_token_credentials.proto | 19 + .../v3/google_default_credentials.proto | 17 + .../insecure/v3/insecure_credentials.proto | 17 + .../local/v3/local_credentials.proto | 17 + .../tls/v3/tls_credentials.proto | 27 + .../xds/v3/xds_credentials.proto | 21 + .../common/v3/common.proto | 24 +- .../service/auth/v3/attribute_context.proto | 222 ++++++++ .../envoy/service/auth/v3/external_auth.proto | 144 +++++ .../proto/envoy/type/matcher/v3/value.proto | 2 +- 31 files changed, 1828 insertions(+), 70 deletions(-) create mode 100644 xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/auth/v3/AuthorizationGrpc.java create mode 100644 xds/third_party/envoy/src/main/proto/envoy/config/common/mutation_rules/v3/mutation_rules.proto create mode 100644 xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto create mode 100644 xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/call_credentials/access_token/v3/access_token_credentials.proto create mode 100644 xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/google_default/v3/google_default_credentials.proto create mode 100644 xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/insecure/v3/insecure_credentials.proto create mode 100644 xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/local/v3/local_credentials.proto create mode 100644 xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/tls/v3/tls_credentials.proto create mode 100644 xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/xds/v3/xds_credentials.proto create mode 100644 xds/third_party/envoy/src/main/proto/envoy/service/auth/v3/attribute_context.proto create mode 100644 xds/third_party/envoy/src/main/proto/envoy/service/auth/v3/external_auth.proto diff --git a/.gitignore b/.gitignore index 92a0e3d6d3a..b078d891adf 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ MODULE.bazel.lock .gitignore bin +# VsCode +.vscode + # OS X .DS_Store diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/auth/v3/AuthorizationGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/auth/v3/AuthorizationGrpc.java new file mode 100644 index 00000000000..df9b7a3514b --- /dev/null +++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/auth/v3/AuthorizationGrpc.java @@ -0,0 +1,377 @@ +package io.envoyproxy.envoy.service.auth.v3; + +import static io.grpc.MethodDescriptor.generateFullMethodName; + +/** + *

    + * A generic interface for performing authorization check on incoming
    + * requests to a networked service.
    + * 
    + */ +@io.grpc.stub.annotations.GrpcGenerated +public final class AuthorizationGrpc { + + private AuthorizationGrpc() {} + + public static final java.lang.String SERVICE_NAME = "envoy.service.auth.v3.Authorization"; + + // Static method descriptors that strictly reflect the proto. + private static volatile io.grpc.MethodDescriptor getCheckMethod; + + @io.grpc.stub.annotations.RpcMethod( + fullMethodName = SERVICE_NAME + '/' + "Check", + requestType = io.envoyproxy.envoy.service.auth.v3.CheckRequest.class, + responseType = io.envoyproxy.envoy.service.auth.v3.CheckResponse.class, + methodType = io.grpc.MethodDescriptor.MethodType.UNARY) + public static io.grpc.MethodDescriptor getCheckMethod() { + io.grpc.MethodDescriptor getCheckMethod; + if ((getCheckMethod = AuthorizationGrpc.getCheckMethod) == null) { + synchronized (AuthorizationGrpc.class) { + if ((getCheckMethod = AuthorizationGrpc.getCheckMethod) == null) { + AuthorizationGrpc.getCheckMethod = getCheckMethod = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.UNARY) + .setFullMethodName(generateFullMethodName(SERVICE_NAME, "Check")) + .setSampledToLocalTracing(true) + .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + io.envoyproxy.envoy.service.auth.v3.CheckRequest.getDefaultInstance())) + .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + io.envoyproxy.envoy.service.auth.v3.CheckResponse.getDefaultInstance())) + .setSchemaDescriptor(new AuthorizationMethodDescriptorSupplier("Check")) + .build(); + } + } + } + return getCheckMethod; + } + + /** + * Creates a new async stub that supports all call types for the service + */ + public static AuthorizationStub newStub(io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public AuthorizationStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AuthorizationStub(channel, callOptions); + } + }; + return AuthorizationStub.newStub(factory, channel); + } + + /** + * Creates a new blocking-style stub that supports all types of calls on the service + */ + public static AuthorizationBlockingV2Stub newBlockingV2Stub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public AuthorizationBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AuthorizationBlockingV2Stub(channel, callOptions); + } + }; + return AuthorizationBlockingV2Stub.newStub(factory, channel); + } + + /** + * Creates a new blocking-style stub that supports unary and streaming output calls on the service + */ + public static AuthorizationBlockingStub newBlockingStub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public AuthorizationBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AuthorizationBlockingStub(channel, callOptions); + } + }; + return AuthorizationBlockingStub.newStub(factory, channel); + } + + /** + * Creates a new ListenableFuture-style stub that supports unary calls on the service + */ + public static AuthorizationFutureStub newFutureStub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public AuthorizationFutureStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AuthorizationFutureStub(channel, callOptions); + } + }; + return AuthorizationFutureStub.newStub(factory, channel); + } + + /** + *
    +   * A generic interface for performing authorization check on incoming
    +   * requests to a networked service.
    +   * 
    + */ + public interface AsyncService { + + /** + *
    +     * Performs authorization check based on the attributes associated with the
    +     * incoming request, and returns status `OK` or not `OK`.
    +     * 
    + */ + default void check(io.envoyproxy.envoy.service.auth.v3.CheckRequest request, + io.grpc.stub.StreamObserver responseObserver) { + io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getCheckMethod(), responseObserver); + } + } + + /** + * Base class for the server implementation of the service Authorization. + *
    +   * A generic interface for performing authorization check on incoming
    +   * requests to a networked service.
    +   * 
    + */ + public static abstract class AuthorizationImplBase + implements io.grpc.BindableService, AsyncService { + + @java.lang.Override public final io.grpc.ServerServiceDefinition bindService() { + return AuthorizationGrpc.bindService(this); + } + } + + /** + * A stub to allow clients to do asynchronous rpc calls to service Authorization. + *
    +   * A generic interface for performing authorization check on incoming
    +   * requests to a networked service.
    +   * 
    + */ + public static final class AuthorizationStub + extends io.grpc.stub.AbstractAsyncStub { + private AuthorizationStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected AuthorizationStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AuthorizationStub(channel, callOptions); + } + + /** + *
    +     * Performs authorization check based on the attributes associated with the
    +     * incoming request, and returns status `OK` or not `OK`.
    +     * 
    + */ + public void check(io.envoyproxy.envoy.service.auth.v3.CheckRequest request, + io.grpc.stub.StreamObserver responseObserver) { + io.grpc.stub.ClientCalls.asyncUnaryCall( + getChannel().newCall(getCheckMethod(), getCallOptions()), request, responseObserver); + } + } + + /** + * A stub to allow clients to do synchronous rpc calls to service Authorization. + *
    +   * A generic interface for performing authorization check on incoming
    +   * requests to a networked service.
    +   * 
    + */ + public static final class AuthorizationBlockingV2Stub + extends io.grpc.stub.AbstractBlockingStub { + private AuthorizationBlockingV2Stub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected AuthorizationBlockingV2Stub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AuthorizationBlockingV2Stub(channel, callOptions); + } + + /** + *
    +     * Performs authorization check based on the attributes associated with the
    +     * incoming request, and returns status `OK` or not `OK`.
    +     * 
    + */ + public io.envoyproxy.envoy.service.auth.v3.CheckResponse check(io.envoyproxy.envoy.service.auth.v3.CheckRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getCheckMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service Authorization. + *
    +   * A generic interface for performing authorization check on incoming
    +   * requests to a networked service.
    +   * 
    + */ + public static final class AuthorizationBlockingStub + extends io.grpc.stub.AbstractBlockingStub { + private AuthorizationBlockingStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected AuthorizationBlockingStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AuthorizationBlockingStub(channel, callOptions); + } + + /** + *
    +     * Performs authorization check based on the attributes associated with the
    +     * incoming request, and returns status `OK` or not `OK`.
    +     * 
    + */ + public io.envoyproxy.envoy.service.auth.v3.CheckResponse check(io.envoyproxy.envoy.service.auth.v3.CheckRequest request) { + return io.grpc.stub.ClientCalls.blockingUnaryCall( + getChannel(), getCheckMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do ListenableFuture-style rpc calls to service Authorization. + *
    +   * A generic interface for performing authorization check on incoming
    +   * requests to a networked service.
    +   * 
    + */ + public static final class AuthorizationFutureStub + extends io.grpc.stub.AbstractFutureStub { + private AuthorizationFutureStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected AuthorizationFutureStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AuthorizationFutureStub(channel, callOptions); + } + + /** + *
    +     * Performs authorization check based on the attributes associated with the
    +     * incoming request, and returns status `OK` or not `OK`.
    +     * 
    + */ + public com.google.common.util.concurrent.ListenableFuture check( + io.envoyproxy.envoy.service.auth.v3.CheckRequest request) { + return io.grpc.stub.ClientCalls.futureUnaryCall( + getChannel().newCall(getCheckMethod(), getCallOptions()), request); + } + } + + private static final int METHODID_CHECK = 0; + + 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 AsyncService serviceImpl; + private final int methodId; + + MethodHandlers(AsyncService 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) { + case METHODID_CHECK: + serviceImpl.check((io.envoyproxy.envoy.service.auth.v3.CheckRequest) request, + (io.grpc.stub.StreamObserver) responseObserver); + break; + default: + throw new AssertionError(); + } + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public io.grpc.stub.StreamObserver invoke( + io.grpc.stub.StreamObserver responseObserver) { + switch (methodId) { + default: + throw new AssertionError(); + } + } + } + + public static final io.grpc.ServerServiceDefinition bindService(AsyncService service) { + return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor()) + .addMethod( + getCheckMethod(), + io.grpc.stub.ServerCalls.asyncUnaryCall( + new MethodHandlers< + io.envoyproxy.envoy.service.auth.v3.CheckRequest, + io.envoyproxy.envoy.service.auth.v3.CheckResponse>( + service, METHODID_CHECK))) + .build(); + } + + private static abstract class AuthorizationBaseDescriptorSupplier + implements io.grpc.protobuf.ProtoFileDescriptorSupplier, io.grpc.protobuf.ProtoServiceDescriptorSupplier { + AuthorizationBaseDescriptorSupplier() {} + + @java.lang.Override + public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() { + return io.envoyproxy.envoy.service.auth.v3.ExternalAuthProto.getDescriptor(); + } + + @java.lang.Override + public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() { + return getFileDescriptor().findServiceByName("Authorization"); + } + } + + private static final class AuthorizationFileDescriptorSupplier + extends AuthorizationBaseDescriptorSupplier { + AuthorizationFileDescriptorSupplier() {} + } + + private static final class AuthorizationMethodDescriptorSupplier + extends AuthorizationBaseDescriptorSupplier + implements io.grpc.protobuf.ProtoMethodDescriptorSupplier { + private final java.lang.String methodName; + + AuthorizationMethodDescriptorSupplier(java.lang.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 (AuthorizationGrpc.class) { + result = serviceDescriptor; + if (result == null) { + serviceDescriptor = result = io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME) + .setSchemaDescriptor(new AuthorizationFileDescriptorSupplier()) + .addMethod(getCheckMethod()) + .build(); + } + } + } + return result; + } +} diff --git a/xds/third_party/envoy/import.sh b/xds/third_party/envoy/import.sh index ba657612586..0ad90f7baf0 100755 --- a/xds/third_party/envoy/import.sh +++ b/xds/third_party/envoy/import.sh @@ -16,8 +16,8 @@ # Update VERSION then execute this script set -e -# import VERSION from the google internal copybara_version.txt for Envoy -VERSION=1128a52d227efb8c798478d293fdc05e8075ebcd +# import VERSION from the google internal go/envoy-import-status +VERSION=b6df993feef0340391e6dbf6ad957ab42884ad05 DOWNLOAD_URL="https://github.com/envoyproxy/envoy/archive/${VERSION}.tar.gz" DOWNLOAD_BASE_DIR="envoy-${VERSION}" SOURCE_PROTO_BASE_DIR="${DOWNLOAD_BASE_DIR}/api" @@ -33,6 +33,7 @@ envoy/config/cluster/v3/circuit_breaker.proto envoy/config/cluster/v3/cluster.proto envoy/config/cluster/v3/filter.proto envoy/config/cluster/v3/outlier_detection.proto +envoy/config/common/mutation_rules/v3/mutation_rules.proto envoy/config/core/v3/address.proto envoy/config/core/v3/backoff.proto envoy/config/core/v3/base.proto @@ -74,12 +75,19 @@ envoy/config/trace/v3/zipkin.proto envoy/data/accesslog/v3/accesslog.proto envoy/extensions/clusters/aggregate/v3/cluster.proto envoy/extensions/filters/common/fault/v3/fault.proto +envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/extensions/filters/http/rate_limit_quota/v3/rate_limit_quota.proto envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.proto envoy/extensions/filters/http/rbac/v3/rbac.proto envoy/extensions/filters/http/router/v3/router.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +envoy/extensions/grpc_service/call_credentials/access_token/v3/access_token_credentials.proto +envoy/extensions/grpc_service/channel_credentials/google_default/v3/google_default_credentials.proto +envoy/extensions/grpc_service/channel_credentials/insecure/v3/insecure_credentials.proto +envoy/extensions/grpc_service/channel_credentials/local/v3/local_credentials.proto +envoy/extensions/grpc_service/channel_credentials/tls/v3/tls_credentials.proto +envoy/extensions/grpc_service/channel_credentials/xds/v3/xds_credentials.proto envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3/client_side_weighted_round_robin.proto envoy/extensions/load_balancing_policies/common/v3/common.proto envoy/extensions/load_balancing_policies/least_request/v3/least_request.proto @@ -92,6 +100,8 @@ envoy/extensions/transport_sockets/tls/v3/cert.proto envoy/extensions/transport_sockets/tls/v3/common.proto envoy/extensions/transport_sockets/tls/v3/secret.proto envoy/extensions/transport_sockets/tls/v3/tls.proto +envoy/service/auth/v3/attribute_context.proto +envoy/service/auth/v3/external_auth.proto envoy/service/discovery/v3/ads.proto envoy/service/discovery/v3/discovery.proto envoy/service/load_stats/v3/lrs.proto diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto b/xds/third_party/envoy/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto index bf65f3df45c..28b1eba6680 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto @@ -41,7 +41,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // ` for more detail. // Bootstrap :ref:`configuration overview `. -// [#next-free-field: 42] +// [#next-free-field: 43] message Bootstrap { option (udpa.annotations.versioning).previous_message_type = "envoy.config.bootstrap.v2.Bootstrap"; @@ -230,6 +230,14 @@ message Bootstrap { bool stats_flush_on_admin = 29 [(validate.rules).bool = {const: true}]; } + oneof stats_eviction { + // Optional duration to perform metric eviction. At every interval, during the stats flush + // the unused metrics are removed from the worker caches and the used metrics + // are marked as unused. Must be a multiple of the ``stats_flush_interval``. + google.protobuf.Duration stats_eviction_interval = 42 + [(validate.rules).duration = {gte {nanos: 1000000}}]; + } + // Optional watchdog configuration. // This is for a single watchdog configuration for the entire system. // Deprecated in favor of ``watchdogs`` which has finer granularity. diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/cluster/v3/cluster.proto b/xds/third_party/envoy/src/main/proto/envoy/config/cluster/v3/cluster.proto index 51180b1e855..c5112458a71 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/cluster/v3/cluster.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/cluster/v3/cluster.proto @@ -652,9 +652,10 @@ message Cluster { // If this is not set, we default to a merge window of 1000ms. To disable it, set the merge // window to 0. // - // Note: merging does not apply to cluster membership changes (e.g.: adds/removes); this is - // because merging those updates isn't currently safe. See - // https://github.com/envoyproxy/envoy/pull/3941. + // .. note:: + // Merging does not apply to cluster membership changes (e.g.: adds/removes); this is + // because merging those updates isn't currently safe. See + // https://github.com/envoyproxy/envoy/pull/3941. google.protobuf.Duration update_merge_window = 4; // If set to true, Envoy will :ref:`exclude ` new hosts @@ -816,12 +817,14 @@ message Cluster { string name = 1 [(validate.rules).string = {min_len: 1}]; // An optional alternative to the cluster name to be used for observability. This name is used - // emitting stats for the cluster and access logging the cluster name. This will appear as + // for emitting stats for the cluster and access logging the cluster name. This will appear as // additional information in configuration dumps of a cluster's current status as // :ref:`observability_name ` - // and as an additional tag "upstream_cluster.name" while tracing. Note: Any ``:`` in the name - // will be converted to ``_`` when emitting statistics. This should not be confused with - // :ref:`Router Filter Header `. + // and as an additional tag "upstream_cluster.name" while tracing. + // + // .. note:: + // Any ``:`` in the name will be converted to ``_`` when emitting statistics. This should not be confused with + // :ref:`Router Filter Header `. string alt_stat_name = 28 [(udpa.annotations.field_migrate).rename = "observability_name"]; oneof cluster_discovery_type { diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/common/mutation_rules/v3/mutation_rules.proto b/xds/third_party/envoy/src/main/proto/envoy/config/common/mutation_rules/v3/mutation_rules.proto new file mode 100644 index 00000000000..c015db21431 --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/config/common/mutation_rules/v3/mutation_rules.proto @@ -0,0 +1,113 @@ +syntax = "proto3"; + +package envoy.config.common.mutation_rules.v3; + +import "envoy/config/core/v3/base.proto"; +import "envoy/type/matcher/v3/regex.proto"; +import "envoy/type/matcher/v3/string.proto"; + +import "google/protobuf/wrappers.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.common.mutation_rules.v3"; +option java_outer_classname = "MutationRulesProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/config/common/mutation_rules/v3;mutation_rulesv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Header mutation rules] + +// The HeaderMutationRules structure specifies what headers may be +// manipulated by a processing filter. This set of rules makes it +// possible to control which modifications a filter may make. +// +// By default, an external processing server may add, modify, or remove +// any header except for an "Envoy internal" header (which is typically +// denoted by an x-envoy prefix) or specific headers that may affect +// further filter processing: +// +// * ``host`` +// * ``:authority`` +// * ``:scheme`` +// * ``:method`` +// +// Every attempt to add, change, append, or remove a header will be +// tested against the rules here. Disallowed header mutations will be +// ignored unless ``disallow_is_error`` is set to true. +// +// Attempts to remove headers are further constrained -- regardless of the +// settings, system-defined headers (that start with ``:``) and the ``host`` +// header may never be removed. +// +// In addition, a counter will be incremented whenever a mutation is +// rejected. In the ext_proc filter, that counter is named +// ``rejected_header_mutations``. +// [#next-free-field: 8] +message HeaderMutationRules { + // By default, certain headers that could affect processing of subsequent + // filters or request routing cannot be modified. These headers are + // ``host``, ``:authority``, ``:scheme``, and ``:method``. Setting this parameter + // to true allows these headers to be modified as well. + google.protobuf.BoolValue allow_all_routing = 1; + + // If true, allow modification of envoy internal headers. By default, these + // start with ``x-envoy`` but this may be overridden in the ``Bootstrap`` + // configuration using the + // :ref:`header_prefix ` + // field. Default is false. + google.protobuf.BoolValue allow_envoy = 2; + + // If true, prevent modification of any system header, defined as a header + // that starts with a ``:`` character, regardless of any other settings. + // A processing server may still override the ``:status`` of an HTTP response + // using an ``ImmediateResponse`` message. Default is false. + google.protobuf.BoolValue disallow_system = 3; + + // If true, prevent modifications of all header values, regardless of any + // other settings. A processing server may still override the ``:status`` + // of an HTTP response using an ``ImmediateResponse`` message. Default is false. + google.protobuf.BoolValue disallow_all = 4; + + // If set, specifically allow any header that matches this regular + // expression. This overrides all other settings except for + // ``disallow_expression``. + type.matcher.v3.RegexMatcher allow_expression = 5; + + // If set, specifically disallow any header that matches this regular + // expression regardless of any other settings. + type.matcher.v3.RegexMatcher disallow_expression = 6; + + // If true, and if the rules in this list cause a header mutation to be + // disallowed, then the filter using this configuration will terminate the + // request with a 500 error. In addition, regardless of the setting of this + // parameter, any attempt to set, add, or modify a disallowed header will + // cause the ``rejected_header_mutations`` counter to be incremented. + // Default is false. + google.protobuf.BoolValue disallow_is_error = 7; +} + +// The HeaderMutation structure specifies an action that may be taken on HTTP +// headers. +message HeaderMutation { + message RemoveOnMatch { + // A string matcher that will be applied to the header key. If the header key + // matches, the header will be removed. + type.matcher.v3.StringMatcher key_matcher = 1 [(validate.rules).message = {required: true}]; + } + + oneof action { + option (validate.required) = true; + + // Remove the specified header if it exists. + string remove = 1 + [(validate.rules).string = {well_known_regex: HTTP_HEADER_VALUE strict: false}]; + + // Append new header by the specified HeaderValueOption. + core.v3.HeaderValueOption append = 2; + + // Remove the header if the key matches the specified string matcher. + RemoveOnMatch remove_on_match = 3; + } +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/address.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/address.proto index 56796fc721a..238494a09c7 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/address.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/address.proto @@ -105,9 +105,6 @@ message SocketAddress { // .. note:: // Setting this parameter requires Envoy to run with the ``CAP_NET_ADMIN`` capability. // - // .. note:: - // Currently only used for Listener sockets. - // // .. attention:: // Network namespaces are only configurable on Linux. Otherwise, this field has no effect. string network_namespace_filepath = 7; diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/config_source.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/config_source.proto index f0effd99e45..430562aa5bd 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/config_source.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/config_source.proto @@ -276,7 +276,8 @@ message ExtensionConfigSource { // to be supplied. bool apply_default_config_without_warming = 3; - // A set of permitted extension type URLs. Extension configuration updates are rejected - // if they do not match any type URL in the set. + // A set of permitted extension type URLs for the type encoded inside of the + // :ref:`TypedExtensionConfig `. Extension + // configuration updates are rejected if they do not match any type URL in the set. repeated string type_urls = 4 [(validate.rules).repeated = {min_items: 1}]; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/grpc_service.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/grpc_service.proto index 5fd7921a806..f8feb2f516f 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/grpc_service.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/grpc_service.proto @@ -64,7 +64,7 @@ message GrpcService { bool skip_envoy_headers = 5; } - // [#next-free-field: 9] + // [#next-free-field: 11] message GoogleGrpc { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.GrpcService.GoogleGrpc"; @@ -249,16 +249,31 @@ message GrpcService { } // The target URI when using the `Google C++ gRPC client - // `_. SSL credentials will be supplied in - // :ref:`channel_credentials `. + // `_. string target_uri = 1 [(validate.rules).string = {min_len: 1}]; + // The channel credentials to use. See `channel credentials + // `_. + // Ignored if ``channel_credentials_plugin`` is set. ChannelCredentials channel_credentials = 2; - // A set of call credentials that can be composed with `channel credentials + // A list of channel credentials plugins. + // The data plane will iterate over the list in order and stop at the first credential type + // that it supports. This provides a mechanism for starting to use new credential types that + // are not yet supported by all data planes. + // [#not-implemented-hide:] + repeated google.protobuf.Any channel_credentials_plugin = 9; + + // The call credentials to use. See `channel credentials // `_. + // Ignored if ``call_credentials_plugin`` is set. repeated CallCredentials call_credentials = 3; + // A list of call credentials plugins. All supported plugins will be used. + // Unsupported plugin types will be ignored. + // [#not-implemented-hide:] + repeated google.protobuf.Any call_credentials_plugin = 10; + // The human readable prefix to use when emitting statistics for the gRPC // service. // diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/health_check.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/health_check.proto index fd4440d8fa5..a4ed6e91818 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/health_check.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/health_check.proto @@ -102,7 +102,8 @@ message HealthCheck { // ``/healthcheck``. string path = 2 [(validate.rules).string = {min_len: 1 well_known_regex: HTTP_HEADER_VALUE}]; - // [#not-implemented-hide:] HTTP specific payload. + // HTTP specific payload to be sent as the request body during health checking. + // If specified, the method should support a request body (POST, PUT, PATCH, etc.). Payload send = 3; // Specifies a list of HTTP expected responses to match in the first ``response_buffer_size`` bytes of the response body. @@ -161,7 +162,8 @@ message HealthCheck { type.matcher.v3.StringMatcher service_name_matcher = 11; // HTTP Method that will be used for health checking, default is "GET". - // GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, PATCH methods are supported, but making request body is not supported. + // GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, PATCH methods are supported. + // Request body payloads are supported for POST, PUT, PATCH, and OPTIONS methods only. // CONNECT method is disallowed because it is not appropriate for health check request. // If a non-200 response is expected by the method, it needs to be set in :ref:`expected_statuses `. RequestMethod method = 13 [(validate.rules).enum = {defined_only: true not_in: 6}]; diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto index edab4cd79c6..74fe641fe3a 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/core/v3/protocol.proto @@ -77,7 +77,7 @@ message QuicProtocolOptions { [(validate.rules).uint32 = {lte: 16777216 gte: 1}]; // Similar to ``initial_stream_window_size``, but for connection-level - // flow-control. Valid values rage from 1 to 25165824 (24MB, maximum supported by QUICHE) and defaults + // flow-control. Valid values range from 1 to 25165824 (24MB, maximum supported by QUICHE) and defaults // to 25165824 (24 * 1024 * 1024). // // .. note:: @@ -111,10 +111,9 @@ message QuicProtocolOptions { // default 600s will be applied. // For internal corporate network, a long timeout is often fine. // But for client facing network, 30s is usually a good choice. - google.protobuf.Duration idle_network_timeout = 8 [(validate.rules).duration = { - lte {seconds: 600} - gte {seconds: 1} - }]; + // Do not add an upper bound here. A long idle timeout is useful for maintaining warm connections at non-front-line proxy for low QPS services." + google.protobuf.Duration idle_network_timeout = 8 + [(validate.rules).duration = {gte {seconds: 1}}]; // Maximum packet length for QUIC connections. It refers to the largest size of a QUIC packet that can be transmitted over the connection. // If not specified, one of the `default values in QUICHE `_ is used. @@ -276,7 +275,7 @@ message HttpProtocolOptions { // The default value for responses can be overridden by setting runtime key ``envoy.reloadable_features.max_response_headers_count``. // Downstream requests that exceed this limit will receive a 431 response for HTTP/1.x and cause a stream // reset for HTTP/2. - // Upstream responses that exceed this limit will result in a 503 response. + // Upstream responses that exceed this limit will result in a 502 response. google.protobuf.UInt32Value max_headers_count = 2 [(validate.rules).uint32 = {gte: 1}]; // The maximum size of response headers. @@ -420,7 +419,7 @@ message Http1ProtocolOptions { // envoy.reloadable_features.http1_use_balsa_parser. // See issue #21245. google.protobuf.BoolValue use_balsa_parser = 9 - [(xds.annotations.v3.field_status).work_in_progress = true]; + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; // [#not-implemented-hide:] Hiding so that field can be removed. // If true, and BalsaParser is used (either `use_balsa_parser` above is true, @@ -503,7 +502,7 @@ message Http2ProtocolOptions { // `Maximum concurrent streams `_ // allowed for peer on one HTTP/2 connection. Valid values range from 1 to 2147483647 (2^31 - 1) - // and defaults to 2147483647. + // and defaults to 1024 for safety and should be sufficient for most use cases. // // For upstream connections, this also limits how many streams Envoy will initiate concurrently // on a single connection. If the limit is reached, Envoy may queue requests or establish @@ -517,8 +516,8 @@ message Http2ProtocolOptions { // `Initial stream-level flow-control window // `_ size. Valid values range from 65535 - // (2^16 - 1, HTTP/2 default) to 2147483647 (2^31 - 1, HTTP/2 maximum) and defaults to 268435456 - // (256 * 1024 * 1024). + // (2^16 - 1, HTTP/2 default) to 2147483647 (2^31 - 1, HTTP/2 maximum) and defaults to + // 16MiB (16 * 1024 * 1024). // // .. note:: // @@ -532,7 +531,7 @@ message Http2ProtocolOptions { [(validate.rules).uint32 = {lte: 2147483647 gte: 65535}]; // Similar to ``initial_stream_window_size``, but for connection-level flow-control - // window. Currently, this has the same minimum/maximum/default as ``initial_stream_window_size``. + // window. The default is 24MiB (24 * 1024 * 1024). google.protobuf.UInt32Value initial_connection_window_size = 4 [(validate.rules).uint32 = {lte: 2147483647 gte: 65535}]; @@ -674,7 +673,7 @@ message GrpcProtocolOptions { } // A message which allows using HTTP/3. -// [#next-free-field: 8] +// [#next-free-field: 9] message Http3ProtocolOptions { QuicProtocolOptions quic_protocol_options = 1; @@ -709,6 +708,10 @@ message Http3ProtocolOptions { // No huffman encoding, zero dynamic table capacity and no cookie crumbing. // This can be useful for trading off CPU vs bandwidth when an upstream HTTP/3 connection multiplexes multiple downstream connections. bool disable_qpack = 7; + + // Disables connection level flow control for HTTP/3 streams. This is useful in situations where the streams share the same connection + // but originate from different end-clients, so that each stream can make progress independently at non-front-line proxies. + bool disable_connection_flow_control_for_streams = 8; } // A message to control transformations to the :scheme header diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/endpoint.proto b/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/endpoint.proto index 894f68310a4..a149f6095c1 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/endpoint.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/endpoint.proto @@ -113,8 +113,9 @@ message ClusterLoadAssignment { // to determine the health of the priority level, or in other words assume each host has a weight of 1 for // this calculation. // - // Note: this is not currently implemented for - // :ref:`locality weighted load balancing `. + // .. note:: + // This is not currently implemented for + // :ref:`locality weighted load balancing `. bool weighted_priority_health = 6; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/load_report.proto b/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/load_report.proto index 32bbfe2d3f6..6d12765cef5 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/load_report.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/endpoint/v3/load_report.proto @@ -38,7 +38,8 @@ message UpstreamLocalityStats { // locality. uint64 total_successful_requests = 2; - // The total number of unfinished requests + // The total number of unfinished requests. A request can be an HTTP request + // or a TCP connection for a TCP connection pool. uint64 total_requests_in_progress = 3; // The total number of requests that failed due to errors at the endpoint, @@ -47,7 +48,8 @@ message UpstreamLocalityStats { // The total number of requests that were issued by this Envoy since // the last report. This information is aggregated over all the - // upstream endpoints in the locality. + // upstream endpoints in the locality. A request can be an HTTP request + // or a TCP connection for a TCP connection pool. uint64 total_issued_requests = 8; // The total number of connections in an established state at the time of the diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/listener_components.proto b/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/listener_components.proto index 33eb349fd06..cfa30afbb68 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/listener_components.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/listener/v3/listener_components.proto @@ -233,7 +233,7 @@ message FilterChain { google.protobuf.BoolValue use_proxy_proto = 4 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; - // [#not-implemented-hide:] filter chain metadata. + // Filter chain metadata. core.v3.Metadata metadata = 5; // Optional custom transport socket implementation to use for downstream connections. diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/metrics/v3/stats.proto b/xds/third_party/envoy/src/main/proto/envoy/config/metrics/v3/stats.proto index e7d7f80d648..02bb23aec9d 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/metrics/v3/stats.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/metrics/v3/stats.proto @@ -298,10 +298,12 @@ message HistogramBucketSettings { // Each value is the upper bound of a bucket. Each bucket must be greater than 0 and unique. // The order of the buckets does not matter. repeated double buckets = 2 [(validate.rules).repeated = { - min_items: 1 unique: true items {double {gt: 0.0}} }]; + + // Initial number of bins for the ``circllhist`` thread local histogram per time series. Default value is 100. + google.protobuf.UInt32Value bins = 3 [(validate.rules).uint32 = {lte: 46082 gt: 0}]; } // Stats configuration proto schema for built-in ``envoy.stat_sinks.statsd`` sink. This sink does not support diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/overload/v3/overload.proto b/xds/third_party/envoy/src/main/proto/envoy/config/overload/v3/overload.proto index 1f267c1863d..b5bc2c4d830 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/overload/v3/overload.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/overload/v3/overload.proto @@ -109,6 +109,13 @@ message ScaleTimersOverloadActionConfig { // :ref:`HttpConnectionManager.common_http_protocol_options.max_connection_duration // `. HTTP_DOWNSTREAM_CONNECTION_MAX = 4; + + // Adjusts the timeout for the downstream codec to flush an ended stream. + // This affects the value of :ref:`RouteAction.flush_timeout + // ` and + // :ref:`HttpConnectionManager.stream_flush_timeout + // ` + HTTP_DOWNSTREAM_STREAM_FLUSH = 5; } message ScaleTimer { @@ -134,9 +141,16 @@ message OverloadAction { option (udpa.annotations.versioning).previous_message_type = "envoy.config.overload.v2alpha.OverloadAction"; - // The name of the overload action. This is just a well-known string that listeners can - // use for registering callbacks. Custom overload actions should be named using reverse - // DNS to ensure uniqueness. + // The name of the overload action. This is just a well-known string that + // listeners can use for registering callbacks. + // Valid known overload actions include: + // - envoy.overload_actions.stop_accepting_requests + // - envoy.overload_actions.disable_http_keepalive + // - envoy.overload_actions.stop_accepting_connections + // - envoy.overload_actions.reject_incoming_connections + // - envoy.overload_actions.shrink_heap + // - envoy.overload_actions.reduce_timeouts + // - envoy.overload_actions.reset_high_memory_stream string name = 1 [(validate.rules).string = {min_len: 1}]; // A set of triggers for this action. The state of the action is the maximum @@ -148,7 +162,7 @@ message OverloadAction { // in this list. repeated Trigger triggers = 2 [(validate.rules).repeated = {min_items: 1}]; - // Configuration for the action being instantiated. + // Configuration for the action being instantiated if applicable. google.protobuf.Any typed_config = 3; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/route/v3/route_components.proto b/xds/third_party/envoy/src/main/proto/envoy/config/route/v3/route_components.proto index 292e5b93558..6837ade69d8 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/route/v3/route_components.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/route/v3/route_components.proto @@ -41,7 +41,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // host header. This allows a single listener to service multiple top level domain path trees. Once // a virtual host is selected based on the domain, the routes are processed in order to see which // upstream cluster to route to or whether to perform a redirect. -// [#next-free-field: 25] +// [#next-free-field: 26] message VirtualHost { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.VirtualHost"; @@ -205,10 +205,37 @@ message VirtualHost { // request header in retries initiated by per try timeouts. bool include_is_timeout_retry_header = 23; - // The maximum bytes which will be buffered for retries and shadowing. - // If set and a route-specific limit is not set, the bytes actually buffered will be the minimum - // value of this and the listener per_connection_buffer_limit_bytes. - google.protobuf.UInt32Value per_request_buffer_limit_bytes = 18; + // The maximum bytes which will be buffered for retries and shadowing. If set, the bytes actually buffered will be + // the minimum value of this and the listener ``per_connection_buffer_limit_bytes``. + // + // .. attention:: + // + // This field has been deprecated. Please use :ref:`request_body_buffer_limit + // ` instead. + // Only one of ``per_request_buffer_limit_bytes`` and ``request_body_buffer_limit`` could be set. + google.protobuf.UInt32Value per_request_buffer_limit_bytes = 18 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; + + // The maximum bytes which will be buffered for request bodies to support large request body + // buffering beyond the ``per_connection_buffer_limit_bytes``. + // + // This limit is specifically for the request body buffering and allows buffering larger payloads while maintaining + // flow control. + // + // Buffer limit precedence (from highest to lowest priority): + // + // 1. If ``request_body_buffer_limit`` is set, then ``request_body_buffer_limit`` will be used. + // 2. If :ref:`per_request_buffer_limit_bytes ` + // is set but ``request_body_buffer_limit`` is not, then ``min(per_request_buffer_limit_bytes, per_connection_buffer_limit_bytes)`` + // will be used. + // 3. If neither is set, then ``per_connection_buffer_limit_bytes`` will be used. + // + // For flow control chunk sizes, ``min(per_connection_buffer_limit_bytes, 16KB)`` will be used. + // + // Only one of :ref:`per_request_buffer_limit_bytes ` + // and ``request_body_buffer_limit`` could be set. + google.protobuf.UInt64Value request_body_buffer_limit = 25 + [(validate.rules).message = {required: false}]; // Specify a set of default request mirroring policies for every route under this virtual host. // It takes precedence over the route config mirror policy entirely. @@ -244,7 +271,7 @@ message RouteList { // // Envoy supports routing on HTTP method via :ref:`header matching // `. -// [#next-free-field: 20] +// [#next-free-field: 21] message Route { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.Route"; @@ -341,7 +368,14 @@ message Route { // The maximum bytes which will be buffered for retries and shadowing. // If set, the bytes actually buffered will be the minimum value of this and the // listener per_connection_buffer_limit_bytes. - google.protobuf.UInt32Value per_request_buffer_limit_bytes = 16; + // + // .. attention:: + // + // This field has been deprecated. Please use :ref:`request_body_buffer_limit + // ` instead. + // Only one of ``per_request_buffer_limit_bytes`` and ``request_body_buffer_limit`` may be set. + google.protobuf.UInt32Value per_request_buffer_limit_bytes = 16 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; // The human readable prefix to use when emitting statistics for this endpoint. // The statistics are rooted at vhost..route.. @@ -357,6 +391,25 @@ message Route { // every application endpoint. This is both not easily maintainable and // statistics use a non-trivial amount of memory(approximately 1KiB per route). string stat_prefix = 19; + + // The maximum bytes which will be buffered for request bodies to support large request body + // buffering beyond the ``per_connection_buffer_limit_bytes``. + // + // This limit is specifically for the request body buffering and allows buffering larger payloads while maintaining + // flow control. + // + // Buffer limit precedence (from highest to lowest priority): + // + // 1. If ``request_body_buffer_limit`` is set: use ``request_body_buffer_limit`` + // 2. If :ref:`per_request_buffer_limit_bytes ` + // is set but ``request_body_buffer_limit`` is not: use ``min(per_request_buffer_limit_bytes, per_connection_buffer_limit_bytes)`` + // 3. If neither is set: use ``per_connection_buffer_limit_bytes`` + // + // For flow control chunk sizes, use ``min(per_connection_buffer_limit_bytes, 16KB)``. + // + // Only one of :ref:`per_request_buffer_limit_bytes ` + // and ``request_body_buffer_limit`` may be set. + google.protobuf.UInt64Value request_body_buffer_limit = 20; } // Compared to the :ref:`cluster ` field that specifies a @@ -365,6 +418,7 @@ message Route { // multiple upstream clusters along with weights that indicate the percentage of // traffic to be forwarded to each cluster. The router selects an upstream cluster based on the // weights. +// [#next-free-field: 6] message WeightedCluster { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.WeightedCluster"; @@ -495,6 +549,10 @@ message WeightedCluster { // the process for the consistency. And the value is a unsigned number between 0 and UINT64_MAX. string header_name = 4 [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME strict: false}]; + + // When set to true, the hash policies will be used to generate the random value for weighted cluster selection. + // This could ensure consistent cluster picking across multiple proxy levels for weighted traffic. + google.protobuf.BoolValue use_hash_policy = 5; } } @@ -740,7 +798,7 @@ message CorsPolicy { google.protobuf.BoolValue forward_not_matching_preflights = 13; } -// [#next-free-field: 42] +// [#next-free-field: 43] message RouteAction { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.RouteAction"; @@ -1265,8 +1323,28 @@ message RouteAction { // If the :ref:`overload action ` "envoy.overload_actions.reduce_timeouts" // is configured, this timeout is scaled according to the value for // :ref:`HTTP_DOWNSTREAM_STREAM_IDLE `. + // + // This timeout may also be used in place of ``flush_timeout`` in very specific cases. See the + // documentation for ``flush_timeout`` for more details. google.protobuf.Duration idle_timeout = 24; + // Specifies the codec stream flush timeout for the route. + // + // If not specified, the first preference is the global :ref:`stream_flush_timeout + // `, + // but only if explicitly configured. + // + // If neither the explicit HCM-wide flush timeout nor this route-specific flush timeout is configured, + // the route's stream idle timeout is reused for this timeout. This is for + // backwards compatibility since both behaviors were historically controlled by the one timeout. + // + // If the route also does not have an idle timeout configured, the global :ref:`stream_idle_timeout + // `. used, again + // for backwards compatibility. That timeout defaults to 5 minutes. + // + // A value of 0 via any of the above paths will completely disable the timeout for a given route. + google.protobuf.Duration flush_timeout = 42; + // Specifies how to send request over TLS early data. // If absent, allows `safe HTTP requests `_ to be sent on early data. // [#extension-category: envoy.route.early_data_policy] diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/opentelemetry.proto b/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/opentelemetry.proto index 59028326f22..5260d9bd6af 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/opentelemetry.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/opentelemetry.proto @@ -6,6 +6,8 @@ import "envoy/config/core/v3/extension.proto"; import "envoy/config/core/v3/grpc_service.proto"; import "envoy/config/core/v3/http_service.proto"; +import "google/protobuf/wrappers.proto"; + import "udpa/annotations/migrate.proto"; import "udpa/annotations/status.proto"; @@ -19,7 +21,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Configuration for the OpenTelemetry tracer. // [#extension: envoy.tracers.opentelemetry] -// [#next-free-field: 6] +// [#next-free-field: 7] message OpenTelemetryConfig { // The upstream gRPC cluster that will receive OTLP traces. // Note that the tracer drops traces if the server does not read data fast enough. @@ -57,4 +59,9 @@ message OpenTelemetryConfig { // See: `OpenTelemetry sampler specification `_ // [#extension-category: envoy.tracers.opentelemetry.samplers] core.v3.TypedExtensionConfig sampler = 5; + + // Envoy caches the span in memory when the OpenTelemetry backend service is temporarily unavailable. + // This field specifies the maximum number of spans that can be cached. If not specified, the + // default is 1024. + google.protobuf.UInt32Value max_cache_size = 6; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/zipkin.proto b/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/zipkin.proto index 2d8f3195c31..7405c596ed5 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/zipkin.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/config/trace/v3/zipkin.proto @@ -2,13 +2,14 @@ syntax = "proto3"; package envoy.config.trace.v3; +import "envoy/config/core/v3/http_service.proto"; + import "google/protobuf/wrappers.proto"; import "envoy/annotations/deprecation.proto"; import "udpa/annotations/migrate.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; -import "validate/validate.proto"; option java_package = "io.envoyproxy.envoy.config.trace.v3"; option java_outer_classname = "ZipkinProto"; @@ -21,10 +22,22 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Configuration for the Zipkin tracer. // [#extension: envoy.tracers.zipkin] -// [#next-free-field: 8] +// [#next-free-field: 10] message ZipkinConfig { option (udpa.annotations.versioning).previous_message_type = "envoy.config.trace.v2.ZipkinConfig"; + // Available trace context options for handling different trace header formats. + enum TraceContextOption { + // Use B3 headers only (default behavior). + USE_B3 = 0; + + // Enable B3 and W3C dual header support: + // - For downstream: Extract from B3 headers first, fallback to W3C traceparent if B3 is unavailable. + // - For upstream: Inject both B3 and W3C traceparent headers. + // When this option is NOT set, only B3 headers are used for both extraction and injection. + USE_B3_WITH_W3C_PROPAGATION = 1; + } + // Available Zipkin collector endpoint versions. enum CollectorEndpointVersion { // Zipkin API v1, JSON over HTTP. @@ -48,11 +61,17 @@ message ZipkinConfig { } // The cluster manager cluster that hosts the Zipkin collectors. - string collector_cluster = 1 [(validate.rules).string = {min_len: 1}]; + // Note: This field will be deprecated in future releases in favor of + // :ref:`collector_service `. + // Either this field or collector_service must be specified. + string collector_cluster = 1; // The API endpoint of the Zipkin service where the spans will be sent. When // using a standard Zipkin installation. - string collector_endpoint = 2 [(validate.rules).string = {min_len: 1}]; + // Note: This field will be deprecated in future releases in favor of + // :ref:`collector_service `. + // Required when using collector_cluster. + string collector_endpoint = 2; // Determines whether a 128bit trace id will be used when creating a new // trace instance. The default value is false, which will result in a 64 bit trace id being used. @@ -67,6 +86,8 @@ message ZipkinConfig { // Optional hostname to use when sending spans to the collector_cluster. Useful for collectors // that require a specific hostname. Defaults to :ref:`collector_cluster ` above. + // Note: This field will be deprecated in future releases in favor of + // :ref:`collector_service `. string collector_hostname = 6; // If this is set to true, then Envoy will be treated as an independent hop in trace chain. A complete span pair will be created for a single @@ -88,4 +109,60 @@ message ZipkinConfig { // Please use that ``spawn_upstream_span`` field to control the span creation. bool split_spans_for_request = 7 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; + + // Determines which trace context format to use for trace header extraction and propagation. + // This controls both downstream request header extraction and upstream request header injection. + // Here is the spec for W3C trace headers: https://www.w3.org/TR/trace-context/ + // The default value is USE_B3 to maintain backward compatibility. + TraceContextOption trace_context_option = 8; + + // HTTP service configuration for the Zipkin collector. + // When specified, this configuration takes precedence over the legacy fields: + // collector_cluster, collector_endpoint, and collector_hostname. + // This provides a complete HTTP service configuration including cluster, URI, timeout, and headers. + // If not specified, the legacy fields above will be used for backward compatibility. + // + // Required fields when using collector_service: + // + // * ``http_uri.cluster`` - Must be specified and non-empty + // * ``http_uri.uri`` - Must be specified and non-empty + // * ``http_uri.timeout`` - Optional + // + // Full URI Support with Automatic Parsing: + // + // The ``uri`` field supports both path-only and full URI formats: + // + // .. code-block:: yaml + // + // tracing: + // provider: + // name: envoy.tracers.zipkin + // typed_config: + // "@type": type.googleapis.com/envoy.config.trace.v3.ZipkinConfig + // collector_service: + // http_uri: + // # Full URI format - hostname and path are extracted automatically + // uri: "https://zipkin-collector.example.com/api/v2/spans" + // cluster: zipkin + // timeout: 5s + // request_headers_to_add: + // - header: + // key: "X-Custom-Token" + // value: "your-custom-token" + // - header: + // key: "X-Service-ID" + // value: "your-service-id" + // + // URI Parsing Behavior: + // + // * Full URI: ``"https://zipkin-collector.example.com/api/v2/spans"`` + // + // * Hostname: ``zipkin-collector.example.com`` (sets HTTP ``Host`` header) + // * Path: ``/api/v2/spans`` (sets HTTP request path) + // + // * Path only: ``"/api/v2/spans"`` + // + // * Hostname: Uses cluster name as fallback + // * Path: ``/api/v2/spans`` + core.v3.HttpService collector_service = 9; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto new file mode 100644 index 00000000000..7cf2aac64aa --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto @@ -0,0 +1,529 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.ext_authz.v3; + +import "envoy/config/common/mutation_rules/v3/mutation_rules.proto"; +import "envoy/config/core/v3/base.proto"; +import "envoy/config/core/v3/config_source.proto"; +import "envoy/config/core/v3/grpc_service.proto"; +import "envoy/config/core/v3/http_uri.proto"; +import "envoy/type/matcher/v3/metadata.proto"; +import "envoy/type/matcher/v3/string.proto"; +import "envoy/type/v3/http_status.proto"; + +import "google/protobuf/struct.proto"; +import "google/protobuf/wrappers.proto"; + +import "envoy/annotations/deprecation.proto"; +import "udpa/annotations/sensitive.proto"; +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3"; +option java_outer_classname = "ExtAuthzProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3;ext_authzv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: External Authorization] +// External Authorization :ref:`configuration overview `. +// [#extension: envoy.filters.http.ext_authz] + +// [#next-free-field: 30] +message ExtAuthz { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.ext_authz.v3.ExtAuthz"; + + reserved 4; + + reserved "use_alpha"; + + // External authorization service configuration. + oneof services { + // gRPC service configuration (default timeout: 200ms). + config.core.v3.GrpcService grpc_service = 1; + + // HTTP service configuration (default timeout: 200ms). + HttpService http_service = 3; + } + + // API version for ext_authz transport protocol. This describes the ext_authz gRPC endpoint and + // version of messages used on the wire. + config.core.v3.ApiVersion transport_api_version = 12 + [(validate.rules).enum = {defined_only: true}]; + + // Changes the filter's behavior on errors: + // + // #. When set to ``true``, the filter will ``accept`` the client request even if communication with + // the authorization service has failed, or if the authorization service has returned an HTTP 5xx + // error. + // + // #. When set to ``false``, the filter will ``reject`` client requests and return ``Forbidden`` + // if communication with the authorization service has failed, or if the authorization service + // has returned an HTTP 5xx error. + // + // Errors can always be tracked in the :ref:`stats `. + bool failure_mode_allow = 2; + + // When ``failure_mode_allow`` and ``failure_mode_allow_header_add`` are both set to ``true``, + // ``x-envoy-auth-failure-mode-allowed: true`` will be added to request headers if the communication + // with the authorization service has failed, or if the authorization service has returned a + // HTTP 5xx error. + bool failure_mode_allow_header_add = 19; + + // Enables the filter to buffer the client request body and send it within the authorization request. + // The ``x-envoy-auth-partial-body: false|true`` metadata header will be added to the authorization + // request indicating whether the body data is partial. + BufferSettings with_request_body = 5; + + // Clears the route cache in order to allow the external authorization service to correctly affect + // routing decisions. The filter clears all cached routes when: + // + // #. The field is set to ``true``. + // + // #. The status returned from the authorization service is an HTTP 200 or gRPC 0. + // + // #. At least one ``authorization response header`` is added to the client request, or is used to + // alter another client request header. + // + bool clear_route_cache = 6; + + // Sets the HTTP status that is returned to the client when the authorization server returns an error + // or cannot be reached. The default status is HTTP 403 Forbidden. + type.v3.HttpStatus status_on_error = 7; + + // When this is set to ``true``, the filter will check the :ref:`ext_authz response + // ` for invalid header and + // query parameter mutations. If the side stream response is invalid, it will send a local reply + // to the downstream request with status HTTP 500 Internal Server Error. + // + // .. note:: + // Both ``headers_to_remove`` and ``query_parameters_to_remove`` are validated, but invalid elements in + // those fields should not affect any headers and thus will not cause the filter to send a local reply. + // + // When set to ``false``, any invalid mutations will be visible to the rest of Envoy and may cause + // unexpected behavior. + // + // If you are using ext_authz with an untrusted ext_authz server, you should set this to ``true``. + bool validate_mutations = 24; + + // Specifies a list of metadata namespaces whose values, if present, will be passed to the + // ext_authz service. The :ref:`filter_metadata ` + // is passed as an opaque ``protobuf::Struct``. + // + // .. note:: + // This field applies exclusively to the gRPC ext_authz service and has no effect on the HTTP service. + // + // For example, if the ``jwt_authn`` filter is used and :ref:`payload_in_metadata + // ` is set, + // then the following will pass the jwt payload to the authorization server. + // + // .. code-block:: yaml + // + // metadata_context_namespaces: + // - envoy.filters.http.jwt_authn + // + repeated string metadata_context_namespaces = 8; + + // Specifies a list of metadata namespaces whose values, if present, will be passed to the + // ext_authz service. :ref:`typed_filter_metadata ` + // is passed as a ``protobuf::Any``. + // + // .. note:: + // This field applies exclusively to the gRPC ext_authz service and has no effect on the HTTP service. + // + // This works similarly to ``metadata_context_namespaces`` but allows Envoy and the ext_authz server to share + // the protobuf message definition in order to perform safe parsing. + // + repeated string typed_metadata_context_namespaces = 16; + + // Specifies a list of route metadata namespaces whose values, if present, will be passed to the + // ext_authz service at :ref:`route_metadata_context ` in + // :ref:`CheckRequest `. + // :ref:`filter_metadata ` is passed as an opaque ``protobuf::Struct``. + repeated string route_metadata_context_namespaces = 21; + + // Specifies a list of route metadata namespaces whose values, if present, will be passed to the + // ext_authz service at :ref:`route_metadata_context ` in + // :ref:`CheckRequest `. + // :ref:`typed_filter_metadata ` is passed as a ``protobuf::Any``. + repeated string route_typed_metadata_context_namespaces = 22; + + // Specifies if the filter is enabled. + // + // If :ref:`runtime_key ` is specified, + // Envoy will lookup the runtime key to get the percentage of requests to filter. + // + // If this field is not specified, the filter will be enabled for all requests. + config.core.v3.RuntimeFractionalPercent filter_enabled = 9; + + // Specifies if the filter is enabled with metadata matcher. + // If this field is not specified, the filter will be enabled for all requests. + type.matcher.v3.MetadataMatcher filter_enabled_metadata = 14; + + // Specifies whether to deny the requests when the filter is disabled. + // If :ref:`runtime_key ` is specified, + // Envoy will lookup the runtime key to determine whether to deny requests for filter-protected paths + // when the filter is disabled. If the filter is disabled in ``typed_per_filter_config`` for the path, + // requests will not be denied. + // + // If this field is not specified, all requests will be allowed when disabled. + // + // If a request is denied due to this setting, the response code in :ref:`status_on_error + // ` will + // be returned. + config.core.v3.RuntimeFeatureFlag deny_at_disable = 11; + + // Specifies if the peer certificate is sent to the external service. + // + // When this field is ``true``, Envoy will include the peer X.509 certificate, if available, in the + // :ref:`certificate`. + bool include_peer_certificate = 10; + + // Optional additional prefix to use when emitting statistics. This allows distinguishing + // emitted statistics between configured ``ext_authz`` filters in an HTTP filter chain. For example: + // + // .. code-block:: yaml + // + // http_filters: + // - name: envoy.filters.http.ext_authz + // typed_config: + // "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz + // stat_prefix: waf # This emits ext_authz.waf.ok, ext_authz.waf.denied, etc. + // - name: envoy.filters.http.ext_authz + // typed_config: + // "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz + // stat_prefix: blocker # This emits ext_authz.blocker.ok, ext_authz.blocker.denied, etc. + // + string stat_prefix = 13; + + // Optional labels that will be passed to :ref:`labels` in + // :ref:`destination`. + // The labels will be read from :ref:`metadata` with the specified key. + string bootstrap_metadata_labels_key = 15; + + // Check request to authorization server will include the client request headers that have a correspondent match + // in the :ref:`list `. If this option isn't specified, then + // all client request headers are included in the check request to a gRPC authorization server, whereas no client request headers + // (besides the ones allowed by default - see note below) are included in the check request to an HTTP authorization server. + // This inconsistency between gRPC and HTTP servers is to maintain backwards compatibility with legacy behavior. + // + // .. note:: + // + // For requests to an HTTP authorization server: in addition to the user's supplied matchers, ``Host``, ``Method``, ``Path``, + // ``Content-Length``, and ``Authorization`` are **additionally included** in the list. + // + // .. note:: + // + // For requests to an HTTP authorization server: the value of ``Content-Length`` will be set to ``0`` and the request to the + // authorization server will not have a message body. However, the check request can include the buffered + // client request body (controlled by :ref:`with_request_body + // ` setting); + // consequently, the value of ``Content-Length`` in the authorization request reflects the size of its payload. + // + // .. note:: + // + // This can be overridden by the field ``disallowed_headers`` below. That is, if a header + // matches for both ``allowed_headers`` and ``disallowed_headers``, the header will NOT be sent. + type.matcher.v3.ListStringMatcher allowed_headers = 17; + + // If set, specifically disallow any header in this list to be forwarded to the external + // authentication server. This overrides the above ``allowed_headers`` if a header matches both. + type.matcher.v3.ListStringMatcher disallowed_headers = 25; + + // Specifies if the TLS session level details like SNI are sent to the external service. + // + // When this field is ``true``, Envoy will include the SNI name used for TLSClientHello, if available, in the + // :ref:`tls_session`. + bool include_tls_session = 18; + + // Whether to increment cluster statistics (e.g. cluster..upstream_rq_*) on authorization failure. + // Defaults to ``true``. + google.protobuf.BoolValue charge_cluster_response_stats = 20; + + // Whether to encode the raw headers (i.e., unsanitized values and unconcatenated multi-line headers) + // in the authorization request. Works with both HTTP and gRPC clients. + // + // When this is set to ``true``, header values are not sanitized. Headers with the same key will also + // not be combined into a single, comma-separated header. + // Requests to gRPC services will populate the field + // :ref:`header_map`. + // Requests to HTTP services will be constructed with the unsanitized header values and preserved + // multi-line headers with the same key. + // + // If this field is set to ``false``, header values will be sanitized, with any non-UTF-8-compliant + // bytes replaced with ``'!'``. Headers with the same key will have their values concatenated into a + // single comma-separated header value. + // Requests to gRPC services will populate the field + // :ref:`headers`. + // Requests to HTTP services will have their header values sanitized and will not preserve + // multi-line headers with the same key. + // + // It is recommended to set this to ``true`` unless you rely on the previous behavior. + // + // It is set to ``false`` by default for backwards compatibility. + bool encode_raw_headers = 23; + + // Rules for what modifications an ext_authz server may make to the request headers before + // continuing decoding / forwarding upstream. + // + // If set to anything, enables header mutation checking against configured rules. Note that + // :ref:`HeaderMutationRules ` + // has defaults that change ext_authz behavior. Also note that if this field is set to anything, + // ext_authz can no longer append to :-prefixed headers. + // + // If empty, header mutation rule checking is completely disabled. + // + // Regardless of what is configured here, ext_authz cannot remove :-prefixed headers. + // + // This field and ``validate_mutations`` have different use cases. ``validate_mutations`` enables + // correctness checks for all header / query parameter mutations (e.g. for invalid characters). + // This field allows the filter to reject mutations to specific headers. + config.common.mutation_rules.v3.HeaderMutationRules decoder_header_mutation_rules = 26; + + // Enable or disable ingestion of dynamic metadata from the ext_authz service. + // + // If ``false``, the filter will ignore dynamic metadata injected by the ext_authz service. If the + // ext_authz service tries injecting dynamic metadata, the filter will log, increment the + // ``ignored_dynamic_metadata`` stat, then continue handling the response. + // + // If ``true``, the filter will ingest dynamic metadata entries as normal. + // + // If unset, defaults to ``true``. + google.protobuf.BoolValue enable_dynamic_metadata_ingestion = 27; + + // Additional metadata to be added to the filter state for logging purposes. The metadata will be + // added to StreamInfo's filter state under the namespace corresponding to the ext_authz filter + // name. + google.protobuf.Struct filter_metadata = 28; + + // When set to ``true``, the filter will emit per-stream stats for access logging. The filter state + // key will be the same as the filter name. + // + // If using Envoy gRPC, emits latency, bytes sent / received, upstream info, and upstream cluster + // info. If not using Envoy gRPC, emits only latency. Note that stats are ONLY added to filter + // state if a check request is actually made to an ext_authz service. + // + // If this is ``false`` the filter will not emit stats, but filter_metadata will still be respected if + // it has a value. + // + // Field ``latency_us`` is exposed for CEL and logging when using gRPC or HTTP service. + // Fields ``bytesSent`` and ``bytesReceived`` are exposed for CEL and logging only when using gRPC service. + bool emit_filter_state_stats = 29; +} + +// Configuration for buffering the request data. +message BufferSettings { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.ext_authz.v2.BufferSettings"; + + // Sets the maximum size of a message body that the filter will hold in memory. Envoy will return + // ``HTTP 413`` and will *not* initiate the authorization process when the buffer reaches the size + // set in this field. Note that this setting will have precedence over :ref:`failure_mode_allow + // `. + uint32 max_request_bytes = 1 [(validate.rules).uint32 = {gt: 0}]; + + // When this field is ``true``, Envoy will buffer the message until ``max_request_bytes`` is reached. + // The authorization request will be dispatched and no 413 HTTP error will be returned by the + // filter. + bool allow_partial_message = 2; + + // If ``true``, the body sent to the external authorization service is set as raw bytes and populates + // :ref:`raw_body` + // in the HTTP request attribute context. Otherwise, :ref:`body + // ` will be populated + // with a UTF-8 string request body. + // + // This field only affects configurations using a :ref:`grpc_service + // `. In configurations that use + // an :ref:`http_service `, this + // has no effect. + bool pack_as_bytes = 3; +} + +// HttpService is used for raw HTTP communication between the filter and the authorization service. +// When configured, the filter will parse the client request and use these attributes to call the +// authorization server. Depending on the response, the filter may reject or accept the client +// request. Note that in any of these events, metadata can be added, removed or overridden by the +// filter: +// +// On authorization request, a list of allowed request headers may be supplied. See +// :ref:`allowed_headers +// ` +// for details. Additional headers metadata may be added to the authorization request. See +// :ref:`headers_to_add +// ` for +// details. +// +// On authorization response status ``HTTP 200 OK``, the filter will allow traffic to the upstream and +// additional headers metadata may be added to the original client request. See +// :ref:`allowed_upstream_headers +// ` +// for details. Additionally, the filter may add additional headers to the client's response. See +// :ref:`allowed_client_headers_on_success +// ` +// for details. +// +// On other authorization response statuses, the filter will not allow traffic. Additional headers +// metadata as well as body may be added to the client's response. See :ref:`allowed_client_headers +// ` +// for details. +// [#next-free-field: 9] +message HttpService { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.ext_authz.v2.HttpService"; + + reserved 3, 4, 5, 6; + + // Sets the HTTP server URI which the authorization requests must be sent to. + config.core.v3.HttpUri server_uri = 1; + + // Sets a prefix to the value of authorization request header ``Path``. + string path_prefix = 2; + + // Settings used for controlling authorization request metadata. + AuthorizationRequest authorization_request = 7; + + // Settings used for controlling authorization response metadata. + AuthorizationResponse authorization_response = 8; +} + +message AuthorizationRequest { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.ext_authz.v2.AuthorizationRequest"; + + // Authorization request includes the client request headers that have a corresponding match + // in the :ref:`list `. + // This field has been deprecated in favor of :ref:`allowed_headers + // `. + // + // .. note:: + // + // In addition to the user's supplied matchers, ``Host``, ``Method``, ``Path``, + // ``Content-Length``, and ``Authorization`` are **automatically included** in the list. + // + // .. note:: + // + // By default, the ``Content-Length`` header is set to ``0`` and the request to the authorization + // service has no message body. However, the authorization request *may* include the buffered + // client request body (controlled by :ref:`with_request_body + // ` + // setting); hence the value of its ``Content-Length`` reflects the size of its payload. + // + type.matcher.v3.ListStringMatcher allowed_headers = 1 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; + + // Sets a list of headers that will be included in the request to the authorization service. Note that + // client request headers with the same key will be overridden. + repeated config.core.v3.HeaderValue headers_to_add = 2; +} + +// [#next-free-field: 6] +message AuthorizationResponse { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.ext_authz.v2.AuthorizationResponse"; + + // When this :ref:`list ` is set, authorization + // response headers that have a correspondent match will be added to the original client request. + // Note that coexistent headers will be overridden. + type.matcher.v3.ListStringMatcher allowed_upstream_headers = 1; + + // When this :ref:`list ` is set, authorization + // response headers that have a correspondent match will be added to the original client request. + // Note that coexistent headers will be appended. + type.matcher.v3.ListStringMatcher allowed_upstream_headers_to_append = 3; + + // When this :ref:`list ` is set, authorization + // response headers that have a correspondent match will be added to the client's response. Note + // that when this list is *not* set, all the authorization response headers, except ``Authority + // (Host)`` will be in the response to the client. When a header is included in this list, ``Path``, + // ``Status``, ``Content-Length``, ``WWWAuthenticate`` and ``Location`` are automatically added. + type.matcher.v3.ListStringMatcher allowed_client_headers = 2; + + // When this :ref:`list ` is set, authorization + // response headers that have a correspondent match will be added to the client's response when + // the authorization response itself is successful, i.e. not failed or denied. When this list is + // *not* set, no additional headers will be added to the client's response on success. + type.matcher.v3.ListStringMatcher allowed_client_headers_on_success = 4; + + // When this :ref:`list ` is set, authorization + // response headers that have a correspondent match will be emitted as dynamic metadata to be consumed + // by the next filter. This metadata lives in a namespace specified by the canonical name of extension filter + // that requires it: + // + // - :ref:`envoy.filters.http.ext_authz ` for HTTP filter. + // - :ref:`envoy.filters.network.ext_authz ` for network filter. + type.matcher.v3.ListStringMatcher dynamic_metadata_from_headers = 5; +} + +// Extra settings on a per virtualhost/route/weighted-cluster level. +message ExtAuthzPerRoute { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.ext_authz.v2.ExtAuthzPerRoute"; + + oneof override { + option (validate.required) = true; + + // Disable the ext auth filter for this particular vhost or route. + // If disabled is specified in multiple per-filter-configs, the most specific one will be used. + // If the filter is disabled by default and this is set to ``false``, the filter will be enabled + // for this vhost or route. + bool disabled = 1; + + // Check request settings for this route. + CheckSettings check_settings = 2 [(validate.rules).message = {required: true}]; + } +} + +// Extra settings for the check request. +// [#next-free-field: 6] +message CheckSettings { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.ext_authz.v2.CheckSettings"; + + // Context extensions to set on the CheckRequest's + // :ref:`AttributeContext.context_extensions` + // + // You can use this to provide extra context for the external authorization server on specific + // virtual hosts/routes. For example, adding a context extension on the virtual host level can + // give the ext-authz server information on what virtual host is used without needing to parse the + // host header. If CheckSettings is specified in multiple per-filter-configs, they will be merged + // in order, and the result will be used. + // + // Merge semantics for this field are such that keys from more specific configs override. + // + // .. note:: + // These settings are only applied to a filter configured with a + // :ref:`grpc_service`. + map context_extensions = 1 [(udpa.annotations.sensitive) = true]; + + // When set to ``true``, disable the configured :ref:`with_request_body + // ` for a specific route. + // + // Only one of ``disable_request_body_buffering`` and + // :ref:`with_request_body ` + // may be specified. + bool disable_request_body_buffering = 2; + + // Enable or override request body buffering, which is configured using the + // :ref:`with_request_body ` + // option for a specific route. + // + // Only one of ``with_request_body`` and + // :ref:`disable_request_body_buffering ` + // may be specified. + BufferSettings with_request_body = 3; + + // Override the external authorization service for this route. + // This allows different routes to use different external authorization service backends + // and service types (gRPC or HTTP). If specified, this overrides the filter-level service + // configuration regardless of the original service type. + oneof service_override { + // Override with a gRPC service configuration. + config.core.v3.GrpcService grpc_service = 4; + + // Override with an HTTP service configuration. + HttpService http_service = 5; + } +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index e0282af86e6..730e065e6c4 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -37,7 +37,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 59] +// [#next-free-field: 60] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"; @@ -527,16 +527,6 @@ message HttpConnectionManager { // is terminated with a 408 Request Timeout error code if no upstream response // header has been received, otherwise a stream reset occurs. // - // This timeout also specifies the amount of time that Envoy will wait for the peer to open enough - // window to write any remaining stream data once the entirety of stream data (local end stream is - // true) has been buffered pending available window. In other words, this timeout defends against - // a peer that does not release enough window to completely write the stream, even though all - // data has been proxied within available flow control windows. If the timeout is hit in this - // case, the :ref:`tx_flush_timeout ` counter will be - // incremented. Note that :ref:`max_stream_duration - // ` does not apply to - // this corner case. - // // If the :ref:`overload action ` "envoy.overload_actions.reduce_timeouts" // is configured, this timeout is scaled according to the value for // :ref:`HTTP_DOWNSTREAM_STREAM_IDLE `. @@ -549,9 +539,29 @@ message HttpConnectionManager { // // A value of 0 will completely disable the connection manager stream idle // timeout, although per-route idle timeout overrides will continue to apply. + // + // This timeout is also used as the default value for :ref:`stream_flush_timeout + // `. google.protobuf.Duration stream_idle_timeout = 24 [(udpa.annotations.security).configure_for_untrusted_downstream = true]; + // The stream flush timeout for connections managed by the connection manager. + // + // If not specified, the value of stream_idle_timeout is used. This is for backwards compatibility + // since this was the original behavior. In essence this timeout is an override for the + // stream_idle_timeout that applies specifically to the end of stream flush case. + // + // This timeout specifies the amount of time that Envoy will wait for the peer to open enough + // window to write any remaining stream data once the entirety of stream data (local end stream is + // true) has been buffered pending available window. In other words, this timeout defends against + // a peer that does not release enough window to completely write the stream, even though all + // data has been proxied within available flow control windows. If the timeout is hit in this + // case, the :ref:`tx_flush_timeout ` counter will be + // incremented. Note that :ref:`max_stream_duration + // ` does not apply to + // this corner case. + google.protobuf.Duration stream_flush_timeout = 59; + // The amount of time that Envoy will wait for the entire request to be received. // The timer is activated when the request is initiated, and is disarmed when the last byte of the // request is sent upstream (i.e. all decoding filters have processed the request), OR when the @@ -1036,7 +1046,7 @@ message Rds { "envoy.config.filter.network.http_connection_manager.v2.Rds"; // Configuration source specifier for RDS. - config.core.v3.ConfigSource config_source = 1 [(validate.rules).message = {required: true}]; + config.core.v3.ConfigSource config_source = 1; // The name of the route configuration. This name will be passed to the RDS // API. This allows an Envoy configuration with multiple HTTP listeners (and diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/call_credentials/access_token/v3/access_token_credentials.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/call_credentials/access_token/v3/access_token_credentials.proto new file mode 100644 index 00000000000..45ee3839e6f --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/call_credentials/access_token/v3/access_token_credentials.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package envoy.extensions.grpc_service.call_credentials.access_token.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.grpc_service.call_credentials.access_token.v3"; +option java_outer_classname = "AccessTokenCredentialsProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/call_credentials/access_token/v3;access_tokenv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: gRPC Access Token Credentials] + +// [#not-implemented-hide:] +message AccessTokenCredentials { + // The access token. + string token = 1; +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/google_default/v3/google_default_credentials.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/google_default/v3/google_default_credentials.proto new file mode 100644 index 00000000000..77c3af41fdd --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/google_default/v3/google_default_credentials.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package envoy.extensions.grpc_service.channel_credentials.google_default.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.google_default.v3"; +option java_outer_classname = "GoogleDefaultCredentialsProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/google_default/v3;google_defaultv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: gRPC Google Default Credentials] + +// [#not-implemented-hide:] +message GoogleDefaultCredentials { +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/insecure/v3/insecure_credentials.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/insecure/v3/insecure_credentials.proto new file mode 100644 index 00000000000..70d58451e2d --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/insecure/v3/insecure_credentials.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package envoy.extensions.grpc_service.channel_credentials.insecure.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.insecure.v3"; +option java_outer_classname = "InsecureCredentialsProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/insecure/v3;insecurev3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: gRPC Insecure Credentials] + +// [#not-implemented-hide:] +message InsecureCredentials { +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/local/v3/local_credentials.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/local/v3/local_credentials.proto new file mode 100644 index 00000000000..00514a0e847 --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/local/v3/local_credentials.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package envoy.extensions.grpc_service.channel_credentials.local.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.local.v3"; +option java_outer_classname = "LocalCredentialsProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/local/v3;localv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: gRPC Local Credentials] + +// [#not-implemented-hide:] +message LocalCredentials { +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/tls/v3/tls_credentials.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/tls/v3/tls_credentials.proto new file mode 100644 index 00000000000..f64c16bb684 --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/tls/v3/tls_credentials.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package envoy.extensions.grpc_service.channel_credentials.tls.v3; + +import "envoy/extensions/transport_sockets/tls/v3/tls.proto"; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.tls.v3"; +option java_outer_classname = "TlsCredentialsProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/tls/v3;tlsv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: gRPC TLS Credentials] + +// [#not-implemented-hide:] +message TlsCredentials { + // The certificate provider instance for the root cert. Must be set. + transport_sockets.tls.v3.CommonTlsContext.CertificateProviderInstance root_certificate_provider = + 1; + + // The certificate provider instance for the identity cert. Optional; + // if unset, no identity certificate will be sent to the server. + transport_sockets.tls.v3.CommonTlsContext.CertificateProviderInstance + identity_certificate_provider = 2; +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/xds/v3/xds_credentials.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/xds/v3/xds_credentials.proto new file mode 100644 index 00000000000..ba8d471dd49 --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/grpc_service/channel_credentials/xds/v3/xds_credentials.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package envoy.extensions.grpc_service.channel_credentials.xds.v3; + +import "google/protobuf/any.proto"; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.xds.v3"; +option java_outer_classname = "XdsCredentialsProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/xds/v3;xdsv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: gRPC xDS Credentials] + +// [#not-implemented-hide:] +message XdsCredentials { + // Fallback credentials. Required. + google.protobuf.Any fallback_credentials = 1; +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/common/v3/common.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/common/v3/common.proto index 7868fb02b1a..22faf11b9c5 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/common/v3/common.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/load_balancing_policies/common/v3/common.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package envoy.extensions.load_balancing_policies.common.v3; import "envoy/config/core/v3/base.proto"; +import "envoy/config/route/v3/route_components.proto"; import "envoy/type/v3/percent.proto"; import "google/protobuf/duration.proto"; @@ -23,8 +24,17 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; message LocalityLbConfig { // Configuration for :ref:`zone aware routing // `. - // [#next-free-field: 6] + // [#next-free-field: 7] message ZoneAwareLbConfig { + // Basis for computing per-locality percentages in zone-aware routing. + enum LocalityBasis { + // Use the number of healthy hosts in each locality. + HEALTHY_HOSTS_NUM = 0; + + // Use the weights of healthy hosts in each locality. + HEALTHY_HOSTS_WEIGHT = 1; + } + // Configures Envoy to always route requests to the local zone regardless of the // upstream zone structure. In Envoy's default configuration, traffic is distributed proportionally // across all upstream hosts while trying to maximize local routing when possible. The approach @@ -66,6 +76,12 @@ message LocalityLbConfig { [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; ForceLocalZone force_local_zone = 5; + + // Determines how locality percentages are computed: + // - HEALTHY_HOSTS_NUM: proportional to the count of healthy hosts. + // - HEALTHY_HOSTS_WEIGHT: proportional to the weights of healthy hosts. + // Default value is HEALTHY_HOSTS_NUM if unset. + LocalityBasis locality_basis = 6; } // Configuration for :ref:`locality weighted load balancing @@ -136,4 +152,10 @@ message ConsistentHashingLbConfig { // This is an O(N) algorithm, unlike other load balancers. Using a lower ``hash_balance_factor`` results in more hosts // being probed, so use a higher value if you require better performance. google.protobuf.UInt32Value hash_balance_factor = 2 [(validate.rules).uint32 = {gte: 100}]; + + // Specifies a list of hash policies to use for ring hash load balancing. If ``hash_policy`` is + // set, then + // :ref:`route level hash policy ` + // will be ignored. + repeated config.route.v3.RouteAction.HashPolicy hash_policy = 3; } diff --git a/xds/third_party/envoy/src/main/proto/envoy/service/auth/v3/attribute_context.proto b/xds/third_party/envoy/src/main/proto/envoy/service/auth/v3/attribute_context.proto new file mode 100644 index 00000000000..2c4fbb4b73e --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/service/auth/v3/attribute_context.proto @@ -0,0 +1,222 @@ +syntax = "proto3"; + +package envoy.service.auth.v3; + +import "envoy/config/core/v3/address.proto"; +import "envoy/config/core/v3/base.proto"; + +import "google/protobuf/timestamp.proto"; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.service.auth.v3"; +option java_outer_classname = "AttributeContextProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3;authv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Attribute context] + +// See :ref:`network filter configuration overview ` +// and :ref:`HTTP filter configuration overview `. + +// An attribute is a piece of metadata that describes an activity on a network. +// For example, the size of an HTTP request, or the status code of an HTTP response. +// +// Each attribute has a type and a name, which is logically defined as a proto message field +// of the ``AttributeContext``. The ``AttributeContext`` is a collection of individual attributes +// supported by Envoy authorization system. +// [#comment: The following items are left out of this proto +// Request.Auth field for JWTs +// Request.Api for api management +// Origin peer that originated the request +// Caching Protocol +// request_context return values to inject back into the filter chain +// peer.claims -- from X.509 extensions +// Configuration +// - field mask to send +// - which return values from request_context are copied back +// - which return values are copied into request_headers] +// [#next-free-field: 14] +message AttributeContext { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.auth.v2.AttributeContext"; + + // This message defines attributes for a node that handles a network request. + // The node can be either a service or an application that sends, forwards, + // or receives the request. Service peers should fill in the ``service``, + // ``principal``, and ``labels`` as appropriate. + // [#next-free-field: 6] + message Peer { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.auth.v2.AttributeContext.Peer"; + + // The address of the peer, this is typically the IP address. + // It can also be UDS path, or others. + config.core.v3.Address address = 1; + + // The canonical service name of the peer. + // It should be set to :ref:`the HTTP x-envoy-downstream-service-cluster + // ` + // If a more trusted source of the service name is available through mTLS/secure naming, it + // should be used. + string service = 2; + + // The labels associated with the peer. + // These could be pod labels for Kubernetes or tags for VMs. + // The source of the labels could be an X.509 certificate or other configuration. + map labels = 3; + + // The authenticated identity of this peer. + // For example, the identity associated with the workload such as a service account. + // If an X.509 certificate is used to assert the identity this field should be sourced from + // ``URI Subject Alternative Names``, ``DNS Subject Alternate Names`` or ``Subject`` in that order. + // The primary identity should be the principal. The principal format is issuer specific. + // + // Examples: + // + // - SPIFFE format is ``spiffe://trust-domain/path``. + // - Google account format is ``https://accounts.google.com/{userid}``. + string principal = 4; + + // The X.509 certificate used to authenticate the identify of this peer. + // When present, the certificate contents are encoded in URL and PEM format. + string certificate = 5; + } + + // Represents a network request, such as an HTTP request. + message Request { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.auth.v2.AttributeContext.Request"; + + // The timestamp when the proxy receives the first byte of the request. + google.protobuf.Timestamp time = 1; + + // Represents an HTTP request or an HTTP-like request. + HttpRequest http = 2; + } + + // This message defines attributes for an HTTP request. + // HTTP/1.x, HTTP/2, gRPC are all considered as HTTP requests. + // [#next-free-field: 14] + message HttpRequest { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.auth.v2.AttributeContext.HttpRequest"; + + // The unique ID for a request, which can be propagated to downstream + // systems. The ID should have low probability of collision + // within a single day for a specific service. + // For HTTP requests, it should be X-Request-ID or equivalent. + string id = 1; + + // The HTTP request method, such as ``GET``, ``POST``. + string method = 2; + + // The HTTP request headers. If multiple headers share the same key, they + // must be merged according to the HTTP spec. All header keys must be + // lower-cased, because HTTP header keys are case-insensitive. + // Header value is encoded as UTF-8 string. Non-UTF-8 characters will be replaced by "!". + // This field will not be set if + // :ref:`encode_raw_headers ` + // is set to true. + map headers = 3 + [(udpa.annotations.field_migrate).oneof_promotion = "headers_type"]; + + // A list of the raw HTTP request headers. This is used instead of + // :ref:`headers ` when + // :ref:`encode_raw_headers ` + // is set to true. + // + // Note that this is not actually a map type. ``header_map`` contains a single repeated field + // ``headers``. + // + // Here, only the ``key`` and ``raw_value`` fields will be populated for each HeaderValue, and + // that is only when + // :ref:`encode_raw_headers ` + // is set to true. + // + // Also, unlike the + // :ref:`headers ` + // field, headers with the same key are not combined into a single comma separated header. + config.core.v3.HeaderMap header_map = 13 + [(udpa.annotations.field_migrate).oneof_promotion = "headers_type"]; + + // The request target, as it appears in the first line of the HTTP request. This includes + // the URL path and query-string. No decoding is performed. + string path = 4; + + // The HTTP request ``Host`` or ``:authority`` header value. + string host = 5; + + // The HTTP URL scheme, such as ``http`` and ``https``. + string scheme = 6; + + // This field is always empty, and exists for compatibility reasons. The HTTP URL query is + // included in ``path`` field. + string query = 7; + + // This field is always empty, and exists for compatibility reasons. The URL fragment is + // not submitted as part of HTTP requests; it is unknowable. + string fragment = 8; + + // The HTTP request size in bytes. If unknown, it must be -1. + int64 size = 9; + + // The network protocol used with the request, such as "HTTP/1.0", "HTTP/1.1", or "HTTP/2". + // + // See :repo:`headers.h:ProtocolStrings ` for a list of all + // possible values. + string protocol = 10; + + // The HTTP request body. + string body = 11; + + // The HTTP request body in bytes. This is used instead of + // :ref:`body ` when + // :ref:`pack_as_bytes ` + // is set to true. + bytes raw_body = 12; + } + + // This message defines attributes for the underlying TLS session. + message TLSSession { + // SNI used for TLS session. + string sni = 1; + } + + // The source of a network activity, such as starting a TCP connection. + // In a multi hop network activity, the source represents the sender of the + // last hop. + Peer source = 1; + + // The destination of a network activity, such as accepting a TCP connection. + // In a multi hop network activity, the destination represents the receiver of + // the last hop. + Peer destination = 2; + + // Represents a network request, such as an HTTP request. + Request request = 4; + + // This is analogous to http_request.headers, however these contents will not be sent to the + // upstream server. Context_extensions provide an extension mechanism for sending additional + // information to the auth server without modifying the proto definition. It maps to the + // internal opaque context in the filter chain. + map context_extensions = 10; + + // Dynamic metadata associated with the request. + config.core.v3.Metadata metadata_context = 11; + + // Metadata associated with the selected route. + config.core.v3.Metadata route_metadata_context = 13; + + // TLS session details of the underlying connection. + // This is not populated by default and will be populated only if the ext_authz filter has + // been specifically configured to include this information. + // For HTTP ext_authz, that requires :ref:`include_tls_session ` + // to be set to true. + // For network ext_authz, that requires :ref:`include_tls_session ` + // to be set to true. + TLSSession tls_session = 12; +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/service/auth/v3/external_auth.proto b/xds/third_party/envoy/src/main/proto/envoy/service/auth/v3/external_auth.proto new file mode 100644 index 00000000000..1f3ed5787d8 --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/service/auth/v3/external_auth.proto @@ -0,0 +1,144 @@ +syntax = "proto3"; + +package envoy.service.auth.v3; + +import "envoy/config/core/v3/base.proto"; +import "envoy/service/auth/v3/attribute_context.proto"; +import "envoy/type/v3/http_status.proto"; + +import "google/protobuf/struct.proto"; +import "google/rpc/status.proto"; + +import "envoy/annotations/deprecation.proto"; +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.service.auth.v3"; +option java_outer_classname = "ExternalAuthProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3;authv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Authorization service] + +// The authorization service request messages used by external authorization :ref:`network filter +// ` and :ref:`HTTP filter `. + +// A generic interface for performing authorization check on incoming +// requests to a networked service. +service Authorization { + // Performs authorization check based on the attributes associated with the + // incoming request, and returns status `OK` or not `OK`. + rpc Check(CheckRequest) returns (CheckResponse) { + } +} + +message CheckRequest { + option (udpa.annotations.versioning).previous_message_type = "envoy.service.auth.v2.CheckRequest"; + + // The request attributes. + AttributeContext attributes = 1; +} + +// HTTP attributes for a denied response. +message DeniedHttpResponse { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.auth.v2.DeniedHttpResponse"; + + // This field allows the authorization service to send an HTTP response status code to the + // downstream client. If not set, Envoy sends ``403 Forbidden`` HTTP status code by default. + type.v3.HttpStatus status = 1; + + // This field allows the authorization service to send HTTP response headers + // to the downstream client. Note that the :ref:`append field in HeaderValueOption ` defaults to + // false when used in this message. + repeated config.core.v3.HeaderValueOption headers = 2; + + // This field allows the authorization service to send a response body data + // to the downstream client. + string body = 3; +} + +// HTTP attributes for an OK response. +// [#next-free-field: 9] +message OkHttpResponse { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.auth.v2.OkHttpResponse"; + + // HTTP entity headers in addition to the original request headers. This allows the authorization + // service to append, to add or to override headers from the original request before + // dispatching it to the upstream. Note that the :ref:`append field in HeaderValueOption ` defaults to + // false when used in this message. By setting the ``append`` field to ``true``, + // the filter will append the correspondent header value to the matched request header. + // By leaving ``append`` as false, the filter will either add a new header, or override an existing + // one if there is a match. + repeated config.core.v3.HeaderValueOption headers = 2; + + // HTTP entity headers to remove from the original request before dispatching + // it to the upstream. This allows the authorization service to act on auth + // related headers (like ``Authorization``), process them, and consume them. + // Under this model, the upstream will either receive the request (if it's + // authorized) or not receive it (if it's not), but will not see headers + // containing authorization credentials. + // + // Pseudo headers (such as ``:authority``, ``:method``, ``:path`` etc), as well as + // the header ``Host``, may not be removed as that would make the request + // malformed. If mentioned in ``headers_to_remove`` these special headers will + // be ignored. + // + // When using the HTTP service this must instead be set by the HTTP + // authorization service as a comma separated list like so: + // ``x-envoy-auth-headers-to-remove: one-auth-header, another-auth-header``. + repeated string headers_to_remove = 5; + + // This field has been deprecated in favor of :ref:`CheckResponse.dynamic_metadata + // `. Until it is removed, + // setting this field overrides :ref:`CheckResponse.dynamic_metadata + // `. + google.protobuf.Struct dynamic_metadata = 3 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; + + // This field allows the authorization service to send HTTP response headers + // to the downstream client on success. Note that the :ref:`append field in HeaderValueOption ` + // defaults to false when used in this message. + repeated config.core.v3.HeaderValueOption response_headers_to_add = 6; + + // This field allows the authorization service to set (and overwrite) query + // string parameters on the original request before it is sent upstream. + repeated config.core.v3.QueryParameter query_parameters_to_set = 7; + + // This field allows the authorization service to specify which query parameters + // should be removed from the original request before it is sent upstream. Each + // element in this list is a case-sensitive query parameter name to be removed. + repeated string query_parameters_to_remove = 8; +} + +// Intended for gRPC and Network Authorization servers ``only``. +message CheckResponse { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.auth.v2.CheckResponse"; + + // Status ``OK`` allows the request. Any other status indicates the request should be denied, and + // for HTTP filter, if not overridden by :ref:`denied HTTP response status ` + // Envoy sends ``403 Forbidden`` HTTP status code by default. + google.rpc.Status status = 1; + + // An message that contains HTTP response attributes. This message is + // used when the authorization service needs to send custom responses to the + // downstream client or, to modify/add request headers being dispatched to the upstream. + oneof http_response { + // Supplies http attributes for a denied response. + DeniedHttpResponse denied_response = 2; + + // Supplies http attributes for an ok response. + OkHttpResponse ok_response = 3; + } + + // Optional response metadata that will be emitted as dynamic metadata to be consumed by the next + // filter. This metadata lives in a namespace specified by the canonical name of extension filter + // that requires it: + // + // - :ref:`envoy.filters.http.ext_authz ` for HTTP filter. + // - :ref:`envoy.filters.network.ext_authz ` for network filter. + google.protobuf.Struct dynamic_metadata = 4; +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/value.proto b/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/value.proto index d773c6057fc..8d65c457ccc 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/value.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/type/matcher/v3/value.proto @@ -17,7 +17,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Value matcher] -// Specifies the way to match a ProtobufWkt::Value. Primitive values and ListValue are supported. +// Specifies the way to match a Protobuf::Value. Primitive values and ListValue are supported. // StructValue is not supported and is always not matched. // [#next-free-field: 8] message ValueMatcher { From 6e430122fb160ad11dd9fb03e5aa3208e55e9637 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 14 Nov 2025 15:21:29 -0800 Subject: [PATCH 463/591] Update README etc to reference 1.77.0 --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c25fc73934f..7bd0e67f275 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.76.0/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.76.0/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.77.0/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.77.0/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,34 +56,34 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.76.0 + 1.77.0 runtime io.grpc grpc-protobuf - 1.76.0 + 1.77.0 io.grpc grpc-stub - 1.76.0 + 1.77.0 ``` Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.76.0' -implementation 'io.grpc:grpc-protobuf:1.76.0' -implementation 'io.grpc:grpc-stub:1.76.0' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.77.0' +implementation 'io.grpc:grpc-protobuf:1.77.0' +implementation 'io.grpc:grpc-stub:1.77.0' ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.76.0' -implementation 'io.grpc:grpc-protobuf-lite:1.76.0' -implementation 'io.grpc:grpc-stub:1.76.0' +implementation 'io.grpc:grpc-okhttp:1.77.0' +implementation 'io.grpc:grpc-protobuf-lite:1.77.0' +implementation 'io.grpc:grpc-stub:1.77.0' ``` For [Bazel](https://bazel.build), you can either @@ -91,7 +91,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.76.0 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.77.0 Development snapshots are available in [Sonatypes's snapshot repository](https://central.sonatype.com/repository/maven-snapshots/). @@ -123,7 +123,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use com.google.protobuf:protoc:3.25.8:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.76.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.77.0:exe:${os.detected.classifier} @@ -153,7 +153,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.77.0' } } generateProtoTasks { @@ -186,7 +186,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.76.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.77.0' } } generateProtoTasks { From 725ab22f33ec64108305159f8777389820f77c16 Mon Sep 17 00:00:00 2001 From: Dayuxiaoshui <792179245@qq.com> Date: Mon, 17 Nov 2025 22:49:14 +0800 Subject: [PATCH 464/591] Add RISC-V 64-bit architecture support to compiler build configuration Add riscv_64 platform definition and include it in the supported architectures list for the protoc plugin compiler. This enables the gRPC Java compiler to generate native code for RISC-V 64-bit architecture. The changes allow the compiler module to recognize and build for RISC-V architecture, enabling native code generation for RISC-V platforms. This has been tested and verified on RISC-V SG2044 servers. Co-authored-by: gong-flying --- compiler/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compiler/build.gradle b/compiler/build.gradle index c5acccebae7..9b02f8286a1 100644 --- a/compiler/build.gradle +++ b/compiler/build.gradle @@ -76,6 +76,7 @@ model { aarch_64 { architecture "aarch_64" } s390_64 { architecture "s390_64" } loongarch_64 { architecture "loongarch_64" } + riscv_64 { architecture "riscv_64" } } components { @@ -84,6 +85,7 @@ model { 'x86_32', 'x86_64', 'ppcle_64', + 'riscv_64', 'aarch_64', 's390_64', 'loongarch_64' From 6d611f7f8cd1f9763e5946288c2a0a626317a654 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 19 Nov 2025 09:33:07 -0800 Subject: [PATCH 465/591] buildSrc: checkForUpdates comments to tune version search This will make it more likely that we notice minor and patch releases for dependencies that we can't update to the brand newest while also reducing the checkForUpdate noise/toil. --- build.gradle | 2 +- .../io/grpc/gradle/CheckForUpdatesTask.java | 43 +++++++++++++++++-- gradle/libs.versions.toml | 18 +++++++- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 38ee6220169..50b7419df64 100644 --- a/build.gradle +++ b/build.gradle @@ -533,5 +533,5 @@ configurations { } } -tasks.register('checkForUpdates', CheckForUpdatesTask, project.configurations.checkForUpdates, "libs") +tasks.register('checkForUpdates', CheckForUpdatesTask, project.configurations.checkForUpdates, "libs", layout.projectDirectory.file("gradle/libs.versions.toml")) diff --git a/buildSrc/src/main/java/io/grpc/gradle/CheckForUpdatesTask.java b/buildSrc/src/main/java/io/grpc/gradle/CheckForUpdatesTask.java index 9d0156a1b72..b7c28dbbb2d 100644 --- a/buildSrc/src/main/java/io/grpc/gradle/CheckForUpdatesTask.java +++ b/buildSrc/src/main/java/io/grpc/gradle/CheckForUpdatesTask.java @@ -16,11 +16,15 @@ package io.grpc.gradle; +import java.io.IOException; +import java.nio.file.Files; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; import org.gradle.api.DefaultTask; import org.gradle.api.artifacts.Configuration; @@ -32,6 +36,7 @@ import org.gradle.api.artifacts.result.ResolvedComponentResult; import org.gradle.api.artifacts.result.ResolvedDependencyResult; import org.gradle.api.artifacts.result.UnresolvedDependencyResult; +import org.gradle.api.file.RegularFile; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Nested; @@ -45,7 +50,23 @@ public abstract class CheckForUpdatesTask extends DefaultTask { private final Set libraries; @Inject - public CheckForUpdatesTask(Configuration updateConf, String catalog) { + public CheckForUpdatesTask(Configuration updateConf, String catalog, RegularFile commentFile) + throws IOException { + // Check for overrides to the default version selection ('+'), using comments of the form: + // # checkForUpdates: library-name:1.2.+ + List fileComments = Files.lines(commentFile.getAsFile().toPath()) + .filter(l -> l.matches("# *checkForUpdates:.*")) + .map(l -> l.replaceFirst("# *checkForUpdates:", "").trim()) + .collect(Collectors.toList()); + Map aliasToVersionSelector = new HashMap<>(2*fileComments.size()); + for (String comment : fileComments) { + String[] parts = comment.split(":", 2); + String name = parts[0].replaceAll("[_-]", "."); + if (aliasToVersionSelector.put(name, parts[1]) != null) { + throw new RuntimeException("Duplicate checkForUpdates comment for library: " + name); + } + } + updateConf.setVisible(false); updateConf.setTransitive(false); VersionCatalog versionCatalog = getProject().getExtensions().getByType(VersionCatalogsExtension.class).named(catalog); @@ -59,8 +80,12 @@ public CheckForUpdatesTask(Configuration updateConf, String catalog) { oldConf.getDependencies().add(oldDep); Configuration newConf = updateConf.copy(); + String versionSelector = aliasToVersionSelector.remove(name); + if (versionSelector == null) { + versionSelector = "+"; + } Dependency newDep = getProject().getDependencies().create( - depMap(dep.getGroup(), dep.getName(), "+", "pom")); + depMap(dep.getGroup(), dep.getName(), versionSelector, "pom")); newConf.getDependencies().add(newDep); libraries.add(new Library( @@ -68,6 +93,10 @@ public CheckForUpdatesTask(Configuration updateConf, String catalog) { oldConf.getIncoming().getResolutionResult().getRootComponent(), newConf.getIncoming().getResolutionResult().getRootComponent())); } + if (!aliasToVersionSelector.isEmpty()) { + throw new RuntimeException( + "Unused checkForUpdates comments: " + aliasToVersionSelector.keySet()); + } this.libraries = Collections.unmodifiableSet(libraries); } @@ -96,10 +125,16 @@ public void checkForUpdates() { "- Current version of libs.%s not resolved", name)); continue; } + DependencyResult newResult = lib.getNewResult().get().getDependencies().iterator().next(); + if (newResult instanceof UnresolvedDependencyResult) { + System.out.println(String.format( + "- New version of libs.%s not resolved", name)); + continue; + } ModuleVersionIdentifier oldId = ((ResolvedDependencyResult) oldResult).getSelected().getModuleVersion(); - ModuleVersionIdentifier newId = ((ResolvedDependencyResult) lib.getNewResult().get() - .getDependencies().iterator().next()).getSelected().getModuleVersion(); + ModuleVersionIdentifier newId = + ((ResolvedDependencyResult) newResult).getSelected().getModuleVersion(); if (oldId != newId) { System.out.println(String.format( "libs.%s = %s %s -> %s", diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3d923af7d40..f4914c1ea71 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,9 +11,11 @@ protobuf = "3.25.8" [libraries] android-annotations = "com.google.android:annotations:4.1.1.4" # androidx-annotation 1.9.1+ uses Kotlin and requires Android Gradle Plugin 9+ +# checkForUpdates: androidx-annotation:1.9.0 androidx-annotation = "androidx.annotation:annotation:1.9.0" # 1.15.0 requires libraries and applications that depend on it to compile against # version 35 or later of the Android APIs. +# checkForUpdates: androidx-core:1.13.+ androidx-core = "androidx.core:core:1.13.1" # androidx-lifecycle 2.9+ requires Java 17 androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.8.7" @@ -33,15 +35,19 @@ cronet-api = "org.chromium.net:cronet-api:119.6045.31" cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31" errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.36.0" # error-prone 2.32.0+ require Java 17+ +# checkForUpdates: errorprone-core:2.31.+ errorprone-core = "com.google.errorprone:error_prone_core:2.31.0" google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.59.2" # google-auth-library 1.25.0+ requires error_prone_annotations 2.31.0+, which # breaks the Android build +# checkForUpdates: google-auth-credentials:1.24.+ google-auth-credentials = "com.google.auth:google-auth-library-credentials:1.24.1" +# checkForUpdates: google-auth-oauth2Http:1.24.+ google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.24.1" # Release notes: https://cloud.google.com/logging/docs/release-notes google-cloud-logging = "com.google.cloud:google-cloud-logging:3.23.1" # 2.13.0 requires error_prone_annotations:2.37.0, but we are stuck with 2.36.0 +# checkForUpdates: gson:2.12.+ gson = "com.google.code.gson:gson:2.12.1" # 33.4.8 requires com.google.errorprone:error_prone_annotations:2.36.0 guava = "com.google.guava:guava:33.4.8-android" @@ -52,9 +58,11 @@ guava-testlib = "com.google.guava:guava-testlib:33.4.8-android" guava-jre = "com.google.guava:guava:33.4.8-jre" hdrhistogram = "org.hdrhistogram:HdrHistogram:2.2.2" # 6.0.0+ use java.lang.Deprecated forRemoval and since from Java 9 +# checkForUpdates: jakarta-servlet-api:5.+ jakarta-servlet-api = "jakarta.servlet:jakarta.servlet-api:5.0.0" javax-servlet-api = "javax.servlet:javax.servlet-api:4.0.1" # 12.0.0+ require Java 17+ +# checkForUpdates: jetty-client:11.+ jetty-client = "org.eclipse.jetty:jetty-client:11.0.24" jetty-http2-server = "org.eclipse.jetty.http2:jetty-http2-server:12.0.23" jetty-http2-server10 = "org.eclipse.jetty.http2:http2-server:10.0.20" @@ -79,6 +87,7 @@ netty-transport-epoll = { module = "io.netty:netty-transport-native-epoll", vers netty-unix-common = { module = "io.netty:netty-transport-native-unix-common", version.ref = "netty" } okhttp = "com.squareup.okhttp:okhttp:2.7.5" # okio 3.5+ uses Kotlin 1.9+ which requires Android Gradle Plugin 9+ +# checkForUpdates: okio:3.4.+ okio = "com.squareup.okio:okio:3.4.0" opencensus-api = { module = "io.opencensus:opencensus-api", version.ref = "opencensus" } opencensus-contrib-grpc-metrics = { module = "io.opencensus:opencensus-contrib-grpc-metrics", version.ref = "opencensus" } @@ -101,14 +110,19 @@ s2a-proto = "com.google.s2a.proto.v2:s2a-proto:0.1.2" signature-android = "net.sf.androidscents.signature:android-api-level-21:5.0.1_r2" signature-java = "org.codehaus.mojo.signature:java18:1.0" # 11.0.0+ require Java 17+ +# checkForUpdates: tomcat-embed-core:10.+ tomcat-embed-core = "org.apache.tomcat.embed:tomcat-embed-core:10.1.31" +# checkForUpdates: tomcat-embed-core9:9.+ tomcat-embed-core9 = "org.apache.tomcat.embed:tomcat-embed-core:9.0.89" truth = "com.google.truth:truth:1.4.4" +# checkForUpdates: undertow-servlet22:2.2.+ undertow-servlet22 = "io.undertow:undertow-servlet:2.2.37.Final" undertow-servlet = "io.undertow:undertow-servlet:2.3.18.Final" -# Do not update: Pinned to the last version supporting Java 8. -# See https://checkstyle.sourceforge.io/releasenotes.html#Release_10.1 +# checkstyle 10.0+ requires Java 11+ +# See https://checkstyle.sourceforge.io/releasenotes_old_8-35_10-26.html#Release_10.0 +# checkForUpdates: checkstylejava8:9.+ checkstylejava8 = "com.puppycrawl.tools:checkstyle:9.3" # 2.11.0+ requires JDK 11+ (See https://github.com/google/error-prone/releases/tag/v2.11.0) +# checkForUpdates: errorprone-corejava8:2.10.+ errorprone-corejava8 = "com.google.errorprone:error_prone_core:2.10.0" From 97695d523eef447cf7a254781da6bc0cbf22ad21 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 21 Nov 2025 10:28:06 -0800 Subject: [PATCH 466/591] examples: Document how to preserve META-INF/services in uber jars --- examples/build.gradle | 9 ++++++ .../maven-assembly-jar-with-dependencies.xml | 27 +++++++++++++++++ examples/pom.xml | 29 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 examples/maven-assembly-jar-with-dependencies.xml diff --git a/examples/build.gradle b/examples/build.gradle index 1cd8eea75fc..3c7464ea2d2 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -51,6 +51,15 @@ protobuf { } } +// gRPC uses java.util.ServiceLoader, which reads class names from +// META-INF/services in jars. If you package your application as a "fat" jar +// that includes dependencies, you need to make sure the packaging tool +// concatenates duplicate files in META-INF/services. +// +// For the Shadow Gradle Plugin, use call mergeServiceFiles() within the +// shadowJar task. +// https://gradleup.com/shadow/configuration/merging/#merging-service-descriptor-files + startScripts.enabled = false // Creates start scripts for a class name and adds it to the distribution. The diff --git a/examples/maven-assembly-jar-with-dependencies.xml b/examples/maven-assembly-jar-with-dependencies.xml new file mode 100644 index 00000000000..6c8abbfe7e8 --- /dev/null +++ b/examples/maven-assembly-jar-with-dependencies.xml @@ -0,0 +1,27 @@ + + + jar-with-dependencies + + jar + + false + + + / + true + true + runtime + + + + + metaInf-services + + + diff --git a/examples/pom.xml b/examples/pom.xml index 63921323fb9..2f4629ec201 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -124,6 +124,35 @@ + + + + + + + + maven-assembly-plugin + 3.7.1 + + ${project.basedir}/maven-assembly-jar-with-dependencies.xml + + + + make-assembly + package + + single + + + + From 753ed1be62caf7ae2ba3020402c5ce661b74548b Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 25 Nov 2025 11:47:29 +0530 Subject: [PATCH 467/591] xds: Avoid using empty SNI string that was never sent, for SAN validation (#12532) --- .../xds/internal/security/SecurityProtocolNegotiators.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java b/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java index 0da06b3a753..37259afa1a9 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/SecurityProtocolNegotiators.java @@ -219,8 +219,10 @@ public void handlerAdded(ChannelHandlerContext ctx) throws Exception { String sniToUse = upstreamTlsContext.getAutoHostSni() && !Strings.isNullOrEmpty(endpointHostname) ? endpointHostname : upstreamTlsContext.getSni(); - if (sniToUse.isEmpty() && CertificateUtils.useChannelAuthorityIfNoSniApplicable) { - sniToUse = grpcHandler.getAuthority(); + if (sniToUse.isEmpty()) { + if (CertificateUtils.useChannelAuthorityIfNoSniApplicable) { + sniToUse = grpcHandler.getAuthority(); + } autoSniSanValidationDoesNotApply = true; } else { autoSniSanValidationDoesNotApply = false; From 2b9509e543b8c6f6021d0b6fdfe27ff76770b232 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:44:50 -0800 Subject: [PATCH 468/591] s2a: Fix FakeS2AServerTest flakiness Wait for server response to be received before shutting down resources. Fixes #12501 --- .../s2a/internal/handshaker/FakeS2AServerTest.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java index d8374a8a382..c3155b864b3 100644 --- a/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java +++ b/s2a/src/test/java/io/grpc/s2a/internal/handshaker/FakeS2AServerTest.java @@ -45,6 +45,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeoutException; import java.util.logging.Logger; import org.junit.After; import org.junit.Before; @@ -77,7 +78,10 @@ public void tearDown() throws Exception { @Test public void callS2AServerOnce_getTlsConfiguration_returnsValidResult() - throws InterruptedException, IOException, java.util.concurrent.ExecutionException { + throws InterruptedException, + IOException, + java.util.concurrent.ExecutionException, + TimeoutException { ExecutorService executor = Executors.newSingleThreadExecutor(); logger.info("Client connecting to: " + serverAddress); ManagedChannel channel = @@ -121,6 +125,7 @@ public void onCompleted() { // Mark the end of requests. requestObserver.onCompleted(); // Wait for receiving to happen. + respFuture.get(5, SECONDS); } finally { channel.shutdown(); channel.awaitTermination(1, SECONDS); @@ -165,7 +170,7 @@ public void onCompleted() { @Test public void callS2AServerOnce_validatePeerCertifiate_returnsValidResult() - throws InterruptedException, java.util.concurrent.ExecutionException { + throws InterruptedException, java.util.concurrent.ExecutionException, TimeoutException { ExecutorService executor = Executors.newSingleThreadExecutor(); logger.info("Client connecting to: " + serverAddress); ManagedChannel channel = @@ -212,6 +217,7 @@ public void onCompleted() { // Mark the end of requests. requestObserver.onCompleted(); // Wait for receiving to happen. + respFuture.get(5, SECONDS); } finally { channel.shutdown(); channel.awaitTermination(1, SECONDS); From 7a077528c2fd43aec53a782026382be7d4a5ad55 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 25 Nov 2025 09:01:09 -0800 Subject: [PATCH 469/591] xds: Use separate key for ATTR_ADDRESS_NAME on a subchannel XdsInternalAttributes.ATTR_ADDRESS_NAME is annotated with `@EquivalentAddressGroup.Attr`, so it should only be used in the EAG. --- .../main/java/io/grpc/xds/ClusterImplLoadBalancer.java | 8 +++++--- .../java/io/grpc/xds/ClusterImplLoadBalancerTest.java | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index ad5dff3cea1..ec4bec7f25c 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -87,6 +87,9 @@ final class ClusterImplLoadBalancer extends LoadBalancer { private static final Attributes.Key> ATTR_CLUSTER_LOCALITY = Attributes.Key.create("io.grpc.xds.ClusterImplLoadBalancer.clusterLocality"); + @VisibleForTesting + static final Attributes.Key ATTR_SUBCHANNEL_ADDRESS_NAME = + Attributes.Key.create("io.grpc.xds.ClusterImplLoadBalancer.addressName"); private final XdsLogger logger; private final Helper helper; @@ -243,7 +246,7 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { String hostname = args.getAddresses().get(0).getAttributes() .get(XdsInternalAttributes.ATTR_ADDRESS_NAME); if (hostname != null) { - attrsBuilder.set(XdsInternalAttributes.ATTR_ADDRESS_NAME, hostname); + attrsBuilder.set(ATTR_SUBCHANNEL_ADDRESS_NAME, hostname); } } args = args.toBuilder().setAddresses(addresses).setAttributes(attrsBuilder.build()).build(); @@ -442,8 +445,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { && args.getCallOptions().getOption(XdsNameResolver.AUTO_HOST_REWRITE_KEY)) { result = PickResult.withSubchannel(result.getSubchannel(), result.getStreamTracerFactory(), - result.getSubchannel().getAttributes().get( - XdsInternalAttributes.ATTR_ADDRESS_NAME)); + result.getSubchannel().getAttributes().get(ATTR_SUBCHANNEL_ADDRESS_NAME)); } } return result; diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index af618beda6a..9277675385a 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static io.grpc.xds.ClusterImplLoadBalancer.ATTR_SUBCHANNEL_ADDRESS_NAME; import static io.grpc.xds.XdsNameResolver.AUTO_HOST_REWRITE_KEY; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; @@ -909,7 +910,7 @@ public void endpointAddressesAttachedWithClusterName() { new FixedResultPicker(PickResult.withSubchannel(subchannel))); } }); - assertThat(subchannel.getAttributes().get(XdsInternalAttributes.ATTR_ADDRESS_NAME)).isEqualTo( + assertThat(subchannel.getAttributes().get(ATTR_SUBCHANNEL_ADDRESS_NAME)).isEqualTo( "authority-host-name"); for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) { assertThat(eag.getAttributes().get(XdsInternalAttributes.ATTR_ADDRESS_NAME)) @@ -961,7 +962,7 @@ public void endpointAddressesAttachedWithClusterName() { } }); // Sub Channel wrapper args won't have the address name although addresses will. - assertThat(subchannel.getAttributes().get(XdsInternalAttributes.ATTR_ADDRESS_NAME)).isNull(); + assertThat(subchannel.getAttributes().get(ATTR_SUBCHANNEL_ADDRESS_NAME)).isNull(); for (EquivalentAddressGroup eag : subchannel.getAllAddresses()) { assertThat(eag.getAttributes().get(XdsInternalAttributes.ATTR_ADDRESS_NAME)) .isEqualTo("authority-host-name"); From 6f3d3f66d7384a35fed0395c03f81bb9d66938cc Mon Sep 17 00:00:00 2001 From: Alex Panchenko <440271+panchenko@users.noreply.github.com> Date: Sat, 22 Nov 2025 10:25:22 +0200 Subject: [PATCH 470/591] core: remove unused method from CompositeReadableBuffer --- .../internal/CompositeReadableBuffer.java | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/CompositeReadableBuffer.java b/core/src/main/java/io/grpc/internal/CompositeReadableBuffer.java index 4407eb8a2a2..4c8cb72a7ce 100644 --- a/core/src/main/java/io/grpc/internal/CompositeReadableBuffer.java +++ b/core/src/main/java/io/grpc/internal/CompositeReadableBuffer.java @@ -23,7 +23,6 @@ import java.nio.InvalidMarkException; import java.util.ArrayDeque; import java.util.Deque; -import java.util.Queue; import javax.annotation.Nullable; /** @@ -39,7 +38,6 @@ public class CompositeReadableBuffer extends AbstractReadableBuffer { private final Deque readableBuffers; private Deque rewindableBuffers; private int readableBytes; - private final Queue buffers = new ArrayDeque(2); private boolean marked; public CompositeReadableBuffer(int initialCapacity) { @@ -161,31 +159,6 @@ public void readBytes(OutputStream dest, int length) throws IOException { execute(STREAM_OP, length, dest, 0); } - /** - * Reads {@code length} bytes from this buffer and writes them to the destination buffer. - * Increments the read position by {@code length}. If the required bytes are not readable, throws - * {@link IndexOutOfBoundsException}. - * - * @param dest the destination buffer to receive the bytes. - * @param length the number of bytes to be copied. - * @throws IndexOutOfBoundsException if required bytes are not readable - */ - public void readBytes(CompositeReadableBuffer dest, int length) { - checkReadable(length); - readableBytes -= length; - - while (length > 0) { - ReadableBuffer buffer = buffers.peek(); - if (buffer.readableBytes() > length) { - dest.addBuffer(buffer.readBytes(length)); - length = 0; - } else { - dest.addBuffer(buffers.poll()); - length -= buffer.readableBytes(); - } - } - } - @Override public ReadableBuffer readBytes(int length) { if (length <= 0) { From 02e98a806d4738a113519115d55fc242243e98d6 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 25 Nov 2025 15:37:49 -0800 Subject: [PATCH 471/591] core: Fix shutdown failing accepted RPCs This fixes a race where RPCs could fail with "UNAVAILABLE: Channel shutdown invoked" even though they were created before channel.shutdown(). This basically adopts the internalStart() logic from DelayedStream, although the stream is a bit different because it has APIs that can be called before start() and doesn't need to handle cancel() without start(). The ManagedChannelImpltest had the number of due tasks increase because start() running earlier creates a DelayedStream. Previously the stream wasn't created until runDueTasks() so the mockPicker had already been installed and it could use a real stream from the beginning. But that's specific to the test; in practice it'd be a delayed stream before and after this change. See #12536 --- .../io/grpc/internal/DelayedClientCall.java | 31 +++++++++++++------ .../grpc/internal/DelayedClientCallTest.java | 4 ++- .../grpc/internal/ManagedChannelImplTest.java | 2 +- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/DelayedClientCall.java b/core/src/main/java/io/grpc/internal/DelayedClientCall.java index 253237c3c7d..b568bb12c46 100644 --- a/core/src/main/java/io/grpc/internal/DelayedClientCall.java +++ b/core/src/main/java/io/grpc/internal/DelayedClientCall.java @@ -64,6 +64,8 @@ public class DelayedClientCall extends ClientCall { * order, but also used if an error occurs before {@code realCall} is set. */ private Listener listener; + // No need to synchronize; start() synchronization provides a happens-before + private Metadata startHeaders; // Must hold {@code this} lock when setting. private ClientCall realCall; @GuardedBy("this") @@ -161,13 +163,23 @@ public void run() { */ // When this method returns, passThrough is guaranteed to be true public final Runnable setCall(ClientCall call) { + Listener savedDelayedListener; synchronized (this) { // If realCall != null, then either setCall() or cancel() has been called. if (realCall != null) { return null; } setRealCall(checkNotNull(call, "call")); + // start() not yet called + if (delayedListener == null) { + assert pendingRunnables.isEmpty(); + pendingRunnables = null; + passThrough = true; + return null; + } + savedDelayedListener = this.delayedListener; } + internalStart(savedDelayedListener); return new ContextRunnable(context) { @Override public void runInContext() { @@ -176,8 +188,15 @@ public void runInContext() { }; } + private void internalStart(Listener listener) { + Metadata savedStartHeaders = this.startHeaders; + this.startHeaders = null; + context.run(() -> realCall.start(listener, savedStartHeaders)); + } + @Override public final void start(Listener listener, final Metadata headers) { + checkNotNull(headers, "headers"); checkState(this.listener == null, "already started"); Status savedError; boolean savedPassThrough; @@ -188,6 +207,7 @@ public final void start(Listener listener, final Metadata headers) { savedPassThrough = passThrough; if (!savedPassThrough) { listener = delayedListener = new DelayedListener<>(listener); + startHeaders = headers; } } if (savedError != null) { @@ -196,15 +216,7 @@ public final void start(Listener listener, final Metadata headers) { } if (savedPassThrough) { realCall.start(listener, headers); - } else { - final Listener finalListener = listener; - delayOrExecute(new Runnable() { - @Override - public void run() { - realCall.start(finalListener, headers); - } - }); - } + } // else realCall.start() will be called by setCall } // When this method returns, passThrough is guaranteed to be true @@ -253,6 +265,7 @@ public void run() { if (listenerToClose != null) { callExecutor.execute(new CloseListenerRunnable(listenerToClose, status)); } + internalStart(listenerToClose); // listener instance doesn't matter drainPendingCalls(); } callCancelled(); diff --git a/core/src/test/java/io/grpc/internal/DelayedClientCallTest.java b/core/src/test/java/io/grpc/internal/DelayedClientCallTest.java index 45682b3a385..557ddfe5ace 100644 --- a/core/src/test/java/io/grpc/internal/DelayedClientCallTest.java +++ b/core/src/test/java/io/grpc/internal/DelayedClientCallTest.java @@ -151,10 +151,12 @@ public void startThenSetCall() { delayedClientCall.request(1); Runnable r = delayedClientCall.setCall(mockRealCall); assertThat(r).isNotNull(); - r.run(); @SuppressWarnings("unchecked") ArgumentCaptor> listenerCaptor = ArgumentCaptor.forClass(Listener.class); + // start() must be called before setCall() returns (not in runnable), to ensure the in-use + // counts keeping the channel alive after shutdown() don't momentarily decrease to zero. verify(mockRealCall).start(listenerCaptor.capture(), any(Metadata.class)); + r.run(); Listener realCallListener = listenerCaptor.getValue(); verify(mockRealCall).request(1); realCallListener.onMessage(1); diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index efc582703ba..91a9f506bc8 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -1343,7 +1343,7 @@ public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata header PickResult.withSubchannel(subchannel)); updateBalancingStateSafely(helper, READY, mockPicker); - assertEquals(2, executor.runDueTasks()); + assertEquals(3, executor.runDueTasks()); verify(mockPicker).pickSubchannel(any(PickSubchannelArgs.class)); verify(mockTransport).newStream( From f385add314a45794401d9edae18cef16b06b4655 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Sun, 30 Nov 2025 13:01:33 +0530 Subject: [PATCH 472/591] xds: gRFC A88 - Changes to XdsClient Watcher APIs (#12446) Implements https://github.com/grpc/proposal/blob/master/A88-xds-data-error-handling.md#a88-xds-data-error-handling --- .../io/grpc/xds/XdsDependencyManager.java | 48 +- .../java/io/grpc/xds/XdsServerWrapper.java | 134 +-- .../io/grpc/xds/client/BootstrapperImpl.java | 4 +- .../grpc/xds/client/ControlPlaneClient.java | 4 +- .../java/io/grpc/xds/client/XdsClient.java | 32 +- .../io/grpc/xds/client/XdsClientImpl.java | 103 +- .../io/grpc/xds/CdsLoadBalancer2Test.java | 17 +- .../xds/ClusterResolverLoadBalancerTest.java | 26 +- .../grpc/xds/GrpcXdsClientImplTestBase.java | 1045 +++++++++++------ .../io/grpc/xds/XdsClientFallbackTest.java | 235 ++-- .../io/grpc/xds/XdsClientFederationTest.java | 37 +- .../XdsClientWrapperForServerSdsTestMisc.java | 11 +- .../io/grpc/xds/XdsDependencyManagerTest.java | 18 +- .../java/io/grpc/xds/XdsNameResolverTest.java | 163 ++- .../io/grpc/xds/XdsServerBuilderTest.java | 10 +- .../java/io/grpc/xds/XdsServerTestHelper.java | 19 +- .../io/grpc/xds/XdsServerWrapperTest.java | 42 +- 17 files changed, 1225 insertions(+), 723 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java index 21b0ad7dc66..919836ddd9c 100644 --- a/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java +++ b/xds/src/main/java/io/grpc/xds/XdsDependencyManager.java @@ -632,6 +632,9 @@ private abstract class XdsWatcherBase @Nullable private StatusOr data; + @Nullable + @SuppressWarnings("unused") + private Status ambientError; private XdsWatcherBase(XdsResourceType type, String resourceName) { @@ -640,42 +643,39 @@ private XdsWatcherBase(XdsResourceType type, String resourceName) { } @Override - public void onError(Status error) { - checkNotNull(error, "error"); + public void onResourceChanged(StatusOr update) { if (cancelled) { return; } - // Don't update configuration on error, if we've already received configuration - if (!hasDataValue()) { - this.data = StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( - String.format("Error retrieving %s: %s: %s", - toContextString(), error.getCode(), error.getDescription()))); - maybePublishConfig(); - } - } + ambientError = null; + if (update.hasValue()) { + data = update; + subscribeToChildren(update.getValue()); + } else { + Status status = update.getStatus(); + Status translatedStatus = Status.UNAVAILABLE.withDescription( + String.format("Error retrieving %s: %s. Details: %s%s", + toContextString(), + status.getCode(), + status.getDescription() != null ? status.getDescription() : "", + nodeInfo())); - @Override - public void onResourceDoesNotExist(String resourceName) { - if (cancelled) { - return; + data = StatusOr.fromStatus(translatedStatus); } - - checkArgument(this.resourceName.equals(resourceName), "Resource name does not match"); - this.data = StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( - toContextString() + " does not exist" + nodeInfo())); maybePublishConfig(); } @Override - public void onChanged(T update) { - checkNotNull(update, "update"); + public void onAmbientError(Status error) { if (cancelled) { return; } - - this.data = StatusOr.fromValue(update); - subscribeToChildren(update); - maybePublishConfig(); + ambientError = error.withDescription( + String.format("Ambient error for %s: %s. Details: %s%s", + toContextString(), + error.getCode(), + error.getDescription() != null ? error.getDescription() : "", + nodeInfo())); } protected abstract void subscribeToChildren(T update); diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index f54c4af5cff..5529f96c7a2 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -42,6 +42,7 @@ import io.grpc.ServerServiceDefinition; import io.grpc.Status; import io.grpc.StatusException; +import io.grpc.StatusOr; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; import io.grpc.internal.GrpcUtil; @@ -401,18 +402,30 @@ private DiscoveryState(String resourceName) { } @Override - public void onChanged(final LdsUpdate update) { + public void onResourceChanged(final StatusOr update) { if (stopped) { return; } - logger.log(Level.FINEST, "Received Lds update {0}", update); - if (update.listener() == null) { - onResourceDoesNotExist("Non-API"); + + if (!update.hasValue()) { + Status status = update.getStatus(); + StatusException statusException = Status.UNAVAILABLE.withDescription( + String.format("Listener %s unavailable: %s", resourceName, status.getDescription())) + .withCause(status.asException()) + .asException(); + handleConfigNotFoundOrMismatch(statusException); return; } - String ldsAddress = update.listener().address(); - if (ldsAddress == null || update.listener().protocol() != Protocol.TCP + final LdsUpdate ldsUpdate = update.getValue(); + logger.log(Level.FINEST, "Received Lds update {0}", ldsUpdate); + if (ldsUpdate.listener() == null) { + handleConfigNotFoundOrMismatch( + Status.NOT_FOUND.withDescription("Listener is null in LdsUpdate").asException()); + return; + } + String ldsAddress = ldsUpdate.listener().address(); + if (ldsAddress == null || ldsUpdate.listener().protocol() != Protocol.TCP || !ipAddressesMatch(ldsAddress)) { handleConfigNotFoundOrMismatch( Status.UNKNOWN.withDescription( @@ -421,16 +434,15 @@ public void onChanged(final LdsUpdate update) { listenerAddress, ldsAddress)).asException()); return; } + if (!pendingRds.isEmpty()) { // filter chain state has not yet been applied to filterChainSelectorManager and there - // are two sets of sslContextProviderSuppliers, so we release the old ones. releaseSuppliersInFlight(); pendingRds.clear(); } - filterChains = update.listener().filterChains(); - defaultFilterChain = update.listener().defaultFilterChain(); - // Filters are loaded even if the server isn't serving yet. + filterChains = ldsUpdate.listener().filterChains(); + defaultFilterChain = ldsUpdate.listener().defaultFilterChain(); updateActiveFilters(); List allFilterChains = filterChains; @@ -469,31 +481,8 @@ public void onChanged(final LdsUpdate update) { } } - private boolean ipAddressesMatch(String ldsAddress) { - HostAndPort ldsAddressHnP = HostAndPort.fromString(ldsAddress); - HostAndPort listenerAddressHnP = HostAndPort.fromString(listenerAddress); - if (!ldsAddressHnP.hasPort() || !listenerAddressHnP.hasPort() - || ldsAddressHnP.getPort() != listenerAddressHnP.getPort()) { - return false; - } - InetAddress listenerIp = InetAddresses.forString(listenerAddressHnP.getHost()); - InetAddress ldsIp = InetAddresses.forString(ldsAddressHnP.getHost()); - return listenerIp.equals(ldsIp); - } - - @Override - public void onResourceDoesNotExist(final String resourceName) { - if (stopped) { - return; - } - StatusException statusException = Status.UNAVAILABLE.withDescription( - String.format("Listener %s unavailable, xDS node ID: %s", resourceName, - xdsClient.getBootstrapInfo().node().getId())).asException(); - handleConfigNotFoundOrMismatch(statusException); - } - @Override - public void onError(final Status error) { + public void onAmbientError(final Status error) { if (stopped) { return; } @@ -501,11 +490,24 @@ public void onError(final Status error) { Status errorWithNodeId = error.withDescription( description + "xDS node ID: " + xdsClient.getBootstrapInfo().node().getId()); logger.log(Level.FINE, "Error from XdsClient", errorWithNodeId); + if (!isServing) { listener.onNotServing(errorWithNodeId.asException()); } } + private boolean ipAddressesMatch(String ldsAddress) { + HostAndPort ldsAddressHnP = HostAndPort.fromString(ldsAddress); + HostAndPort listenerAddressHnP = HostAndPort.fromString(listenerAddress); + if (!ldsAddressHnP.hasPort() || !listenerAddressHnP.hasPort() + || ldsAddressHnP.getPort() != listenerAddressHnP.getPort()) { + return false; + } + InetAddress listenerIp = InetAddresses.forString(listenerAddressHnP.getHost()); + InetAddress ldsIp = InetAddresses.forString(ldsAddressHnP.getHost()); + return listenerIp.equals(ldsIp); + } + private void shutdown() { stopped = true; cleanUpRouteDiscoveryStates(); @@ -794,54 +796,42 @@ private RouteDiscoveryState(String resourceName) { } @Override - public void onChanged(final RdsUpdate update) { - syncContext.execute(new Runnable() { - @Override - public void run() { - if (!routeDiscoveryStates.containsKey(resourceName)) { - return; - } - if (savedVirtualHosts == null && !isPending) { - logger.log(Level.WARNING, "Received valid Rds {0} configuration.", resourceName); - } - savedVirtualHosts = ImmutableList.copyOf(update.virtualHosts); - updateRdsRoutingConfig(); - maybeUpdateSelector(); + public void onResourceChanged(final StatusOr update) { + syncContext.execute(() -> { + if (!routeDiscoveryStates.containsKey(resourceName)) { + return; // Watcher has been cancelled. } - }); - } - @Override - public void onResourceDoesNotExist(final String resourceName) { - syncContext.execute(new Runnable() { - @Override - public void run() { - if (!routeDiscoveryStates.containsKey(resourceName)) { - return; + if (update.hasValue()) { + if (savedVirtualHosts == null && !isPending) { + logger.log(Level.WARNING, "Received valid Rds {0} configuration.", resourceName); } - logger.log(Level.WARNING, "Rds {0} unavailable", resourceName); + savedVirtualHosts = ImmutableList.copyOf(update.getValue().virtualHosts); + } else { + logger.log(Level.WARNING, "Rds {0} unavailable: {1}", + new Object[]{resourceName, update.getStatus()}); savedVirtualHosts = null; - updateRdsRoutingConfig(); - maybeUpdateSelector(); } + // In both cases, a change has occurred that requires a config update. + updateRdsRoutingConfig(); + maybeUpdateSelector(); }); } @Override - public void onError(final Status error) { - syncContext.execute(new Runnable() { - @Override - public void run() { - if (!routeDiscoveryStates.containsKey(resourceName)) { - return; - } - String description = error.getDescription() == null ? "" : error.getDescription() + " "; - Status errorWithNodeId = error.withDescription( - description + "xDS node ID: " + xdsClient.getBootstrapInfo().node().getId()); - logger.log(Level.WARNING, "Error loading RDS resource {0} from XdsClient: {1}.", - new Object[]{resourceName, errorWithNodeId}); - maybeUpdateSelector(); + public void onAmbientError(final Status error) { + syncContext.execute(() -> { + if (!routeDiscoveryStates.containsKey(resourceName)) { + return; // Watcher has been cancelled. } + String description = error.getDescription() == null ? "" : error.getDescription() + " "; + Status errorWithNodeId = error.withDescription( + description + "xDS node ID: " + xdsClient.getBootstrapInfo().node().getId()); + logger.log(Level.WARNING, "Error loading RDS resource {0} from XdsClient: {1}.", + new Object[]{resourceName, errorWithNodeId}); + + // Per gRFC A88, ambient errors should not trigger a configuration change. + // Therefore, we do NOT call maybeUpdateSelector() here. }); } diff --git a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java index 423c1a118e8..22c794e1129 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -262,7 +262,9 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo List serverFeatures = JsonUtil.getList(serverConfig, "server_features"); if (serverFeatures != null) { logger.log(XdsLogLevel.INFO, "Server features: {0}", serverFeatures); - ignoreResourceDeletion = serverFeatures.contains(SERVER_FEATURE_IGNORE_RESOURCE_DELETION); + if (serverFeatures.contains(SERVER_FEATURE_IGNORE_RESOURCE_DELETION)) { + ignoreResourceDeletion = true; + } resourceTimerIsTransientError = xdsDataErrorHandlingEnabled && serverFeatures.contains(SERVER_FEATURE_RESOURCE_TIMER_IS_TRANSIENT_ERROR); } diff --git a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java index 82a0e2f9220..59f439d3687 100644 --- a/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java +++ b/xds/src/main/java/io/grpc/xds/client/ControlPlaneClient.java @@ -457,10 +457,10 @@ private void handleRpcStreamClosed(Status status) { if (responseReceived) { // A closed ADS stream after a successful response is not considered an error. Servers may // close streams for various reasons during normal operation, such as load balancing or - // underlying connection hitting its max connection age limit (see gRFC A9). + // underlying connection hitting its max connection age limit (see gRFC A9). if (!status.isOk()) { newStatus = Status.OK; - logger.log( XdsLogLevel.DEBUG, "ADS stream closed with error {0}: {1}. However, a " + logger.log(XdsLogLevel.DEBUG, "ADS stream closed with error {0}: {1}. However, a " + "response was received, so this will not be treated as an error. Cause: {2}", status.getCode(), status.getDescription(), status.getCause()); } else { diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClient.java b/xds/src/main/java/io/grpc/xds/client/XdsClient.java index cd545c00c1d..982fb6651a9 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClient.java @@ -27,6 +27,7 @@ import com.google.protobuf.Any; import io.grpc.ExperimentalApi; import io.grpc.Status; +import io.grpc.StatusOr; import io.grpc.xds.client.Bootstrapper.ServerInfo; import java.net.URI; import java.net.URISyntaxException; @@ -139,30 +140,29 @@ public interface ResourceUpdate {} /** * Watcher interface for a single requested xDS resource. + * + *

    Note that we expect that the implementer to: + * - Comply with the guarantee to not generate certain statuses by the library: + * https://grpc.github.io/grpc/core/md_doc_statuscodes.html. If the code needs to be + * propagated to the channel, override it with {@link io.grpc.Status.Code#UNAVAILABLE}. + * - Keep {@link Status} description in one form or another, as it contains valuable debugging + * information. */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10862") public interface ResourceWatcher { /** - * Called when the resource discovery RPC encounters some transient error. - * - *

    Note that we expect that the implementer to: - * - Comply with the guarantee to not generate certain statuses by the library: - * https://grpc.github.io/grpc/core/md_doc_statuscodes.html. If the code needs to be - * propagated to the channel, override it with {@link io.grpc.Status.Code#UNAVAILABLE}. - * - Keep {@link Status} description in one form or another, as it contains valuable debugging - * information. + * Called to deliver a resource update or an error. If an error is passed after a valid + * resource has been delivered, the watcher should stop using the previously delivered + * resource. */ - void onError(Status error); + void onResourceChanged(StatusOr update); /** - * Called when the requested resource is not available. - * - * @param resourceName name of the resource requested in discovery request. - */ - void onResourceDoesNotExist(String resourceName); - - void onChanged(T update); + * Called to deliver a transient error that should not affect the watcher's use of any + * previously received resource. + * */ + void onAmbientError(Status error); } /** diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index 6a4c20cf02b..2bf1286babc 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -33,6 +33,7 @@ import io.grpc.Internal; import io.grpc.InternalLogId; import io.grpc.Status; +import io.grpc.StatusOr; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; import io.grpc.internal.BackoffPolicy; @@ -730,17 +731,20 @@ void addWatcher(ResourceWatcher watcher, Executor watcherExecutor) { Status savedError = lastError; watcherExecutor.execute(() -> { if (errorDescription != null) { - watcher.onError(Status.INVALID_ARGUMENT.withDescription(errorDescription)); - return; - } - if (savedError != null) { - watcher.onError(savedError); + watcher.onResourceChanged(StatusOr.fromStatus( + Status.INVALID_ARGUMENT.withDescription(errorDescription))); return; } if (savedData != null) { - notifyWatcher(watcher, savedData); + watcher.onResourceChanged(StatusOr.fromValue(savedData)); + if (savedError != null) { + watcher.onAmbientError(savedError); + } + } else if (savedError != null) { + watcher.onResourceChanged(StatusOr.fromStatus(savedError)); } else if (savedAbsent) { - watcher.onResourceDoesNotExist(resource); + watcher.onResourceChanged(StatusOr.fromStatus( + Status.NOT_FOUND.withDescription("Resource " + resource + " does not exist"))); } }); } @@ -768,8 +772,8 @@ class ResourceNotFound implements Runnable { public void run() { logger.log(XdsLogLevel.INFO, "{0} resource {1} initial fetch timeout", type, resource); - respTimer = null; onAbsent(null, activeCpc.getServerInfo()); + respTimer = null; } @Override @@ -825,8 +829,8 @@ void onData(ParsedResource parsedResource, String version, long updateTime, } ResourceUpdate oldData = this.data; this.data = parsedResource.getResourceUpdate(); - this.metadata = ResourceMetadata - .newResourceMetadataAcked(parsedResource.getRawResource(), version, updateTime); + this.metadata = ResourceMetadata.newResourceMetadataAcked( + parsedResource.getRawResource(), version, updateTime); absent = false; lastError = null; if (resourceDeletionIgnored) { @@ -836,11 +840,12 @@ void onData(ParsedResource parsedResource, String version, long updateTime, resourceDeletionIgnored = false; } if (!Objects.equals(oldData, data)) { + StatusOr update = StatusOr.fromValue(data); for (ResourceWatcher watcher : watchers.keySet()) { processingTracker.startTask(); watchers.get(watcher).execute(() -> { try { - notifyWatcher(watcher, data); + watcher.onResourceChanged(update); } finally { processingTracker.onComplete(); } @@ -871,6 +876,9 @@ void onAbsent(@Nullable ProcessingTracker processingTracker, ServerInfo serverIn serverInfo.target(), type, resource); resourceDeletionIgnored = true; } + Status deletionStatus = Status.NOT_FOUND.withDescription( + "Resource " + resource + " deleted from server"); + onAmbientError(deletionStatus, processingTracker); return; } @@ -879,21 +887,30 @@ void onAbsent(@Nullable ProcessingTracker processingTracker, ServerInfo serverIn data = null; absent = true; lastError = null; - metadata = serverInfo.resourceTimerIsTransientError() - ? ResourceMetadata.newResourceMetadataTimeout() - : ResourceMetadata.newResourceMetadataDoesNotExist(); - for (ResourceWatcher watcher : watchers.keySet()) { + + Status status; + if (respTimer == null) { + status = Status.NOT_FOUND.withDescription("Resource " + resource + " does not exist"); + metadata = ResourceMetadata.newResourceMetadataDoesNotExist(); + } else { + status = serverInfo.resourceTimerIsTransientError() + ? Status.UNAVAILABLE.withDescription( + "Timed out waiting for resource " + resource + " from xDS server") + : Status.NOT_FOUND.withDescription( + "Timed out waiting for resource " + resource + " from xDS server"); + metadata = serverInfo.resourceTimerIsTransientError() + ? ResourceMetadata.newResourceMetadataTimeout() + : ResourceMetadata.newResourceMetadataDoesNotExist(); + } + + StatusOr update = StatusOr.fromStatus(status); + for (Map.Entry, Executor> entry : watchers.entrySet()) { if (processingTracker != null) { processingTracker.startTask(); } - watchers.get(watcher).execute(() -> { + entry.getValue().execute(() -> { try { - if (serverInfo.resourceTimerIsTransientError()) { - watcher.onError(Status.UNAVAILABLE.withDescription( - "Timed out waiting for resource " + resource + " from xDS server")); - } else { - watcher.onResourceDoesNotExist(resource); - } + entry.getKey().onResourceChanged(update); } finally { if (processingTracker != null) { processingTracker.onComplete(); @@ -918,13 +935,37 @@ void onError(Status error, @Nullable ProcessingTracker tracker) { .withCause(error.getCause()); this.lastError = errorAugmented; - for (ResourceWatcher watcher : watchers.keySet()) { + if (data != null) { + // We have cached data, so this is an ambient error. + onAmbientError(errorAugmented, tracker); + } else { + // No data, this is a definitive resource error. + StatusOr update = StatusOr.fromStatus(errorAugmented); + for (Map.Entry, Executor> entry : watchers.entrySet()) { + if (tracker != null) { + tracker.startTask(); + } + entry.getValue().execute(() -> { + try { + entry.getKey().onResourceChanged(update); + } finally { + if (tracker != null) { + tracker.onComplete(); + } + } + }); + } + } + } + + private void onAmbientError(Status error, @Nullable ProcessingTracker tracker) { + for (Map.Entry, Executor> entry : watchers.entrySet()) { if (tracker != null) { tracker.startTask(); } - watchers.get(watcher).execute(() -> { + entry.getValue().execute(() -> { try { - watcher.onError(errorAugmented); + entry.getKey().onAmbientError(error); } finally { if (tracker != null) { tracker.onComplete(); @@ -939,10 +980,6 @@ void onRejected(String rejectedVersion, long rejectedTime, String rejectedDetail .newResourceMetadataNacked(metadata, rejectedVersion, rejectedTime, rejectedDetails, data != null); } - - private void notifyWatcher(ResourceWatcher watcher, T update) { - watcher.onChanged(update); - } } private class ResponseHandler implements XdsResponseHandler { @@ -991,7 +1028,12 @@ public void handleStreamClosed(Status status, boolean shouldTryFallback) { for (Map> subscriberMap : resourceSubscribers.values()) { for (ResourceSubscriber subscriber : subscriberMap.values()) { - if (subscriber.hasResult() || !authoritiesForClosedCpc.contains(subscriber.authority)) { + if (!authoritiesForClosedCpc.contains(subscriber.authority)) { + continue; + } + // If subscriber already has data, this is an ambient error. + if (subscriber.hasResult()) { + subscriber.onError(status, null); continue; } @@ -1008,7 +1050,6 @@ public void handleStreamClosed(Status status, boolean shouldTryFallback) { } } } - } private static class CpcWithFallbackState { diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java index 790f4445823..c1d3322faa2 100644 --- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java +++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java @@ -315,8 +315,10 @@ public void nonAggregateCluster_resourceNotExist_returnErrorPicker() { startXdsDepManager(); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS resource " + CLUSTER + " does not exist nodeID: " + NODE_ID); + String expectedDescription = "Error retrieving CDS resource " + CLUSTER + ": NOT_FOUND. " + + "Details: Timed out waiting for resource " + CLUSTER + + " from xDS server nodeID: " + NODE_ID; + Status unavailable = Status.UNAVAILABLE.withDescription(expectedDescription); assertPickerStatus(pickerCaptor.getValue(), unavailable); assertThat(childBalancers).isEmpty(); } @@ -372,8 +374,9 @@ public void nonAggregateCluster_resourceRevoked() { controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of()); assertThat(childBalancer.shutdown).isTrue(); - Status unavailable = Status.UNAVAILABLE.withDescription( - "CDS resource " + CLUSTER + " does not exist nodeID: " + NODE_ID); + String expectedDescription = "Error retrieving CDS resource " + CLUSTER + ": NOT_FOUND. " + + "Details: Resource " + CLUSTER + " does not exist nodeID: " + NODE_ID; + Status unavailable = Status.UNAVAILABLE.withDescription(expectedDescription); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); assertPickerStatus(pickerCaptor.getValue(), unavailable); @@ -583,8 +586,10 @@ public void aggregateCluster_noNonAggregateClusterExits_returnErrorPicker() { verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - Status status = Status.UNAVAILABLE.withDescription( - "CDS resource " + cluster1 + " does not exist nodeID: " + NODE_ID); + String expectedDescription = "Error retrieving CDS resource " + cluster1 + ": NOT_FOUND. " + + "Details: Timed out waiting for resource " + cluster1 + " from xDS server nodeID: " + + NODE_ID; + Status status = Status.UNAVAILABLE.withDescription(expectedDescription); assertPickerStatus(pickerCaptor.getValue(), status); assertThat(childBalancers).isEmpty(); } diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index 983ae17818c..5c710699a96 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -688,11 +688,10 @@ public void onlyEdsClusters_resourceNeverExist_returnErrorPicker() { verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - assertPicker( - pickerCaptor.getValue(), - Status.UNAVAILABLE.withDescription( - "CDS resource " + CLUSTER + " does not exist nodeID: node-id"), - null); + String expectedDescription = "Error retrieving CDS resource " + CLUSTER + ": NOT_FOUND. " + + "Details: Timed out waiting for resource " + CLUSTER + " from xDS server nodeID: node-id"; + Status expectedError = Status.UNAVAILABLE.withDescription(expectedDescription); + assertPicker(pickerCaptor.getValue(), expectedError, null); } @Test @@ -712,8 +711,10 @@ public void cdsMissing_handledDirectly() { assertThat(childBalancers).hasSize(0); // no child LB policy created verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - Status expectedError = Status.UNAVAILABLE.withDescription( - "CDS resource " + CLUSTER + " does not exist nodeID: node-id"); + String expectedDescription = "Error retrieving CDS resource " + CLUSTER + ": NOT_FOUND. " + + "Details: Timed out waiting for resource " + CLUSTER + " from xDS server nodeID: node-id"; + Status expectedError = Status.UNAVAILABLE.withDescription(expectedDescription); + assertPicker(pickerCaptor.getValue(), expectedError, null); assertPicker(pickerCaptor.getValue(), expectedError, null); } @@ -741,8 +742,9 @@ public void cdsRevoked_handledDirectly() { controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of()); verify(helper).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); - Status expectedError = Status.UNAVAILABLE.withDescription( - "CDS resource " + CLUSTER + " does not exist nodeID: node-id"); + String expectedDescription = "Error retrieving CDS resource " + CLUSTER + ": NOT_FOUND. " + + "Details: Resource " + CLUSTER + " does not exist nodeID: node-id"; + Status expectedError = Status.UNAVAILABLE.withDescription(expectedDescription); assertPicker(pickerCaptor.getValue(), expectedError, null); assertThat(childBalancer.shutdown).isTrue(); } @@ -756,8 +758,10 @@ public void edsMissing_handledByChildPolicy() { FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.upstreamError).isNotNull(); assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Status.Code.UNAVAILABLE); - assertThat(childBalancer.upstreamError.getDescription()) - .isEqualTo("EDS resource " + EDS_SERVICE_NAME + " does not exist nodeID: node-id"); + String expectedDescription = "Error retrieving EDS resource " + EDS_SERVICE_NAME + + ": NOT_FOUND. Details: Timed out waiting for resource " + EDS_SERVICE_NAME + + " from xDS server nodeID: node-id"; + assertThat(childBalancer.upstreamError.getDescription()).isEqualTo(expectedDescription); assertThat(childBalancer.shutdown).isFalse(); } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 9ff19c6d1b0..6b9d601b2cf 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -19,12 +19,14 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -56,6 +58,7 @@ import io.grpc.Server; import io.grpc.Status; import io.grpc.Status.Code; +import io.grpc.StatusOr; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.internal.BackoffPolicy; @@ -126,7 +129,6 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; import org.mockito.Captor; import org.mockito.InOrder; import org.mockito.Mock; @@ -251,15 +253,13 @@ public long currentTimeNanos() { private Message lbEndpointZeroWeight; private Any testClusterLoadAssignment; @Captor - private ArgumentCaptor ldsUpdateCaptor; + private ArgumentCaptor> ldsUpdateCaptor; @Captor - private ArgumentCaptor rdsUpdateCaptor; + private ArgumentCaptor> rdsUpdateCaptor; @Captor - private ArgumentCaptor cdsUpdateCaptor; + private ArgumentCaptor> cdsUpdateCaptor; @Captor - private ArgumentCaptor edsUpdateCaptor; - @Captor - private ArgumentCaptor errorCaptor; + private ArgumentCaptor> edsUpdateCaptor; @Mock private BackoffPolicy.Provider backoffPolicyProvider; @@ -683,7 +683,10 @@ public void ldsResourceNotFound() { verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); // Server failed to return subscribed resource within expected time window. fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(ldsResourceWatcher).onResourceDoesNotExist(LDS_RESOURCE); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isFalse(); + assertThat(statusOrUpdate.getStatus().getCode()).isEqualTo(Status.Code.NOT_FOUND); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataDoesNotExist(LDS, LDS_RESOURCE); // Check metric data. @@ -696,11 +699,11 @@ public void ldsResourceUpdated_withXdstpResourceName_withUnknownAuthority() { "xdstp://unknown.example.com/envoy.config.listener.v3.Listener/listener1"; xdsClient.watchXdsResource(XdsListenerResource.getInstance(), ldsResourceName, ldsResourceWatcher); - verify(ldsResourceWatcher).onError(errorCaptor.capture()); - Status error = errorCaptor.getValue(); - assertThat(error.getCode()).isEqualTo(Code.INVALID_ARGUMENT); - assertThat(error.getDescription()).isEqualTo( - "Wrong configuration: xds server does not exist for resource " + ldsResourceName); + verify(ldsResourceWatcher).onResourceChanged(argThat(statusOr -> + !statusOr.hasValue() + && statusOr.getStatus().getCode() == Status.Code.INVALID_ARGUMENT + && statusOr.getStatus().getDescription().equals( + "Wrong configuration: xds server does not exist for resource " + ldsResourceName))); assertThat(resourceDiscoveryCalls.poll()).isNull(); xdsClient.cancelXdsResourceWatch(XdsListenerResource.getInstance(), ldsResourceName, ldsResourceWatcher); @@ -713,13 +716,20 @@ public void ldsResource_onError_cachedForNewWatcher() { ldsResourceWatcher); DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); call.sendCompleted(); - verify(ldsResourceWatcher).onError(errorCaptor.capture()); - Status initialError = errorCaptor.getValue(); + @SuppressWarnings("unchecked") + ArgumentCaptor> errorCaptor = + ArgumentCaptor.forClass(StatusOr.class); + verify(ldsResourceWatcher, timeout(1000)).onResourceChanged(errorCaptor.capture()); + StatusOr initialError = errorCaptor.getValue(); + assertThat(initialError.hasValue()).isFalse(); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher2); - ArgumentCaptor secondErrorCaptor = ArgumentCaptor.forClass(Status.class); - verify(ldsResourceWatcher2).onError(secondErrorCaptor.capture()); - Status cachedError = secondErrorCaptor.getValue(); + @SuppressWarnings("unchecked") + ArgumentCaptor> secondErrorCaptor = + ArgumentCaptor.forClass(StatusOr.class); + verify(ldsResourceWatcher2, timeout(1000)).onResourceChanged(secondErrorCaptor.capture()); + StatusOr cachedError = secondErrorCaptor.getValue(); assertThat(cachedError).isEqualTo(initialError); assertThat(resourceDiscoveryCalls.poll()).isNull(); @@ -760,7 +770,7 @@ public void ldsResponseErrorHandling_someResourcesFailedUnpack() { verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); // The response is NACKed with the same error message. call.verifyRequestNack(LDS, LDS_RESOURCE, "", "0000", NODE, errors); - verify(ldsResourceWatcher).onChanged(any(LdsUpdate.class)); + verify(ldsResourceWatcher).onResourceChanged(any()); } /** @@ -928,8 +938,10 @@ public void ldsResourceFound_containsVirtualHosts() { // Client sends an ACK LDS request. call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); @@ -943,8 +955,10 @@ public void wrappedLdsResource() { // Client sends an ACK LDS request. call.sendResponse(LDS, mf.buildWrappedResource(testListenerVhosts), VERSION_1, "0000"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); @@ -962,8 +976,10 @@ public void wrappedLdsResource_preferWrappedName() { call.sendResponse(LDS, mf.buildWrappedResourceWithName(innerResource, LDS_RESOURCE), VERSION_1, "0000"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, innerResource, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); @@ -977,8 +993,10 @@ public void ldsResourceFound_containsRdsName() { // Client sends an ACK LDS request. call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerRds(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerRds(statusOrUpdate.getValue()); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerRds, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); @@ -996,8 +1014,10 @@ public void cachedLdsResource_data() { ResourceWatcher watcher = mock(ResourceWatcher.class); xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, watcher); - verify(watcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerRds(ldsUpdateCaptor.getValue()); + verify(watcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerRds(statusOrUpdate.getValue()); call.verifyNoMoreRequest(); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerRds, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); @@ -1009,11 +1029,17 @@ public void cachedLdsResource_absent() { DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(ldsResourceWatcher).onResourceDoesNotExist(LDS_RESOURCE); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isFalse(); + assertThat(statusOrUpdate.getStatus().getCode()).isEqualTo(Status.Code.NOT_FOUND); // Add another watcher. ResourceWatcher watcher = mock(ResourceWatcher.class); xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, watcher); - verify(watcher).onResourceDoesNotExist(LDS_RESOURCE); + verify(watcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate1 = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate1.hasValue()).isFalse(); + assertThat(statusOrUpdate1.getStatus().getCode()).isEqualTo(Status.Code.NOT_FOUND); call.verifyNoMoreRequest(); verifyResourceMetadataDoesNotExist(LDS, LDS_RESOURCE); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); @@ -1028,15 +1054,19 @@ public void ldsResourceUpdated() { // Initial LDS response. call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_1, TIME_INCREMENT); // Updated LDS response. call.sendResponse(LDS, testListenerRds, VERSION_2, "0001"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_2, "0001", NODE); - verify(ldsResourceWatcher, times(2)).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerRds(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher, times(2)).onResourceChanged(ldsUpdateCaptor.capture()); + statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerRds(statusOrUpdate.getValue()); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerRds, VERSION_2, TIME_INCREMENT * 2); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); assertThat(channelForCustomAuthority).isNull(); @@ -1052,8 +1082,10 @@ public void cancelResourceWatcherNotRemoveUrlSubscribers() { // Initial LDS response. call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_1, TIME_INCREMENT); xdsClient.watchXdsResource(XdsListenerResource.getInstance(), @@ -1066,8 +1098,10 @@ public void cancelResourceWatcherNotRemoveUrlSubscribers() { mf.buildRouteConfiguration("new", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); call.sendResponse(LDS, testListenerVhosts2, VERSION_2, "0001"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_2, "0001", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts2, VERSION_2, TIME_INCREMENT * 2); } @@ -1085,8 +1119,10 @@ public void ldsResourceUpdated_withXdstpResourceName() { mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); call.verifyRequest(LDS, ldsResourceName, VERSION_1, "0000", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); verifyResourceMetadataAcked( LDS, ldsResourceName, testListenerVhosts, VERSION_1, TIME_INCREMENT); } @@ -1103,8 +1139,10 @@ public void ldsResourceUpdated_withXdstpResourceName_withEmptyAuthority() { mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); call.verifyRequest(LDS, ldsResourceName, VERSION_1, "0000", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); verifyResourceMetadataAcked( LDS, ldsResourceName, testListenerVhosts, VERSION_1, TIME_INCREMENT); } @@ -1172,8 +1210,12 @@ public void rdsResourceUpdated_withXdstpResourceName_unknownAuthority() { "xdstp://unknown.example.com/envoy.config.route.v3.RouteConfiguration/route1"; xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), rdsResourceName, rdsResourceWatcher); - verify(rdsResourceWatcher).onError(errorCaptor.capture()); - Status error = errorCaptor.getValue(); + @SuppressWarnings("unchecked") + ArgumentCaptor> rdsUpdateCaptor = ArgumentCaptor.forClass(StatusOr.class); + verify(rdsResourceWatcher).onResourceChanged(rdsUpdateCaptor.capture()); + StatusOr capturedUpdate = rdsUpdateCaptor.getValue(); + assertThat(capturedUpdate.hasValue()).isFalse(); + Status error = capturedUpdate.getStatus(); assertThat(error.getCode()).isEqualTo(Code.INVALID_ARGUMENT); assertThat(error.getDescription()).isEqualTo( "Wrong configuration: xds server does not exist for resource " + rdsResourceName); @@ -1207,8 +1249,12 @@ public void cdsResourceUpdated_withXdstpResourceName_unknownAuthority() { String cdsResourceName = "xdstp://unknown.example.com/envoy.config.cluster.v3.Cluster/cluster1"; xdsClient.watchXdsResource(XdsClusterResource.getInstance(), cdsResourceName, cdsResourceWatcher); - verify(cdsResourceWatcher).onError(errorCaptor.capture()); - Status error = errorCaptor.getValue(); + @SuppressWarnings("unchecked") + ArgumentCaptor> cdsUpdateCaptor = ArgumentCaptor.forClass(StatusOr.class); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr capturedUpdate = cdsUpdateCaptor.getValue(); + assertThat(capturedUpdate.hasValue()).isFalse(); + Status error = capturedUpdate.getStatus(); assertThat(error.getCode()).isEqualTo(Code.INVALID_ARGUMENT); assertThat(error.getDescription()).isEqualTo( "Wrong configuration: xds server does not exist for resource " + cdsResourceName); @@ -1247,8 +1293,12 @@ public void edsResourceUpdated_withXdstpResourceName_unknownAuthority() { "xdstp://unknown.example.com/envoy.config.endpoint.v3.ClusterLoadAssignment/cluster1"; xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), edsResourceName, edsResourceWatcher); - verify(edsResourceWatcher).onError(errorCaptor.capture()); - Status error = errorCaptor.getValue(); + @SuppressWarnings("unchecked") + ArgumentCaptor> edsUpdateCaptor = ArgumentCaptor.forClass(StatusOr.class); + verify(edsResourceWatcher).onResourceChanged(edsUpdateCaptor.capture()); + StatusOr capturedUpdate = edsUpdateCaptor.getValue(); + assertThat(capturedUpdate.hasValue()).isFalse(); + Status error = capturedUpdate.getStatus(); assertThat(error.getCode()).isEqualTo(Code.INVALID_ARGUMENT); assertThat(error.getDescription()).isEqualTo( "Wrong configuration: xds server does not exist for resource " + edsResourceName); @@ -1297,11 +1347,13 @@ public void ldsResourceUpdate_withFaultInjection() { // Client sends an ACK LDS request. call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, listener, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); - LdsUpdate ldsUpdate = ldsUpdateCaptor.getValue(); + LdsUpdate ldsUpdate = statusOrUpdate.getValue(); assertThat(ldsUpdate.httpConnectionManager().virtualHosts()).hasSize(2); assertThat(ldsUpdate.httpConnectionManager().httpFilterConfigs().get(0).name) .isEqualTo("envoy.fault"); @@ -1326,6 +1378,7 @@ public void ldsResourceUpdate_withFaultInjection() { @Test public void ldsResourceDeleted() { Assume.assumeFalse(ignoreResourceDeletion()); + InOrder inOrder = inOrder(ldsResourceWatcher); DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); @@ -1334,15 +1387,20 @@ public void ldsResourceDeleted() { // Initial LDS response. call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + inOrder.verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); // Empty LDS response deletes the listener. call.sendResponse(LDS, Collections.emptyList(), VERSION_2, "0001"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_2, "0001", NODE); - verify(ldsResourceWatcher).onResourceDoesNotExist(LDS_RESOURCE); + inOrder.verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate1 = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate1.hasValue()).isFalse(); + assertThat(statusOrUpdate1.getStatus().getCode()).isEqualTo(Status.Code.NOT_FOUND); verifyResourceMetadataDoesNotExist(LDS, LDS_RESOURCE); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); } @@ -1362,8 +1420,8 @@ public void ldsResourceDeleted_ignoreResourceDeletion() { // Initial LDS response. call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue().getValue()); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); @@ -1371,19 +1429,21 @@ public void ldsResourceDeleted_ignoreResourceDeletion() { call.sendResponse(LDS, Collections.emptyList(), VERSION_2, "0001"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_2, "0001", NODE); // The resource is still ACKED at VERSION_1 (no changes). + verify(ldsResourceWatcher).onAmbientError( + argThat(status -> status.getCode() == Status.Code.NOT_FOUND)); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); - // onResourceDoesNotExist not called - verify(ldsResourceWatcher, never()).onResourceDoesNotExist(LDS_RESOURCE); // Next update is correct, and contains the listener again. - call.sendResponse(LDS, testListenerVhosts, VERSION_3, "0003"); + Any updatedListener = Any.pack(mf.buildListenerWithApiListener(LDS_RESOURCE, + mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE + 1)))); + call.sendResponse(LDS, updatedListener, VERSION_3, "0003"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_3, "0003", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher, times(2)).onResourceChanged(ldsUpdateCaptor.capture()); + assertThat(ldsUpdateCaptor.getValue().getValue().httpConnectionManager().virtualHosts()) + .hasSize(VHOST_SIZE + 1); // LDS is now ACKEd at VERSION_3. - verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_3, - TIME_INCREMENT * 3); + verifyResourceMetadataAcked(LDS, LDS_RESOURCE, updatedListener, VERSION_3, TIME_INCREMENT * 3); verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); verifyNoMoreInteractions(ldsResourceWatcher); } @@ -1405,9 +1465,12 @@ public void multipleLdsWatchers() { verifySubscribedResourcesMetadataSizes(2, 0, 0, 0); fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(ldsResourceWatcher).onResourceDoesNotExist(LDS_RESOURCE); - verify(watcher1).onResourceDoesNotExist(ldsResourceTwo); - verify(watcher2).onResourceDoesNotExist(ldsResourceTwo); + verify(ldsResourceWatcher).onResourceChanged(argThat(statusOr -> + !statusOr.hasValue() && statusOr.getStatus().getDescription().contains(LDS_RESOURCE))); + verify(watcher1).onResourceChanged(argThat(statusOr -> + !statusOr.hasValue() && statusOr.getStatus().getDescription().contains(ldsResourceTwo))); + verify(watcher2).onResourceChanged(argThat(statusOr -> + !statusOr.hasValue() && statusOr.getStatus().getDescription().contains(ldsResourceTwo))); verifyResourceMetadataDoesNotExist(LDS, LDS_RESOURCE); verifyResourceMetadataDoesNotExist(LDS, ldsResourceTwo); verifySubscribedResourcesMetadataSizes(2, 0, 0, 0); @@ -1415,16 +1478,22 @@ public void multipleLdsWatchers() { Any listenerTwo = Any.pack(mf.buildListenerWithApiListenerForRds(ldsResourceTwo, RDS_RESOURCE)); call.sendResponse(LDS, ImmutableList.of(testListenerVhosts, listenerTwo), VERSION_1, "0000"); // ResourceWatcher called with listenerVhosts. - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher, times(2)).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); // watcher1 called with listenerTwo. - verify(watcher1).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerRds(ldsUpdateCaptor.getValue()); - assertThat(ldsUpdateCaptor.getValue().httpConnectionManager().virtualHosts()).isNull(); + verify(watcher1, times(2)).onResourceChanged(ldsUpdateCaptor.capture()); + statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerRds(statusOrUpdate.getValue()); + assertThat(statusOrUpdate.getValue().httpConnectionManager().virtualHosts()).isNull(); // watcher2 called with listenerTwo. - verify(watcher2).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerRds(ldsUpdateCaptor.getValue()); - assertThat(ldsUpdateCaptor.getValue().httpConnectionManager().virtualHosts()).isNull(); + verify(watcher2, times(2)).onResourceChanged(ldsUpdateCaptor.capture()); + statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerRds(statusOrUpdate.getValue()); + assertThat(statusOrUpdate.getValue().httpConnectionManager().virtualHosts()).isNull(); // Metadata of both listeners is stored. verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(LDS, ldsResourceTwo, listenerTwo, VERSION_1, TIME_INCREMENT); @@ -1446,7 +1515,8 @@ public void rdsResourceNotFound() { verifySubscribedResourcesMetadataSizes(0, 0, 1, 0); // Server failed to return subscribed resource within expected time window. fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(rdsResourceWatcher).onResourceDoesNotExist(RDS_RESOURCE); + verify(rdsResourceWatcher).onResourceChanged(argThat( + arg -> !arg.hasValue() && arg.getStatus().getDescription().contains(RDS_RESOURCE))); assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataDoesNotExist(RDS, RDS_RESOURCE); verifySubscribedResourcesMetadataSizes(0, 0, 1, 0); @@ -1487,7 +1557,7 @@ public void rdsResponseErrorHandling_someResourcesFailedUnpack() { verifySubscribedResourcesMetadataSizes(0, 0, 1, 0); // The response is NACKed with the same error message. call.verifyRequestNack(RDS, RDS_RESOURCE, "", "0000", NODE, errors); - verify(rdsResourceWatcher).onChanged(any(RdsUpdate.class)); + verify(rdsResourceWatcher).onResourceChanged(any()); } @Test @@ -1495,6 +1565,7 @@ public void rdsResponseErrorHandling_nackWeightedSumZero() { DiscoveryRpcCall call = startResourceWatcher(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, rdsResourceWatcher); verifyResourceMetadataRequested(RDS, RDS_RESOURCE); + String expectedErrorDetail = "Sum of cluster weights should be above 0"; io.envoyproxy.envoy.config.route.v3.RouteAction routeAction = io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() @@ -1528,11 +1599,14 @@ public void rdsResponseErrorHandling_nackWeightedSumZero() { "RDS response RouteConfiguration \'route-configuration.googleapis.com\' validation error: " + "RouteConfiguration contains invalid virtual host: Virtual host [do not care] " + "contains invalid route : Route [route-blade] contains invalid RouteAction: " - + "Sum of cluster weights should be above 0."); + + expectedErrorDetail); verifySubscribedResourcesMetadataSizes(0, 0, 1, 0); // The response is NACKed with the same error message. call.verifyRequestNack(RDS, RDS_RESOURCE, "", "0000", NODE, errors); - verify(rdsResourceWatcher, never()).onChanged(any(RdsUpdate.class)); + verify(rdsResourceWatcher).onResourceChanged(argThat( + statusOr -> !statusOr.hasValue() && statusOr.getStatus().getDescription() + .contains(expectedErrorDetail))); + verify(rdsResourceWatcher, never()).onResourceChanged(argThat(StatusOr::hasValue)); } /** @@ -1614,8 +1688,10 @@ public void rdsResourceFound() { // Client sends an ACK RDS request. call.verifyRequest(RDS, RDS_RESOURCE, VERSION_1, "0000", NODE); - verify(rdsResourceWatcher).onChanged(rdsUpdateCaptor.capture()); - verifyGoldenRouteConfig(rdsUpdateCaptor.getValue()); + verify(rdsResourceWatcher).onResourceChanged(rdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = rdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenRouteConfig(statusOrUpdate.getValue()); assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataAcked(RDS, RDS_RESOURCE, testRouteConfig, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(0, 0, 1, 0); @@ -1629,8 +1705,10 @@ public void wrappedRdsResource() { // Client sends an ACK RDS request. call.verifyRequest(RDS, RDS_RESOURCE, VERSION_1, "0000", NODE); - verify(rdsResourceWatcher).onChanged(rdsUpdateCaptor.capture()); - verifyGoldenRouteConfig(rdsUpdateCaptor.getValue()); + verify(rdsResourceWatcher).onResourceChanged(rdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = rdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenRouteConfig(statusOrUpdate.getValue()); assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataAcked(RDS, RDS_RESOURCE, testRouteConfig, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(0, 0, 1, 0); @@ -1648,8 +1726,10 @@ public void cachedRdsResource_data() { ResourceWatcher watcher = mock(ResourceWatcher.class); xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, watcher); - verify(watcher).onChanged(rdsUpdateCaptor.capture()); - verifyGoldenRouteConfig(rdsUpdateCaptor.getValue()); + verify(watcher).onResourceChanged(rdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = rdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenRouteConfig(statusOrUpdate.getValue()); call.verifyNoMoreRequest(); verifyResourceMetadataAcked(RDS, RDS_RESOURCE, testRouteConfig, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(0, 0, 1, 0); @@ -1661,11 +1741,15 @@ public void cachedRdsResource_absent() { DiscoveryRpcCall call = startResourceWatcher(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, rdsResourceWatcher); fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(rdsResourceWatcher).onResourceDoesNotExist(RDS_RESOURCE); + verify(rdsResourceWatcher).onResourceChanged(argThat(statusOr -> + !statusOr.hasValue() && statusOr.getStatus().getDescription().contains(RDS_RESOURCE) + && statusOr.getStatus().getDescription().contains(RDS_RESOURCE))); // Add another watcher. ResourceWatcher watcher = mock(ResourceWatcher.class); xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, watcher); - verify(watcher).onResourceDoesNotExist(RDS_RESOURCE); + verify(watcher).onResourceChanged(argThat(statusOr -> + !statusOr.hasValue() && statusOr.getStatus().getDescription().contains(RDS_RESOURCE) + && statusOr.getStatus().getDescription().contains(RDS_RESOURCE))); call.verifyNoMoreRequest(); verifyResourceMetadataDoesNotExist(RDS, RDS_RESOURCE); verifySubscribedResourcesMetadataSizes(0, 0, 1, 0); @@ -1680,8 +1764,10 @@ public void rdsResourceUpdated() { // Initial RDS response. call.sendResponse(RDS, testRouteConfig, VERSION_1, "0000"); call.verifyRequest(RDS, RDS_RESOURCE, VERSION_1, "0000", NODE); - verify(rdsResourceWatcher).onChanged(rdsUpdateCaptor.capture()); - verifyGoldenRouteConfig(rdsUpdateCaptor.getValue()); + verify(rdsResourceWatcher).onResourceChanged(rdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = rdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenRouteConfig(statusOrUpdate.getValue()); verifyResourceMetadataAcked(RDS, RDS_RESOURCE, testRouteConfig, VERSION_1, TIME_INCREMENT); // Updated RDS response. @@ -1691,8 +1777,10 @@ public void rdsResourceUpdated() { // Client sends an ACK RDS request. call.verifyRequest(RDS, RDS_RESOURCE, VERSION_2, "0001", NODE); - verify(rdsResourceWatcher, times(2)).onChanged(rdsUpdateCaptor.capture()); - assertThat(rdsUpdateCaptor.getValue().virtualHosts).hasSize(4); + verify(rdsResourceWatcher, times(2)).onResourceChanged(rdsUpdateCaptor.capture()); + statusOrUpdate = rdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + assertThat(statusOrUpdate.getValue().virtualHosts).hasSize(4); verifyResourceMetadataAcked(RDS, RDS_RESOURCE, routeConfigUpdated, VERSION_2, TIME_INCREMENT * 2); } @@ -1739,15 +1827,19 @@ public void rdsResourceDeletedByLdsApiListener() { DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); call.sendResponse(LDS, testListenerRds, VERSION_1, "0000"); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerRds(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerRds(statusOrUpdate.getValue()); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerRds, VERSION_1, TIME_INCREMENT); verifyResourceMetadataRequested(RDS, RDS_RESOURCE); verifySubscribedResourcesMetadataSizes(1, 0, 1, 0); call.sendResponse(RDS, testRouteConfig, VERSION_1, "0000"); - verify(rdsResourceWatcher).onChanged(rdsUpdateCaptor.capture()); - verifyGoldenRouteConfig(rdsUpdateCaptor.getValue()); + verify(rdsResourceWatcher).onResourceChanged(rdsUpdateCaptor.capture()); + StatusOr statusOrUpdate1 = rdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenRouteConfig(statusOrUpdate1.getValue()); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerRds, VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(RDS, RDS_RESOURCE, testRouteConfig, VERSION_1, TIME_INCREMENT * 2); verifySubscribedResourcesMetadataSizes(1, 0, 1, 0); @@ -1757,8 +1849,10 @@ public void rdsResourceDeletedByLdsApiListener() { // Note that this must work the same despite the ignore_resource_deletion feature is on. // This happens because the Listener is getting replaced, and not deleted. call.sendResponse(LDS, testListenerVhosts, VERSION_2, "0001"); - verify(ldsResourceWatcher, times(2)).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + verify(ldsResourceWatcher, times(2)).onResourceChanged(ldsUpdateCaptor.capture()); + statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); verifyNoMoreInteractions(rdsResourceWatcher); verifyResourceMetadataAcked(RDS, RDS_RESOURCE, testRouteConfig, VERSION_1, TIME_INCREMENT * 2); verifyResourceMetadataAcked( @@ -1790,11 +1884,13 @@ public void rdsResourcesDeletedByLdsTcpListener() { // referencing RDS_RESOURCE. DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); call.sendResponse(LDS, packedListener, VERSION_1, "0000"); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); - assertThat(ldsUpdateCaptor.getValue().listener().filterChains()).hasSize(1); + assertThat(statusOrUpdate.getValue().listener().filterChains()).hasSize(1); FilterChain parsedFilterChain = Iterables.getOnlyElement( - ldsUpdateCaptor.getValue().listener().filterChains()); + statusOrUpdate.getValue().listener().filterChains()); assertThat(parsedFilterChain.httpConnectionManager().rdsName()).isEqualTo(RDS_RESOURCE); verifyResourceMetadataAcked(LDS, LISTENER_RESOURCE, packedListener, VERSION_1, TIME_INCREMENT); verifyResourceMetadataRequested(RDS, RDS_RESOURCE); @@ -1802,8 +1898,10 @@ public void rdsResourcesDeletedByLdsTcpListener() { // Simulates receiving the requested RDS resource. call.sendResponse(RDS, testRouteConfig, VERSION_1, "0000"); - verify(rdsResourceWatcher).onChanged(rdsUpdateCaptor.capture()); - verifyGoldenRouteConfig(rdsUpdateCaptor.getValue()); + verify(rdsResourceWatcher).onResourceChanged(rdsUpdateCaptor.capture()); + StatusOr statusOrUpdate1 = rdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenRouteConfig(statusOrUpdate1.getValue()); verifyResourceMetadataAcked(RDS, RDS_RESOURCE, testRouteConfig, VERSION_1, TIME_INCREMENT * 2); // Simulates receiving an updated version of the requested LDS resource as a TCP listener @@ -1821,12 +1919,15 @@ public void rdsResourcesDeletedByLdsTcpListener() { packedListener = Any.pack(mf.buildListenerWithFilterChain(LISTENER_RESOURCE, 7000, "0.0.0.0", filterChain)); call.sendResponse(LDS, packedListener, VERSION_2, "0001"); - verify(ldsResourceWatcher, times(2)).onChanged(ldsUpdateCaptor.capture()); - assertThat(ldsUpdateCaptor.getValue().listener().filterChains()).hasSize(1); + verify(ldsResourceWatcher, times(2)).onResourceChanged(ldsUpdateCaptor.capture()); + statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + assertThat(statusOrUpdate.getValue().listener().filterChains()).hasSize(1); parsedFilterChain = Iterables.getOnlyElement( - ldsUpdateCaptor.getValue().listener().filterChains()); + statusOrUpdate.getValue().listener().filterChains()); assertThat(parsedFilterChain.httpConnectionManager().virtualHosts()).hasSize(VHOST_SIZE); - verify(rdsResourceWatcher, never()).onResourceDoesNotExist(RDS_RESOURCE); + verify(rdsResourceWatcher, never()).onResourceChanged(argThat(statusOr -> + !statusOr.hasValue() && statusOr.getStatus().getDescription().equals(RDS_RESOURCE))); verifyResourceMetadataAcked(RDS, RDS_RESOURCE, testRouteConfig, VERSION_1, TIME_INCREMENT * 2); verifyResourceMetadataAcked( LDS, LISTENER_RESOURCE, packedListener, VERSION_2, TIME_INCREMENT * 3); @@ -1851,16 +1952,25 @@ public void multipleRdsWatchers() { verifySubscribedResourcesMetadataSizes(0, 0, 2, 0); fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(rdsResourceWatcher).onResourceDoesNotExist(RDS_RESOURCE); - verify(watcher1).onResourceDoesNotExist(rdsResourceTwo); - verify(watcher2).onResourceDoesNotExist(rdsResourceTwo); + verify(rdsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Status.Code.NOT_FOUND)); + verify(watcher1).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Status.Code.NOT_FOUND)); + verify(watcher2).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Status.Code.NOT_FOUND)); verifyResourceMetadataDoesNotExist(RDS, RDS_RESOURCE); verifyResourceMetadataDoesNotExist(RDS, rdsResourceTwo); verifySubscribedResourcesMetadataSizes(0, 0, 2, 0); call.sendResponse(RDS, testRouteConfig, VERSION_1, "0000"); - verify(rdsResourceWatcher).onChanged(rdsUpdateCaptor.capture()); - verifyGoldenRouteConfig(rdsUpdateCaptor.getValue()); + ArgumentCaptor> rdsUpdateCaptor = ArgumentCaptor.forClass(StatusOr.class); + verify(rdsResourceWatcher, times(2)).onResourceChanged(rdsUpdateCaptor.capture()); + StatusOr capturedUpdate1 = rdsUpdateCaptor.getAllValues().get(1); + assertThat(capturedUpdate1.hasValue()).isTrue(); + verifyGoldenRouteConfig(capturedUpdate1.getValue()); verifyNoMoreInteractions(watcher1, watcher2); verifyResourceMetadataAcked(RDS, RDS_RESOURCE, testRouteConfig, VERSION_1, TIME_INCREMENT); verifyResourceMetadataDoesNotExist(RDS, rdsResourceTwo); @@ -1869,13 +1979,22 @@ public void multipleRdsWatchers() { Any routeConfigTwo = Any.pack(mf.buildRouteConfiguration(rdsResourceTwo, mf.buildOpaqueVirtualHosts(4))); call.sendResponse(RDS, routeConfigTwo, VERSION_2, "0002"); - verify(watcher1).onChanged(rdsUpdateCaptor.capture()); - assertThat(rdsUpdateCaptor.getValue().virtualHosts).hasSize(4); - verify(watcher2).onChanged(rdsUpdateCaptor.capture()); - assertThat(rdsUpdateCaptor.getValue().virtualHosts).hasSize(4); + ArgumentCaptor> watcher1Captor = + ArgumentCaptor.forClass(StatusOr.class); + verify(watcher1, times(2)).onResourceChanged(watcher1Captor.capture()); + StatusOr capturedUpdate2 = watcher1Captor.getAllValues().get(1); + assertThat(capturedUpdate2.hasValue()).isTrue(); + assertThat(capturedUpdate2.getValue().virtualHosts).hasSize(4); + ArgumentCaptor> watcher2Captor = + ArgumentCaptor.forClass(StatusOr.class); + verify(watcher2, times(2)).onResourceChanged(watcher2Captor.capture()); + StatusOr capturedUpdate3 = watcher2Captor.getAllValues().get(1); + assertThat(capturedUpdate3.hasValue()).isTrue(); + assertThat(capturedUpdate3.getValue().virtualHosts).hasSize(4); verifyNoMoreInteractions(rdsResourceWatcher); verifyResourceMetadataAcked(RDS, RDS_RESOURCE, testRouteConfig, VERSION_1, TIME_INCREMENT); - verifyResourceMetadataAcked(RDS, rdsResourceTwo, routeConfigTwo, VERSION_2, TIME_INCREMENT * 2); + verifyResourceMetadataAcked(RDS, rdsResourceTwo, routeConfigTwo, VERSION_2, + TIME_INCREMENT * 2); verifySubscribedResourcesMetadataSizes(0, 0, 2, 0); } @@ -1898,7 +2017,8 @@ public void cdsResourceNotFound() { verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); // Server failed to return subscribed resource within expected time window. fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(cdsResourceWatcher).onResourceDoesNotExist(CDS_RESOURCE); + verify(cdsResourceWatcher).onResourceChanged(argThat( + arg -> !arg.hasValue() && arg.getStatus().getDescription().contains(CDS_RESOURCE))); assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataDoesNotExist(CDS, CDS_RESOURCE); verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); @@ -1940,7 +2060,7 @@ public void cdsResponseErrorHandling_someResourcesFailedUnpack() { verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); // The response is NACKed with the same error message. call.verifyRequestNack(CDS, CDS_RESOURCE, "", "0000", NODE, errors); - verify(cdsResourceWatcher).onChanged(any(CdsUpdate.class)); + verify(cdsResourceWatcher).onResourceChanged(any()); } /** @@ -2130,8 +2250,10 @@ public void cdsResourceFound() { // Client sent an ACK CDS request. call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - verifyGoldenClusterRoundRobin(cdsUpdateCaptor.getValue()); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenClusterRoundRobin(statusOrUpdate.getValue()); assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataAcked(CDS, CDS_RESOURCE, testClusterRoundRobin, VERSION_1, TIME_INCREMENT); @@ -2146,8 +2268,10 @@ public void wrappedCdsResource() { // Client sent an ACK CDS request. call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - verifyGoldenClusterRoundRobin(cdsUpdateCaptor.getValue()); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenClusterRoundRobin(statusOrUpdate.getValue()); assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataAcked(CDS, CDS_RESOURCE, testClusterRoundRobin, VERSION_1, TIME_INCREMENT); @@ -2167,8 +2291,10 @@ public void cdsResourceFound_leastRequestLbPolicy() { // Client sent an ACK CDS request. call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + CdsUpdate cdsUpdate = statusOrUpdate.getValue(); assertThat(cdsUpdate.clusterName()).isEqualTo(CDS_RESOURCE); assertThat(cdsUpdate.clusterType()).isEqualTo(ClusterType.EDS); assertThat(cdsUpdate.edsServiceName()).isNull(); @@ -2199,8 +2325,10 @@ public void cdsResourceFound_ringHashLbPolicy() { // Client sent an ACK CDS request. call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + CdsUpdate cdsUpdate = statusOrUpdate.getValue(); assertThat(cdsUpdate.clusterName()).isEqualTo(CDS_RESOURCE); assertThat(cdsUpdate.clusterType()).isEqualTo(ClusterType.EDS); assertThat(cdsUpdate.edsServiceName()).isNull(); @@ -2230,8 +2358,10 @@ public void cdsResponseWithAggregateCluster() { // Client sent an ACK CDS request. call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + CdsUpdate cdsUpdate = statusOrUpdate.getValue(); assertThat(cdsUpdate.clusterName()).isEqualTo(CDS_RESOURCE); assertThat(cdsUpdate.clusterType()).isEqualTo(ClusterType.AGGREGATE); LbConfig lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(cdsUpdate.lbPolicyConfig()); @@ -2257,8 +2387,8 @@ public void cdsResponseWithEmptyAggregateCluster() { String errorMsg = "CDS response Cluster 'cluster.googleapis.com' validation error: " + "Cluster cluster.googleapis.com: aggregate ClusterConfig.clusters must not be empty"; call.verifyRequestNack(CDS, CDS_RESOURCE, "", "0000", NODE, ImmutableList.of(errorMsg)); - verify(cdsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + verifyStatusWithNodeId(cdsUpdateCaptor.getValue().getStatus(), Code.UNAVAILABLE, errorMsg); } @Test @@ -2272,8 +2402,10 @@ public void cdsResponseWithCircuitBreakers() { // Client sent an ACK CDS request. call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + CdsUpdate cdsUpdate = statusOrUpdate.getValue(); assertThat(cdsUpdate.clusterName()).isEqualTo(CDS_RESOURCE); assertThat(cdsUpdate.clusterType()).isEqualTo(ClusterType.EDS); assertThat(cdsUpdate.edsServiceName()).isNull(); @@ -2315,8 +2447,10 @@ public void cdsResponseWithUpstreamTlsContext() { // Client sent an ACK CDS request. call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); verify(cdsResourceWatcher, times(1)) - .onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + .onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + CdsUpdate cdsUpdate = statusOrUpdate.getValue(); CertificateProviderPluginInstance certificateProviderInstance = cdsUpdate.upstreamTlsContext().getCommonTlsContext().getCombinedValidationContext() .getDefaultValidationContext().getCaCertificateProviderInstance(); @@ -2350,8 +2484,10 @@ public void cdsResponseWithNewUpstreamTlsContext() { // Client sent an ACK CDS request. call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); - verify(cdsResourceWatcher, times(1)).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + verify(cdsResourceWatcher, times(1)).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + CdsUpdate cdsUpdate = statusOrUpdate.getValue(); CertificateProviderPluginInstance certificateProviderInstance = cdsUpdate.upstreamTlsContext().getCommonTlsContext().getValidationContext() .getCaCertificateProviderInstance(); @@ -2383,8 +2519,8 @@ public void cdsResponseErrorHandling_badUpstreamTlsContext() { + "ca_certificate_provider_instance or system_root_certs is required in " + "upstream-tls-context"; call.verifyRequestNack(CDS, CDS_RESOURCE, "", "0000", NODE, ImmutableList.of(errorMsg)); - verify(cdsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + verifyStatusWithNodeId(cdsUpdateCaptor.getValue().getStatus(), Code.UNAVAILABLE, errorMsg); } /** @@ -2425,8 +2561,10 @@ public void cdsResponseWithOutlierDetection() { // Client sent an ACK CDS request. call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); - verify(cdsResourceWatcher, times(1)).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + verify(cdsResourceWatcher, times(1)).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + CdsUpdate cdsUpdate = statusOrUpdate.getValue(); // The outlier detection config in CdsUpdate should match what we get from xDS. EnvoyServerProtoData.OutlierDetection outlierDetection = cdsUpdate.outlierDetection(); @@ -2486,8 +2624,8 @@ public void cdsResponseWithInvalidOutlierDetectionNacks() { + "io.grpc.xds.client.XdsResourceType$ResourceInvalidException: outlier_detection " + "max_ejection_percent is > 100"; call.verifyRequestNack(CDS, CDS_RESOURCE, "", "0000", NODE, ImmutableList.of(errorMsg)); - verify(cdsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + verifyStatusWithNodeId(cdsUpdateCaptor.getValue().getStatus(), Code.UNAVAILABLE, errorMsg); } @Test(expected = ResourceInvalidException.class) @@ -2581,8 +2719,8 @@ public void cdsResponseErrorHandling_badTransportSocketName() { String errorMsg = "CDS response Cluster 'cluster.googleapis.com' validation error: " + "transport-socket with name envoy.transport_sockets.bad not supported."; call.verifyRequestNack(CDS, CDS_RESOURCE, "", "0000", NODE, ImmutableList.of(errorMsg)); - verify(cdsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + verifyStatusWithNodeId(cdsUpdateCaptor.getValue().getStatus(), Code.UNAVAILABLE, errorMsg); } @Test @@ -2624,8 +2762,10 @@ public void cachedCdsResource_data() { ResourceWatcher watcher = mock(ResourceWatcher.class); xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CDS_RESOURCE, watcher); - verify(watcher).onChanged(cdsUpdateCaptor.capture()); - verifyGoldenClusterRoundRobin(cdsUpdateCaptor.getValue()); + verify(watcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenClusterRoundRobin(statusOrUpdate.getValue()); call.verifyNoMoreRequest(); verifyResourceMetadataAcked(CDS, CDS_RESOURCE, testClusterRoundRobin, VERSION_1, TIME_INCREMENT); @@ -2639,10 +2779,12 @@ public void cachedCdsResource_absent() { DiscoveryRpcCall call = startResourceWatcher(XdsClusterResource.getInstance(), CDS_RESOURCE, cdsResourceWatcher); fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(cdsResourceWatcher).onResourceDoesNotExist(CDS_RESOURCE); + verify(cdsResourceWatcher).onResourceChanged(argThat( + arg -> !arg.hasValue() && arg.getStatus().getDescription().contains(CDS_RESOURCE))); ResourceWatcher watcher = mock(ResourceWatcher.class); xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CDS_RESOURCE, watcher); - verify(watcher).onResourceDoesNotExist(CDS_RESOURCE); + verify(watcher).onResourceChanged(argThat( + arg -> !arg.hasValue() && arg.getStatus().getDescription().contains(CDS_RESOURCE))); call.verifyNoMoreRequest(); verifyResourceMetadataDoesNotExist(CDS, CDS_RESOURCE); verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); @@ -2662,8 +2804,10 @@ public void cdsResourceUpdated() { null, null, false, null, null)); call.sendResponse(CDS, clusterDns, VERSION_1, "0000"); call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + CdsUpdate cdsUpdate = statusOrUpdate.getValue(); assertThat(cdsUpdate.clusterName()).isEqualTo(CDS_RESOURCE); assertThat(cdsUpdate.clusterType()).isEqualTo(ClusterType.LOGICAL_DNS); assertThat(cdsUpdate.dnsHostName()).isEqualTo(dnsHostAddr + ":" + dnsHostPort); @@ -2685,8 +2829,10 @@ public void cdsResourceUpdated() { )); call.sendResponse(CDS, clusterEds, VERSION_2, "0001"); call.verifyRequest(CDS, CDS_RESOURCE, VERSION_2, "0001", NODE); - verify(cdsResourceWatcher, times(2)).onChanged(cdsUpdateCaptor.capture()); - cdsUpdate = cdsUpdateCaptor.getValue(); + verify(cdsResourceWatcher, times(2)).onResourceChanged(cdsUpdateCaptor.capture()); + statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + cdsUpdate = statusOrUpdate.getValue(); assertThat(cdsUpdate.clusterName()).isEqualTo(CDS_RESOURCE); assertThat(cdsUpdate.clusterType()).isEqualTo(ClusterType.EDS); assertThat(cdsUpdate.edsServiceName()).isEqualTo(edsService); @@ -2727,27 +2873,27 @@ public void cdsResourceUpdatedWithDuplicate() { // Configure with round robin, the update should be sent to the watcher. call.sendResponse(CDS, roundRobinConfig, VERSION_2, "0001"); - verify(cdsResourceWatcher, times(1)).onChanged(isA(CdsUpdate.class)); + verify(cdsResourceWatcher, times(1)).onResourceChanged(argThat(StatusOr::hasValue)); // Second update is identical, watcher should not get an additional update. call.sendResponse(CDS, roundRobinConfig, VERSION_2, "0002"); - verify(cdsResourceWatcher, times(1)).onChanged(isA(CdsUpdate.class)); + verify(cdsResourceWatcher, times(1)).onResourceChanged(any()); // Now we switch to ring hash so the watcher should be notified. call.sendResponse(CDS, ringHashConfig, VERSION_2, "0003"); - verify(cdsResourceWatcher, times(2)).onChanged(isA(CdsUpdate.class)); + verify(cdsResourceWatcher, times(2)).onResourceChanged(argThat(StatusOr::hasValue)); // Second update to ring hash should not result in watcher being notified. call.sendResponse(CDS, ringHashConfig, VERSION_2, "0004"); - verify(cdsResourceWatcher, times(2)).onChanged(isA(CdsUpdate.class)); + verify(cdsResourceWatcher, times(2)).onResourceChanged(any()); // Now we switch to least request so the watcher should be notified. call.sendResponse(CDS, leastRequestConfig, VERSION_2, "0005"); - verify(cdsResourceWatcher, times(3)).onChanged(isA(CdsUpdate.class)); + verify(cdsResourceWatcher, times(3)).onResourceChanged(argThat(StatusOr::hasValue)); // Second update to least request should not result in watcher being notified. call.sendResponse(CDS, leastRequestConfig, VERSION_2, "0006"); - verify(cdsResourceWatcher, times(3)).onChanged(isA(CdsUpdate.class)); + verify(cdsResourceWatcher, times(3)).onResourceChanged(any()); } @Test @@ -2761,8 +2907,10 @@ public void cdsResourceDeleted() { // Initial CDS response. call.sendResponse(CDS, testClusterRoundRobin, VERSION_1, "0000"); call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - verifyGoldenClusterRoundRobin(cdsUpdateCaptor.getValue()); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenClusterRoundRobin(statusOrUpdate.getValue()); verifyResourceMetadataAcked(CDS, CDS_RESOURCE, testClusterRoundRobin, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); @@ -2770,7 +2918,8 @@ public void cdsResourceDeleted() { // Empty CDS response deletes the cluster. call.sendResponse(CDS, Collections.emptyList(), VERSION_2, "0001"); call.verifyRequest(CDS, CDS_RESOURCE, VERSION_2, "0001", NODE); - verify(cdsResourceWatcher).onResourceDoesNotExist(CDS_RESOURCE); + verify(cdsResourceWatcher).onResourceChanged(argThat( + arg -> !arg.hasValue() && arg.getStatus().getDescription().contains(CDS_RESOURCE))); verifyResourceMetadataDoesNotExist(CDS, CDS_RESOURCE); verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); } @@ -2790,8 +2939,10 @@ public void cdsResourceDeleted_ignoreResourceDeletion() { // Initial CDS response. call.sendResponse(CDS, testClusterRoundRobin, VERSION_1, "0000"); call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - verifyGoldenClusterRoundRobin(cdsUpdateCaptor.getValue()); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenClusterRoundRobin(statusOrUpdate.getValue()); verifyResourceMetadataAcked(CDS, CDS_RESOURCE, testClusterRoundRobin, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); @@ -2805,13 +2956,16 @@ public void cdsResourceDeleted_ignoreResourceDeletion() { TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); // onResourceDoesNotExist must not be called. - verify(ldsResourceWatcher, never()).onResourceDoesNotExist(CDS_RESOURCE); + verify(ldsResourceWatcher, never()).onResourceChanged(argThat( + arg -> !arg.hasValue() && arg.getStatus().getDescription().contains(CDS_RESOURCE))); // Next update is correct, and contains the cluster again. call.sendResponse(CDS, testClusterRoundRobin, VERSION_3, "0003"); call.verifyRequest(CDS, CDS_RESOURCE, VERSION_3, "0003", NODE); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - verifyGoldenClusterRoundRobin(cdsUpdateCaptor.getValue()); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenClusterRoundRobin(statusOrUpdate.getValue()); verifyResourceMetadataAcked(CDS, CDS_RESOURCE, testClusterRoundRobin, VERSION_3, TIME_INCREMENT * 3); verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); @@ -2834,9 +2988,12 @@ public void multipleCdsWatchers() { verifySubscribedResourcesMetadataSizes(0, 2, 0, 0); fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(cdsResourceWatcher).onResourceDoesNotExist(CDS_RESOURCE); - verify(watcher1).onResourceDoesNotExist(cdsResourceTwo); - verify(watcher2).onResourceDoesNotExist(cdsResourceTwo); + verify(cdsResourceWatcher).onResourceChanged(argThat(statusOr -> + statusOr.getStatus().getDescription().contains(CDS_RESOURCE))); + verify(watcher1).onResourceChanged(argThat(statusOr -> + statusOr.getStatus().getDescription().contains(cdsResourceTwo))); + verify(watcher2).onResourceChanged(argThat(statusOr -> + statusOr.getStatus().getDescription().contains(cdsResourceTwo))); verifyResourceMetadataDoesNotExist(CDS, CDS_RESOURCE); verifyResourceMetadataDoesNotExist(CDS, cdsResourceTwo); verifySubscribedResourcesMetadataSizes(0, 2, 0, 0); @@ -2850,45 +3007,54 @@ public void multipleCdsWatchers() { Any.pack(mf.buildEdsCluster(cdsResourceTwo, edsService, "round_robin", null, null, true, null, "envoy.transport_sockets.tls", null, null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); - assertThat(cdsUpdate.clusterName()).isEqualTo(CDS_RESOURCE); - assertThat(cdsUpdate.clusterType()).isEqualTo(ClusterType.LOGICAL_DNS); - assertThat(cdsUpdate.dnsHostName()).isEqualTo(dnsHostAddr + ":" + dnsHostPort); - LbConfig lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(cdsUpdate.lbPolicyConfig()); + ArgumentCaptor> cdsUpdateCaptor = ArgumentCaptor.forClass(StatusOr.class); + verify(cdsResourceWatcher, times(2)).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr capturedUpdate1 = cdsUpdateCaptor.getAllValues().get(1); + assertThat(capturedUpdate1.hasValue()).isTrue(); + CdsUpdate cdsUpdate1 = capturedUpdate1.getValue(); + assertThat(cdsUpdate1.clusterName()).isEqualTo(CDS_RESOURCE); + assertThat(cdsUpdate1.clusterType()).isEqualTo(ClusterType.LOGICAL_DNS); + assertThat(cdsUpdate1.dnsHostName()).isEqualTo(dnsHostAddr + ":" + dnsHostPort); + LbConfig lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(cdsUpdate1.lbPolicyConfig()); assertThat(lbConfig.getPolicyName()).isEqualTo("wrr_locality_experimental"); List childConfigs = ServiceConfigUtil.unwrapLoadBalancingConfigList( JsonUtil.getListOfObjects(lbConfig.getRawConfigValue(), "childPolicy")); assertThat(childConfigs.get(0).getPolicyName()).isEqualTo("round_robin"); - assertThat(cdsUpdate.lrsServerInfo()).isNull(); - assertThat(cdsUpdate.maxConcurrentRequests()).isNull(); - assertThat(cdsUpdate.upstreamTlsContext()).isNull(); - verify(watcher1).onChanged(cdsUpdateCaptor.capture()); - cdsUpdate = cdsUpdateCaptor.getValue(); - assertThat(cdsUpdate.clusterName()).isEqualTo(cdsResourceTwo); - assertThat(cdsUpdate.clusterType()).isEqualTo(ClusterType.EDS); - assertThat(cdsUpdate.edsServiceName()).isEqualTo(edsService); - lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(cdsUpdate.lbPolicyConfig()); + assertThat(cdsUpdate1.lrsServerInfo()).isNull(); + assertThat(cdsUpdate1.maxConcurrentRequests()).isNull(); + assertThat(cdsUpdate1.upstreamTlsContext()).isNull(); + ArgumentCaptor> watcher1Captor = ArgumentCaptor.forClass(StatusOr.class); + verify(watcher1, times(2)).onResourceChanged(watcher1Captor.capture()); + StatusOr capturedUpdate2 = watcher1Captor.getAllValues().get(1); + assertThat(capturedUpdate2.hasValue()).isTrue(); + CdsUpdate cdsUpdate2 = capturedUpdate2.getValue(); + assertThat(cdsUpdate2.clusterName()).isEqualTo(cdsResourceTwo); + assertThat(cdsUpdate2.clusterType()).isEqualTo(ClusterType.EDS); + assertThat(cdsUpdate2.edsServiceName()).isEqualTo(edsService); + lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(cdsUpdate2.lbPolicyConfig()); assertThat(lbConfig.getPolicyName()).isEqualTo("wrr_locality_experimental"); childConfigs = ServiceConfigUtil.unwrapLoadBalancingConfigList( JsonUtil.getListOfObjects(lbConfig.getRawConfigValue(), "childPolicy")); assertThat(childConfigs.get(0).getPolicyName()).isEqualTo("round_robin"); - assertThat(cdsUpdate.lrsServerInfo()).isEqualTo(xdsServerInfo); - assertThat(cdsUpdate.maxConcurrentRequests()).isNull(); - assertThat(cdsUpdate.upstreamTlsContext()).isNull(); - verify(watcher2).onChanged(cdsUpdateCaptor.capture()); - cdsUpdate = cdsUpdateCaptor.getValue(); - assertThat(cdsUpdate.clusterName()).isEqualTo(cdsResourceTwo); - assertThat(cdsUpdate.clusterType()).isEqualTo(ClusterType.EDS); - assertThat(cdsUpdate.edsServiceName()).isEqualTo(edsService); - lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(cdsUpdate.lbPolicyConfig()); + assertThat(cdsUpdate2.lrsServerInfo()).isEqualTo(xdsServerInfo); + assertThat(cdsUpdate2.maxConcurrentRequests()).isNull(); + assertThat(cdsUpdate2.upstreamTlsContext()).isNull(); + ArgumentCaptor> watcher2Captor = ArgumentCaptor.forClass(StatusOr.class); + verify(watcher2, times(2)).onResourceChanged(watcher2Captor.capture()); + StatusOr capturedUpdate3 = watcher2Captor.getAllValues().get(1); + assertThat(capturedUpdate3.hasValue()).isTrue(); + CdsUpdate cdsUpdate3 = capturedUpdate3.getValue(); + assertThat(cdsUpdate3.clusterName()).isEqualTo(cdsResourceTwo); + assertThat(cdsUpdate3.clusterType()).isEqualTo(ClusterType.EDS); + assertThat(cdsUpdate3.edsServiceName()).isEqualTo(edsService); + lbConfig = ServiceConfigUtil.unwrapLoadBalancingConfig(cdsUpdate3.lbPolicyConfig()); assertThat(lbConfig.getPolicyName()).isEqualTo("wrr_locality_experimental"); childConfigs = ServiceConfigUtil.unwrapLoadBalancingConfigList( JsonUtil.getListOfObjects(lbConfig.getRawConfigValue(), "childPolicy")); assertThat(childConfigs.get(0).getPolicyName()).isEqualTo("round_robin"); - assertThat(cdsUpdate.lrsServerInfo()).isEqualTo(xdsServerInfo); - assertThat(cdsUpdate.maxConcurrentRequests()).isNull(); - assertThat(cdsUpdate.upstreamTlsContext()).isNull(); + assertThat(cdsUpdate3.lrsServerInfo()).isEqualTo(xdsServerInfo); + assertThat(cdsUpdate3.maxConcurrentRequests()).isNull(); + assertThat(cdsUpdate3.upstreamTlsContext()).isNull(); // Metadata of both clusters is stored. verifyResourceMetadataAcked(CDS, CDS_RESOURCE, clusters.get(0), VERSION_1, TIME_INCREMENT); verifyResourceMetadataAcked(CDS, cdsResourceTwo, clusters.get(1), VERSION_1, TIME_INCREMENT); @@ -2912,7 +3078,8 @@ public void edsResourceNotFound() { verifySubscribedResourcesMetadataSizes(0, 0, 0, 1); // Server failed to return subscribed resource within expected time window. fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(edsResourceWatcher).onResourceDoesNotExist(EDS_RESOURCE); + verify(edsResourceWatcher).onResourceChanged(argThat(statusOr -> + statusOr.getStatus().getDescription().contains(EDS_RESOURCE))); assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataDoesNotExist(EDS, EDS_RESOURCE); verifySubscribedResourcesMetadataSizes(0, 0, 0, 1); @@ -2936,7 +3103,7 @@ public void edsCleanupNonceAfterUnsubscription() { call.sendResponse(EDS, resourcesV1.values().asList(), VERSION_1, "0000"); // {A.1} -> ACK, version 1 call.verifyRequest(EDS, "A.1", VERSION_1, "0000", NODE); - verify(edsResourceWatcher, times(1)).onChanged(any()); + verify(edsResourceWatcher, times(1)).onResourceChanged(any()); // trigger an EDS resource unsubscription. xdsClient.cancelXdsResourceWatch(XdsEndpointResource.getInstance(), "A.1", edsResourceWatcher); @@ -2987,8 +3154,10 @@ public void edsResponseErrorHandling_someResourcesFailedUnpack() { verifySubscribedResourcesMetadataSizes(0, 0, 0, 1); // The response is NACKed with the same error message. call.verifyRequestNack(EDS, EDS_RESOURCE, "", "0000", NODE, errors); - verify(edsResourceWatcher).onChanged(edsUpdateCaptor.capture()); - EdsUpdate edsUpdate = edsUpdateCaptor.getValue(); + verify(edsResourceWatcher).onResourceChanged(edsUpdateCaptor.capture()); + StatusOr statusOrUpdate = edsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + EdsUpdate edsUpdate = statusOrUpdate.getValue(); assertThat(edsUpdate.clusterName).isEqualTo(EDS_RESOURCE); } @@ -3072,8 +3241,10 @@ public void edsResourceFound() { // Client sent an ACK EDS request. call.verifyRequest(EDS, EDS_RESOURCE, VERSION_1, "0000", NODE); - verify(edsResourceWatcher).onChanged(edsUpdateCaptor.capture()); - validateGoldenClusterLoadAssignment(edsUpdateCaptor.getValue()); + verify(edsResourceWatcher).onResourceChanged(edsUpdateCaptor.capture()); + StatusOr statusOrUpdate = edsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + validateGoldenClusterLoadAssignment(statusOrUpdate.getValue()); verifyResourceMetadataAcked(EDS, EDS_RESOURCE, testClusterLoadAssignment, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(0, 0, 0, 1); @@ -3087,8 +3258,10 @@ public void wrappedEdsResourceFound() { // Client sent an ACK EDS request. call.verifyRequest(EDS, EDS_RESOURCE, VERSION_1, "0000", NODE); - verify(edsResourceWatcher).onChanged(edsUpdateCaptor.capture()); - validateGoldenClusterLoadAssignment(edsUpdateCaptor.getValue()); + verify(edsResourceWatcher).onResourceChanged(edsUpdateCaptor.capture()); + StatusOr statusOrUpdate = edsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + validateGoldenClusterLoadAssignment(statusOrUpdate.getValue()); verifyResourceMetadataAcked(EDS, EDS_RESOURCE, testClusterLoadAssignment, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(0, 0, 0, 1); @@ -3106,8 +3279,10 @@ public void cachedEdsResource_data() { // Add another watcher. ResourceWatcher watcher = mock(ResourceWatcher.class); xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), EDS_RESOURCE, watcher); - verify(watcher).onChanged(edsUpdateCaptor.capture()); - validateGoldenClusterLoadAssignment(edsUpdateCaptor.getValue()); + verify(watcher).onResourceChanged(edsUpdateCaptor.capture()); + StatusOr statusOrUpdate = edsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + validateGoldenClusterLoadAssignment(statusOrUpdate.getValue()); call.verifyNoMoreRequest(); verifyResourceMetadataAcked(EDS, EDS_RESOURCE, testClusterLoadAssignment, VERSION_1, TIME_INCREMENT); @@ -3120,10 +3295,12 @@ public void cachedEdsResource_absent() { DiscoveryRpcCall call = startResourceWatcher(XdsEndpointResource.getInstance(), EDS_RESOURCE, edsResourceWatcher); fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(edsResourceWatcher).onResourceDoesNotExist(EDS_RESOURCE); + verify(edsResourceWatcher).onResourceChanged(argThat(statusOr -> + statusOr.getStatus().getDescription().contains(EDS_RESOURCE))); ResourceWatcher watcher = mock(ResourceWatcher.class); xdsClient.watchXdsResource(XdsEndpointResource.getInstance(), EDS_RESOURCE, watcher); - verify(watcher).onResourceDoesNotExist(EDS_RESOURCE); + verify(watcher).onResourceChanged(argThat(statusOr -> + statusOr.getStatus().getDescription().contains(EDS_RESOURCE))); call.verifyNoMoreRequest(); verifyResourceMetadataDoesNotExist(EDS, EDS_RESOURCE); verifySubscribedResourcesMetadataSizes(0, 0, 0, 1); @@ -3154,7 +3331,7 @@ public void flowControlAbsent() throws Exception { fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); assertThat(fakeWatchClock.getPendingTasks().size()).isEqualTo(2); CyclicBarrier barrier = new CyclicBarrier(2); - doAnswer(blockUpdate(barrier)).when(cdsResourceWatcher).onChanged(any(CdsUpdate.class)); + doAnswer(blockUpdate(barrier)).when(cdsResourceWatcher).onResourceChanged(any()); CountDownLatch latch = new CountDownLatch(1); new Thread(() -> { @@ -3176,16 +3353,16 @@ public void flowControlAbsent() throws Exception { verifyResourceMetadataAcked( CDS, CDS_RESOURCE, testClusterRoundRobin, VERSION_1, TIME_INCREMENT); barrier.await(); - verify(cdsResourceWatcher, atLeastOnce()).onChanged(any()); + verify(cdsResourceWatcher, atLeastOnce()).onResourceChanged(any()); String errorMsg = "CDS response Cluster 'cluster.googleapis.com2' validation error: " + "Cluster cluster.googleapis.com2: unspecified cluster discovery type"; call.verifyRequestNack(CDS, Arrays.asList(CDS_RESOURCE, anotherCdsResource), VERSION_1, "0001", NODE, Arrays.asList(errorMsg)); barrier.await(); latch.await(10, TimeUnit.SECONDS); - verify(cdsResourceWatcher, times(2)).onChanged(any()); - verify(anotherWatcher).onResourceDoesNotExist(eq(anotherCdsResource)); - verify(anotherWatcher).onError(any()); + verify(cdsResourceWatcher, times(2)).onResourceChanged(any()); + verify(anotherWatcher, times(2)).onResourceChanged( + argThat(statusOr -> statusOr.getStatus().getDescription().contains(anotherCdsResource))); } @Test @@ -3281,9 +3458,11 @@ public void resourceTimerIsTransientError_callsOnErrorUnavailable() { call.verifyRequest(CDS, ImmutableList.of(timeoutResource), "", "", NODE); fakeClock.forwardTime(XdsClientImpl.EXTENDED_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); fakeClock.runDueTasks(); - ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Status.class); - verify(timeoutWatcher).onError(errorCaptor.capture()); - Status error = errorCaptor.getValue(); + @SuppressWarnings("unchecked") + ArgumentCaptor> statusOrCaptor = ArgumentCaptor.forClass(StatusOr.class); + verify(timeoutWatcher).onResourceChanged(statusOrCaptor.capture()); + StatusOr statusOr = statusOrCaptor.getValue(); + Status error = statusOr.getStatus(); assertThat(error.getCode()).isEqualTo(Status.Code.UNAVAILABLE); assertThat(error.getDescription()).isEqualTo( "Timed out waiting for resource " + timeoutResource + " from xDS server"); @@ -3326,7 +3505,7 @@ public void simpleFlowControl() throws Exception { assertThat(call.isReady()).isFalse(); CyclicBarrier barrier = new CyclicBarrier(2); - doAnswer(blockUpdate(barrier)).when(edsResourceWatcher).onChanged(any(EdsUpdate.class)); + doAnswer(blockUpdate(barrier)).when(edsResourceWatcher).onResourceChanged(any()); CountDownLatch latch = new CountDownLatch(1); new Thread(() -> { @@ -3341,12 +3520,14 @@ public void simpleFlowControl() throws Exception { verifyResourceMetadataAcked(EDS, EDS_RESOURCE, testClusterLoadAssignment, VERSION_1, TIME_INCREMENT); barrier.await(); - verify(edsResourceWatcher, atLeastOnce()).onChanged(edsUpdateCaptor.capture()); - EdsUpdate edsUpdate = edsUpdateCaptor.getAllValues().get(0); + verify(edsResourceWatcher, atLeastOnce()).onResourceChanged(edsUpdateCaptor.capture()); + StatusOr statusOrUpdate = edsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + EdsUpdate edsUpdate = statusOrUpdate.getValue(); validateGoldenClusterLoadAssignment(edsUpdate); barrier.await(); latch.await(10, TimeUnit.SECONDS); - verify(edsResourceWatcher, times(2)).onChanged(any()); + verify(edsResourceWatcher, times(2)).onResourceChanged(any()); verifyResourceMetadataAcked(EDS, EDS_RESOURCE, updatedClusterLoadAssignment, VERSION_2, TIME_INCREMENT * 2); } @@ -3358,7 +3539,7 @@ public void flowControlUnknownType() { call.sendResponse(CDS, testClusterRoundRobin, VERSION_1, "0000"); call.sendResponse(EDS, testClusterLoadAssignment, VERSION_1, "0000"); call.verifyRequest(EDS, EDS_RESOURCE, VERSION_1, "0000", NODE); - verify(edsResourceWatcher).onChanged(any()); + verify(edsResourceWatcher).onResourceChanged(any()); } @Test @@ -3370,8 +3551,10 @@ public void edsResourceUpdated() { // Initial EDS response. call.sendResponse(EDS, testClusterLoadAssignment, VERSION_1, "0000"); call.verifyRequest(EDS, EDS_RESOURCE, VERSION_1, "0000", NODE); - verify(edsResourceWatcher).onChanged(edsUpdateCaptor.capture()); - EdsUpdate edsUpdate = edsUpdateCaptor.getValue(); + verify(edsResourceWatcher).onResourceChanged(edsUpdateCaptor.capture()); + StatusOr statusOrUpdate = edsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + EdsUpdate edsUpdate = statusOrUpdate.getValue(); validateGoldenClusterLoadAssignment(edsUpdate); verifyResourceMetadataAcked(EDS, EDS_RESOURCE, testClusterLoadAssignment, VERSION_1, TIME_INCREMENT); @@ -3383,8 +3566,10 @@ public void edsResourceUpdated() { ImmutableList.of())); call.sendResponse(EDS, updatedClusterLoadAssignment, VERSION_2, "0001"); - verify(edsResourceWatcher, times(2)).onChanged(edsUpdateCaptor.capture()); - edsUpdate = edsUpdateCaptor.getValue(); + verify(edsResourceWatcher, times(2)).onResourceChanged(edsUpdateCaptor.capture()); + statusOrUpdate = edsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + edsUpdate = statusOrUpdate.getValue(); assertThat(edsUpdate.clusterName).isEqualTo(EDS_RESOURCE); assertThat(edsUpdate.dropPolicies).isEmpty(); assertThat(edsUpdate.localityLbEndpointsMap) @@ -3422,8 +3607,12 @@ public void edsDuplicateLocalityInTheSamePriority() { + "locality:Locality{region=region2, zone=zone2, subZone=subzone2} for priority:1"; call.verifyRequestNack(EDS, EDS_RESOURCE, "", "0001", NODE, ImmutableList.of( errorMsg)); - verify(edsResourceWatcher).onError(errorCaptor.capture()); - assertThat(errorCaptor.getValue().getDescription()).contains(errorMsg); + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = ArgumentCaptor.forClass(StatusOr.class); + verify(edsResourceWatcher).onResourceChanged(captor.capture()); + StatusOr statusOrUpdate = captor.getValue(); + assertThat(statusOrUpdate.hasValue()).isFalse(); + assertThat(statusOrUpdate.getStatus().getDescription()).contains(errorMsg); } @Test @@ -3450,12 +3639,13 @@ public void edsResourceDeletedByCds() { Any.pack(mf.buildEdsCluster(CDS_RESOURCE, EDS_RESOURCE, "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null, null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); - verify(cdsWatcher).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + ArgumentCaptor> cdsUpdateCaptor = ArgumentCaptor.forClass(StatusOr.class); + verify(cdsWatcher, times(1)).onResourceChanged(cdsUpdateCaptor.capture()); + CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue().getValue(); assertThat(cdsUpdate.edsServiceName()).isEqualTo(null); assertThat(cdsUpdate.lrsServerInfo()).isEqualTo(xdsServerInfo); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - cdsUpdate = cdsUpdateCaptor.getValue(); + verify(cdsResourceWatcher, times(1)).onResourceChanged(cdsUpdateCaptor.capture()); + cdsUpdate = cdsUpdateCaptor.getValue().getValue(); assertThat(cdsUpdate.edsServiceName()).isEqualTo(EDS_RESOURCE); assertThat(cdsUpdate.lrsServerInfo()).isNull(); verifyResourceMetadataAcked(CDS, resource, clusters.get(0), VERSION_1, TIME_INCREMENT); @@ -3479,10 +3669,11 @@ public void edsResourceDeletedByCds() { "endpoint-host-name"), 1, 0)), ImmutableList.of(mf.buildDropOverload("lb", 100))))); call.sendResponse(EDS, clusterLoadAssignments, VERSION_1, "0000"); - verify(edsWatcher).onChanged(edsUpdateCaptor.capture()); - assertThat(edsUpdateCaptor.getValue().clusterName).isEqualTo(resource); - verify(edsResourceWatcher).onChanged(edsUpdateCaptor.capture()); - assertThat(edsUpdateCaptor.getValue().clusterName).isEqualTo(EDS_RESOURCE); + ArgumentCaptor> edsUpdateCaptor = ArgumentCaptor.forClass(StatusOr.class); + verify(edsWatcher, times(1)).onResourceChanged(edsUpdateCaptor.capture()); + assertThat(edsUpdateCaptor.getValue().getValue().clusterName).isEqualTo(resource); + verify(edsResourceWatcher, times(1)).onResourceChanged(edsUpdateCaptor.capture()); + assertThat(edsUpdateCaptor.getValue().getValue().clusterName).isEqualTo(EDS_RESOURCE); verifyResourceMetadataAcked( EDS, EDS_RESOURCE, clusterLoadAssignments.get(0), VERSION_1, TIME_INCREMENT * 2); @@ -3500,12 +3691,8 @@ public void edsResourceDeletedByCds() { "envoy.transport_sockets.tls", null, null ))); call.sendResponse(CDS, clusters, VERSION_2, "0001"); - verify(cdsResourceWatcher, times(2)).onChanged(cdsUpdateCaptor.capture()); - assertThat(cdsUpdateCaptor.getValue().edsServiceName()).isNull(); - // Note that the endpoint must be deleted even if the ignore_resource_deletion feature. - // This happens because the cluster CDS_RESOURCE is getting replaced, and not deleted. - verify(edsResourceWatcher, never()).onResourceDoesNotExist(EDS_RESOURCE); - verify(edsResourceWatcher, never()).onResourceDoesNotExist(resource); + verify(cdsResourceWatcher, times(2)).onResourceChanged(cdsUpdateCaptor.capture()); + assertThat(cdsUpdateCaptor.getValue().getValue().edsServiceName()).isNull(); verifyNoMoreInteractions(cdsWatcher, edsWatcher); verifyResourceMetadataAcked( EDS, EDS_RESOURCE, clusterLoadAssignments.get(0), VERSION_1, TIME_INCREMENT * 2); @@ -3531,16 +3718,24 @@ public void multipleEdsWatchers() { verifySubscribedResourcesMetadataSizes(0, 0, 0, 2); fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(edsResourceWatcher).onResourceDoesNotExist(EDS_RESOURCE); - verify(watcher1).onResourceDoesNotExist(edsResourceTwo); - verify(watcher2).onResourceDoesNotExist(edsResourceTwo); + verify(edsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getDescription().contains(EDS_RESOURCE))); + verify(watcher1).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getDescription().contains(edsResourceTwo))); + verify(watcher2).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getDescription().contains(edsResourceTwo))); verifyResourceMetadataDoesNotExist(EDS, EDS_RESOURCE); verifyResourceMetadataDoesNotExist(EDS, edsResourceTwo); verifySubscribedResourcesMetadataSizes(0, 0, 0, 2); call.sendResponse(EDS, testClusterLoadAssignment, VERSION_1, "0000"); - verify(edsResourceWatcher).onChanged(edsUpdateCaptor.capture()); - EdsUpdate edsUpdate = edsUpdateCaptor.getValue(); + verify(edsResourceWatcher, times(2)).onResourceChanged(edsUpdateCaptor.capture()); + StatusOr statusOrUpdate = edsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + EdsUpdate edsUpdate = statusOrUpdate.getValue(); validateGoldenClusterLoadAssignment(edsUpdate); verifyNoMoreInteractions(watcher1, watcher2); verifyResourceMetadataAcked( @@ -3557,8 +3752,10 @@ public void multipleEdsWatchers() { ImmutableList.of())); call.sendResponse(EDS, clusterLoadAssignmentTwo, VERSION_2, "0001"); - verify(watcher1).onChanged(edsUpdateCaptor.capture()); - edsUpdate = edsUpdateCaptor.getValue(); + verify(watcher1, times(2)).onResourceChanged(edsUpdateCaptor.capture()); + statusOrUpdate = edsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + edsUpdate = statusOrUpdate.getValue(); assertThat(edsUpdate.clusterName).isEqualTo(edsResourceTwo); assertThat(edsUpdate.dropPolicies).isEmpty(); assertThat(edsUpdate.localityLbEndpointsMap) @@ -3569,8 +3766,10 @@ public void multipleEdsWatchers() { LbEndpoint.create("172.44.2.2", 8000, 3, true, "endpoint-host-name", ImmutableMap.of())), 2, 0, ImmutableMap.of())); - verify(watcher2).onChanged(edsUpdateCaptor.capture()); - edsUpdate = edsUpdateCaptor.getValue(); + verify(watcher2, times(2)).onResourceChanged(edsUpdateCaptor.capture()); + statusOrUpdate = edsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + edsUpdate = statusOrUpdate.getValue(); assertThat(edsUpdate.clusterName).isEqualTo(edsResourceTwo); assertThat(edsUpdate.dropPolicies).isEmpty(); assertThat(edsUpdate.localityLbEndpointsMap) @@ -3601,10 +3800,13 @@ public void useIndependentRpcContext() { // The inbound RPC finishes and closes its context. The outbound RPC's control plane RPC // should not be impacted. cancellableContext.close(); - verify(ldsResourceWatcher, never()).onError(any(Status.class)); + verify(ldsResourceWatcher, never()).onAmbientError(any(Status.class)); + verify(ldsResourceWatcher, never()).onResourceChanged(argThat( + statusOr -> !statusOr.hasValue() + )); call.sendResponse(LDS, testListenerRds, VERSION_1, "0000"); - verify(ldsResourceWatcher).onChanged(any(LdsUpdate.class)); + verify(ldsResourceWatcher).onResourceChanged(any()); } finally { cancellableContext.detach(prevContext); } @@ -3624,12 +3826,15 @@ public void streamClosedWithNoResponse() { // Check metric data. callback_ReportServerConnection(); verifyServerConnection(1, false, xdsServerInfo.target()); - verify(ldsResourceWatcher, Mockito.timeout(1000).times(1)) - .onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, + verify(ldsResourceWatcher, Mockito.timeout(1000)).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr ldsStatusOr = ldsUpdateCaptor.getValue(); + assertThat(ldsStatusOr.hasValue()).isFalse(); + verifyStatusWithNodeId(ldsStatusOr.getStatus(), Code.UNAVAILABLE, "ADS stream closed with OK before receiving a response"); - verify(rdsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, + verify(rdsResourceWatcher, Mockito.timeout(1000)).onResourceChanged(rdsUpdateCaptor.capture()); + StatusOr rdsStatusOr = rdsUpdateCaptor.getValue(); + assertThat(rdsStatusOr.hasValue()).isFalse(); + verifyStatusWithNodeId(rdsStatusOr.getStatus(), Code.UNAVAILABLE, "ADS stream closed with OK before receiving a response"); } @@ -3658,17 +3863,21 @@ public void streamClosedAfterSendingResponses() { // Check metric data. callback_ReportServerConnection(); verifyServerConnection(3, true, xdsServerInfo.target()); - verify(ldsResourceWatcher, never()).onError(errorCaptor.capture()); - verify(rdsResourceWatcher, never()).onError(errorCaptor.capture()); + verify(ldsResourceWatcher, never()).onAmbientError(any(Status.class)); + verify(rdsResourceWatcher, never()).onAmbientError(any(Status.class)); + verify(ldsResourceWatcher, times(1)).onResourceChanged(any()); + verify(rdsResourceWatcher, times(1)).onResourceChanged(any()); } @Test public void streamClosedAndRetryWithBackoff() { - InOrder inOrder = Mockito.inOrder(backoffPolicyProvider, backoffPolicy1, backoffPolicy2); + InOrder inOrder = inOrder(backoffPolicyProvider, backoffPolicy1, backoffPolicy2); + InOrder ldsWatcherInOrder = inOrder(ldsResourceWatcher); + InOrder rdsWatcherInOrder = inOrder(rdsResourceWatcher); + InOrder cdsWatcherInOrder = inOrder(cdsResourceWatcher); + InOrder edsWatcherInOrder = inOrder(edsResourceWatcher); + when(backoffPolicyProvider.get()).thenReturn(backoffPolicy1, backoffPolicy2, backoffPolicy2); xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); - // Check metric data. - callback_ReportServerConnection(); - verifyServerConnection(1, true, xdsServerInfo.target()); xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, rdsResourceWatcher); xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CDS_RESOURCE, cdsResourceWatcher); @@ -3679,25 +3888,25 @@ public void streamClosedAndRetryWithBackoff() { call.verifyRequest(CDS, CDS_RESOURCE, "", "", NODE); call.verifyRequest(EDS, EDS_RESOURCE, "", "", NODE); - // Management server closes the RPC stream with an error. + // Management server closes the RPC stream with an error. No response received yet. fakeClock.forwardNanos(1000L); // Make sure retry isn't based on stopwatch 0 call.sendError(Status.UNKNOWN.asException()); - verify(ldsResourceWatcher, Mockito.timeout(1000).times(1)) - .onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNKNOWN, ""); - verify(rdsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNKNOWN, ""); - verify(cdsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNKNOWN, ""); - verify(edsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNKNOWN, ""); + ldsWatcherInOrder.verify(ldsResourceWatcher, timeout(1000)).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNKNOWN)); + rdsWatcherInOrder.verify(rdsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNKNOWN)); + cdsWatcherInOrder.verify(cdsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNKNOWN)); + edsWatcherInOrder.verify(edsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNKNOWN)); - // Check metric data. - callback_ReportServerConnection(); - verifyServerConnection(1, false, xdsServerInfo.target()); + verifyServerFailureCount(1, 1, xdsServerInfo.target()); // Retry after backoff. - inOrder.verify(backoffPolicyProvider).get(); inOrder.verify(backoffPolicy1).nextBackoffNanos(); ScheduledTask retryTask = Iterables.getOnlyElement(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)); @@ -3709,25 +3918,23 @@ public void streamClosedAndRetryWithBackoff() { call.verifyRequest(CDS, CDS_RESOURCE, "", "", NODE); call.verifyRequest(EDS, EDS_RESOURCE, "", "", NODE); - // Check metric data. - callback_ReportServerConnection(); - verifyServerConnection(2, false, xdsServerInfo.target()); - - // Management server becomes unreachable. + // Management server becomes unreachable. No response received on this stream either. String errorMsg = "my fault"; call.sendError(Status.UNAVAILABLE.withDescription(errorMsg).asException()); - verify(ldsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); - verify(rdsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); - verify(cdsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); - verify(edsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + ldsWatcherInOrder.verify(ldsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); + rdsWatcherInOrder.verify(rdsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); + cdsWatcherInOrder.verify(cdsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); + edsWatcherInOrder.verify(edsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); - // Check metric data. - callback_ReportServerConnection(); - verifyServerConnection(3, false, xdsServerInfo.target()); + verifyServerFailureCount(2, 1, xdsServerInfo.target()); // Retry after backoff. inOrder.verify(backoffPolicy1).nextBackoffNanos(); @@ -3746,36 +3953,25 @@ public void streamClosedAndRetryWithBackoff() { mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(2))))); call.sendResponse(LDS, listeners, "63", "3242"); call.verifyRequest(LDS, LDS_RESOURCE, "63", "3242", NODE); - // Check metric data. - callback_ReportServerConnection(); - verifyServerConnection(2, true, xdsServerInfo.target()); + ldsWatcherInOrder.verify(ldsResourceWatcher).onResourceChanged( + argThat(statusOr -> statusOr.hasValue())); List routeConfigs = ImmutableList.of( Any.pack(mf.buildRouteConfiguration(RDS_RESOURCE, mf.buildOpaqueVirtualHosts(2)))); call.sendResponse(RDS, routeConfigs, "5", "6764"); call.verifyRequest(RDS, RDS_RESOURCE, "5", "6764", NODE); + rdsWatcherInOrder.verify(rdsResourceWatcher).onResourceChanged( + argThat(statusOr -> statusOr.hasValue())); - call.sendError(Status.DEADLINE_EXCEEDED.asException()); - fakeClock.forwardNanos(100L); - call = resourceDiscoveryCalls.poll(); + // Stream fails AFTER a response. Error is suppressed and no watcher notification occurs. call.sendError(Status.DEADLINE_EXCEEDED.asException()); - // Already received LDS and RDS, so they only error twice. - verify(ldsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verify(rdsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verify(cdsResourceWatcher, times(3)).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.DEADLINE_EXCEEDED, ""); - verify(edsResourceWatcher, times(3)).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.DEADLINE_EXCEEDED, ""); - - // Check metric data. - callback_ReportServerConnection(); - verifyServerConnection(2, true, xdsServerInfo.target()); - verifyServerConnection(4, false, xdsServerInfo.target()); + // Failure count does NOT increase. + verifyServerFailureCount(2, 1, xdsServerInfo.target()); // Reset backoff sequence and retry after backoff. inOrder.verify(backoffPolicyProvider).get(); - inOrder.verify(backoffPolicy2, times(2)).nextBackoffNanos(); + inOrder.verify(backoffPolicy2).nextBackoffNanos(); retryTask = Iterables.getOnlyElement(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)); fakeClock.forwardNanos(retryTask.getDelay(TimeUnit.NANOSECONDS)); @@ -3785,23 +3981,21 @@ public void streamClosedAndRetryWithBackoff() { call.verifyRequest(CDS, CDS_RESOURCE, "", "", NODE); call.verifyRequest(EDS, EDS_RESOURCE, "", "", NODE); - // Check metric data, should be in error since haven't gotten a response. - callback_ReportServerConnection(); - verifyServerConnection(2, true, xdsServerInfo.target()); - verifyServerConnection(5, false, xdsServerInfo.target()); - - // Management server becomes unreachable again. + // Management server becomes unreachable again. This is on a new stream, so error propagates. call.sendError(Status.UNAVAILABLE.asException()); - verify(ldsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verify(rdsResourceWatcher, times(2)).onError(errorCaptor.capture()); - verify(cdsResourceWatcher, times(4)).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); - verify(edsResourceWatcher, times(4)).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); - - // Check metric data. - callback_ReportServerConnection(); - verifyServerConnection(6, false, xdsServerInfo.target()); + ldsWatcherInOrder.verify(ldsResourceWatcher).onAmbientError( + argThat(status -> status.getCode() == Code.UNAVAILABLE)); + rdsWatcherInOrder.verify(rdsResourceWatcher).onAmbientError( + argThat(status -> status.getCode() == Code.UNAVAILABLE)); + cdsWatcherInOrder.verify(cdsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); + edsWatcherInOrder.verify(edsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); + + // Failure count is now 3. + verifyServerFailureCount(3, 1, xdsServerInfo.target()); // Retry after backoff. inOrder.verify(backoffPolicy2).nextBackoffNanos(); @@ -3815,16 +4009,41 @@ public void streamClosedAndRetryWithBackoff() { call.verifyRequest(CDS, CDS_RESOURCE, "", "", NODE); call.verifyRequest(EDS, EDS_RESOURCE, "", "", NODE); - // Check metric data. - callback_ReportServerConnection(); - verifyServerConnection(7, false, xdsServerInfo.target()); - - // Send a response so CPC is considered working + // Send a response so CPC is considered working and close gracefully. call.sendResponse(LDS, listeners, "63", "3242"); - callback_ReportServerConnection(); - verifyServerConnection(3, true, xdsServerInfo.target()); + call.sendCompleted(); + + // Final failure count is still 3. + verifyServerFailureCount(3, 1, xdsServerInfo.target()); + } + + @Test + public void newWatcher_receivesCachedDataAndAmbientError() throws Exception { + InOrder inOrder = inOrder(ldsResourceWatcher); + DiscoveryRpcCall call1 = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE, + ldsResourceWatcher); + call1.sendResponse(LDS, testListenerRds, VERSION_1, "0000"); + inOrder.verify(ldsResourceWatcher, timeout(5000)) + .onResourceChanged(argThat(statusOr -> statusOr.hasValue())); + + call1.sendError(Status.DEADLINE_EXCEEDED.asException()); + ScheduledTask retryTask = + Iterables.getOnlyElement(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)); + fakeClock.forwardNanos(retryTask.getDelay(TimeUnit.NANOSECONDS)); + DiscoveryRpcCall call2 = resourceDiscoveryCalls.poll(); + Status propagatedError = Status.UNAVAILABLE.withDescription("real failure"); + call2.sendError(propagatedError.asException()); + inOrder.verify(ldsResourceWatcher, timeout(5000)).onAmbientError( + argThat(status -> status.getCode() == Code.UNAVAILABLE)); + @SuppressWarnings("unchecked") + ResourceWatcher ldsResourceWatcher2 = mock(ResourceWatcher.class); + xdsClient.watchXdsResource( + XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher2); - inOrder.verifyNoMoreInteractions(); + verify(ldsResourceWatcher2, timeout(5000)).onResourceChanged( + argThat(statusOr -> statusOr.hasValue())); + verify(ldsResourceWatcher2, timeout(5000)).onAmbientError( + argThat(status -> status.getCode() == Code.UNAVAILABLE)); } @Test @@ -3839,10 +4058,10 @@ public void streamClosedAndRetryRaceWithAddRemoveWatchers() { verifyServerConnection(1, true, xdsServerInfo.target()); call.sendError(Status.UNAVAILABLE.asException()); verify(ldsResourceWatcher, Mockito.timeout(1000).times(1)) - .onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); - verify(rdsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); + .onResourceChanged(ldsUpdateCaptor.capture()); + verifyStatusWithNodeId(ldsUpdateCaptor.getValue().getStatus(), Code.UNAVAILABLE, ""); + verify(rdsResourceWatcher).onResourceChanged(rdsUpdateCaptor.capture()); + verifyStatusWithNodeId(rdsUpdateCaptor.getValue().getStatus(), Code.UNAVAILABLE, ""); ScheduledTask retryTask = Iterables.getOnlyElement(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)); assertThat(retryTask.getDelay(TimeUnit.NANOSECONDS)).isEqualTo(10L); @@ -3915,22 +4134,29 @@ public void streamClosedAndRetryRestartsResourceInitialFetchTimerForUnresolvedRe call.sendError(Status.UNAVAILABLE.asException()); assertThat(cdsResourceTimeout.isCancelled()).isTrue(); assertThat(edsResourceTimeout.isCancelled()).isTrue(); - verify(ldsResourceWatcher, never()).onError(errorCaptor.capture()); - verify(rdsResourceWatcher, never()).onError(errorCaptor.capture()); - verify(cdsResourceWatcher, never()).onError(errorCaptor.capture()); - verify(edsResourceWatcher, never()).onError(errorCaptor.capture()); - // Check metric data. + + // With the reverted logic, the first error is suppressed because a response was received. + // We verify that no error callbacks are invoked at this point. + verify(ldsResourceWatcher, never()).onAmbientError(any(Status.class)); + verify(rdsResourceWatcher, never()).onAmbientError(any(Status.class)); + + // The metric report for a failed server connection is also suppressed. callback_ReportServerConnection(); verifyServerConnection(4, true, xdsServerInfo.target()); - verify(cdsResourceWatcher, never()).onError(errorCaptor.capture()); // We had a response fakeClock.forwardTime(5, TimeUnit.SECONDS); DiscoveryRpcCall call2 = resourceDiscoveryCalls.poll(); call2.sendError(Status.UNAVAILABLE.asException()); - verify(cdsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); - verify(edsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, ""); + + // Now, verify the watchers are notified as expected. + verify(ldsResourceWatcher).onAmbientError(any(Status.class)); + verify(rdsResourceWatcher).onAmbientError(any(Status.class)); + verify(cdsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); + verify(edsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); fakeClock.forwardTime(5, TimeUnit.SECONDS); DiscoveryRpcCall call3 = resourceDiscoveryCalls.poll(); @@ -3988,8 +4214,10 @@ public void serverSideListenerFound() { call.sendResponse(LDS, listeners, "0", "0000"); // Client sends an ACK LDS request. call.verifyRequest(LDS, Collections.singletonList(LISTENER_RESOURCE), "0", "0000", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - EnvoyServerProtoData.Listener parsedListener = ldsUpdateCaptor.getValue().listener(); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + EnvoyServerProtoData.Listener parsedListener = statusOrUpdate.getValue().listener(); assertThat(parsedListener.name()).isEqualTo(LISTENER_RESOURCE); assertThat(parsedListener.address()).isEqualTo("0.0.0.0:7000"); assertThat(parsedListener.defaultFilterChain()).isNull(); @@ -4026,7 +4254,8 @@ public void serverSideListenerNotFound() { verifyNoInteractions(ldsResourceWatcher); fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(ldsResourceWatcher).onResourceDoesNotExist(LISTENER_RESOURCE); + verify(ldsResourceWatcher).onResourceChanged(argThat( + statusOr -> statusOr.getStatus().getDescription().contains(LISTENER_RESOURCE))); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); } @@ -4052,8 +4281,10 @@ public void serverSideListenerResponseErrorHandling_badDownstreamTlsContext() { + "0.0.0.0:7000\' validation error: " + "common-tls-context is required in downstream-tls-context"; call.verifyRequestNack(LDS, LISTENER_RESOURCE, "", "0000", NODE, ImmutableList.of(errorMsg)); - verify(ldsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isFalse(); + verifyStatusWithNodeId(statusOrUpdate.getStatus(), Code.UNAVAILABLE, errorMsg); } @Test @@ -4079,8 +4310,8 @@ public void serverSideListenerResponseErrorHandling_badTransportSocketName() { + "transport-socket with name envoy.transport_sockets.bad1 not supported."; call.verifyRequestNack(LDS, LISTENER_RESOURCE, "", "0000", NODE, ImmutableList.of( errorMsg)); - verify(ldsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + verifyStatusWithNodeId(ldsUpdateCaptor.getValue().getStatus(), Code.UNAVAILABLE, errorMsg); } @Test @@ -4110,16 +4341,17 @@ public void sendingToStoppedServer() throws Exception { .build() .start()); fakeClock.forwardTime(5, TimeUnit.SECONDS); - verify(ldsResourceWatcher, never()).onResourceDoesNotExist(LDS_RESOURCE); + verify(ldsResourceWatcher, never()).onResourceChanged(argThat( + statusOr -> statusOr.getStatus().getDescription().contains(LDS_RESOURCE))); fakeClock.forwardTime(20, TimeUnit.SECONDS); // Trigger rpcRetryTimer DiscoveryRpcCall call = resourceDiscoveryCalls.poll(3, TimeUnit.SECONDS); // Check metric data. callback_ReportServerConnection(); - verifyServerConnection(2, false, xdsServerInfo.target()); if (call == null) { // The first rpcRetry may have happened before the channel was ready fakeClock.forwardTime(50, TimeUnit.SECONDS); call = resourceDiscoveryCalls.poll(3, TimeUnit.SECONDS); } + verifyServerConnection(2, false, xdsServerInfo.target()); // Check metric data. callback_ReportServerConnection(); @@ -4131,8 +4363,12 @@ public void sendingToStoppedServer() throws Exception { // Send a response and do verifications call.sendResponse(LDS, mf.buildWrappedResource(testListenerVhosts), VERSION_1, "0001"); call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0001", NODE); - verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - verifyGoldenListenerVhosts(ldsUpdateCaptor.getValue()); + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = ArgumentCaptor.forClass(StatusOr.class); + verify(ldsResourceWatcher, timeout(1000).atLeast(2)).onResourceChanged(captor.capture()); + StatusOr lastValue = captor.getAllValues().get(captor.getAllValues().size() - 1); + assertThat(lastValue.hasValue()).isTrue(); + verifyGoldenListenerVhosts(lastValue.getValue()); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_1, TIME_INCREMENT); verifySubscribedResourcesMetadataSizes(1, 1, 0, 0); @@ -4153,8 +4389,8 @@ public void sendToBadUrl() throws Exception { client.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); fakeClock.forwardTime(20, TimeUnit.SECONDS); verify(ldsResourceWatcher, Mockito.timeout(5000).atLeastOnce()) - .onError(errorCaptor.capture()); - assertThat(errorCaptor.getValue().getDescription()).contains(garbageUri); + .onResourceChanged(ldsUpdateCaptor.capture()); + assertThat(ldsUpdateCaptor.getValue().getStatus().getDescription()).contains(garbageUri); client.shutdown(); } @@ -4169,8 +4405,10 @@ public void circuitBreakingConversionOf32bitIntTo64bitLongForMaxRequestNegativeV // Client sent an ACK CDS request. call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + CdsUpdate cdsUpdate = statusOrUpdate.getValue(); assertThat(cdsUpdate.clusterName()).isEqualTo(CDS_RESOURCE); assertThat(cdsUpdate.clusterType()).isEqualTo(ClusterType.EDS); @@ -4185,7 +4423,10 @@ public void sendToNonexistentServer() throws Exception { // file. Assume localhost doesn't speak HTTP/2 on the finger port XdsClientImpl client = createXdsClient("localhost:79"); client.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); - verify(ldsResourceWatcher, Mockito.timeout(5000).times(1)).onError(ArgumentMatchers.any()); + verify(ldsResourceWatcher, Mockito.timeout(5000)).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isFalse(); + assertThat(statusOrUpdate.getStatus().getCode()).isEqualTo(Status.Code.UNAVAILABLE); assertThat(fakeClock.numPendingTasks()).isEqualTo(1); //retry assertThat(fakeClock.getPendingTasks().iterator().next().toString().contains("RpcRetryTask")) .isTrue(); @@ -4251,19 +4492,27 @@ public void serverFailureMetricReport() { DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); // Management server closes the RPC stream before sending any response. call.sendCompleted(); - verify(ldsResourceWatcher, Mockito.timeout(1000).times(1)) - .onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, + verify(ldsResourceWatcher, Mockito.timeout(1000)).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr ldsStatusOr = ldsUpdateCaptor.getValue(); + assertThat(ldsStatusOr.hasValue()).isFalse(); + verifyStatusWithNodeId(ldsStatusOr.getStatus(), Code.UNAVAILABLE, "ADS stream closed with OK before receiving a response"); - verify(rdsResourceWatcher).onError(errorCaptor.capture()); - verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, + verify(rdsResourceWatcher).onResourceChanged(rdsUpdateCaptor.capture()); + StatusOr rdsStatusOr = rdsUpdateCaptor.getValue(); + assertThat(rdsStatusOr.hasValue()).isFalse(); + verifyStatusWithNodeId(rdsStatusOr.getStatus(), Code.UNAVAILABLE, "ADS stream closed with OK before receiving a response"); verifyServerFailureCount(1, 1, xdsServerInfo.target()); } @Test public void serverFailureMetricReport_forRetryAndBackoff() { - InOrder inOrder = Mockito.inOrder(backoffPolicyProvider, backoffPolicy1, backoffPolicy2); + InOrder inOrder = inOrder(backoffPolicyProvider, backoffPolicy1, backoffPolicy2); + InOrder ldsWatcherInOrder = inOrder(ldsResourceWatcher); + InOrder rdsWatcherInOrder = inOrder(rdsResourceWatcher); + InOrder cdsWatcherInOrder = inOrder(cdsResourceWatcher); + InOrder edsWatcherInOrder = inOrder(edsResourceWatcher); + when(backoffPolicyProvider.get()).thenReturn(backoffPolicy1, backoffPolicy2, backoffPolicy2); xdsClient.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher); xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_RESOURCE, rdsResourceWatcher); @@ -4273,6 +4522,18 @@ public void serverFailureMetricReport_forRetryAndBackoff() { // Management server closes the RPC stream with an error. call.sendError(Status.UNKNOWN.asException()); + ldsWatcherInOrder.verify(ldsResourceWatcher, timeout(1000)).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNKNOWN)); + rdsWatcherInOrder.verify(rdsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNKNOWN)); + cdsWatcherInOrder.verify(cdsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNKNOWN)); + edsWatcherInOrder.verify(edsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNKNOWN)); verifyServerFailureCount(1, 1, xdsServerInfo.target()); // Retry after backoff. @@ -4287,6 +4548,18 @@ public void serverFailureMetricReport_forRetryAndBackoff() { // Management server becomes unreachable. String errorMsg = "my fault"; call.sendError(Status.UNAVAILABLE.withDescription(errorMsg).asException()); + ldsWatcherInOrder.verify(ldsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); + rdsWatcherInOrder.verify(rdsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); + cdsWatcherInOrder.verify(cdsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); + edsWatcherInOrder.verify(edsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); verifyServerFailureCount(2, 1, xdsServerInfo.target()); // Retry after backoff. @@ -4299,13 +4572,18 @@ public void serverFailureMetricReport_forRetryAndBackoff() { List resources = ImmutableList.of(FAILING_ANY, testListenerRds, FAILING_ANY); call.sendResponse(LDS, resources, "63", "3242"); + ldsWatcherInOrder.verify(ldsResourceWatcher).onResourceChanged( + argThat(statusOr -> statusOr.hasValue())); List routeConfigs = ImmutableList.of(FAILING_ANY, testRouteConfig, FAILING_ANY); call.sendResponse(RDS, routeConfigs, "5", "6764"); + rdsWatcherInOrder.verify(rdsResourceWatcher).onResourceChanged( + argThat(statusOr -> statusOr.hasValue())); + // Stream fails AFTER a response. Error is suppressed and no watcher notification occurs. call.sendError(Status.DEADLINE_EXCEEDED.asException()); - // Server Failure metric will not be reported, as stream is closed with an error after receiving - // a response + + // Failure count does NOT increase because the error was suppressed. It is still 2. verifyServerFailureCount(2, 1, xdsServerInfo.target()); // Reset backoff sequence and retry after backoff. @@ -4317,8 +4595,20 @@ public void serverFailureMetricReport_forRetryAndBackoff() { fakeClock.forwardNanos(20L); call = resourceDiscoveryCalls.poll(); - // Management server becomes unreachable again. + // Management server becomes unreachable again. This is on a new stream, so error propagates. call.sendError(Status.UNAVAILABLE.asException()); + ldsWatcherInOrder.verify(ldsResourceWatcher).onAmbientError( + argThat(status -> status.getCode() == Code.UNAVAILABLE)); + rdsWatcherInOrder.verify(rdsResourceWatcher).onAmbientError( + argThat(status -> status.getCode() == Code.UNAVAILABLE)); + cdsWatcherInOrder.verify(cdsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); + edsWatcherInOrder.verify(edsResourceWatcher).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Code.UNAVAILABLE)); + + // Server failure count is now 3. verifyServerFailureCount(3, 1, xdsServerInfo.target()); // Retry after backoff. @@ -4332,12 +4622,11 @@ public void serverFailureMetricReport_forRetryAndBackoff() { List clusters = ImmutableList.of(FAILING_ANY, testClusterRoundRobin); call.sendResponse(CDS, clusters, VERSION_1, "0000"); call.sendCompleted(); - // Server Failure metric will not be reported once again, as stream is closed after receiving a - // response + + // Final failure count is still 3 as the stream closed gracefully. verifyServerFailureCount(3, 1, xdsServerInfo.target()); } - private XdsClientImpl createXdsClient(String serverUri) { BootstrapInfo bootstrapInfo = buildBootStrap(serverUri); return new XdsClientImpl( diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java index b2b4e2c14cf..27ee8d22825 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java @@ -20,7 +20,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -34,9 +34,13 @@ import io.grpc.Grpc; import io.grpc.MetricRecorder; import io.grpc.Status; +import io.grpc.StatusOr; import io.grpc.internal.ExponentialBackoffPolicy; import io.grpc.internal.FakeClock; import io.grpc.internal.ObjectPool; +import io.grpc.xds.XdsClusterResource.CdsUpdate; +import io.grpc.xds.XdsListenerResource.LdsUpdate; +import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import io.grpc.xds.client.Bootstrapper; import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.client.LoadReportClient; @@ -98,25 +102,25 @@ public class XdsClientFallbackTest { private XdsClientMetricReporter xdsClientMetricReporter; @Captor - private ArgumentCaptor errorCaptor; - + private ArgumentCaptor> ldsUpdateCaptor; + @Captor + private ArgumentCaptor> rdsUpdateCaptor; private final XdsClient.ResourceWatcher raalLdsWatcher = - new XdsClient.ResourceWatcher() { - - @Override - public void onChanged(XdsListenerResource.LdsUpdate update) { - log.log(Level.FINE, "LDS update: " + update); - } + new XdsClient.ResourceWatcher() { @Override - public void onError(Status error) { - log.log(Level.FINE, "LDS update error: " + error.getDescription()); + public void onResourceChanged(StatusOr update) { + if (update.hasValue()) { + log.log(Level.FINE, "LDS update: " + update.getValue()); + } else { + log.log(Level.FINE, "LDS resource error: " + update.getStatus().getDescription()); + } } @Override - public void onResourceDoesNotExist(String resourceName) { - log.log(Level.FINE, "LDS resource does not exist: " + resourceName); + public void onAmbientError(Status error) { + log.log(Level.FINE, "LDS ambient error: " + error.getDescription()); } }; @@ -133,30 +137,30 @@ public void onResourceDoesNotExist(String resourceName) { @Mock private XdsClient.ResourceWatcher rdsWatcher3; - private final XdsClient.ResourceWatcher raalCdsWatcher = - new XdsClient.ResourceWatcher() { + private final XdsClient.ResourceWatcher raalCdsWatcher = + new XdsClient.ResourceWatcher() { @Override - public void onChanged(XdsClusterResource.CdsUpdate update) { - log.log(Level.FINE, "CDS update: " + update); + public void onResourceChanged(StatusOr update) { + if (update.hasValue()) { + log.log(Level.FINE, "CDS update: " + update.getValue()); + } else { + log.log(Level.FINE, "CDS resource error: " + update.getStatus().getDescription()); + } } @Override - public void onError(Status error) { - log.log(Level.FINE, "CDS update error: " + error.getDescription()); - } - - @Override - public void onResourceDoesNotExist(String resourceName) { - log.log(Level.FINE, "CDS resource does not exist: " + resourceName); + public void onAmbientError(Status error) { + // Logic from the old onError method for transient errors. + log.log(Level.FINE, "CDS ambient error: " + error.getDescription()); } }; @SuppressWarnings("unchecked") - private final XdsClient.ResourceWatcher cdsWatcher = + private final XdsClient.ResourceWatcher cdsWatcher = mock(XdsClient.ResourceWatcher.class, delegatesTo(raalCdsWatcher)); @Mock - private XdsClient.ResourceWatcher cdsWatcher2; + private XdsClient.ResourceWatcher cdsWatcher2; @Rule(order = 0) public ControlPlaneRule mainXdsServer = @@ -223,12 +227,14 @@ public void everything_okay() { fallbackServer.restartXdsServer(); xdsClient = xdsClientPool.getObject(); xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); - verify(ldsWatcher, timeout(5000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener( - MAIN_HTTP_CONNECTION_MANAGER)); + verify(ldsWatcher, timeout(5000)).onResourceChanged(ldsUpdateCaptor.capture()); + assertThat(ldsUpdateCaptor.getValue().hasValue()).isTrue(); + assertThat(ldsUpdateCaptor.getValue().getValue()).isEqualTo( + LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_NAME, rdsWatcher); - verify(rdsWatcher, timeout(5000)).onChanged(any()); + verify(rdsWatcher, timeout(5000)).onResourceChanged(rdsUpdateCaptor.capture()); + assertThat(rdsUpdateCaptor.getValue().hasValue()).isTrue(); } @Test @@ -240,9 +246,9 @@ public void mainServerDown_fallbackServerUp() { xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); - verify(ldsWatcher, timeout(5000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener( - FALLBACK_HTTP_CONNECTION_MANAGER)); + verify(ldsWatcher, timeout(5000)).onResourceChanged( + StatusOr.fromValue(XdsListenerResource.LdsUpdate.forApiListener( + FALLBACK_HTTP_CONNECTION_MANAGER))); } @Test @@ -253,7 +259,8 @@ public void useBadAuthority() { String badPrefix = "xdstp://authority.xds.bad/envoy.config.listener.v3.Listener/"; xdsClient.watchXdsResource(XdsListenerResource.getInstance(), badPrefix + "listener.googleapis.com", ldsWatcher); - inOrder.verify(ldsWatcher, timeout(5000)).onError(any()); + inOrder.verify(ldsWatcher, timeout(5000)).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue())); xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), badPrefix + "route-config.googleapis.bad", rdsWatcher); @@ -261,14 +268,20 @@ public void useBadAuthority() { badPrefix + "route-config2.googleapis.bad", rdsWatcher2); xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), badPrefix + "route-config3.googleapis.bad", rdsWatcher3); - inOrder.verify(rdsWatcher, timeout(5000).times(1)).onError(any()); - inOrder.verify(rdsWatcher2, timeout(5000).times(1)).onError(any()); - inOrder.verify(rdsWatcher3, timeout(5000).times(1)).onError(any()); - verify(rdsWatcher, never()).onChanged(any()); + inOrder.verify(rdsWatcher, timeout(5000)).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue())); + inOrder.verify(rdsWatcher2, timeout(5000)).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue())); + inOrder.verify(rdsWatcher3, timeout(5000)).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue())); + verify(rdsWatcher, never()).onResourceChanged(argThat(StatusOr::hasValue)); // even after an error, a valid one will still work xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher2); - verify(ldsWatcher2, timeout(5000)).onChanged( + verify(ldsWatcher2, timeout(5000)).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOr = ldsUpdateCaptor.getValue(); + assertThat(statusOr.hasValue()).isTrue(); + assertThat(statusOr.getValue()).isEqualTo( XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); } @@ -279,21 +292,24 @@ public void both_down_restart_main() { xdsClient = xdsClientPool.getObject(); xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); - verify(ldsWatcher, timeout(5000).atLeastOnce()).onError(any()); - verify(ldsWatcher, timeout(5000).times(0)).onChanged(any()); + verify(ldsWatcher, timeout(5000).atLeastOnce()) + .onResourceChanged(argThat(statusOr -> !statusOr.hasValue())); + verify(ldsWatcher, never()).onResourceChanged(argThat(StatusOr::hasValue)); xdsClient.watchXdsResource( XdsRouteConfigureResource.getInstance(), RDS_NAME, rdsWatcher2); - verify(rdsWatcher2, timeout(5000).atLeastOnce()).onError(any()); + verify(rdsWatcher2, timeout(5000).atLeastOnce()) + .onResourceChanged(argThat(statusOr -> !statusOr.hasValue())); mainXdsServer.restartXdsServer(); xdsClient.watchXdsResource( XdsRouteConfigureResource.getInstance(), RDS_NAME, rdsWatcher); - verify(ldsWatcher, timeout(16000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); - verify(rdsWatcher, timeout(5000)).onChanged(any()); - verify(rdsWatcher2, timeout(5000)).onChanged(any()); + verify(ldsWatcher, timeout(16000)).onResourceChanged( + argThat(statusOr -> statusOr.hasValue() && statusOr.getValue().equals( + XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)))); + verify(rdsWatcher, timeout(5000)).onResourceChanged(argThat(StatusOr::hasValue)); + verify(rdsWatcher2, timeout(5000)).onResourceChanged(argThat(StatusOr::hasValue)); } @Test @@ -304,10 +320,16 @@ public void mainDown_fallbackUp_restart_main() { InOrder inOrder = inOrder(ldsWatcher, rdsWatcher, cdsWatcher, cdsWatcher2); xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); - inOrder.verify(ldsWatcher, timeout(5000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER)); + inOrder.verify(ldsWatcher, timeout(5000)).onResourceChanged( + StatusOr.fromValue(XdsListenerResource.LdsUpdate.forApiListener( + FALLBACK_HTTP_CONNECTION_MANAGER))); + + // Watch another resource, also from the fallback server. xdsClient.watchXdsResource(XdsClusterResource.getInstance(), FALLBACK_CLUSTER_NAME, cdsWatcher); - inOrder.verify(cdsWatcher, timeout(5000)).onChanged(any()); + @SuppressWarnings("unchecked") + ArgumentCaptor> cdsUpdateCaptor1 = ArgumentCaptor.forClass(StatusOr.class); + inOrder.verify(cdsWatcher, timeout(5000)).onResourceChanged(cdsUpdateCaptor1.capture()); + assertThat(cdsUpdateCaptor1.getValue().getStatus().isOk()).isTrue(); assertThat(fallbackServer.getService().getSubscriberCounts() .get("type.googleapis.com/envoy.config.listener.v3.Listener")).isEqualTo(1); @@ -315,15 +337,26 @@ public void mainDown_fallbackUp_restart_main() { mainXdsServer.restartXdsServer(); - verify(ldsWatcher, timeout(5000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + // The existing ldsWatcher should receive a new update from the main server. + // Note: This is not an inOrder verification because the timing of the switchover + // can vary. We just need to verify it happens. + verify(ldsWatcher, timeout(5000)).onResourceChanged( + StatusOr.fromValue(XdsListenerResource.LdsUpdate.forApiListener( + MAIN_HTTP_CONNECTION_MANAGER))); + // Watch a new resource; should now come from the main server. xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_NAME, rdsWatcher); - inOrder.verify(rdsWatcher, timeout(5000)).onChanged(any()); + @SuppressWarnings("unchecked") + ArgumentCaptor> rdsUpdateCaptor = ArgumentCaptor.forClass(StatusOr.class); + inOrder.verify(rdsWatcher, timeout(5000)).onResourceChanged(rdsUpdateCaptor.capture()); + assertThat(rdsUpdateCaptor.getValue().getStatus().isOk()).isTrue(); verifyNoSubscribers(fallbackServer); xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CLUSTER_NAME, cdsWatcher2); - inOrder.verify(cdsWatcher2, timeout(5000)).onChanged(any()); + @SuppressWarnings("unchecked") + ArgumentCaptor> cdsUpdateCaptor2 = ArgumentCaptor.forClass(StatusOr.class); + inOrder.verify(cdsWatcher2, timeout(5000)).onResourceChanged(cdsUpdateCaptor2.capture()); + assertThat(cdsUpdateCaptor2.getValue().getStatus().isOk()).isTrue(); verifyNoSubscribers(fallbackServer); assertThat(mainXdsServer.getService().getSubscriberCounts() @@ -363,11 +396,12 @@ public XdsTransport create(Bootstrapper.ServerInfo serverInfo) { xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); - verify(ldsWatcher, timeout(5000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + // Initial resource fetch from the main server + verify(ldsWatcher, timeout(5000)).onResourceChanged( + StatusOr.fromValue(LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER))); xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_NAME, rdsWatcher); - verify(rdsWatcher, timeout(5000)).onChanged(any()); + verify(rdsWatcher, timeout(5000)).onResourceChanged(argThat(StatusOr::hasValue)); mainXdsServer.getServer().shutdownNow(); // Sleep for the ADS stream disconnect to be processed and for the retry to fail. Between those @@ -379,38 +413,43 @@ public XdsTransport create(Bootstrapper.ServerInfo serverInfo) { } // Shouldn't do fallback since all watchers are loaded - verify(ldsWatcher, never()).onChanged( - XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER)); + verify(ldsWatcher, never()).onResourceChanged(StatusOr.fromValue( + XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER))); // Should just get from cache xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher2); xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(), RDS_NAME, rdsWatcher2); - verify(ldsWatcher2, timeout(5000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); - verify(ldsWatcher, never()).onChanged( - XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER)); + verify(ldsWatcher2, timeout(5000)).onResourceChanged(StatusOr.fromValue( + XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER))); + verify(ldsWatcher, never()).onResourceChanged(StatusOr.fromValue( + XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER))); // Make sure that rdsWatcher wasn't called again - verify(rdsWatcher, times(1)).onChanged(any()); - verify(rdsWatcher2, timeout(5000)).onChanged(any()); + verify(rdsWatcher, times(1)).onResourceChanged(any()); + verify(rdsWatcher2, timeout(5000)).onResourceChanged(argThat(StatusOr::hasValue)); // Asking for something not in cache should force a fallback xdsClient.watchXdsResource(XdsClusterResource.getInstance(), FALLBACK_CLUSTER_NAME, cdsWatcher); - verify(ldsWatcher, timeout(5000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER)); - verify(ldsWatcher2, timeout(5000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER)); - verify(cdsWatcher, timeout(5000)).onChanged(any()); + verify(ldsWatcher, timeout(5000)).onResourceChanged(StatusOr.fromValue( + XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER))); + verify(ldsWatcher2, timeout(5000)).onResourceChanged(StatusOr.fromValue( + XdsListenerResource.LdsUpdate.forApiListener(FALLBACK_HTTP_CONNECTION_MANAGER))); + verify(cdsWatcher, timeout(5000)).onResourceChanged(argThat(StatusOr::hasValue)); xdsClient.watchXdsResource( XdsRouteConfigureResource.getInstance(), FALLBACK_RDS_NAME, rdsWatcher3); - verify(rdsWatcher3, timeout(5000)).onChanged(any()); + verify(rdsWatcher3, timeout(5000)).onResourceChanged(argThat(StatusOr::hasValue)); // Test that resource defined in main but not fallback is handled correctly xdsClient.watchXdsResource( XdsClusterResource.getInstance(), CLUSTER_NAME, cdsWatcher2); - verify(cdsWatcher2, never()).onResourceDoesNotExist(eq(CLUSTER_NAME)); + verify(cdsWatcher2, never()).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Status.Code.NOT_FOUND)); fakeClock.forwardTime(15000, TimeUnit.MILLISECONDS); // Does not exist timer - verify(cdsWatcher2, timeout(5000)).onResourceDoesNotExist(eq(CLUSTER_NAME)); + verify(cdsWatcher2, timeout(5000)).onResourceChanged( + argThat(statusOr -> !statusOr.hasValue() + && statusOr.getStatus().getCode() == Status.Code.NOT_FOUND + && statusOr.getStatus().getDescription().contains(CLUSTER_NAME))); xdsClient.shutdown(); executor.shutdown(); } @@ -419,22 +458,21 @@ public XdsTransport create(Bootstrapper.ServerInfo serverInfo) { public void connect_then_mainServerRestart_fallbackServerdown() { mainXdsServer.restartXdsServer(); xdsClient = xdsClientPool.getObject(); - xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); - verify(ldsWatcher, timeout(5000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); - + verify(ldsWatcher, timeout(5000)).onResourceChanged( + argThat(statusOr -> statusOr.hasValue() && statusOr.getValue().equals( + LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)))); mainXdsServer.getServer().shutdownNow(); fallbackServer.getServer().shutdownNow(); - xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CLUSTER_NAME, cdsWatcher); - mainXdsServer.restartXdsServer(); - verify(cdsWatcher, timeout(5000)).onChanged(any()); - verify(ldsWatcher, timeout(5000).atLeastOnce()).onChanged( - XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + verify(cdsWatcher, timeout(5000)).onResourceChanged( + argThat(statusOr -> statusOr.hasValue())); + verify(ldsWatcher, timeout(5000).atLeastOnce()).onResourceChanged( + argThat(statusOr -> statusOr.hasValue() && statusOr.getValue().equals( + LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)))); } @Test @@ -454,10 +492,10 @@ public void fallbackFromBadUrlToGoodOne() { client.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); fakeClock.forwardTime(20, TimeUnit.SECONDS); - verify(ldsWatcher, timeout(5000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener( - MAIN_HTTP_CONNECTION_MANAGER)); - verify(ldsWatcher, never()).onError(any()); + verify(ldsWatcher, timeout(5000)).onResourceChanged( + StatusOr.fromValue(XdsListenerResource.LdsUpdate.forApiListener( + MAIN_HTTP_CONNECTION_MANAGER))); + verify(ldsWatcher, never()).onAmbientError(any(Status.class)); client.shutdown(); } @@ -478,10 +516,13 @@ public void testGoodUrlFollowedByBadUrl() { xdsClientMetricReporter); client.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); - verify(ldsWatcher, timeout(5000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener( - MAIN_HTTP_CONNECTION_MANAGER)); - verify(ldsWatcher, never()).onError(any()); + verify(ldsWatcher, timeout(5000)).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOr = ldsUpdateCaptor.getValue(); + assertThat(statusOr.hasValue()).isTrue(); + assertThat(statusOr.getValue()).isEqualTo( + XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + verify(ldsWatcher, never()).onAmbientError(any()); + verify(ldsWatcher, times(1)).onResourceChanged(any()); client.shutdown(); } @@ -503,9 +544,12 @@ public void testTwoBadUrl() { client.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); fakeClock.forwardTime(20, TimeUnit.SECONDS); - verify(ldsWatcher, Mockito.timeout(5000).atLeastOnce()).onError(errorCaptor.capture()); - assertThat(errorCaptor.getValue().getDescription()).contains(garbageUri2); - verify(ldsWatcher, never()).onChanged(any()); + verify(ldsWatcher, Mockito.timeout(5000).atLeastOnce()) + .onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOr = ldsUpdateCaptor.getValue(); + assertThat(statusOr.hasValue()).isFalse(); + assertThat(statusOr.getStatus().getDescription()).contains(garbageUri2); + verify(ldsWatcher, never()).onResourceChanged(argThat(StatusOr::hasValue)); client.shutdown(); } @@ -525,8 +569,8 @@ public void used_then_mainServerRestart_fallbackServerUp() { xdsClient.watchXdsResource(XdsListenerResource.getInstance(), MAIN_SERVER, ldsWatcher); - verify(ldsWatcher, timeout(5000)).onChanged( - XdsListenerResource.LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER)); + verify(ldsWatcher, timeout(5000)).onResourceChanged( + StatusOr.fromValue(LdsUpdate.forApiListener(MAIN_HTTP_CONNECTION_MANAGER))); mainXdsServer.restartXdsServer(); @@ -535,7 +579,7 @@ public void used_then_mainServerRestart_fallbackServerUp() { xdsClient.watchXdsResource(XdsClusterResource.getInstance(), CLUSTER_NAME, cdsWatcher); - verify(cdsWatcher, timeout(5000)).onChanged(any()); + verify(cdsWatcher, timeout(5000)).onResourceChanged(any()); assertThat(getLrsServerInfo("localhost:" + fallbackServer.getServer().getPort())).isNull(); } @@ -563,5 +607,4 @@ public void used_then_mainServerRestart_fallbackServerUp() { "fallback-policy", "fallback" ); } - } diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java index 24ef60c4685..da310871c25 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFederationTest.java @@ -17,6 +17,9 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -24,6 +27,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import io.grpc.MetricRecorder; +import io.grpc.Status; +import io.grpc.StatusOr; import io.grpc.internal.ObjectPool; import io.grpc.xds.Filter.NamedFilterConfig; import io.grpc.xds.XdsListenerResource.LdsUpdate; @@ -46,6 +51,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -107,14 +113,19 @@ public void isolatedResourceDeletions() { xdsClient.watchXdsResource(XdsListenerResource.getInstance(), "xdstp://server-one/envoy.config.listener.v3.Listener/test-server", mockDirectPathWatcher); - verify(mockWatcher, timeout(10000)).onChanged( - LdsUpdate.forApiListener( - HttpConnectionManager.forRdsName(0, "route-config.googleapis.com", ImmutableList.of( - new NamedFilterConfig("terminal-filter", RouterFilter.ROUTER_CONFIG))))); - verify(mockDirectPathWatcher, timeout(10000)).onChanged( - LdsUpdate.forApiListener( - HttpConnectionManager.forRdsName(0, "route-config.googleapis.com", ImmutableList.of( - new NamedFilterConfig("terminal-filter", RouterFilter.ROUTER_CONFIG))))); + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = ArgumentCaptor.forClass(StatusOr.class); + LdsUpdate expectedUpdate = LdsUpdate.forApiListener( + HttpConnectionManager.forRdsName(0, "route-config.googleapis.com", ImmutableList.of( + new NamedFilterConfig("terminal-filter", RouterFilter.ROUTER_CONFIG)))); + + verify(mockWatcher, timeout(10000)).onResourceChanged(captor.capture()); + assertThat(captor.getValue().hasValue()).isTrue(); + assertThat(captor.getValue().getValue()).isEqualTo(expectedUpdate); + + verify(mockDirectPathWatcher, timeout(10000)).onResourceChanged(captor.capture()); + assertThat(captor.getValue().hasValue()).isTrue(); + assertThat(captor.getValue().getValue()).isEqualTo(expectedUpdate); // By setting the LDS config with a new server name we effectively make the old server to go // away as it is not in the configuration anymore. This change in one control plane (here the @@ -122,9 +133,13 @@ public void isolatedResourceDeletions() { // watcher of another control plane (here the DirectPath one). trafficdirector.setLdsConfig(ControlPlaneRule.buildServerListener(), ControlPlaneRule.buildClientListener("new-server")); - verify(mockWatcher, timeout(20000)).onResourceDoesNotExist("test-server"); - verify(mockDirectPathWatcher, times(0)).onResourceDoesNotExist( - "xdstp://server-one/envoy.config.listener.v3.Listener/test-server"); + verify(mockWatcher, timeout(20000)).onResourceChanged(argThat(statusOr -> { + return !statusOr.hasValue() + && statusOr.getStatus().getCode() == Status.Code.NOT_FOUND + && statusOr.getStatus().getDescription().contains("test-server"); + })); + verify(mockDirectPathWatcher, times(1)).onResourceChanged(any()); + verify(mockDirectPathWatcher, never()).onAmbientError(any()); } /** diff --git a/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java b/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java index 53dcd15dd3b..81186d0639c 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java @@ -36,6 +36,7 @@ import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.Status; +import io.grpc.StatusOr; import io.grpc.inprocess.InProcessSocketAddress; import io.grpc.internal.TestUtils.NoopChannelLogger; import io.grpc.netty.GrpcHttp2ConnectionHandler; @@ -172,7 +173,7 @@ public void run() { null, Protocol.TCP); LdsUpdate listenerUpdate = LdsUpdate.forTcpListener(tcpListener); - xdsClient.ldsWatcher.onChanged(listenerUpdate); + xdsClient.ldsWatcher.onResourceChanged(StatusOr.fromValue(listenerUpdate)); verify(listener, timeout(5000)).onServing(); start.get(START_WAIT_AFTER_LISTENER_MILLIS, TimeUnit.MILLISECONDS); FilterChainSelector selector = selectorManager.getSelectorToUpdateSelector(); @@ -193,7 +194,8 @@ public void run() { } }); String ldsWatched = xdsClient.ldsResource.get(5, TimeUnit.SECONDS); - xdsClient.ldsWatcher.onResourceDoesNotExist(ldsWatched); + Status status = Status.NOT_FOUND.withDescription("Resource not found: " + ldsWatched); + xdsClient.ldsWatcher.onResourceChanged(StatusOr.fromStatus(status)); verify(listener, timeout(5000)).onNotServing(any()); try { start.get(START_WAIT_AFTER_LISTENER_MILLIS, TimeUnit.MILLISECONDS); @@ -278,7 +280,8 @@ public void releaseOldSupplierOnNotFound_verifyClose() throws Exception { getSslContextProviderSupplier(selectorManager.getSelectorToUpdateSelector()); assertThat(returnedSupplier.getTlsContext()).isSameInstanceAs(tlsContext1); callUpdateSslContext(returnedSupplier); - xdsClient.ldsWatcher.onResourceDoesNotExist("not-found Error"); + Status status = Status.NOT_FOUND.withDescription("not-found Error"); + xdsClient.ldsWatcher.onResourceChanged(StatusOr.fromStatus(status)); verify(tlsContextManager, times(1)).releaseServerSslContextProvider(eq(sslContextProvider1)); } @@ -295,7 +298,7 @@ public void releaseOldSupplierOnTemporaryError_noClose() throws Exception { getSslContextProviderSupplier(selectorManager.getSelectorToUpdateSelector()); assertThat(returnedSupplier.getTlsContext()).isSameInstanceAs(tlsContext1); callUpdateSslContext(returnedSupplier); - xdsClient.ldsWatcher.onError(Status.CANCELLED); + xdsClient.ldsWatcher.onAmbientError(Status.CANCELLED); verify(tlsContextManager, never()).releaseServerSslContextProvider(eq(sslContextProvider1)); } diff --git a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java index fca7b5220e6..7bae7000eaf 100644 --- a/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java @@ -853,7 +853,7 @@ public void ldsUpdateAfterShutdown() { serverName, resourceWatcher, MoreExecutors.directExecutor()); - verify(resourceWatcher).onChanged(any()); + verify(resourceWatcher).onResourceChanged(argThat(StatusOr::hasValue)); syncContext.execute(() -> { // Shutdown before any updates. This will unsubscribe from XdsClient, but only after this @@ -862,7 +862,7 @@ public void ldsUpdateAfterShutdown() { XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS2", "CDS", "EDS", ENDPOINT_HOSTNAME, ENDPOINT_PORT); - verify(resourceWatcher, times(2)).onChanged(any()); + verify(resourceWatcher, times(2)).onResourceChanged(argThat(StatusOr::hasValue)); xdsClient.cancelXdsResourceWatch( XdsListenerResource.getInstance(), serverName, resourceWatcher); }); @@ -885,7 +885,7 @@ public void rdsUpdateAfterShutdown() { "RDS", resourceWatcher, MoreExecutors.directExecutor()); - verify(resourceWatcher).onChanged(any()); + verify(resourceWatcher).onResourceChanged(argThat(StatusOr::hasValue)); syncContext.execute(() -> { // Shutdown before any updates. This will unsubscribe from XdsClient, but only after this @@ -894,7 +894,7 @@ public void rdsUpdateAfterShutdown() { XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS2", "EDS", ENDPOINT_HOSTNAME, ENDPOINT_PORT); - verify(resourceWatcher, times(2)).onChanged(any()); + verify(resourceWatcher, times(2)).onResourceChanged(argThat(StatusOr::hasValue)); xdsClient.cancelXdsResourceWatch( XdsRouteConfigureResource.getInstance(), serverName, resourceWatcher); }); @@ -910,14 +910,14 @@ public void cdsUpdateAfterShutdown() { verify(xdsConfigWatcher).onUpdate(any()); @SuppressWarnings("unchecked") - XdsClient.ResourceWatcher resourceWatcher = + XdsClient.ResourceWatcher resourceWatcher = mock(XdsClient.ResourceWatcher.class); xdsClient.watchXdsResource( XdsClusterResource.getInstance(), "CDS", resourceWatcher, MoreExecutors.directExecutor()); - verify(resourceWatcher).onChanged(any()); + verify(resourceWatcher).onResourceChanged(argThat(StatusOr::hasValue)); syncContext.execute(() -> { // Shutdown before any updates. This will unsubscribe from XdsClient, but only after this @@ -926,7 +926,7 @@ public void cdsUpdateAfterShutdown() { XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS2", ENDPOINT_HOSTNAME, ENDPOINT_PORT); - verify(resourceWatcher, times(2)).onChanged(any()); + verify(resourceWatcher, times(2)).onResourceChanged(argThat(StatusOr::hasValue)); xdsClient.cancelXdsResourceWatch( XdsClusterResource.getInstance(), serverName, resourceWatcher); }); @@ -949,7 +949,7 @@ public void edsUpdateAfterShutdown() { "EDS", resourceWatcher, MoreExecutors.directExecutor()); - verify(resourceWatcher).onChanged(any()); + verify(resourceWatcher).onResourceChanged(argThat(StatusOr::hasValue)); syncContext.execute(() -> { // Shutdown before any updates. This will unsubscribe from XdsClient, but only after this @@ -958,7 +958,7 @@ public void edsUpdateAfterShutdown() { XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS", ENDPOINT_HOSTNAME + "2", ENDPOINT_PORT); - verify(resourceWatcher, times(2)).onChanged(any()); + verify(resourceWatcher, times(2)).onResourceChanged(argThat(StatusOr::hasValue)); xdsClient.cancelXdsResourceWatch( XdsEndpointResource.getInstance(), serverName, resourceWatcher); }); diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index d090687a072..6bb37cd4483 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -69,6 +69,7 @@ import io.grpc.NoopClientCall.NoopClientCallListener; import io.grpc.Status; import io.grpc.Status.Code; +import io.grpc.StatusOr; import io.grpc.SynchronizationContext; import io.grpc.internal.AutoConfiguredLoadBalancerFactory; import io.grpc.internal.FakeClock; @@ -376,7 +377,7 @@ public void resolving_targetAuthorityInAuthoritiesMap() { } @Test - public void resolving_ldsResourceNotFound() { + public void resolving_ldsResourceNotFound() { // hi resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsResourceNotFound(); @@ -553,7 +554,7 @@ public void resolving_encounterErrorLdsWatcherOnly() { Status error = selectResult.getStatus(); assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(error.getDescription()).contains(AUTHORITY); - assertThat(error.getDescription()).contains("UNAVAILABLE: server unreachable"); + assertThat(error.getDescription()).contains("server unreachable"); } @Test @@ -569,7 +570,7 @@ public void resolving_translateErrorLds() { Status error = selectResult.getStatus(); assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(error.getDescription()).contains(AUTHORITY); - assertThat(error.getDescription()).contains("NOT_FOUND: server unreachable"); + assertThat(error.getDescription()).contains("server unreachable"); assertThat(error.getCause()).isNull(); } @@ -586,8 +587,10 @@ public void resolving_encounterErrorLdsAndRdsWatchers() { newPickSubchannelArgs(call1.methodDescriptor, new Metadata(), CallOptions.DEFAULT)); Status error = selectResult.getStatus(); assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); - assertThat(error.getDescription()).contains(RDS_RESOURCE_NAME); - assertThat(error.getDescription()).contains("UNAVAILABLE: server unreachable"); + // XdsDepManager.buildUpdate doesn't allow this + // assertThat(error.getDescription()).contains(RDS_RESOURCE_NAME); + assertThat(error.getDescription()).contains(expectedLdsResourceName); + assertThat(error.getDescription()).contains("server unreachable"); } @SuppressWarnings("unchecked") @@ -1552,6 +1555,33 @@ public void filterState_shutdown_onLdsNotFound() { assertThat(lds1Filter2.isShutdown()).isTrue(); } + @Test + public void filterState_noShutdown_onLdsDeletion() { + StatefulFilter.Provider statefulFilterProvider = filterStateTestSetupResolver(); + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + VirtualHost vhost = filterStateTestVhost(); + + xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + createAndDeliverClusterUpdates(xdsClient, cluster1); + assertClusterResolutionResult(call1, cluster1); + ImmutableList lds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); + StatefulFilter lds1Filter1 = lds1Snapshot.get(0); + StatefulFilter lds1Filter2 = lds1Snapshot.get(1); + + // LDS 2: Deliver a resource deletion, which is now an ambient error. + reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); + xdsClient.deliverLdsResourceDeletion(); + + // With an ambient error, no new resolution should happen. + verify(mockListener, never()).onResult2(any()); + + // Verify that the filters are NOT shut down. + assertThat(lds1Filter1.isShutdown()).isFalse(); + assertThat(lds1Filter2.isShutdown()).isFalse(); + } + /** * Verifies that all filter instances are shutdown (closed) on LDS ResourceWatcher shutdown. */ @@ -1586,6 +1616,35 @@ public void filterState_shutdown_onResolverShutdown() { public void filterState_shutdown_onRdsNotFound() { StatefulFilter.Provider statefulFilterProvider = filterStateTestSetupResolver(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + xdsClient.deliverLdsUpdateForRdsNameWithFilters( + RDS_RESOURCE_NAME, + filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); + xdsClient.deliverRdsUpdate( + RDS_RESOURCE_NAME, + Collections.singletonList(filterStateTestVhost())); + createAndDeliverClusterUpdates(xdsClient, cluster1); + assertClusterResolutionResult(call1, cluster1); + + ImmutableList rds1Snapshot = statefulFilterProvider.getAllInstances(); + assertWithMessage("RDS 1: Expected to create filter instances").that(rds1Snapshot).hasSize(2); + StatefulFilter rds1Filter1 = rds1Snapshot.get(0); + StatefulFilter rds1Filter2 = rds1Snapshot.get(1); + assertThat(rds1Filter1.isShutdown()).isFalse(); + assertThat(rds1Filter2.isShutdown()).isFalse(); + + reset(mockListener); + when(mockListener.onResult2(any())).thenReturn(Status.OK); + xdsClient.deliverRdsResourceNotFound(RDS_RESOURCE_NAME); + + assertEmptyResolutionResult(RDS_RESOURCE_NAME); + assertThat(rds1Filter1.isShutdown()).isTrue(); + assertThat(rds1Filter2.isShutdown()).isTrue(); + } + + @Test + public void filterState_noShutdown_onRdsAmbientError() { + StatefulFilter.Provider statefulFilterProvider = filterStateTestSetupResolver(); + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); // LDS 1. xdsClient.deliverLdsUpdateForRdsNameWithFilters(RDS_RESOURCE_NAME, @@ -1603,10 +1662,10 @@ public void filterState_shutdown_onRdsNotFound() { // RDS 2: RDS_RESOURCE_NAME not found. reset(mockListener); when(mockListener.onResult2(any())).thenReturn(Status.OK); - xdsClient.deliverRdsResourceNotFound(RDS_RESOURCE_NAME); - assertEmptyResolutionResult(RDS_RESOURCE_NAME); - assertThat(lds1Filter1.isShutdown()).isTrue(); - assertThat(lds1Filter2.isShutdown()).isTrue(); + xdsClient.deliverRdsAmbientError(RDS_RESOURCE_NAME, Status.NOT_FOUND); + verify(mockListener, never()).onResult2(any()); + assertThat(lds1Filter1.isShutdown()).isFalse(); + assertThat(lds1Filter2.isShutdown()).isFalse(); } private StatefulFilter.Provider filterStateTestSetupResolver() { @@ -2523,10 +2582,22 @@ public void cancelXdsResourceWatch(XdsResourceType } } + void deliverRdsAmbientError(String resourceName, Status status) { + if (!rdsWatchers.containsKey(resourceName)) { + return; + } + syncContext.execute(() -> { + List> resourceWatchers = + ImmutableList.copyOf(rdsWatchers.get(resourceName)); + resourceWatchers.forEach(w -> w.onAmbientError(status)); + }); + } + void deliverLdsUpdateOnly(long httpMaxStreamDurationNano, List virtualHosts) { syncContext.execute(() -> { - ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( - httpMaxStreamDurationNano, virtualHosts, null))); + LdsUpdate ldsUpdate = LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( + httpMaxStreamDurationNano, virtualHosts, null)); + ldsWatcher.onResourceChanged(StatusOr.fromValue(ldsUpdate)); }); } @@ -2537,8 +2608,9 @@ void deliverLdsUpdate(long httpMaxStreamDurationNano, List virtualH } syncContext.execute(() -> { - ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( - httpMaxStreamDurationNano, virtualHosts, null))); + LdsUpdate ldsUpdate = LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( + httpMaxStreamDurationNano, virtualHosts, null)); + ldsWatcher.onResourceChanged(StatusOr.fromValue(ldsUpdate)); createAndDeliverClusterUpdates(this, clusterNames.toArray(new String[0])); }); } @@ -2551,8 +2623,9 @@ void deliverLdsUpdate(final List routes) { List clusterNames = getClusterNames(routes); syncContext.execute(() -> { - ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( - 0L, Collections.singletonList(virtualHost), null))); + LdsUpdate ldsUpdate = LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( + 0L, Collections.singletonList(virtualHost), null)); + ldsWatcher.onResourceChanged(StatusOr.fromValue(ldsUpdate)); if (!clusterNames.isEmpty()) { createAndDeliverClusterUpdates(this, clusterNames.toArray(new String[0])); } @@ -2561,8 +2634,9 @@ void deliverLdsUpdate(final List routes) { void deliverLdsUpdateWithFilters(VirtualHost vhost, List filterConfigs) { syncContext.execute(() -> { - ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( - 0L, Collections.singletonList(vhost), filterConfigs))); + LdsUpdate ldsUpdate = LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( + 0L, Collections.singletonList(vhost), filterConfigs)); + ldsWatcher.onResourceChanged(StatusOr.fromValue(ldsUpdate)); }); } @@ -2609,8 +2683,9 @@ void deliverLdsUpdateWithFaultInjection( Collections.singletonList(route), overrideConfig); syncContext.execute(() -> { - ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( - 0L, Collections.singletonList(virtualHost), filterChain))); + LdsUpdate ldsUpdate = LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( + 0L, Collections.singletonList(virtualHost), filterChain)); + ldsWatcher.onResourceChanged(StatusOr.fromValue(ldsUpdate)); createAndDeliverClusterUpdates(this, cluster); }); } @@ -2625,8 +2700,9 @@ void deliverLdsUpdateForRdsNameWithFaultInjection( new NamedFilterConfig(FAULT_FILTER_INSTANCE_NAME, httpFilterFaultConfig), new NamedFilterConfig(ROUTER_FILTER_INSTANCE_NAME, RouterFilter.ROUTER_CONFIG)); syncContext.execute(() -> { - ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forRdsName( - 0L, rdsName, filterChain))); + LdsUpdate ldsUpdate = LdsUpdate.forApiListener(HttpConnectionManager.forRdsName( + 0L, rdsName, filterChain)); + ldsWatcher.onResourceChanged(StatusOr.fromValue(ldsUpdate)); }); } @@ -2638,14 +2714,27 @@ void deliverLdsUpdateForRdsNameWithFilters( String rdsName, @Nullable List filterConfigs) { syncContext.execute(() -> { - ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forRdsName( - 0, rdsName, filterConfigs))); + LdsUpdate ldsUpdate = LdsUpdate.forApiListener(HttpConnectionManager.forRdsName( + 0, rdsName, filterConfigs)); + ldsWatcher.onResourceChanged(StatusOr.fromValue(ldsUpdate)); + }); + } + + void deliverLdsResourceDeletion() { + Status status = Status.NOT_FOUND.withDescription( + "Resource not found: " + expectedLdsResourceName); + syncContext.execute(() -> { + ldsWatcher.onAmbientError(status); }); } void deliverLdsResourceNotFound() { + Status notFoundStatus = Status.UNAVAILABLE.withDescription( + "Resource not found: " + expectedLdsResourceName); syncContext.execute(() -> { - ldsWatcher.onResourceDoesNotExist(expectedLdsResourceName); + if (ldsWatcher != null) { + ldsWatcher.onResourceChanged(StatusOr.fromStatus(notFoundStatus)); + } }); } @@ -2715,7 +2804,7 @@ void deliverRdsUpdate(String resourceName, List virtualHosts) { RdsUpdate update = new RdsUpdate(virtualHosts); List> resourceWatchers = ImmutableList.copyOf(rdsWatchers.get(resourceName)); - resourceWatchers.forEach(w -> w.onChanged(update)); + resourceWatchers.forEach(w -> w.onResourceChanged(StatusOr.fromValue(update))); }); } @@ -2730,7 +2819,8 @@ void deliverRdsResourceNotFound(String resourceName) { syncContext.execute(() -> { List> resourceWatchers = ImmutableList.copyOf(rdsWatchers.get(resourceName)); - resourceWatchers.forEach(w -> w.onResourceDoesNotExist(resourceName)); + Status status = Status.UNAVAILABLE.withDescription("Resource not found: " + resourceName); + resourceWatchers.forEach(w -> w.onResourceChanged(StatusOr.fromStatus(status))); }); } @@ -2741,7 +2831,7 @@ private void deliverCdsUpdate(String clusterName, CdsUpdate update) { syncContext.execute(() -> { List> resourceWatchers = ImmutableList.copyOf(cdsWatchers.get(clusterName)); - resourceWatchers.forEach(w -> w.onChanged(update)); + resourceWatchers.forEach(w -> w.onResourceChanged(StatusOr.fromValue(update))); }); } @@ -2752,7 +2842,7 @@ private void deliverEdsUpdate(String name, EdsUpdate update) { } List> resourceWatchers = ImmutableList.copyOf(edsWatchers.get(name)); - resourceWatchers.forEach(w -> w.onChanged(update)); + resourceWatchers.forEach(w -> w.onResourceChanged(StatusOr.fromValue(update))); }); } @@ -2760,16 +2850,19 @@ private void deliverEdsUpdate(String name, EdsUpdate update) { void deliverError(final Status error) { if (ldsWatcher != null) { syncContext.execute(() -> { - ldsWatcher.onError(error); + ldsWatcher.onResourceChanged(StatusOr.fromStatus(error)); }); } syncContext.execute(() -> { - rdsWatchers.values().stream() - .flatMap(List::stream) - .forEach(w -> w.onError(error)); - cdsWatchers.values().stream() - .flatMap(List::stream) - .forEach(w -> w.onError(error)); + List> rdsCopy = rdsWatchers.values().stream() + .flatMap(List::stream).collect(java.util.stream.Collectors.toList()); + List> cdsCopy = cdsWatchers.values().stream() + .flatMap(List::stream).collect(java.util.stream.Collectors.toList()); + List> edsCopy = edsWatchers.values().stream() + .flatMap(List::stream).collect(java.util.stream.Collectors.toList()); + rdsCopy.forEach(w -> w.onResourceChanged(StatusOr.fromStatus(error))); + cdsCopy.forEach(w -> w.onResourceChanged(StatusOr.fromStatus(error))); + edsCopy.forEach(w -> w.onResourceChanged(StatusOr.fromStatus(error))); }); } } diff --git a/xds/src/test/java/io/grpc/xds/XdsServerBuilderTest.java b/xds/src/test/java/io/grpc/xds/XdsServerBuilderTest.java index 40225d3860d..ac990226259 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerBuilderTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerBuilderTest.java @@ -34,6 +34,7 @@ import io.grpc.ServerServiceDefinition; import io.grpc.Status; import io.grpc.StatusException; +import io.grpc.StatusOr; import io.grpc.testing.GrpcCleanupRule; import io.grpc.xds.XdsListenerResource.LdsUpdate; import io.grpc.xds.XdsServerTestHelper.FakeXdsClient; @@ -209,13 +210,14 @@ public void xdsServer_discoverState() throws Exception { CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"), tlsContextManager); future.get(5000, TimeUnit.MILLISECONDS); - xdsClient.ldsWatcher.onError(Status.ABORTED); + xdsClient.ldsWatcher.onAmbientError(Status.ABORTED); verify(mockXdsServingStatusListener, never()).onNotServing(any(StatusException.class)); reset(mockXdsServingStatusListener); - xdsClient.ldsWatcher.onError(Status.CANCELLED); + xdsClient.ldsWatcher.onAmbientError(Status.CANCELLED); verify(mockXdsServingStatusListener, never()).onNotServing(any(StatusException.class)); reset(mockXdsServingStatusListener); - xdsClient.ldsWatcher.onResourceDoesNotExist("not found error"); + Status notFoundStatus = Status.NOT_FOUND.withDescription("not found error"); + xdsClient.ldsWatcher.onResourceChanged(StatusOr.fromStatus(notFoundStatus)); verify(mockXdsServingStatusListener).onNotServing(any(StatusException.class)); reset(mockXdsServingStatusListener); XdsServerTestHelper.generateListenerUpdate( @@ -266,7 +268,7 @@ public void xdsServerStartSecondUpdateAndError() tlsContextManager); verify(mockXdsServingStatusListener, never()).onNotServing(any(Throwable.class)); verifyServer(future, mockXdsServingStatusListener, null); - xdsClient.ldsWatcher.onError(Status.ABORTED); + xdsClient.ldsWatcher.onAmbientError(Status.ABORTED); verifyServer(null, mockXdsServingStatusListener, null); } diff --git a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java index a9236ac77ca..386793299d8 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java @@ -24,6 +24,8 @@ import io.envoyproxy.envoy.config.core.v3.SocketAddress.Protocol; import io.grpc.InsecureChannelCredentials; import io.grpc.MetricRecorder; +import io.grpc.Status; +import io.grpc.StatusOr; import io.grpc.internal.ObjectPool; import io.grpc.xds.EnvoyServerProtoData.ConnectionSourceType; import io.grpc.xds.EnvoyServerProtoData.FilterChain; @@ -301,13 +303,14 @@ private String awaitLdsResource(Duration timeout) { void deliverLdsUpdateWithApiListener(long httpMaxStreamDurationNano, List virtualHosts) { execute(() -> { - ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( - httpMaxStreamDurationNano, virtualHosts, null))); + LdsUpdate update = LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( + httpMaxStreamDurationNano, virtualHosts, null)); + ldsWatcher.onResourceChanged(StatusOr.fromValue(update)); }); } void deliverLdsUpdate(LdsUpdate ldsUpdate) { - execute(() -> ldsWatcher.onChanged(ldsUpdate)); + execute(() -> ldsWatcher.onResourceChanged(StatusOr.fromValue(ldsUpdate))); } void deliverLdsUpdate( @@ -322,11 +325,14 @@ void deliverLdsUpdate(FilterChain filterChain, @Nullable FilterChain defaultFilt } void deliverLdsResourceNotFound() { - execute(() -> ldsWatcher.onResourceDoesNotExist(awaitLdsResource(DEFAULT_TIMEOUT))); + String resourceName = awaitLdsResource(DEFAULT_TIMEOUT); + Status status = Status.NOT_FOUND.withDescription("Resource not found: " + resourceName); + execute(() -> ldsWatcher.onResourceChanged(StatusOr.fromStatus(status))); } void deliverRdsUpdate(String resourceName, List virtualHosts) { - execute(() -> rdsWatchers.get(resourceName).onChanged(new RdsUpdate(virtualHosts))); + RdsUpdate update = new RdsUpdate(virtualHosts); + execute(() -> rdsWatchers.get(resourceName).onResourceChanged(StatusOr.fromValue(update))); } void deliverRdsUpdate(String resourceName, VirtualHost virtualHost) { @@ -334,7 +340,8 @@ void deliverRdsUpdate(String resourceName, VirtualHost virtualHost) { } void deliverRdsResourceNotFound(String resourceName) { - execute(() -> rdsWatchers.get(resourceName).onResourceDoesNotExist(resourceName)); + Status status = Status.NOT_FOUND.withDescription("Resource not found: " + resourceName); + execute(() -> rdsWatchers.get(resourceName).onResourceChanged(StatusOr.fromStatus(status))); } } } diff --git a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java index 99e9907f7d0..99e3911307a 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java @@ -49,6 +49,7 @@ import io.grpc.ServerInterceptor; import io.grpc.Status; import io.grpc.StatusException; +import io.grpc.StatusOr; import io.grpc.SynchronizationContext; import io.grpc.internal.FakeClock; import io.grpc.testing.TestMethodDescriptors; @@ -345,7 +346,8 @@ public void run() { } }); String ldsResource = xdsClient.ldsResource.get(5, TimeUnit.SECONDS); - xdsClient.ldsWatcher.onResourceDoesNotExist(ldsResource); + Status notFoundStatus = Status.NOT_FOUND.withDescription("Resource not found: " + ldsResource); + xdsClient.ldsWatcher.onResourceChanged(StatusOr.fromStatus(notFoundStatus)); verify(listener, timeout(5000)).onNotServing(any()); try { start.get(START_WAIT_AFTER_LISTENER_MILLIS, TimeUnit.MILLISECONDS); @@ -534,7 +536,8 @@ public void run() { verify(mockServer).start(); // server shutdown after resourceDoesNotExist - xdsClient.ldsWatcher.onResourceDoesNotExist(ldsResource); + Status notFoundStatus = Status.NOT_FOUND.withDescription("Resource not found: " + ldsResource); + xdsClient.ldsWatcher.onResourceChanged(StatusOr.fromStatus(notFoundStatus)); verify(mockServer).shutdown(); // re-deliver lds resource @@ -853,7 +856,7 @@ public void run() { EnvoyServerProtoData.FilterChain f1 = createFilterChain("filter-chain-1", createRds("r0")); xdsClient.deliverLdsUpdate(Arrays.asList(f0, f1), null); xdsClient.awaitRds(FakeXdsClient.DEFAULT_TIMEOUT); - xdsClient.rdsWatchers.get("r0").onError(Status.CANCELLED); + xdsClient.rdsWatchers.get("r0").onResourceChanged(StatusOr.fromStatus(Status.CANCELLED)); start.get(5000, TimeUnit.MILLISECONDS); assertThat(selectorManager.getSelectorToUpdateSelector().getRoutingConfigs().size()) .isEqualTo(2); @@ -873,13 +876,14 @@ public void run() { Collections.singletonList(createVirtualHost("virtual-host-1"))); assertThat(realConfig.interceptors()).isEqualTo(ImmutableMap.of()); - xdsClient.rdsWatchers.get("r0").onError(Status.CANCELLED); + xdsClient.rdsWatchers.get("r0").onAmbientError(Status.CANCELLED); realConfig = selectorManager.getSelectorToUpdateSelector().getRoutingConfigs().get(f1).get(); assertThat(realConfig.virtualHosts()).isEqualTo( Collections.singletonList(createVirtualHost("virtual-host-1"))); assertThat(realConfig.interceptors()).isEqualTo(ImmutableMap.of()); - xdsClient.rdsWatchers.get("r0").onResourceDoesNotExist("r0"); + Status notFoundStatus = Status.NOT_FOUND.withDescription("Resource r0 does not exist"); + xdsClient.rdsWatchers.get("r0").onResourceChanged(StatusOr.fromStatus(notFoundStatus)); realConfig = selectorManager.getSelectorToUpdateSelector().getRoutingConfigs().get(f1).get(); assertThat(realConfig.virtualHosts()).isEmpty(); assertThat(realConfig.interceptors()).isEmpty(); @@ -899,7 +903,9 @@ public void run() { } }); String ldsResource = xdsClient.ldsResource.get(5, TimeUnit.SECONDS); - xdsClient.ldsWatcher.onResourceDoesNotExist(ldsResource); + Status notFoundStatus = Status.NOT_FOUND.withDescription( + "FakeXdsClient: Resource not found: " + ldsResource); + xdsClient.ldsWatcher.onResourceChanged(StatusOr.fromStatus(notFoundStatus)); verify(listener, timeout(5000)).onNotServing(any()); try { start.get(START_WAIT_AFTER_LISTENER_MILLIS, TimeUnit.MILLISECONDS); @@ -913,10 +919,10 @@ public void run() { FilterChain filterChain0 = createFilterChain("filter-chain-0", createRds("rds")); SslContextProviderSupplier sslSupplier0 = filterChain0.sslContextProviderSupplier(); xdsClient.deliverLdsUpdate(Collections.singletonList(filterChain0), null); - xdsClient.ldsWatcher.onError(Status.INTERNAL); + ResourceWatcher saveRdsWatcher = xdsClient.rdsWatchers.get("rds"); + xdsClient.ldsWatcher.onResourceChanged(StatusOr.fromStatus(Status.INTERNAL)); assertThat(selectorManager.getSelectorToUpdateSelector()) .isSameInstanceAs(FilterChainSelector.NO_FILTER_CHAIN); - ResourceWatcher saveRdsWatcher = xdsClient.rdsWatchers.get("rds"); verify(mockBuilder, times(1)).build(); verify(listener, times(2)).onNotServing(any(StatusException.class)); assertThat(sslSupplier0.isShutdown()).isFalse(); @@ -952,7 +958,7 @@ public void run() { xdsClient.deliverRdsUpdate("rds", Collections.singletonList(createVirtualHost("virtual-host-2"))); assertThat(sslSupplier1.isShutdown()).isFalse(); - xdsClient.ldsWatcher.onError(Status.DEADLINE_EXCEEDED); + xdsClient.ldsWatcher.onAmbientError(Status.DEADLINE_EXCEEDED); verify(mockBuilder, times(1)).build(); verify(mockServer, times(2)).start(); verify(listener, times(2)).onNotServing(any(StatusException.class)); @@ -967,17 +973,18 @@ public void run() { assertThat(sslSupplier1.isShutdown()).isFalse(); // not serving after serving - xdsClient.ldsWatcher.onResourceDoesNotExist(ldsResource); + xdsClient.ldsWatcher.onResourceChanged(StatusOr.fromStatus(notFoundStatus)); assertThat(xdsClient.rdsWatchers).isEmpty(); - verify(mockServer, times(2)).shutdown(); + verify(mockServer, times(3)).shutdown(); // This is the 3rd shutdown in the test. when(mockServer.isShutdown()).thenReturn(true); assertThat(selectorManager.getSelectorToUpdateSelector()) .isSameInstanceAs(FilterChainSelector.NO_FILTER_CHAIN); verify(listener, times(3)).onNotServing(any(StatusException.class)); assertThat(sslSupplier1.isShutdown()).isTrue(); + assertThat(xdsClient.rdsWatchers.get("rds")).isNull(); // no op - saveRdsWatcher.onChanged( - new RdsUpdate(Collections.singletonList(createVirtualHost("virtual-host-1")))); + saveRdsWatcher.onResourceChanged(StatusOr.fromValue( + new RdsUpdate(Collections.singletonList(createVirtualHost("virtual-host-1"))))); verify(mockBuilder, times(1)).build(); verify(mockServer, times(2)).start(); verify(listener, times(1)).onServing(); @@ -1006,8 +1013,8 @@ public void run() { assertThat(realConfig.interceptors()).isEqualTo(ImmutableMap.of()); assertThat(executor.numPendingTasks()).isEqualTo(1); - xdsClient.ldsWatcher.onResourceDoesNotExist(ldsResource); - verify(mockServer, times(3)).shutdown(); + xdsClient.ldsWatcher.onResourceChanged(StatusOr.fromStatus(notFoundStatus)); + verify(mockServer, times(4)).shutdown(); verify(listener, times(4)).onNotServing(any(StatusException.class)); verify(listener, times(1)).onNotServing(any(IOException.class)); when(mockServer.isShutdown()).thenReturn(true); @@ -1035,7 +1042,7 @@ public void run() { assertThat(realConfig.interceptors()).isEqualTo(ImmutableMap.of()); xdsServerWrapper.shutdown(); - verify(mockServer, times(4)).shutdown(); + verify(mockServer, times(5)).shutdown(); assertThat(sslSupplier3.isShutdown()).isTrue(); when(mockServer.awaitTermination(anyLong(), any(TimeUnit.class))).thenReturn(true); assertThat(xdsServerWrapper.awaitTermination(5, TimeUnit.SECONDS)).isTrue(); @@ -1429,7 +1436,8 @@ public ServerCall.Listener interceptCall(ServerCall Date: Wed, 3 Dec 2025 00:48:28 +0900 Subject: [PATCH 473/591] servlet: disable RECYCLE_FACADES to reduce flaky tests Set discardFacades=false in Tomcat 10 Embedded to avoid premature OutputBuffer recycling. This works around flaky tests in gRPC servlet transport by ensuring facades are not discarded too early. Fixes #12524 --- .../tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java b/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java index 2171c6eb2df..cd73b096ccb 100644 --- a/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java +++ b/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java @@ -93,6 +93,10 @@ public void start(ServerListener listener) throws IOException { .setAsyncSupported(true); ctx.addServletMappingDecoded("/*", "TomcatTransportTest"); tomcatServer.getConnector().addUpgradeProtocol(new Http2Protocol()); + // Workaround for https://github.com/grpc/grpc-java/issues/12540 + // Prevent premature OutputBuffer recycling by disabling facade recycling. + // This should be revisited once the root cause is fixed. + tomcatServer.getConnector().setDiscardFacades(false); try { tomcatServer.start(); } catch (LifecycleException e) { From f36defa2d3950de103d2a2dc73fc7f308d35f624 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 3 Dec 2025 21:49:12 -0800 Subject: [PATCH 474/591] Upgrade dependencies --- MODULE.bazel | 16 +- .../main/java/io/grpc/ConnectivityState.java | 2 +- .../java/io/grpc/LoadBalancerProvider.java | 2 +- .../authz/AuthorizationPolicyTranslator.java | 16 +- .../io/grpc/internal/DnsNameResolver.java | 2 +- .../main/java/io/grpc/internal/JsonUtil.java | 3 +- .../internal/ManagedChannelImplBuilder.java | 2 +- .../grpc/internal/DelayedClientCallTest.java | 2 +- examples/android/clientcache/app/build.gradle | 2 +- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 2 +- .../build.gradle | 4 +- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 8 +- examples/example-opentelemetry/build.gradle | 4 +- examples/pom.xml | 2 +- gradle/libs.versions.toml | 153 ++++++++++-------- .../integration/StressTestClientTest.java | 4 +- netty/src/main/java/io/grpc/netty/Utils.java | 8 +- .../io/grpc/okhttp/OkHttpClientTransport.java | 2 +- repositories.bzl | 16 +- .../S2AProtocolNegotiatorFactory.java | 2 +- .../handshaker/SslContextFactory.java | 6 +- .../io/grpc/servlet/JettyTransportTest.java | 14 +- settings.gradle | 4 +- 25 files changed, 150 insertions(+), 130 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 418a78275b6..f81f287588f 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,19 +8,19 @@ module( # GRPC_DEPS_START IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.android:annotations:4.1.1.4", - "com.google.api.grpc:proto-google-common-protos:2.59.2", - "com.google.auth:google-auth-library-credentials:1.24.1", - "com.google.auth:google-auth-library-oauth2-http:1.24.1", + "com.google.api.grpc:proto-google-common-protos:2.63.1", + "com.google.auth:google-auth-library-credentials:1.40.0", + "com.google.auth:google-auth-library-oauth2-http:1.40.0", "com.google.auto.value:auto-value-annotations:1.11.0", "com.google.auto.value:auto-value:1.11.0", "com.google.code.findbugs:jsr305:3.0.2", "com.google.code.gson:gson:2.12.1", - "com.google.errorprone:error_prone_annotations:2.36.0", + "com.google.errorprone:error_prone_annotations:2.44.0", "com.google.guava:failureaccess:1.0.1", - "com.google.guava:guava:33.4.8-android", + "com.google.guava:guava:33.5.0-android", "com.google.re2j:re2j:1.8", - "com.google.s2a.proto.v2:s2a-proto:0.1.2", - "com.google.truth:truth:1.4.2", + "com.google.s2a.proto.v2:s2a-proto:0.1.3", + "com.google.truth:truth:1.4.5", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day "io.netty:netty-buffer:4.1.127.Final", @@ -42,7 +42,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.perfmark:perfmark-api:0.27.0", "junit:junit:4.13.2", "org.checkerframework:checker-qual:3.49.5", - "org.codehaus.mojo:animal-sniffer-annotations:1.24", + "org.codehaus.mojo:animal-sniffer-annotations:1.26", ] # GRPC_DEPS_END diff --git a/api/src/main/java/io/grpc/ConnectivityState.java b/api/src/main/java/io/grpc/ConnectivityState.java index 677039b2517..a7407efb2e9 100644 --- a/api/src/main/java/io/grpc/ConnectivityState.java +++ b/api/src/main/java/io/grpc/ConnectivityState.java @@ -20,7 +20,7 @@ * The connectivity states. * * @see - * more information + * more information */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4359") public enum ConnectivityState { diff --git a/api/src/main/java/io/grpc/LoadBalancerProvider.java b/api/src/main/java/io/grpc/LoadBalancerProvider.java index bb4c574211e..7dc30d6baaf 100644 --- a/api/src/main/java/io/grpc/LoadBalancerProvider.java +++ b/api/src/main/java/io/grpc/LoadBalancerProvider.java @@ -81,7 +81,7 @@ public abstract class LoadBalancerProvider extends LoadBalancer.Factory { * @return a tuple of the fully parsed and validated balancer configuration, else the Status. * @since 1.20.0 * @see - * A24-lb-policy-config.md + * A24-lb-policy-config.md */ public ConfigOrError parseLoadBalancingPolicyConfig(Map rawLoadBalancingPolicyConfig) { return UNKNOWN_CONFIG; diff --git a/authz/src/main/java/io/grpc/authz/AuthorizationPolicyTranslator.java b/authz/src/main/java/io/grpc/authz/AuthorizationPolicyTranslator.java index ed7e018412c..183ae2c3f55 100644 --- a/authz/src/main/java/io/grpc/authz/AuthorizationPolicyTranslator.java +++ b/authz/src/main/java/io/grpc/authz/AuthorizationPolicyTranslator.java @@ -156,19 +156,19 @@ private static Map parseRules( } /** - * Translates a gRPC authorization policy in JSON string to Envoy RBAC policies. - * On success, will return one of the following - - * 1. One allow RBAC policy or, - * 2. Two RBAC policies, deny policy followed by allow policy. - * If the policy cannot be parsed or is invalid, an exception will be thrown. - */ + * Translates a gRPC authorization policy in JSON string to Envoy RBAC policies. + * On success, will return one of the following - + * 1. One allow RBAC policy or, + * 2. Two RBAC policies, deny policy followed by allow policy. + * If the policy cannot be parsed or is invalid, an exception will be thrown. + */ public static List translate(String authorizationPolicy) throws IllegalArgumentException, IOException { Object jsonObject = JsonParser.parse(authorizationPolicy); if (!(jsonObject instanceof Map)) { throw new IllegalArgumentException( - "Authorization policy should be a JSON object. Found: " - + (jsonObject == null ? null : jsonObject.getClass())); + "Authorization policy should be a JSON object. Found: " + + (jsonObject == null ? null : jsonObject.getClass())); } @SuppressWarnings("unchecked") Map json = (Map)jsonObject; diff --git a/core/src/main/java/io/grpc/internal/DnsNameResolver.java b/core/src/main/java/io/grpc/internal/DnsNameResolver.java index 6c48389b95b..dacbe291b6b 100644 --- a/core/src/main/java/io/grpc/internal/DnsNameResolver.java +++ b/core/src/main/java/io/grpc/internal/DnsNameResolver.java @@ -478,7 +478,7 @@ private static long getNetworkAddressCacheTtlNanos(boolean isAndroid) { * Determines if a given Service Config choice applies, and if so, returns it. * * @see - * Service Config in DNS + * Service Config in DNS * @param choice The service config choice. * @return The service config object or {@code null} if this choice does not apply. */ diff --git a/core/src/main/java/io/grpc/internal/JsonUtil.java b/core/src/main/java/io/grpc/internal/JsonUtil.java index a0d5eef8660..6c9274702b6 100644 --- a/core/src/main/java/io/grpc/internal/JsonUtil.java +++ b/core/src/main/java/io/grpc/internal/JsonUtil.java @@ -361,7 +361,8 @@ private static int parseNanos(String value) throws ParseException { /** * Copy of {@link com.google.protobuf.util.Durations#normalizedDuration}. */ - @SuppressWarnings("NarrowingCompoundAssignment") + // Math.addExact() requires Android API level 24 + @SuppressWarnings({"NarrowingCompoundAssignment", "InlineMeInliner"}) private static long normalizedDuration(long seconds, int nanos) { if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) { seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND); diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java index fc3c7891008..1773c04388d 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java @@ -759,7 +759,7 @@ List getEffectiveInterceptors(String computedTarget) { if (GET_CLIENT_INTERCEPTOR_METHOD != null) { try { statsInterceptor = - (ClientInterceptor) GET_CLIENT_INTERCEPTOR_METHOD + (ClientInterceptor) GET_CLIENT_INTERCEPTOR_METHOD .invoke( null, recordStartedRpcs, diff --git a/core/src/test/java/io/grpc/internal/DelayedClientCallTest.java b/core/src/test/java/io/grpc/internal/DelayedClientCallTest.java index 557ddfe5ace..ff131d29975 100644 --- a/core/src/test/java/io/grpc/internal/DelayedClientCallTest.java +++ b/core/src/test/java/io/grpc/internal/DelayedClientCallTest.java @@ -206,7 +206,7 @@ public void delayedCallsRunUnderContext() throws Exception { Object goldenValue = new Object(); DelayedClientCall delayedClientCall = Context.current().withValue(contextKey, goldenValue).call(() -> - new DelayedClientCall<>(callExecutor, fakeClock.getScheduledExecutorService(), null)); + new DelayedClientCall<>(callExecutor, fakeClock.getScheduledExecutorService(), null)); AtomicReference readyContext = new AtomicReference<>(); delayedClientCall.start(new ClientCall.Listener() { @Override public void onReady() { diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index 02389619418..0a3cd38b6ed 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -59,6 +59,6 @@ dependencies { implementation 'io.grpc:grpc-stub:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION testImplementation 'junit:junit:4.13.2' - testImplementation 'com.google.truth:truth:1.1.5' + testImplementation 'com.google.truth:truth:1.4.5' testImplementation 'io.grpc:grpc-testing:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index fef81b77697..1d305677048 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -30,7 +30,7 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-auth:${grpcVersion}" - implementation "com.google.auth:google-auth-library-oauth2-http:1.23.0" + implementation "com.google.auth:google-auth-library-oauth2-http:1.40.0" implementation "com.google.api.grpc:grpc-google-cloud-pubsub-v1:0.1.24" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" } diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index afae8ee6ec4..73efa507656 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -57,7 +57,7 @@ com.google.auth google-auth-library-oauth2-http - 1.23.0 + 1.40.0 com.google.api.grpc diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index 9b714b0a88f..d02e0d76ec4 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -24,8 +24,8 @@ java { // updating the version in our release process. def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' -def openTelemetryVersion = '1.52.0' -def openTelemetryPrometheusVersion = '1.52.0-alpha' +def openTelemetryVersion = '1.56.0' +def openTelemetryPrometheusVersion = '1.56.0-alpha' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 36e45958b84..447e40f1f3e 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -29,7 +29,7 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-auth:${grpcVersion}" - implementation "com.google.auth:google-auth-library-oauth2-http:1.23.0" + implementation "com.google.auth:google-auth-library-oauth2-http:1.40.0" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index e81f5e50190..85103ec9121 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -50,17 +50,11 @@ io.grpc grpc-auth - - - com.google.auth - google-auth-library-credentials - - com.google.auth google-auth-library-oauth2-http - 1.23.0 + 1.40.0 io.grpc diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index 188e5ff0a00..2c953b3d487 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -23,8 +23,8 @@ java { // updating the version in our release process. def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' -def openTelemetryVersion = '1.52.0' -def openTelemetryPrometheusVersion = '1.52.0-alpha' +def openTelemetryVersion = '1.56.0' +def openTelemetryPrometheusVersion = '1.56.0-alpha' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/pom.xml b/examples/pom.xml index 2f4629ec201..544f737ceb1 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -58,7 +58,7 @@ com.google.j2objc j2objc-annotations - 3.0.0 + 3.1 io.grpc diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f4914c1ea71..9cf74e270cb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,61 +1,68 @@ [versions] -netty = '4.1.127.Final' -# Keep the following references of tcnative version in sync whenever it's updated: -# SECURITY.md -nettytcnative = '2.0.74.Final' opencensus = "0.31.1" -# Not upgrading to 4.x as it is not yet ABI compatible. -# https://github.com/protocolbuffers/protobuf/issues/17247 -protobuf = "3.25.8" [libraries] android-annotations = "com.google.android:annotations:4.1.1.4" -# androidx-annotation 1.9.1+ uses Kotlin and requires Android Gradle Plugin 9+ +# 1.9.1+ uses Kotlin and requires Android Gradle Plugin 9+ # checkForUpdates: androidx-annotation:1.9.0 androidx-annotation = "androidx.annotation:annotation:1.9.0" -# 1.15.0 requires libraries and applications that depend on it to compile against -# version 35 or later of the Android APIs. +# 1.15.0+ requires minSdkVersion 21 in android-interop-testing (1.14.x doesn't exist) # checkForUpdates: androidx-core:1.13.+ androidx-core = "androidx.core:core:1.13.1" -# androidx-lifecycle 2.9+ requires Java 17 +# 2.9+ requires minSdkVersion 21 in android-intetrop-testing +# checkForUpdates: androidx-lifecycle-common:2.8.+ androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.8.7" +# checkForUpdates: androidx-lifecycle-service:2.8.+ androidx-lifecycle-service = "androidx.lifecycle:lifecycle-service:2.8.7" -androidx-test-core = "androidx.test:core:1.6.1" -androidx-test-ext-junit = "androidx.test.ext:junit:1.2.1" +androidx-test-core = "androidx.test:core:1.7.0" +androidx-test-ext-junit = "androidx.test.ext:junit:1.3.0" +# 1.7.0+ requires minSdkVersion 21 in android-interop-testing +# checkForUpdates: androidx-test-rules:1.6.+ androidx-test-rules = "androidx.test:rules:1.6.1" -animalsniffer = "org.codehaus.mojo:animal-sniffer:1.24" -animalsniffer-annotations = "org.codehaus.mojo:animal-sniffer-annotations:1.24" -assertj-core = "org.assertj:assertj-core:3.27.3" +animalsniffer = "org.codehaus.mojo:animal-sniffer:1.26" +animalsniffer-annotations = "org.codehaus.mojo:animal-sniffer-annotations:1.26" +assertj-core = "org.assertj:assertj-core:3.27.6" +# 1.11.1 started converting jsr305 @Nullable to jspecify +# checkForUpdates: auto-value:1.11.0 auto-value = "com.google.auto.value:auto-value:1.11.0" +# checkForUpdates: auto-value-annotations:1.11.0 auto-value-annotations = "com.google.auto.value:auto-value-annotations:1.11.0" -checkstyle = "com.puppycrawl.tools:checkstyle:10.21.2" +# 11.0+ requires Java 17+ +# https://checkstyle.sourceforge.io/releasenotes.html +# checkForUpdates: checkstyle:10.+ +checkstyle = "com.puppycrawl.tools:checkstyle:10.26.1" +# checkstyle 10.0+ requires Java 11+ +# See https://checkstyle.sourceforge.io/releasenotes_old_8-35_10-26.html#Release_10.0 +# checkForUpdates: checkstylejava8:9.+ +checkstylejava8 = "com.puppycrawl.tools:checkstyle:9.3" commons-math3 = "org.apache.commons:commons-math3:3.6.1" conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2" +# 141.7340.3+ requires minSdkVersion 23 +# checkForUpdates: cronet-api:119.6045.31 cronet-api = "org.chromium.net:cronet-api:119.6045.31" +# checkForUpdates: cronet-embedded:119.6045.31 cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31" -errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.36.0" -# error-prone 2.32.0+ require Java 17+ +errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.44.0" +# 2.32.0+ requires Java 17+ # checkForUpdates: errorprone-core:2.31.+ errorprone-core = "com.google.errorprone:error_prone_core:2.31.0" -google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.59.2" -# google-auth-library 1.25.0+ requires error_prone_annotations 2.31.0+, which -# breaks the Android build -# checkForUpdates: google-auth-credentials:1.24.+ -google-auth-credentials = "com.google.auth:google-auth-library-credentials:1.24.1" -# checkForUpdates: google-auth-oauth2Http:1.24.+ -google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.24.1" +# 2.11.0+ requires JDK 11+ (See https://github.com/google/error-prone/releases/tag/v2.11.0) +# checkForUpdates: errorprone-corejava8:2.10.+ +errorprone-corejava8 = "com.google.errorprone:error_prone_core:2.10.0" +google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.63.1" +google-auth-credentials = "com.google.auth:google-auth-library-credentials:1.40.0" +google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.40.0" # Release notes: https://cloud.google.com/logging/docs/release-notes -google-cloud-logging = "com.google.cloud:google-cloud-logging:3.23.1" +google-cloud-logging = "com.google.cloud:google-cloud-logging:3.23.8" # 2.13.0 requires error_prone_annotations:2.37.0, but we are stuck with 2.36.0 # checkForUpdates: gson:2.12.+ gson = "com.google.code.gson:gson:2.12.1" -# 33.4.8 requires com.google.errorprone:error_prone_annotations:2.36.0 -guava = "com.google.guava:guava:33.4.8-android" +guava = "com.google.guava:guava:33.5.0-android" guava-betaChecker = "com.google.guava:guava-beta-checker:1.0" -guava-testlib = "com.google.guava:guava-testlib:33.4.8-android" +guava-testlib = "com.google.guava:guava-testlib:33.5.0-android" # JRE version is needed for projects where its a transitive dependency, f.e. gcp-observability. # May be different from the -android version. -guava-jre = "com.google.guava:guava:33.4.8-jre" +guava-jre = "com.google.guava:guava:33.5.0-jre" hdrhistogram = "org.hdrhistogram:HdrHistogram:2.2.2" # 6.0.0+ use java.lang.Deprecated forRemoval and since from Java 9 # checkForUpdates: jakarta-servlet-api:5.+ @@ -63,28 +70,40 @@ jakarta-servlet-api = "jakarta.servlet:jakarta.servlet-api:5.0.0" javax-servlet-api = "javax.servlet:javax.servlet-api:4.0.1" # 12.0.0+ require Java 17+ # checkForUpdates: jetty-client:11.+ -jetty-client = "org.eclipse.jetty:jetty-client:11.0.24" -jetty-http2-server = "org.eclipse.jetty.http2:jetty-http2-server:12.0.23" -jetty-http2-server10 = "org.eclipse.jetty.http2:http2-server:10.0.20" -jetty-servlet = "org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.16" -jetty-servlet10 = "org.eclipse.jetty:jetty-servlet:10.0.20" +jetty-client = "org.eclipse.jetty:jetty-client:11.0.26" +jetty-http2-server = "org.eclipse.jetty.http2:jetty-http2-server:12.1.4" +# 10.0.25+ uses uses @Deprecated(since=/forRemoval=) from Java 9 +# checkForUpdates: jetty-http2-server10:10.0.24 +jetty-http2-server10 = "org.eclipse.jetty.http2:http2-server:10.0.24" +jetty-servlet = "org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.4" +# checkForUpdates: jetty-servlet10:10.0.24 +jetty-servlet10 = "org.eclipse.jetty:jetty-servlet:10.0.24" jsr305 = "com.google.code.findbugs:jsr305:3.0.2" junit = "junit:junit:4.13.2" -lincheck = "org.jetbrains.lincheck:lincheck:3.2" +lincheck = "org.jetbrains.lincheck:lincheck:3.3.2" # Update notes / 2023-07-19 sergiitk: # Couldn't update to 5.4.0, updated to the last in 4.x line. Version 5.x breaks some tests. # Error log: https://github.com/grpc/grpc-java/pull/10359#issuecomment-1632834435 # Update notes / 2023-10-09 temawi: -# 4.11.0 Has been breaking the android integration tests as mockito now uses streams +# 4.5.0 Has been breaking the android integration tests as mockito now uses streams # (not available in API levels < 24). https://github.com/grpc/grpc-java/issues/10457 +# checkForUpdates: mockito-android:4.4.+ mockito-android = "org.mockito:mockito-android:4.4.0" +# checkForUpdates: mockito-core:4.4.+ mockito-core = "org.mockito:mockito-core:4.4.0" -netty-codec-http2 = { module = "io.netty:netty-codec-http2", version.ref = "netty" } -netty-handler-proxy = { module = "io.netty:netty-handler-proxy", version.ref = "netty" } -netty-tcnative = { module = "io.netty:netty-tcnative-boringssl-static", version.ref = "nettytcnative" } -netty-tcnative-classes = { module = "io.netty:netty-tcnative-classes", version.ref = "nettytcnative" } -netty-transport-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" } -netty-unix-common = { module = "io.netty:netty-transport-native-unix-common", version.ref = "netty" } +# Need to decide when we require users to absorb the breaking changes in 4.2 +# checkForUpdates: netty-codec-http2:4.1.+ +netty-codec-http2 = "io.netty:netty-codec-http2:4.1.127.Final" +# checkForUpdates: netty-handler-proxy:4.1.+ +netty-handler-proxy = "io.netty:netty-handler-proxy:4.1.127.Final" +# Keep the following references of tcnative version in sync whenever it's updated: +# SECURITY.md +netty-tcnative = "io.netty:netty-tcnative-boringssl-static:2.0.74.Final" +netty-tcnative-classes = "io.netty:netty-tcnative-classes:2.0.74.Final" +# checkForUpdates: netty-transport-epoll:4.1.+ +netty-transport-epoll = "io.netty:netty-transport-native-epoll:4.1.127.Final" +# checkForUpdates: netty-unix-common:4.1.+ +netty-unix-common = "io.netty:netty-transport-native-unix-common:4.1.127.Final" okhttp = "com.squareup.okhttp:okhttp:2.7.5" # okio 3.5+ uses Kotlin 1.9+ which requires Android Gradle Plugin 9+ # checkForUpdates: okio:3.4.+ @@ -94,35 +113,33 @@ opencensus-contrib-grpc-metrics = { module = "io.opencensus:opencensus-contrib-g opencensus-exporter-stats-stackdriver = { module = "io.opencensus:opencensus-exporter-stats-stackdriver", version.ref = "opencensus" } opencensus-exporter-trace-stackdriver = { module = "io.opencensus:opencensus-exporter-trace-stackdriver", version.ref = "opencensus" } opencensus-impl = { module = "io.opencensus:opencensus-impl", version.ref = "opencensus" } -opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.52.0" -opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.52.0-alpha" -opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.48.0-alpha" -opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.52.0" -opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.52.0" +opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.56.0" +opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.56.0-alpha" +opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.51.0-alpha" +opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.56.0" +opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.56.0" perfmark-api = "io.perfmark:perfmark-api:0.27.0" -protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } -protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobuf" } -protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" } -protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } +# Not upgrading to 4.x as it is not yet ABI compatible. +# https://github.com/protocolbuffers/protobuf/issues/17247 +# checkForUpdates: protobuf-java:3.+ +protobuf-java = "com.google.protobuf:protobuf-java:3.25.8" +# checkForUpdates: protobuf-java-util:3.+ +protobuf-java-util = "com.google.protobuf:protobuf-java-util:3.25.8" +# checkForUpdates: protobuf-javalite:3.+ +protobuf-javalite = "com.google.protobuf:protobuf-javalite:3.25.8" +# checkForUpdates: protobuf-protoc:3.+ +protobuf-protoc = "com.google.protobuf:protoc:3.25.8" re2j = "com.google.re2j:re2j:1.8" -robolectric = "org.robolectric:robolectric:4.15.1" -s2a-proto = "com.google.s2a.proto.v2:s2a-proto:0.1.2" +robolectric = "org.robolectric:robolectric:4.16" +s2a-proto = "com.google.s2a.proto.v2:s2a-proto:0.1.3" signature-android = "net.sf.androidscents.signature:android-api-level-21:5.0.1_r2" signature-java = "org.codehaus.mojo.signature:java18:1.0" # 11.0.0+ require Java 17+ # checkForUpdates: tomcat-embed-core:10.+ -tomcat-embed-core = "org.apache.tomcat.embed:tomcat-embed-core:10.1.31" +tomcat-embed-core = "org.apache.tomcat.embed:tomcat-embed-core:10.1.49" # checkForUpdates: tomcat-embed-core9:9.+ -tomcat-embed-core9 = "org.apache.tomcat.embed:tomcat-embed-core:9.0.89" -truth = "com.google.truth:truth:1.4.4" +tomcat-embed-core9 = "org.apache.tomcat.embed:tomcat-embed-core:9.0.112" +truth = "com.google.truth:truth:1.4.5" # checkForUpdates: undertow-servlet22:2.2.+ -undertow-servlet22 = "io.undertow:undertow-servlet:2.2.37.Final" -undertow-servlet = "io.undertow:undertow-servlet:2.3.18.Final" - -# checkstyle 10.0+ requires Java 11+ -# See https://checkstyle.sourceforge.io/releasenotes_old_8-35_10-26.html#Release_10.0 -# checkForUpdates: checkstylejava8:9.+ -checkstylejava8 = "com.puppycrawl.tools:checkstyle:9.3" -# 2.11.0+ requires JDK 11+ (See https://github.com/google/error-prone/releases/tag/v2.11.0) -# checkForUpdates: errorprone-corejava8:2.10.+ -errorprone-corejava8 = "com.google.errorprone:error_prone_core:2.10.0" +undertow-servlet22 = "io.undertow:undertow-servlet:2.2.38.Final" +undertow-servlet = "io.undertow:undertow-servlet:2.3.20.Final" diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/StressTestClientTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/StressTestClientTest.java index 95ef5379120..a1a2cb9b5ea 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/StressTestClientTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/StressTestClientTest.java @@ -49,8 +49,8 @@ public class StressTestClientTest { @Test public void ipv6AddressesShouldBeSupported() { StressTestClient client = new StressTestClient(); - client.parseArgs(new String[] {"--server_addresses=[0:0:0:0:0:0:0:1]:8080," - + "[1:2:3:4:f:e:a:b]:8083"}); + client.parseArgs(new String[] { + "--server_addresses=[0:0:0:0:0:0:0:1]:8080,[1:2:3:4:f:e:a:b]:8083"}); assertEquals(2, client.addresses().size()); assertEquals(new InetSocketAddress("0:0:0:0:0:0:0:1", 8080), client.addresses().get(0)); diff --git a/netty/src/main/java/io/grpc/netty/Utils.java b/netty/src/main/java/io/grpc/netty/Utils.java index c0981f5b219..386df20ba0b 100644 --- a/netty/src/main/java/io/grpc/netty/Utils.java +++ b/netty/src/main/java/io/grpc/netty/Utils.java @@ -122,10 +122,10 @@ private static final class ByteBufAllocatorPreferHeapHolder { EPOLL_DOMAIN_CLIENT_CHANNEL_TYPE = epollDomainSocketChannelType(); DEFAULT_SERVER_CHANNEL_FACTORY = new ReflectiveChannelFactory<>(epollServerChannelType()); EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR = epollEventLoopGroupConstructor(); - DEFAULT_BOSS_EVENT_LOOP_GROUP - = new DefaultEventLoopGroupResource(1, "grpc-default-boss-ELG", EventLoopGroupType.EPOLL); - DEFAULT_WORKER_EVENT_LOOP_GROUP - = new DefaultEventLoopGroupResource(0,"grpc-default-worker-ELG", EventLoopGroupType.EPOLL); + DEFAULT_BOSS_EVENT_LOOP_GROUP = new DefaultEventLoopGroupResource( + 1, "grpc-default-boss-ELG", EventLoopGroupType.EPOLL); + DEFAULT_WORKER_EVENT_LOOP_GROUP = new DefaultEventLoopGroupResource( + 0, "grpc-default-worker-ELG", EventLoopGroupType.EPOLL); } else { logger.log(Level.FINE, "Epoll is not available, using Nio.", getEpollUnavailabilityCause()); DEFAULT_SERVER_CHANNEL_FACTORY = nioServerChannelFactory(); diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java index e76799845c7..a9fa345e6d8 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java @@ -337,7 +337,7 @@ private OkHttpClientTransport( ? SocketFactory.getDefault() : transportFactory.socketFactory; this.sslSocketFactory = transportFactory.sslSocketFactory; this.hostnameVerifier = transportFactory.hostnameVerifier != null - ? transportFactory.hostnameVerifier : OkHostnameVerifier.INSTANCE; + ? transportFactory.hostnameVerifier : OkHostnameVerifier.INSTANCE; this.connectionSpec = Preconditions.checkNotNull( transportFactory.connectionSpec, "connectionSpec"); this.stopwatchFactory = Preconditions.checkNotNull(stopwatchFactory, "stopwatchFactory"); diff --git a/repositories.bzl b/repositories.bzl index a635b59a425..f227b8f72c4 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -12,19 +12,19 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # GRPC_DEPS_START IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.android:annotations:4.1.1.4", - "com.google.api.grpc:proto-google-common-protos:2.59.2", - "com.google.auth:google-auth-library-credentials:1.24.1", - "com.google.auth:google-auth-library-oauth2-http:1.24.1", + "com.google.api.grpc:proto-google-common-protos:2.63.1", + "com.google.auth:google-auth-library-credentials:1.40.0", + "com.google.auth:google-auth-library-oauth2-http:1.40.0", "com.google.auto.value:auto-value-annotations:1.11.0", "com.google.auto.value:auto-value:1.11.0", "com.google.code.findbugs:jsr305:3.0.2", "com.google.code.gson:gson:2.12.1", - "com.google.errorprone:error_prone_annotations:2.36.0", + "com.google.errorprone:error_prone_annotations:2.44.0", "com.google.guava:failureaccess:1.0.1", - "com.google.guava:guava:33.4.8-android", + "com.google.guava:guava:33.5.0-android", "com.google.re2j:re2j:1.8", - "com.google.s2a.proto.v2:s2a-proto:0.1.2", - "com.google.truth:truth:1.4.2", + "com.google.s2a.proto.v2:s2a-proto:0.1.3", + "com.google.truth:truth:1.4.5", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day "io.netty:netty-buffer:4.1.127.Final", @@ -46,7 +46,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.perfmark:perfmark-api:0.27.0", "junit:junit:4.13.2", "org.checkerframework:checker-qual:3.49.5", - "org.codehaus.mojo:animal-sniffer-annotations:1.24", + "org.codehaus.mojo:animal-sniffer-annotations:1.26", ] # GRPC_DEPS_END diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java index 3b52f61c9df..9dcbdcf0509 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/S2AProtocolNegotiatorFactory.java @@ -151,7 +151,7 @@ public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { String hostname = getHostNameFromAuthority(grpcHandler.getAuthority()); checkArgument(!isNullOrEmpty(hostname), "hostname should not be null or empty."); return new S2AProtocolNegotiationHandler( - grpcHandler, channel, localIdentity, hostname, service, stub); + grpcHandler, channel, localIdentity, hostname, service, stub); } @Override diff --git a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java index 2dfde16cf2f..5d4ef9eb667 100644 --- a/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java +++ b/s2a/src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java @@ -150,8 +150,8 @@ private static void configureSslContextWithClientTlsConfiguration( ProtoUtil.buildTlsProtocolVersionSet( clientTlsConfiguration.getMinTlsVersion(), clientTlsConfiguration.getMaxTlsVersion()); if (tlsVersions.isEmpty()) { - throw new S2AConnectionException("Set of TLS versions received from S2A server is" - + " empty or not supported."); + throw new S2AConnectionException( + "Set of TLS versions received from S2A server is empty or not supported."); } sslContextBuilder.protocols(tlsVersions); } @@ -184,4 +184,4 @@ private static X509Certificate convertStringToX509Cert(String certificate) } private SslContextFactory() {} -} \ No newline at end of file +} diff --git a/servlet/src/jettyTest/java/io/grpc/servlet/JettyTransportTest.java b/servlet/src/jettyTest/java/io/grpc/servlet/JettyTransportTest.java index c896c7a23ea..58143a8516c 100644 --- a/servlet/src/jettyTest/java/io/grpc/servlet/JettyTransportTest.java +++ b/servlet/src/jettyTest/java/io/grpc/servlet/JettyTransportTest.java @@ -77,9 +77,7 @@ public void start(ServerListener listener) throws IOException { ServerConnector sc = (ServerConnector) jettyServer.getConnectors()[0]; HttpConfiguration httpConfiguration = new HttpConfiguration(); - // Must be set for several tests to pass, so that the request handling can begin before - // content arrives. - httpConfiguration.setDelayDispatchUntilContent(false); + setDelayDispatchUntilContent(httpConfiguration); HTTP2CServerConnectionFactory factory = new HTTP2CServerConnectionFactory(httpConfiguration); @@ -135,6 +133,16 @@ protected InternalServer newServer(int port, return newServer(streamTracerFactories); } + // The future default appears to be false as people are supposed to be migrate to + // EagerContentHandler, but the default is still true. Seems they messed up the migration + // process here by not flipping the default. + @SuppressWarnings("removal") + private static void setDelayDispatchUntilContent(HttpConfiguration httpConfiguration) { + // Must be set for several tests to pass, so that the request handling can begin before + // content arrives. + httpConfiguration.setDelayDispatchUntilContent(false); + } + @Override protected ManagedClientTransport newClientTransport(InternalServer server) { NettyChannelBuilder nettyChannelBuilder = NettyChannelBuilder diff --git a/settings.gradle b/settings.gradle index f4df1105090..51c4bdc0d3d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,9 +20,9 @@ pluginManagement { // https://github.com/kt3k/coveralls-gradle-plugin/tags id "com.github.kt3k.coveralls" version "2.12.2" // https://github.com/GoogleCloudPlatform/appengine-plugins/releases - id "com.google.cloud.tools.appengine" version "2.8.0" + id "com.google.cloud.tools.appengine" version "2.8.6" // https://github.com/GoogleContainerTools/jib/blob/master/jib-gradle-plugin/CHANGELOG.md - id "com.google.cloud.tools.jib" version "3.4.5" + id "com.google.cloud.tools.jib" version "3.5.1" // https://github.com/google/osdetector-gradle-plugin/tags id "com.google.osdetector" version "1.7.3" // https://github.com/google/protobuf-gradle-plugin/releases From 53a092646a0883c29d3bc8f05277b9f0c15a1ce6 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Thu, 4 Dec 2025 12:04:03 +0530 Subject: [PATCH 475/591] xds: fix race in simpleFlowControl (#12547) --- xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 6b9d601b2cf..87758286dc5 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -3521,7 +3521,7 @@ public void simpleFlowControl() throws Exception { TIME_INCREMENT); barrier.await(); verify(edsResourceWatcher, atLeastOnce()).onResourceChanged(edsUpdateCaptor.capture()); - StatusOr statusOrUpdate = edsUpdateCaptor.getValue(); + StatusOr statusOrUpdate = edsUpdateCaptor.getAllValues().get(0); assertThat(statusOrUpdate.hasValue()).isTrue(); EdsUpdate edsUpdate = statusOrUpdate.getValue(); validateGoldenClusterLoadAssignment(edsUpdate); From 55ae1d0541c3482cf9fa2cadb156b1da6852deb4 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 3 Dec 2025 15:10:38 -0800 Subject: [PATCH 476/591] rls: Avoid missed config update from reentrancy Since ChildPolicyWrapper() called into the child before childPolicyMap.put(), it is possible for that child to call back into RLS and further update state without that child being known. When CDS is_dynamic=true (since ca99a8c47), it registers the cluster with XdsDependencyManager, which adds a watch to XdsClient. If XdsClient already has the results cached then the watch callback can be enqueued immediately onto the syncContext and execute still within the constructor. Calling into the child with the lock held isn't great, as it allows for this type of reentrancy bug. But that'll take larger changes to fix. b/464116731 --- .../io/grpc/rls/LbPolicyConfiguration.java | 7 ++-- .../grpc/rls/LbPolicyConfigurationTest.java | 34 +++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java b/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java index 0fb10326f28..77ed080e654 100644 --- a/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java +++ b/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java @@ -245,9 +245,10 @@ ChildPolicyWrapper createOrGet(String target) { RefCountedChildPolicyWrapper pooledChildPolicyWrapper = childPolicyMap.get(target); if (pooledChildPolicyWrapper == null) { ChildPolicyWrapper childPolicyWrapper = new ChildPolicyWrapper( - target, childPolicy, childLbResolvedAddressFactory, childLbHelperProvider); + target, childPolicy, childLbHelperProvider); pooledChildPolicyWrapper = RefCountedChildPolicyWrapper.of(childPolicyWrapper); childPolicyMap.put(target, pooledChildPolicyWrapper); + childPolicyWrapper.start(childLbResolvedAddressFactory); return pooledChildPolicyWrapper.getObject(); } else { ChildPolicyWrapper childPolicyWrapper = pooledChildPolicyWrapper.getObject(); @@ -294,7 +295,6 @@ static final class ChildPolicyWrapper { public ChildPolicyWrapper( String target, ChildLoadBalancingPolicy childPolicy, - final ResolvedAddressFactory childLbResolvedAddressFactory, ChildLoadBalancerHelperProvider childLbHelperProvider) { this.target = target; this.helper = new ChildPolicyReportingHelper(childLbHelperProvider); @@ -307,6 +307,9 @@ public ChildPolicyWrapper( this.childLbConfig = lbConfig.getConfig(); helper.getChannelLogger().log( ChannelLogLevel.DEBUG, "RLS child lb created. config: {0}", childLbConfig); + } + + void start(ResolvedAddressFactory childLbResolvedAddressFactory) { helper.getSynchronizationContext().execute( new Runnable() { @Override diff --git a/rls/src/test/java/io/grpc/rls/LbPolicyConfigurationTest.java b/rls/src/test/java/io/grpc/rls/LbPolicyConfigurationTest.java index fc48e6a5405..de41d0488fc 100644 --- a/rls/src/test/java/io/grpc/rls/LbPolicyConfigurationTest.java +++ b/rls/src/test/java/io/grpc/rls/LbPolicyConfigurationTest.java @@ -21,6 +21,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -44,8 +45,8 @@ import io.grpc.rls.LbPolicyConfiguration.ChildPolicyWrapper.ChildPolicyReportingHelper; import io.grpc.rls.LbPolicyConfiguration.InvalidChildPolicyConfigException; import io.grpc.rls.LbPolicyConfiguration.RefCountedChildPolicyWrapperFactory; -import java.lang.Thread.UncaughtExceptionHandler; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -60,6 +61,9 @@ public class LbPolicyConfigurationTest { private final LoadBalancer lb = mock(LoadBalancer.class); private final SubchannelStateManager subchannelStateManager = new SubchannelStateManagerImpl(); private final SubchannelPicker picker = mock(SubchannelPicker.class); + private final SynchronizationContext syncContext = new SynchronizationContext((t, e) -> { + throw new AssertionError(e); + }); private final ResolvedAddressFactory resolvedAddressFactory = new ResolvedAddressFactory() { @Override @@ -81,15 +85,7 @@ public ResolvedAddresses create(Object childLbConfig) { @Before public void setUp() { doReturn(mock(ChannelLogger.class)).when(helper).getChannelLogger(); - doReturn( - new SynchronizationContext( - new UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread t, Throwable e) { - throw new AssertionError(e); - } - })) - .when(helper).getSynchronizationContext(); + doReturn(syncContext).when(helper).getSynchronizationContext(); doReturn(lb).when(lbProvider).newLoadBalancer(any(Helper.class)); doReturn(ConfigOrError.fromConfig(new Object())) .when(lbProvider).parseLoadBalancingPolicyConfig(ArgumentMatchers.>any()); @@ -186,4 +182,22 @@ public void updateBalancingState_triggersListener() { // picker governs childPickers will be reported to parent LB verify(helper).updateBalancingState(ConnectivityState.READY, picker); } + + @Test + public void refCountedGetOrCreate_addsChildBeforeConfiguringChild() { + AtomicBoolean calledAlready = new AtomicBoolean(); + when(lb.acceptResolvedAddresses(any(ResolvedAddresses.class))).thenAnswer(i -> { + if (!calledAlready.get()) { + calledAlready.set(true); + // Should end up calling this function again, as this child should already be added to the + // list of children. In practice, this can be caused by CDS is_dynamic=true starting a watch + // when XdsClient already has the cluster cached (e.g., from another channel). + syncContext.execute(() -> + factory.acceptResolvedAddressFactory(resolvedAddressFactory)); + } + return Status.OK; + }); + ChildPolicyWrapper unused = factory.createOrGet("foo.google.com"); + verify(lb, times(2)).acceptResolvedAddresses(any(ResolvedAddresses.class)); + } } From b1a94a410e1926fb870e9717d11c7d8f85c62cb6 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Fri, 5 Dec 2025 14:23:30 +0530 Subject: [PATCH 477/591] xds: implement server feature fail_on_data_errors (#12544) Implements server feature `fail_on_data_errors`, per gRFC A88: https://github.com/grpc/proposal/blob/master/A88-xds-data-error-handling.md --- .../java/io/grpc/xds/client/Bootstrapper.java | 12 +- .../io/grpc/xds/client/BootstrapperImpl.java | 6 +- .../io/grpc/xds/client/XdsClientImpl.java | 66 ++- .../io/grpc/xds/GrpcBootstrapperImplTest.java | 46 ++ .../grpc/xds/GrpcXdsClientImplDataTest.java | 2 +- .../grpc/xds/GrpcXdsClientImplTestBase.java | 446 +++++++++++++++++- .../java/io/grpc/xds/XdsNameResolverTest.java | 5 +- .../test/java/io/grpc/xds/XdsTestUtils.java | 2 +- .../client/CommonBootstrapperTestUtils.java | 2 +- 9 files changed, 553 insertions(+), 34 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java index 4fa75f6b335..1d526703299 100644 --- a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java @@ -65,18 +65,22 @@ public abstract static class ServerInfo { public abstract boolean resourceTimerIsTransientError(); + public abstract boolean failOnDataErrors(); + @VisibleForTesting public static ServerInfo create(String target, @Nullable Object implSpecificConfig) { return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, - false, false, false); + false, false, false, false); } @VisibleForTesting public static ServerInfo create( - String target, Object implSpecificConfig, boolean ignoreResourceDeletion, - boolean isTrustedXdsServer, boolean resourceTimerIsTransientError) { + String target, Object implSpecificConfig, + boolean ignoreResourceDeletion, boolean isTrustedXdsServer, + boolean resourceTimerIsTransientError, boolean failOnDataErrors) { return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, - ignoreResourceDeletion, isTrustedXdsServer, resourceTimerIsTransientError); + ignoreResourceDeletion, isTrustedXdsServer, + resourceTimerIsTransientError, failOnDataErrors); } } diff --git a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java index 22c794e1129..b44e32bb2d9 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -58,6 +58,7 @@ public abstract class BootstrapperImpl extends Bootstrapper { private static final String SERVER_FEATURE_TRUSTED_XDS_SERVER = "trusted_xds_server"; private static final String SERVER_FEATURE_RESOURCE_TIMER_IS_TRANSIENT_ERROR = "resource_timer_is_transient_error"; + private static final String SERVER_FEATURE_FAIL_ON_DATA_ERRORS = "fail_on_data_errors"; @VisibleForTesting static boolean enableXdsFallback = GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_FALLBACK, true); @@ -257,6 +258,7 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo boolean resourceTimerIsTransientError = false; boolean ignoreResourceDeletion = false; + boolean failOnDataErrors = false; // "For forward compatibility reasons, the client will ignore any entry in the list that it // does not understand, regardless of type." List serverFeatures = JsonUtil.getList(serverConfig, "server_features"); @@ -267,12 +269,14 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo } resourceTimerIsTransientError = xdsDataErrorHandlingEnabled && serverFeatures.contains(SERVER_FEATURE_RESOURCE_TIMER_IS_TRANSIENT_ERROR); + failOnDataErrors = xdsDataErrorHandlingEnabled + && serverFeatures.contains(SERVER_FEATURE_FAIL_ON_DATA_ERRORS); } servers.add( ServerInfo.create(serverUri, implSpecificConfig, ignoreResourceDeletion, serverFeatures != null && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER), - resourceTimerIsTransientError)); + resourceTimerIsTransientError, failOnDataErrors)); } return servers.build(); } diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index 2bf1286babc..0584a3dbfdd 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -603,16 +603,20 @@ private void handleResourceUpdate( } if (invalidResources.contains(resourceName)) { - // The resource update is invalid. Capture the error without notifying the watchers. + // The resource update is invalid (NACK). Handle as a data error. subscriber.onRejected(args.versionInfo, updateTime, errorDetail); - } - - if (invalidResources.contains(resourceName)) { - // The resource is missing. Reuse the cached resource if possible. - if (subscriber.data == null) { - // No cached data. Notify the watchers of an invalid update. - subscriber.onError(Status.UNAVAILABLE.withDescription(errorDetail), processingTracker); + + // Handle data errors (NACKs) based on fail_on_data_errors server feature. + // When xdsDataErrorHandlingEnabled is true and fail_on_data_errors is present, + // delete cached data so onError will call onResourceChanged instead of onAmbientError. + // When xdsDataErrorHandlingEnabled is false, use old behavior (always keep cached data). + if (BootstrapperImpl.xdsDataErrorHandlingEnabled && subscriber.data != null + && args.serverInfo.failOnDataErrors()) { + subscriber.data = null; } + // Call onError, which will decide whether to call onResourceChanged or onAmbientError + // based on whether data exists after the above deletion. + subscriber.onError(Status.UNAVAILABLE.withDescription(errorDetail), processingTracker); continue; } @@ -866,20 +870,42 @@ void onAbsent(@Nullable ProcessingTracker processingTracker, ServerInfo serverIn return; } - // Ignore deletion of State of the World resources when this feature is on, - // and the resource is reusable. + // Handle data errors (resource deletions) based on fail_on_data_errors server feature. + // When xdsDataErrorHandlingEnabled is true and fail_on_data_errors is not present, + // we treat deletions as ambient errors and keep using the cached resource. + // When fail_on_data_errors is present, we delete the cached resource and fail. + // When xdsDataErrorHandlingEnabled is false, use the old behavior (ignore_resource_deletion). boolean ignoreResourceDeletionEnabled = serverInfo.ignoreResourceDeletion(); - if (ignoreResourceDeletionEnabled && type.isFullStateOfTheWorld() && data != null) { - if (!resourceDeletionIgnored) { - logger.log(XdsLogLevel.FORCE_WARNING, - "xds server {0}: ignoring deletion for resource type {1} name {2}}", - serverInfo.target(), type, resource); - resourceDeletionIgnored = true; + boolean failOnDataErrors = serverInfo.failOnDataErrors(); + boolean xdsDataErrorHandlingEnabled = BootstrapperImpl.xdsDataErrorHandlingEnabled; + + if (type.isFullStateOfTheWorld() && data != null) { + // New behavior (per gRFC A88): Default is to treat deletions as ambient errors + if (xdsDataErrorHandlingEnabled && !failOnDataErrors) { + if (!resourceDeletionIgnored) { + logger.log(XdsLogLevel.FORCE_WARNING, + "xds server {0}: ignoring deletion for resource type {1} name {2}}", + serverInfo.target(), type, resource); + resourceDeletionIgnored = true; + } + Status deletionStatus = Status.NOT_FOUND.withDescription( + "Resource " + resource + " deleted from server"); + onAmbientError(deletionStatus, processingTracker); + return; + } + // Old behavior: Use ignore_resource_deletion server feature + if (!xdsDataErrorHandlingEnabled && ignoreResourceDeletionEnabled) { + if (!resourceDeletionIgnored) { + logger.log(XdsLogLevel.FORCE_WARNING, + "xds server {0}: ignoring deletion for resource type {1} name {2}}", + serverInfo.target(), type, resource); + resourceDeletionIgnored = true; + } + Status deletionStatus = Status.NOT_FOUND.withDescription( + "Resource " + resource + " deleted from server"); + onAmbientError(deletionStatus, processingTracker); + return; } - Status deletionStatus = Status.NOT_FOUND.withDescription( - "Resource " + resource + " deleted from server"); - onAmbientError(deletionStatus, processingTracker); - return; } logger.log(XdsLogLevel.INFO, "Conclude {0} resource {1} not exist", type, resource); diff --git a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java index 2b7bd53d5ef..0a303b7255d 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -723,6 +723,52 @@ public void serverFeatures_ignoresUnknownValues() throws XdsInitializationExcept assertThat(serverInfo.isTrustedXdsServer()).isTrue(); } + @Test + public void serverFeature_failOnDataErrors() throws XdsInitializationException { + BootstrapperImpl.xdsDataErrorHandlingEnabled = true; + String rawData = "{\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\"type\": \"insecure\"}\n" + + " ],\n" + + " \"server_features\": [\"fail_on_data_errors\"]\n" + + " }\n" + + " ]\n" + + "}"; + + bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); + BootstrapInfo info = bootstrapper.bootstrap(); + ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); + assertThat(serverInfo.target()).isEqualTo(SERVER_URI); + assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.failOnDataErrors()).isTrue(); + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + } + + @Test + public void serverFeature_failOnDataErrors_requiresEnvVar() throws XdsInitializationException { + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + String rawData = "{\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\"type\": \"insecure\"}\n" + + " ],\n" + + " \"server_features\": [\"fail_on_data_errors\"]\n" + + " }\n" + + " ]\n" + + "}"; + + bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); + BootstrapInfo info = bootstrapper.bootstrap(); + ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); + // Should be false when env var is not enabled + assertThat(serverInfo.failOnDataErrors()).isFalse(); + } + @Test public void notFound() { bootstrapper.bootstrapPathFromEnvVar = null; diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 975570d8205..be29e5e719f 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -3614,7 +3614,7 @@ private static Filter buildHttpConnectionManagerFilter(HttpFilter... httpFilters private XdsResourceType.Args getXdsResourceTypeArgs(boolean isTrustedServer) { return new XdsResourceType.Args( - ServerInfo.create("http://td", "", false, isTrustedServer, false), "1.0", null, null, null, null + ServerInfo.create("http://td", "", false, isTrustedServer, false, false), "1.0", null, null, null, null ); } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 87758286dc5..887923b169b 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -362,7 +362,7 @@ public void setUp() throws IOException { cleanupRule.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), - true, false); + true, false, false); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -851,6 +851,52 @@ public void ldsResponseErrorHandling_subscribedResourceInvalid() { verifySubscribedResourcesMetadataSizes(3, 0, 0, 0); } + @Test + public void ldsResponseErrorHandling_subscribedResourceInvalid_withDataErrorHandlingEnabled() { + BootstrapperImpl.xdsDataErrorHandlingEnabled = true; + + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), "A", ldsResourceWatcher); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), "B", ldsResourceWatcher); + xdsClient.watchXdsResource(XdsListenerResource.getInstance(), "C", ldsResourceWatcher); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + assertThat(call).isNotNull(); + verifyResourceMetadataRequested(LDS, "A"); + verifyResourceMetadataRequested(LDS, "B"); + verifyResourceMetadataRequested(LDS, "C"); + ImmutableMap resourcesV1 = ImmutableMap.of( + "A", Any.pack(mf.buildListenerWithApiListenerForRds("A", "A.1")), + "B", Any.pack(mf.buildListenerWithApiListenerForRds("B", "B.1")), + "C", Any.pack(mf.buildListenerWithApiListenerForRds("C", "C.1"))); + call.sendResponse(LDS, resourcesV1.values().asList(), VERSION_1, "0000"); + verify(ldsResourceWatcher, times(3)).onResourceChanged(any()); + ImmutableMap resourcesV2 = ImmutableMap.of( + "A", Any.pack(mf.buildListenerWithApiListenerForRds("A", "A.2")), + "B", Any.pack(mf.buildListenerWithApiListenerInvalid("B"))); + call.sendResponse(LDS, resourcesV2.values().asList(), VERSION_2, "0001"); + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); + verify(ldsResourceWatcher, times(2)).onAmbientError(statusCaptor.capture()); + List receivedStatuses = statusCaptor.getAllValues(); + assertThat(receivedStatuses).hasSize(2); + + assertThat( + receivedStatuses.stream().anyMatch( + status -> status.getCode() == Status.Code.UNAVAILABLE + && status.getDescription().contains("LDS response Listener 'B' validation error"))) + .isTrue(); + assertThat( + receivedStatuses.stream().anyMatch( + status -> status.getCode() == Status.Code.NOT_FOUND + && status.getDescription().contains("Resource C deleted from server"))) + .isTrue(); + List errorsV2 = ImmutableList.of("LDS response Listener 'B' validation error: "); + verifyResourceMetadataAcked(LDS, "A", resourcesV2.get("A"), VERSION_2, TIME_INCREMENT * 2); + verifyResourceMetadataNacked(LDS, "B", resourcesV1.get("B"), VERSION_1, TIME_INCREMENT, + VERSION_2, TIME_INCREMENT * 2, errorsV2, true); + verifyResourceMetadataAcked(LDS, "C", resourcesV1.get("C"), VERSION_1, TIME_INCREMENT); + + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + } + @Test public void ldsResponseErrorHandling_subscribedResourceInvalid_withRdsSubscription() { List subscribedResourceNames = ImmutableList.of("A", "B", "C"); @@ -1448,6 +1494,176 @@ public void ldsResourceDeleted_ignoreResourceDeletion() { verifyNoMoreInteractions(ldsResourceWatcher); } + /** + * When fail_on_data_errors server feature is on, xDS client should delete the cached listener + * and fail RPCs when LDS resource is deleted. + */ + @Test + public void ldsResourceDeleted_failOnDataErrors_true() { + BootstrapperImpl.xdsDataErrorHandlingEnabled = true; + xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, + true, false, true); + BootstrapInfo bootstrapInfo = + Bootstrapper.BootstrapInfo.builder() + .servers(Collections.singletonList(xdsServerInfo)) + .node(NODE) + .authorities(ImmutableMap.of( + "", + AuthorityInfo.create( + "xdstp:///envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS))))) + .certProviders(ImmutableMap.of()) + .build(); + xdsClient = new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + timeProvider, + MessagePrinter.INSTANCE, + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); + + InOrder inOrder = inOrder(ldsResourceWatcher); + DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE, + ldsResourceWatcher); + verifyResourceMetadataRequested(LDS, LDS_RESOURCE); + + // Initial LDS response. + call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); + call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); + inOrder.verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); + verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_1, TIME_INCREMENT); + verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); + + // Empty LDS response deletes the listener and fails RPCs. + call.sendResponse(LDS, Collections.emptyList(), VERSION_2, "0001"); + call.verifyRequest(LDS, LDS_RESOURCE, VERSION_2, "0001", NODE); + inOrder.verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate1 = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate1.hasValue()).isFalse(); + assertThat(statusOrUpdate1.getStatus().getCode()).isEqualTo(Status.Code.NOT_FOUND); + verifyResourceMetadataDoesNotExist(LDS, LDS_RESOURCE); + verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); + + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + } + + /** + * When the fail_on_data_errors server feature is not present, the default behavior + * is to treat a resource deletion as an ambient error and preserve the cached resource. + */ + @Test + public void ldsResourceDeleted_failOnDataErrors_false() { + BootstrapperImpl.xdsDataErrorHandlingEnabled = true; + + xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, + true, false, false); + BootstrapInfo bootstrapInfo = + Bootstrapper.BootstrapInfo.builder() + .servers(Collections.singletonList(xdsServerInfo)) + .node(NODE) + .authorities(ImmutableMap.of( + "", + AuthorityInfo.create( + "xdstp:///envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS))))) + .certProviders(ImmutableMap.of()) + .build(); + xdsClient = new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + timeProvider, + MessagePrinter.INSTANCE, + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); + + InOrder inOrder = inOrder(ldsResourceWatcher); + DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE, + ldsResourceWatcher); + verifyResourceMetadataRequested(LDS, LDS_RESOURCE); + + // Initial LDS response. + call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); + call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); + inOrder.verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(statusOrUpdate.getValue()); + verifyResourceMetadataAcked(LDS, LDS_RESOURCE, testListenerVhosts, VERSION_1, TIME_INCREMENT); + verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); + + // Empty LDS response deletes the listener and fails RPCs. + call.sendResponse(LDS, Collections.emptyList(), VERSION_2, "0001"); + call.verifyRequest(LDS, LDS_RESOURCE, VERSION_2, "0001", NODE); + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); + inOrder.verify(ldsResourceWatcher).onAmbientError(statusCaptor.capture()); + Status receivedStatus = statusCaptor.getValue(); + assertThat(receivedStatus.getCode()).isEqualTo(Status.Code.NOT_FOUND); + assertThat(receivedStatus.getDescription()).contains( + "Resource " + LDS_RESOURCE + " deleted from server"); + inOrder.verify(ldsResourceWatcher, never()).onResourceChanged(any()); + verifySubscribedResourcesMetadataSizes(1, 0, 0, 0); + + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + } + + /** + * Tests that fail_on_data_errors feature is ignored if the env var is not enabled, + * and the old behavior (dropping the resource) is used. + */ + @Test + public void ldsResourceDeleted_failOnDataErrorsIgnoredWithoutEnvVar() { + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + + xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, + true, false, true); + BootstrapInfo bootstrapInfo = + Bootstrapper.BootstrapInfo.builder() + .servers(Collections.singletonList(xdsServerInfo)) + .node(NODE) + .authorities(ImmutableMap.of( + "", + AuthorityInfo.create( + "xdstp:///envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS))))) + .certProviders(ImmutableMap.of()) + .build(); + xdsClient = new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + timeProvider, + MessagePrinter.INSTANCE, + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); + + InOrder inOrder = inOrder(ldsResourceWatcher); + DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE, + ldsResourceWatcher); + call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); + inOrder.verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + assertThat(ldsUpdateCaptor.getValue().hasValue()).isTrue(); + call.sendResponse(LDS, Collections.emptyList(), VERSION_2, "0001"); + + inOrder.verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr statusOrUpdate = ldsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isFalse(); + assertThat(statusOrUpdate.getStatus().getCode()).isEqualTo(Status.Code.NOT_FOUND); + } + @Test @SuppressWarnings("unchecked") public void multipleLdsWatchers() { @@ -2972,6 +3188,228 @@ public void cdsResourceDeleted_ignoreResourceDeletion() { verifyNoMoreInteractions(ldsResourceWatcher); } + /** + * When fail_on_data_errors server feature is on, xDS client should delete the cached cluster + * and fail RPCs when CDS resource is deleted. + */ + @Test + public void cdsResourceDeleted_failOnDataErrors_true() { + BootstrapperImpl.xdsDataErrorHandlingEnabled = true; + xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, + true, false, true); + BootstrapInfo bootstrapInfo = + Bootstrapper.BootstrapInfo.builder() + .servers(Collections.singletonList(xdsServerInfo)) + .node(NODE) + .authorities(ImmutableMap.of( + "", + AuthorityInfo.create( + "xdstp:///envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS))))) + .certProviders(ImmutableMap.of()) + .build(); + xdsClient = new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + timeProvider, + MessagePrinter.INSTANCE, + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); + + DiscoveryRpcCall call = startResourceWatcher(XdsClusterResource.getInstance(), CDS_RESOURCE, + cdsResourceWatcher); + verifyResourceMetadataRequested(CDS, CDS_RESOURCE); + + // Initial CDS response. + call.sendResponse(CDS, testClusterRoundRobin, VERSION_1, "0000"); + call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); + verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenClusterRoundRobin(statusOrUpdate.getValue()); + verifyResourceMetadataAcked(CDS, CDS_RESOURCE, testClusterRoundRobin, VERSION_1, + TIME_INCREMENT); + verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); + + // Empty CDS response deletes the cluster and fails RPCs. + call.sendResponse(CDS, Collections.emptyList(), VERSION_2, "0001"); + call.verifyRequest(CDS, CDS_RESOURCE, VERSION_2, "0001", NODE); + verify(cdsResourceWatcher).onResourceChanged(argThat( + arg -> !arg.hasValue() && arg.getStatus().getDescription().contains(CDS_RESOURCE))); + verifyResourceMetadataDoesNotExist(CDS, CDS_RESOURCE); + verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); + + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + } + + /** + * When fail_on_data_errors server feature is on, xDS client should delete the cached cluster + * and fail RPCs when CDS resource is deleted. + */ + @Test + public void cdsResourceDeleted_failOnDataErrors_false() { + BootstrapperImpl.xdsDataErrorHandlingEnabled = true; + // Set failOnDataErrors to false for this test case. + xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, + true, false, false); + BootstrapInfo bootstrapInfo = + Bootstrapper.BootstrapInfo.builder() + .servers(Collections.singletonList(xdsServerInfo)) + .node(NODE) + .authorities(ImmutableMap.of( + "", + AuthorityInfo.create( + "xdstp:///envoy.config.listener.v3.Listener/%s", + ImmutableList.of(Bootstrapper.ServerInfo.create( + SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS))))) + .certProviders(ImmutableMap.of()) + .build(); + xdsClient = new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + timeProvider, + MessagePrinter.INSTANCE, + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); + + InOrder inOrder = inOrder(cdsResourceWatcher); + DiscoveryRpcCall call = startResourceWatcher(XdsClusterResource.getInstance(), CDS_RESOURCE, + cdsResourceWatcher); + verifyResourceMetadataRequested(CDS, CDS_RESOURCE); + + // Initial CDS response. + call.sendResponse(CDS, testClusterRoundRobin, VERSION_1, "0000"); + call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); + inOrder.verify(cdsResourceWatcher).onResourceChanged(cdsUpdateCaptor.capture()); + StatusOr statusOrUpdate = cdsUpdateCaptor.getValue(); + assertThat(statusOrUpdate.hasValue()).isTrue(); + verifyGoldenClusterRoundRobin(statusOrUpdate.getValue()); + verifyResourceMetadataAcked(CDS, CDS_RESOURCE, testClusterRoundRobin, VERSION_1, + TIME_INCREMENT); + verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); + + // Empty CDS response should trigger an ambient error. + call.sendResponse(CDS, Collections.emptyList(), VERSION_2, "0001"); + call.verifyRequest(CDS, CDS_RESOURCE, VERSION_2, "0001", NODE); + + // Verify that onAmbientError() is called. + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); + inOrder.verify(cdsResourceWatcher).onAmbientError(statusCaptor.capture()); + Status receivedStatus = statusCaptor.getValue(); + assertThat(receivedStatus.getCode()).isEqualTo(Status.Code.NOT_FOUND); + assertThat(receivedStatus.getDescription()).contains( + "Resource " + CDS_RESOURCE + " deleted from server"); + + // Verify that onResourceChanged() is NOT called again. + inOrder.verify(cdsResourceWatcher, never()).onResourceChanged(any()); + verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); + + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + } + + /** + * Tests that a NACKed LDS resource update drops the cached resource when fail_on_data_errors + * is enabled. + */ + @Test + public void ldsResourceNacked_withFailOnDataErrors_dropsResource() { + BootstrapperImpl.xdsDataErrorHandlingEnabled = true; + xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, + true, false, true); + BootstrapInfo bootstrapInfo = + Bootstrapper.BootstrapInfo.builder() + .servers(Collections.singletonList(xdsServerInfo)) + .node(NODE) + .build(); + xdsClient = new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + timeProvider, + MessagePrinter.INSTANCE, + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); + + InOrder inOrder = inOrder(ldsResourceWatcher); + DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE, + ldsResourceWatcher); + call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); + call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); + inOrder.verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr initialUpdate = ldsUpdateCaptor.getValue(); + assertThat(initialUpdate.hasValue()).isTrue(); + verifyGoldenListenerVhosts(initialUpdate.getValue()); + Message invalidListener = mf.buildListenerWithApiListenerInvalid(LDS_RESOURCE); + call.sendResponse(LDS, Collections.singletonList(Any.pack(invalidListener)), VERSION_2, "0001"); + String expectedError = "LDS response Listener '" + LDS_RESOURCE + "' validation error"; + call.verifyRequestNack(LDS, LDS_RESOURCE, VERSION_1, "0001", NODE, + Collections.singletonList(expectedError)); + + inOrder.verify(ldsResourceWatcher).onResourceChanged(ldsUpdateCaptor.capture()); + StatusOr finalUpdate = ldsUpdateCaptor.getValue(); + assertThat(finalUpdate.hasValue()).isFalse(); + assertThat(finalUpdate.getStatus().getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(finalUpdate.getStatus().getDescription()).contains(expectedError); + + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + } + + /** + * Tests that a NACKed LDS resource update is treated as an ambient error when + * fail_on_data_errors is disabled. + */ + @Test + public void ldsResourceNacked_withFailOnDataErrorsDisabled_isAmbientError() { + BootstrapperImpl.xdsDataErrorHandlingEnabled = true; + xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, + true, false, false); + BootstrapInfo bootstrapInfo = + Bootstrapper.BootstrapInfo.builder() + .servers(Collections.singletonList(xdsServerInfo)) + .node(NODE) + .build(); + xdsClient = new XdsClientImpl( + xdsTransportFactory, + bootstrapInfo, + fakeClock.getScheduledExecutorService(), + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + timeProvider, + MessagePrinter.INSTANCE, + new TlsContextManagerImpl(bootstrapInfo), + xdsClientMetricReporter); + InOrder inOrder = inOrder(ldsResourceWatcher); + DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE, + ldsResourceWatcher); + + call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); + call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0000", NODE); + inOrder.verify(ldsResourceWatcher).onResourceChanged(any()); + Message invalidListener = mf.buildListenerWithApiListenerInvalid(LDS_RESOURCE); + call.sendResponse(LDS, Collections.singletonList(Any.pack(invalidListener)), VERSION_2, "0001"); + + String expectedError = "LDS response Listener '" + LDS_RESOURCE + "' validation error"; + call.verifyRequestNack(LDS, LDS_RESOURCE, VERSION_1, "0001", NODE, + Collections.singletonList(expectedError)); + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); + inOrder.verify(ldsResourceWatcher).onAmbientError(statusCaptor.capture()); + Status receivedStatus = statusCaptor.getValue(); + assertThat(receivedStatus.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(receivedStatus.getDescription()).contains(expectedError); + inOrder.verify(ldsResourceWatcher, never()).onResourceChanged(any()); + + BootstrapperImpl.xdsDataErrorHandlingEnabled = false; + } + @Test @SuppressWarnings("unchecked") public void multipleCdsWatchers() { @@ -3369,7 +3807,7 @@ public void flowControlAbsent() throws Exception { public void resourceTimerIsTransientError_schedulesExtendedTimeout() { BootstrapperImpl.xdsDataErrorHandlingEnabled = true; ServerInfo serverInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, - false, true, true); + false, true, true, false); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(serverInfo)) @@ -3414,7 +3852,7 @@ public void resourceTimerIsTransientError_schedulesExtendedTimeout() { public void resourceTimerIsTransientError_callsOnErrorUnavailable() { BootstrapperImpl.xdsDataErrorHandlingEnabled = true; xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), - true, true); + true, true, false); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -4644,7 +5082,7 @@ private XdsClientImpl createXdsClient(String serverUri) { private BootstrapInfo buildBootStrap(String serverUri) { ServerInfo xdsServerInfo = ServerInfo.create(serverUri, CHANNEL_CREDENTIALS, - ignoreResourceDeletion(), true, false); + ignoreResourceDeletion(), true, false, false); return Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 6bb37cd4483..45a96ee172f 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -358,13 +358,14 @@ public void resolving_targetAuthorityInAuthoritiesMap() { String serviceAuthority = "[::FFFF:129.144.52.38]:80"; bootstrapInfo = BootstrapInfo.builder() .servers(ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true, true, false))) + "td.googleapis.com", InsecureChannelCredentials.create(), true, true, false, false))) .node(Node.newBuilder().build()) .authorities( ImmutableMap.of(targetAuthority, AuthorityInfo.create( "xdstp://" + targetAuthority + "/envoy.config.listener.v3.Listener/%s?foo=1&bar=2", ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true, true, false))))) + "td.googleapis.com", InsecureChannelCredentials.create(), + true, true, false, false))))) .build(); expectedLdsResourceName = "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/" + "%5B::FFFF:129.144.52.38%5D:80?bar=2&foo=1"; // query param canonified diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java index becfe00c79b..f81957ee311 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -87,7 +87,7 @@ public class XdsTestUtils { + ".HttpConnectionManager"; static final Bootstrapper.ServerInfo EMPTY_BOOTSTRAPPER_SERVER_INFO = Bootstrapper.ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), false, true, false); + "td.googleapis.com", InsecureChannelCredentials.create(), false, true, false, false); public static final String ENDPOINT_HOSTNAME = "data-host"; public static final int ENDPOINT_PORT = 1234; diff --git a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java index 754e903f8a9..e3760bd983f 100644 --- a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java @@ -203,7 +203,7 @@ public static Bootstrapper.BootstrapInfo buildBootStrap(List serverUris) List serverInfos = new ArrayList<>(); for (String uri : serverUris) { - serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, false, true, false)); + serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, false, true, false, false)); } EnvoyProtoData.Node node = EnvoyProtoData.Node.newBuilder().setId("node-id").build(); From 8d49dc1c9129fc42c6b80584f5dbad1a543009b5 Mon Sep 17 00:00:00 2001 From: Kim Jin Young Date: Fri, 5 Dec 2025 20:11:46 +0900 Subject: [PATCH 478/591] okhttp: Fix race condition overwriting MAX_CONCURRENT_STREAMS (#12548) ### What this PR does This PR fixes a race condition in `OkHttpClientTransport` where `MAX_CONCURRENT_STREAMS` sent by the server could be incorrectly overwritten by the client's default initialization. The fix simply reorders the initialization to happen **before** starting the reader thread, ensuring that any updates from the server are preserved. ### Note on Testing I attempted to add a deterministic reproduction test, but reliably simulating this specific race condition proved difficult without intrusive changes. I request reviewers to primarily verify the logical correctness of the reordering. I am open to collaborating with the team to develop a suitable test case if required. ### Future Work This PR covers **Step 1** (Fixing the race condition) of the plan discussed in #11985. I plan to follow up with **Step 2** (Adding assertions to verify no pending streams exist) in a separate PR. Part of #11985 --- .../src/main/java/io/grpc/okhttp/OkHttpClientTransport.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java index a9fa345e6d8..2b344554644 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java @@ -800,13 +800,13 @@ public void run() { if (connectingCallback != null) { connectingCallback.run(); } - // ClientFrameHandler need to be started after connectionPreface / settings, otherwise it - // may send goAway immediately. - executor.execute(clientFrameHandler); synchronized (lock) { maxConcurrentStreams = Integer.MAX_VALUE; startPendingStreams(); } + // ClientFrameHandler need to be started after connectionPreface / settings, otherwise it + // may send goAway immediately. + executor.execute(clientFrameHandler); if (connectedFuture != null) { connectedFuture.set(null); } From 58ae5f808cf8e20c5864033c9a8f485b237f9dfc Mon Sep 17 00:00:00 2001 From: Kannan J Date: Fri, 5 Dec 2025 16:42:43 +0530 Subject: [PATCH 479/591] compiler: Upgrade to protobuf 33.1 (#12534) --- buildscripts/kokoro/macos.sh | 11 +++- buildscripts/kokoro/windows32.bat | 18 +++--- buildscripts/kokoro/windows64.bat | 18 +++--- buildscripts/make_dependencies.bat | 5 +- buildscripts/make_dependencies.sh | 91 ++++++++++++++++++------------ compiler/build.gradle | 4 +- 6 files changed, 88 insertions(+), 59 deletions(-) diff --git a/buildscripts/kokoro/macos.sh b/buildscripts/kokoro/macos.sh index 018d15dd2f9..0240c0650f7 100755 --- a/buildscripts/kokoro/macos.sh +++ b/buildscripts/kokoro/macos.sh @@ -1,5 +1,6 @@ #!/bin/bash set -veux -o pipefail +CMAKE_VERSION=3.31.10 if [[ -f /VERSION ]]; then cat /VERSION @@ -7,6 +8,10 @@ fi readonly GRPC_JAVA_DIR="$(cd "$(dirname "$0")"/../.. && pwd)" +DOWNLOAD_DIR=/tmp/source +mkdir -p ${DOWNLOAD_DIR} +curl -Ls https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-macos-universal.tar.gz | tar xz -C ${DOWNLOAD_DIR} + # We had problems with random tests timing out because it took seconds to do # trivial (ns) operations. The Kokoro Mac machines have 2 cores with 4 logical # threads, so Gradle should be using 4 workers by default. @@ -15,7 +20,9 @@ export GRADLE_FLAGS="${GRADLE_FLAGS:-} --max-workers=2" . "$GRPC_JAVA_DIR"/buildscripts/kokoro/kokoro.sh trap spongify_logs EXIT -export -n JAVA_HOME -export PATH="$(/usr/libexec/java_home -v"1.8.0")/bin:${PATH}" +brew install --cask temurin@8 +export PATH="$(/usr/libexec/java_home -v"1.8.0")/bin:${DOWNLOAD_DIR}/cmake-${CMAKE_VERSION}-macos-universal/CMake.app/Contents/bin:${PATH}" +export JAVA_HOME="$(/usr/libexec/java_home -v"1.8.0")" +brew install maven "$GRPC_JAVA_DIR"/buildscripts/kokoro/unix.sh diff --git a/buildscripts/kokoro/windows32.bat b/buildscripts/kokoro/windows32.bat index 2b925c3095c..e98cceb0905 100644 --- a/buildscripts/kokoro/windows32.bat +++ b/buildscripts/kokoro/windows32.bat @@ -25,7 +25,7 @@ cd "%WORKSPACE%" SET TARGET_ARCH=x86_32 SET FAIL_ON_WARNINGS=true -SET PROTOBUF_VER=26.1 +SET PROTOBUF_VER=33.1 SET PKG_CONFIG_PATH=%ESCWORKSPACE%\\grpc-java-helper32\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\lib\\pkgconfig SET VC_PROTOBUF_LIBS=/LIBPATH:%ESCWORKSPACE%\\grpc-java-helper32\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\lib SET VC_PROTOBUF_INCLUDE=%ESCWORKSPACE%\\grpc-java-helper32\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\include @@ -66,14 +66,16 @@ for /f "tokens=*" %%a in ('pkg-config --libs protobuf') do ( set lib=!lib:~2! @rem remove spaces set lib=!lib: =! - @rem Because protobuf is specified as libprotobuf and elsewhere - if !lib! NEQ protobuf ( + set libprefix=!lib:~0,4! + if !libprefix!==absl ( set lib=!lib!.lib - if "!libs_list!"=="" ( - set libs_list=!lib! - ) else ( - set libs_list=!libs_list!,!lib! - ) + ) else ( + set lib=lib!lib!.lib + ) + if "!libs_list!"=="" ( + set libs_list=!lib! + ) else ( + set libs_list=!libs_list!,!lib! ) ) ) diff --git a/buildscripts/kokoro/windows64.bat b/buildscripts/kokoro/windows64.bat index 0bed4612bcf..d5798c634d0 100644 --- a/buildscripts/kokoro/windows64.bat +++ b/buildscripts/kokoro/windows64.bat @@ -24,7 +24,7 @@ cd "%WORKSPACE%" SET TARGET_ARCH=x86_64 SET FAIL_ON_WARNINGS=true -SET PROTOBUF_VER=26.1 +SET PROTOBUF_VER=33.1 SET PKG_CONFIG_PATH=%ESCWORKSPACE%\\grpc-java-helper64\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\lib\\pkgconfig SET VC_PROTOBUF_LIBS=/LIBPATH:%ESCWORKSPACE%\\grpc-java-helper64\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\lib SET VC_PROTOBUF_INCLUDE=%ESCWORKSPACE%\\grpc-java-helper64\\protobuf-%PROTOBUF_VER%\\build\\protobuf-%PROTOBUF_VER%\\include @@ -50,14 +50,16 @@ for /f "tokens=*" %%a in ('pkg-config --libs protobuf') do ( set lib=!lib:~2! @rem remove spaces set lib=!lib: =! - @rem Because protobuf is specified as libprotobuf and elsewhere - if !lib! NEQ protobuf ( + set libprefix=!lib:~0,4! + if !libprefix!==absl ( set lib=!lib!.lib - if "!libs_list!"=="" ( - set libs_list=!lib! - ) else ( - set libs_list=!libs_list!,!lib! - ) + ) else ( + set lib=lib!lib!.lib + ) + if "!libs_list!"=="" ( + set libs_list=!lib! + ) else ( + set libs_list=!libs_list!,!lib! ) ) ) diff --git a/buildscripts/make_dependencies.bat b/buildscripts/make_dependencies.bat index adb7a1fbcbc..a167b8e59b2 100644 --- a/buildscripts/make_dependencies.bat +++ b/buildscripts/make_dependencies.bat @@ -1,7 +1,7 @@ choco install -y pkgconfiglite choco install -y openjdk --version=17.0 set PATH=%PATH%;"c:\Program Files\OpenJDK\jdk-17\bin" -set PROTOBUF_VER=26.1 +set PROTOBUF_VER=33.1 set ABSL_VERSION=20250127.1 set CMAKE_NAME=cmake-3.26.3-windows-x86_64 @@ -30,7 +30,6 @@ del protobuf.zip powershell -command "$ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'stop'; & { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; iwr https://github.com/abseil/abseil-cpp/archive/refs/tags/%ABSL_VERSION%.zip -OutFile absl.zip }" || exit /b 1 powershell -command "$ErrorActionPreference = 'stop'; & { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('absl.zip', '.') }" || exit /b 1 del absl.zip -rmdir protobuf-%PROTOBUF_VER%\third_party\abseil-cpp move abseil-cpp-%ABSL_VERSION% protobuf-%PROTOBUF_VER%\third_party\abseil-cpp mkdir protobuf-%PROTOBUF_VER%\build pushd protobuf-%PROTOBUF_VER%\build @@ -51,7 +50,7 @@ for /f "tokens=4 delims=\" %%a in ("%VCINSTALLDIR%") do ( for /f "tokens=1 delims=." %%a in ("%VisualStudioVersion%") do ( SET visual_studio_major_version=%%a ) -cmake -DABSL_MSVC_STATIC_RUNTIME=ON -Dprotobuf_BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX=%cd%\protobuf-%PROTOBUF_VER% -DCMAKE_PREFIX_PATH=%cd%\protobuf-%PROTOBUF_VER% -G "Visual Studio %visual_studio_major_version% %VC_YEAR%" %CMAKE_VSARCH% .. || exit /b 1 +cmake -DCMAKE_CXX_STANDARD=17 -DABSL_MSVC_STATIC_RUNTIME=ON -Dprotobuf_BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX=%cd%\protobuf-%PROTOBUF_VER% -DCMAKE_PREFIX_PATH=%cd%\protobuf-%PROTOBUF_VER% -G "Visual Studio %visual_studio_major_version% %VC_YEAR%" %CMAKE_VSARCH% .. || exit /b 1 cmake --build . --config Release --target install || exit /b 1 popd goto :eof diff --git a/buildscripts/make_dependencies.sh b/buildscripts/make_dependencies.sh index 7dc94299daf..9612199540b 100755 --- a/buildscripts/make_dependencies.sh +++ b/buildscripts/make_dependencies.sh @@ -3,54 +3,33 @@ # Build protoc set -evux -o pipefail -PROTOBUF_VERSION=26.1 +PROTOBUF_VERSION=33.1 ABSL_VERSION=20250127.1 -CMAKE_VERSION=3.26.3 # ARCH is x86_64 bit unless otherwise specified. ARCH="${ARCH:-x86_64}" DOWNLOAD_DIR=/tmp/source INSTALL_DIR="/tmp/protobuf-cache/$PROTOBUF_VERSION/$(uname -s)-$ARCH" BUILDSCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)" -mkdir -p $DOWNLOAD_DIR -cd "$DOWNLOAD_DIR" - -# Start with a sane default -NUM_CPU=4 -if [[ $(uname) == 'Linux' ]]; then - NUM_CPU=$(nproc) -fi -if [[ $(uname) == 'Darwin' ]]; then - NUM_CPU=$(sysctl -n hw.ncpu) -fi -# Make protoc -# Can't check for presence of directory as cache auto-creates it. -if [ -f ${INSTALL_DIR}/bin/protoc ]; then - echo "Not building protobuf. Already built" -# TODO(ejona): swap to `brew install --devel protobuf` once it is up-to-date -else - if [[ ! -d "protobuf-${PROTOBUF_VERSION}" ]]; then - curl -Ls "https://github.com/google/protobuf/releases/download/v${PROTOBUF_VERSION}/protobuf-${PROTOBUF_VERSION}.tar.gz" | tar xz - curl -Ls "https://github.com/abseil/abseil-cpp/archive/refs/tags/${ABSL_VERSION}.tar.gz" | tar xz - rmdir "protobuf-$PROTOBUF_VERSION/third_party/abseil-cpp" - mv "abseil-cpp-$ABSL_VERSION" "protobuf-$PROTOBUF_VERSION/third_party/abseil-cpp" +function build_and_install() { + if [[ "$1" == "abseil" ]]; then + TESTS_OFF_ARG=ABSL_BUILD_TEST_HELPERS + else + TESTS_OFF_ARG=protobuf_BUILD_TESTS fi - # the same source dir is used for 32 and 64 bit builds, so we need to clean stale data first - rm -rf "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" - mkdir "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" - pushd "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" - # install here so we don't need sudo if [[ "$(uname -s)" == "Darwin" ]]; then cmake .. \ - -DCMAKE_CXX_STANDARD=14 -Dprotobuf_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=OFF \ - -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" -DABSL_INTERNAL_AT_LEAST_CXX17=0 \ + -DCMAKE_CXX_STANDARD=17 -D${TESTS_OFF_ARG}=OFF -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" \ + -DCMAKE_PREFIX_PATH="$INSTALL_DIR" \ -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \ -B. || exit 1 elif [[ "$ARCH" == x86* ]]; then CFLAGS=-m${ARCH#*_} CXXFLAGS=-m${ARCH#*_} cmake .. \ - -DCMAKE_CXX_STANDARD=14 -Dprotobuf_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=OFF \ - -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" -DABSL_INTERNAL_AT_LEAST_CXX17=0 \ + -DCMAKE_CXX_STANDARD=17 -D${TESTS_OFF_ARG}=OFF -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" \ + -DCMAKE_PREFIX_PATH="$INSTALL_DIR" \ -B. || exit 1 else if [[ "$ARCH" == aarch_64 ]]; then @@ -66,17 +45,56 @@ else exit 1 fi cmake .. \ - -DCMAKE_CXX_STANDARD=14 -Dprotobuf_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=OFF \ - -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" -DABSL_INTERNAL_AT_LEAST_CXX17=0 \ + -DCMAKE_CXX_STANDARD=17 -D${TESTS_OFF_ARG}=OFF -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" \ + -DCMAKE_PREFIX_PATH="$INSTALL_DIR" \ -Dcrosscompile_ARCH="$GCC_ARCH" \ -DCMAKE_TOOLCHAIN_FILE=$BUILDSCRIPTS_DIR/toolchain.cmake \ -B. || exit 1 fi export CMAKE_BUILD_PARALLEL_LEVEL="$NUM_CPU" cmake --build . || exit 1 + # install here so we don't need sudo cmake --install . || exit 1 - [ -d "$INSTALL_DIR/lib64" ] && mv "$INSTALL_DIR/lib64" "$INSTALL_DIR/lib" +} + +mkdir -p $DOWNLOAD_DIR +cd "$DOWNLOAD_DIR" + +# Start with a sane default +NUM_CPU=4 +if [[ $(uname) == 'Linux' ]]; then + NUM_CPU=$(nproc) +fi +if [[ $(uname) == 'Darwin' ]]; then + NUM_CPU=$(sysctl -n hw.ncpu) +fi +export CMAKE_BUILD_PARALLEL_LEVEL="$NUM_CPU" + +# Make protoc +# Can't check for presence of directory as cache auto-creates it. +if [ -f ${INSTALL_DIR}/bin/protoc ]; then + echo "Not building protobuf. Already built" +# TODO(ejona): swap to `brew install --devel protobuf` once it is up-to-date +else + if [[ ! -d "protobuf-${PROTOBUF_VERSION}" ]]; then + curl -Ls "https://github.com/google/protobuf/releases/download/v${PROTOBUF_VERSION}/protobuf-${PROTOBUF_VERSION}.tar.gz" | tar xz + curl -Ls "https://github.com/abseil/abseil-cpp/archive/refs/tags/${ABSL_VERSION}.tar.gz" | tar xz + fi + # the same source dir is used for 32 and 64 bit builds, so we need to clean stale data first + rm -rf "$DOWNLOAD_DIR/abseil-cpp-${ABSL_VERSION}/build" + mkdir "$DOWNLOAD_DIR/abseil-cpp-${ABSL_VERSION}/build" + pushd "$DOWNLOAD_DIR/abseil-cpp-${ABSL_VERSION}/build" + build_and_install "abseil" + popd + + rm -rf "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" + mkdir "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" + pushd "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" + build_and_install "protobuf" popd + + [ -d "$INSTALL_DIR/lib64" ] && mv "$INSTALL_DIR/lib64" "$INSTALL_DIR/lib" fi # If /tmp/protobuf exists then we just assume it's a symlink created by us. @@ -94,3 +112,4 @@ export CXXFLAGS="$(PKG_CONFIG_PATH=/tmp/protobuf/lib/pkgconfig pkg-config --cfla export LIBRARY_PATH=/tmp/protobuf/lib export LD_LIBRARY_PATH=/tmp/protobuf/lib EOF + diff --git a/compiler/build.gradle b/compiler/build.gradle index 9b02f8286a1..f970f629e19 100644 --- a/compiler/build.gradle +++ b/compiler/build.gradle @@ -102,7 +102,7 @@ model { all { if (toolChain in Gcc || toolChain in Clang) { cppCompiler.define("GRPC_VERSION", version) - cppCompiler.args "--std=c++14" + cppCompiler.args "--std=c++17" addEnvArgs("CXXFLAGS", cppCompiler.args) addEnvArgs("CPPFLAGS", cppCompiler.args) if (project.hasProperty('buildUniversal') && @@ -132,7 +132,7 @@ model { } else if (toolChain in VisualCpp) { usingVisualCpp = true cppCompiler.define("GRPC_VERSION", version) - cppCompiler.args "/EHsc", "/MT" + cppCompiler.args "/EHsc", "/MT", "/std:c++17" if (rootProject.hasProperty('vcProtobufInclude')) { cppCompiler.args "/I${rootProject.vcProtobufInclude}" } From eb8a63cefb827337cc9fd4c5a3877d96a238c1d6 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Fri, 5 Dec 2025 14:43:15 -0800 Subject: [PATCH 480/591] Introduce io.grpc.Uri. (#12535) `io.grpc.Uri` is an implementation of RFC 3986 tailored for grpc-java's needs. It lifts some of the limitations of `java.net.URI` that currently prevent us from resolving target URIs like `intent:#Intent;...` See #12244 for more. Marked `@Internal` for now but the plan is to eventually use this to replace `java.net.URI` in our public APIs such as NameResolver.Factory. --- api/build.gradle | 1 + api/src/main/java/io/grpc/Uri.java | 1021 ++++++++++++++++++++++++ api/src/test/java/io/grpc/UriTest.java | 601 ++++++++++++++ 3 files changed, 1623 insertions(+) create mode 100644 api/src/main/java/io/grpc/Uri.java create mode 100644 api/src/test/java/io/grpc/UriTest.java diff --git a/api/build.gradle b/api/build.gradle index 415a17f61f8..745fa00b3f1 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -73,6 +73,7 @@ tasks.named("javadoc").configure { exclude 'io/grpc/Internal?*.java' exclude 'io/grpc/MetricRecorder.java' exclude 'io/grpc/MetricSink.java' + exclude 'io/grpc/Uri.java' } tasks.named("sourcesJar").configure { diff --git a/api/src/main/java/io/grpc/Uri.java b/api/src/main/java/io/grpc/Uri.java new file mode 100644 index 00000000000..01aaed31c74 --- /dev/null +++ b/api/src/main/java/io/grpc/Uri.java @@ -0,0 +1,1021 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.base.VerifyException; +import com.google.common.collect.ImmutableList; +import com.google.common.net.InetAddresses; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.net.InetAddress; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.util.BitSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import javax.annotation.Nullable; + +/** + * A not-quite-general-purpose representation of a Uniform Resource Identifier (URI), as defined by + * RFC 3986. + * + *

    The URI

    + * + *

    A URI identifies a resource by its name or location or both. The resource could be a file, + * service, or some other abstract entity. + * + *

    Examples

    + * + *
      + *
    • http://admin@example.com:8080/controlpanel?filter=users#settings + *
    • ftp://[2001:db8::7]/docs/report.pdf + *
    • file:///My%20Computer/Documents/letter.doc + *
    • dns://8.8.8.8/storage.googleapis.com + *
    • mailto:John.Doe@example.com + *
    • tel:+1-206-555-1212 + *
    • urn:isbn:978-1492082798 + *
    + * + *

    Limitations

    + * + *

    This class aims to meet the needs of grpc-java itself and RPC related code that depend on it. + * It isn't quite general-purpose. It definitely would not be suitable for building an HTTP user + * agent or proxy server. In particular, it: + * + *

      + *
    • Can only represent a URI, not a "URI-reference" or "relative reference". In other words, a + * "scheme" is always required. + *
    • Has no knowledge of the particulars of any scheme, with respect to normalization and + * comparison. We don't know https://google.com is the same as + * https://google.com:443, that file:/// is the same as + * file://localhost, or that joe@example.com is the same as + * joe@EXAMPLE.COM. No one class can or should know everything about every scheme so + * all this is better handled at a higher layer. + *
    • Implements {@link #equals(Object)} as a char-by-char comparison. Expect false negatives. + *
    • Does not support "IPvFuture" literal addresses. + *
    • Does not reflect how web browsers parse user input or the URL Living Standard. + *
    • Does not support different character encodings. Assumes UTF-8 in several places. + *
    + * + *

    Migrating from RFC 2396 and {@link java.net.URI}

    + * + *

    Those migrating from {@link java.net.URI} and/or its primary specification in RFC 2396 should + * note some differences. + * + *

    Uniform Hierarchical Syntax

    + * + *

    RFC 3986 unifies the older ideas of "hierarchical" and "opaque" URIs into a single generic + * syntax. What RFC 2396 called an opaque "scheme-specific part" is always broken out by RFC 3986 + * into an authority and path hierarchy, followed by query and fragment components. Accordingly, + * this class has only getters for those components but no {@link + * java.net.URI#getSchemeSpecificPart()} analog. + * + *

    The RFC 3986 definition of path is now more liberal to accommodate this: + * + *

      + *
    • Path doesn't have to start with a slash. For example, the path of + * urn:isbn:978-1492082798 is isbn:978-1492082798 even though it doesn't + * look much like a file system path. + *
    • The path can now be empty. So Android's + * intent:#Intent;action=MAIN;category=LAUNCHER;end is now a valid {@link Uri}. Even + * the scheme-only about: is now valid. + *
    + * + *

    The uniform syntax always understands what follows a '?' to be a query string. For example, + * mailto:me@example.com?subject=foo now has a query component whereas RFC 2396 + * considered everything after the mailto: scheme to be opaque. + * + *

    Same goes for fragment. data:image/png;...#xywh=0,0,10,10 now has a fragment + * whereas RFC 2396 considered everything after the scheme to be opaque. + * + *

    Uniform Authority Syntax

    + * + *

    RFC 2396 tried to guess if an authority was a "server" (host:port) or "registry-based" + * (arbitrary string) based on its contents. RFC 3986 expects every authority to look like + * [userinfo@]host[:port] and loosens the definition of a "host" to accommodate. Accordingly, this + * class has no equivalent to {@link java.net.URI#parseServerAuthority()} -- authority was parsed + * into its components and checked for validity when the {@link Uri} was created. + * + *

    Other Specific Differences

    + * + *

    RFC 2396 does not allow underscores in a host name, meaning {@link java.net.URI} switches to + * opaque mode when it sees one. {@link Uri} does allow underscores in host, to accommodate + * registries other than DNS. So http://my_site.com:8080/index.html now parses as a + * host, port and path rather than a single opaque scheme-specific part. + * + *

    {@link Uri} strictly *requires* square brackets in the query string and fragment to be + * percent-encoded whereas RFC 2396 merely recommended doing so. + * + *

    Other URx classes are "liberal in what they accept and strict in what they produce." {@link + * Uri#parse(String)} and {@link Uri#create(String)}, however, are strict in what they accept and + * transparent when asked to reproduce it via {@link Uri#toString()}. The former policy may be + * appropriate for parsing user input or web content, but this class is meant for gRPC clients, + * servers and plugins like name resolvers where human error at runtime is less likely and best + * detected early. {@link java.net.URI#create(String)} is similarly strict, which makes migration + * easy, except for the server/registry-based ambiguity addressed by {@link + * java.net.URI#parseServerAuthority()}. + * + *

    {@link java.net.URI} and {@link Uri} both support IPv6 literals in square brackets as defined + * by RFC 2732. + */ +@Internal +public final class Uri { + // Components are stored percent-encoded, just as originally parsed for transparent parse/toString + // round-tripping. + private final String scheme; // != null since we don't support relative references. + @Nullable private final String userInfo; + @Nullable private final String host; + @Nullable private final String port; + private final String path; // In RFC 3986, path is always defined (but can be empty). + @Nullable private final String query; + @Nullable private final String fragment; + + private Uri(Builder builder) { + this.scheme = checkNotNull(builder.scheme, "scheme"); + this.userInfo = builder.userInfo; + this.host = builder.host; + this.port = builder.port; + this.path = builder.path; + this.query = builder.query; + this.fragment = builder.fragment; + + // Checks common to the parse() and Builder code paths. + if (hasAuthority()) { + if (!path.isEmpty() && !path.startsWith("/")) { + throw new IllegalArgumentException("Has authority -- Non-empty path must start with '/'"); + } + } else { + if (path.startsWith("//")) { + throw new IllegalArgumentException("No authority -- Path cannot start with '//'"); + } + } + } + + /** + * Parses a URI from its string form. + * + * @throws URISyntaxException if 's' is not a valid RFC 3986 URI. + */ + public static Uri parse(String s) throws URISyntaxException { + try { + return create(s); + } catch (IllegalArgumentException e) { + throw new URISyntaxException(s, e.getMessage()); + } + } + + /** + * Creates a URI from a string assumed to be valid. + * + *

    Useful for defining URI constants in code. Not for user input. + * + * @throws IllegalArgumentException if 's' is not a valid RFC 3986 URI. + */ + public static Uri create(String s) { + Builder builder = new Builder(); + int i = 0; + final int n = s.length(); + + // 3.1. Scheme: Look for a ':' before '/', '?', or '#'. + int schemeColon = -1; + for (; i < n; ++i) { + char c = s.charAt(i); + if (c == ':') { + schemeColon = i; + break; + } else if (c == '/' || c == '?' || c == '#') { + break; + } + } + if (schemeColon < 0) { + throw new IllegalArgumentException("Missing required scheme."); + } + builder.setRawScheme(s.substring(0, schemeColon)); + + // 3.2. Authority. Look for '//' then keep scanning until '/', '?', or '#'. + i = schemeColon + 1; + if (i + 1 < n && s.charAt(i) == '/' && s.charAt(i + 1) == '/') { + // "//" just means we have an authority. Skip over it. + i += 2; + + int authorityStart = i; + for (; i < n; ++i) { + char c = s.charAt(i); + if (c == '/' || c == '?' || c == '#') { + break; + } + } + String authority = s.substring(authorityStart, i); + + // 3.2.1. UserInfo. Easy, because '@' cannot appear unencoded inside userinfo or host. + int userInfoEnd = authority.indexOf('@'); + if (userInfoEnd >= 0) { + builder.setRawUserInfo(authority.substring(0, userInfoEnd)); + } + + // 3.2.2/3. Host/Port. + int hostStart = userInfoEnd >= 0 ? userInfoEnd + 1 : 0; + int portStartColon = findPortStartColon(authority, hostStart); + if (portStartColon < 0) { + builder.setRawHost(authority.substring(hostStart, authority.length())); + } else { + builder.setRawHost(authority.substring(hostStart, portStartColon)); + builder.setRawPort(authority.substring(portStartColon + 1)); + } + } + + // 3.3. Path: Whatever is left before '?' or '#'. + int pathStart = i; + for (; i < n; ++i) { + char c = s.charAt(i); + if (c == '?' || c == '#') { + break; + } + } + builder.setRawPath(s.substring(pathStart, i)); + + // 3.4. Query, if we stopped at '?'. + if (i < n && s.charAt(i) == '?') { + i++; // Skip '?' + int queryStart = i; + for (; i < n; ++i) { + char c = s.charAt(i); + if (c == '#') { + break; + } + } + builder.setRawQuery(s.substring(queryStart, i)); + } + + // 3.5. Fragment, if we stopped at '#'. + if (i < n && s.charAt(i) == '#') { + ++i; // Skip '#' + builder.setRawFragment(s.substring(i)); + } + + return builder.build(); + } + + private static int findPortStartColon(String authority, int hostStart) { + for (int i = authority.length() - 1; i >= hostStart; --i) { + char c = authority.charAt(i); + if (c == ':') { + return i; + } + if (c == ']') { + // Hit the end of IP-literal. Any further colon is inside it and couldn't indicate a port. + break; + } + if (!digitChars.get(c)) { + // Found a non-digit, non-colon, non-bracket. + // This means there is no valid port (e.g. host is "example.com") + break; + } + } + return -1; + } + + // Checks a raw path for validity and parses it into segments. Let 'out' be null to just validate. + private static void parseAssumedUtf8PathIntoSegments( + String path, ImmutableList.Builder out) { + // Skip the first slash so it doesn't count as an empty segment at the start. + // (e.g., "/a" -> ["a"], not ["", "a"]) + int start = path.startsWith("/") ? 1 : 0; + + for (int i = start; i < path.length(); ) { + int nextSlash = path.indexOf('/', i); + String segment; + if (nextSlash >= 0) { + // Typical segment case (e.g., "foo" in "/foo/bar"). + segment = path.substring(i, nextSlash); + i = nextSlash + 1; + } else { + // Final segment case (e.g., "bar" in "/foo/bar"). + segment = path.substring(i); + i = path.length(); + } + if (out != null) { + out.add(percentDecodeAssumedUtf8(segment)); + } else { + checkPercentEncodedArg(segment, "path segment", pChars); + } + } + + // RFC 3986 says a trailing slash creates a final empty segment. + // (e.g., "/foo/" -> ["foo", ""]) + if (path.endsWith("/") && out != null) { + out.add(""); + } + } + + /** Returns the scheme of this URI. */ + public String getScheme() { + return scheme; + } + + /** + * Returns the percent-decoded "Authority" component of this URI, or null if not present. + * + *

    NB: This method assumes the "host" component was encoded as UTF-8, as mandated by RFC 3986. + * This method also assumes the "user information" part of authority was encoded as UTF-8, + * although RFC 3986 doesn't specify an encoding. + * + *

    Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in + * the output. Callers who want to detect and handle errors in some other way should call {@link + * #getRawAuthority()}, {@link #percentDecode(CharSequence)}, then decode the bytes for + * themselves. + */ + @Nullable + public String getAuthority() { + return percentDecodeAssumedUtf8(getRawAuthority()); + } + + private boolean hasAuthority() { + return host != null; + } + + /** + * Returns the "authority" component of this URI in its originally parsed, possibly + * percent-encoded form. + */ + @Nullable + public String getRawAuthority() { + if (hasAuthority()) { + StringBuilder sb = new StringBuilder(); + appendAuthority(sb); + return sb.toString(); + } + return null; + } + + private void appendAuthority(StringBuilder sb) { + if (userInfo != null) { + sb.append(userInfo).append('@'); + } + if (host != null) { + sb.append(host); + } + if (port != null) { + sb.append(':').append(port); + } + } + + /** + * Returns the percent-decoded "User Information" component of this URI, or null if not present. + * + *

    NB: This method *assumes* this component was encoded as UTF-8, although RFC 3986 doesn't + * specify an encoding. + * + *

    Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in + * the output. Callers who want to detect and handle errors in some other way should call {@link + * #getRawUserInfo()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves. + */ + @Nullable + public String getUserInfo() { + return percentDecodeAssumedUtf8(userInfo); + } + + /** + * Returns the "User Information" component of this URI in its originally parsed, possibly + * percent-encoded form. + */ + @Nullable + public String getRawUserInfo() { + return userInfo; + } + + /** + * Returns the percent-decoded "host" component of this URI, or null if not present. + * + *

    This method assumes the host was encoded as UTF-8, as mandated by RFC 3986. + * + *

    Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in + * the output. Callers who want to detect and handle errors in some other way should call {@link + * #getRawHost()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves. + */ + @Nullable + public String getHost() { + return percentDecodeAssumedUtf8(host); + } + + /** + * Returns the host component of this URI in its originally parsed, possibly percent-encoded form. + */ + @Nullable + public String getRawHost() { + return host; + } + + /** Returns the "port" component of this URI, or -1 if not present. */ + public int getPort() { + return port != null ? Integer.parseInt(port) : -1; + } + + /** Returns the raw port component of this URI in its originally parsed form. */ + @Nullable + public String getRawPort() { + return port; + } + + /** + * Returns the (possibly empty) percent-decoded "path" component of this URI. + * + *

    NB: This method *assumes* the path was encoded as UTF-8, although RFC 3986 doesn't specify + * an encoding. + * + *

    Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in + * the output. Callers who want to detect and handle errors in some other way should call {@link + * #getRawPath()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves. + * + *

    NB: Prefer {@link #getPathSegments()} because this method's decoding is lossy. For example, + * consider these (different) URIs: + * + *

      + *
    • file:///home%2Ffolder/my%20file + *
    • file:///home/folder/my%20file + *
    + * + *

    Calling getPath() on each returns the same string: /home/folder/my file. You + * can't tell whether the second '/' character is part of the first path segment or separates the + * first and second path segments. This method only exists to ease migration from {@link + * java.net.URI}. + */ + public String getPath() { + return percentDecodeAssumedUtf8(path); + } + + /** + * Returns this URI's path as a list of path segments not including the '/' segment delimiters. + * + *

    Prefer this method over {@link #getPath()} because it preserves the distinction between + * segment separators and literal '/'s within a path segment. + * + *

    The returned list is immutable. + */ + public List getPathSegments() { + // Returned list must be immutable but we intentionally keep guava out of the public API. + ImmutableList.Builder segmentsBuilder = ImmutableList.builder(); + parseAssumedUtf8PathIntoSegments(path, segmentsBuilder); + return segmentsBuilder.build(); + } + + /** + * Returns the path component of this URI in its originally parsed, possibly percent-encoded form. + */ + public String getRawPath() { + return path; + } + + /** + * Returns the percent-decoded "query" component of this URI, or null if not present. + * + *

    NB: This method assumes the query was encoded as UTF-8, although RFC 3986 doesn't specify an + * encoding. + * + *

    Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in + * the output. Callers who want to detect and handle errors in some other way should call {@link + * #getRawQuery()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves. + */ + @Nullable + public String getQuery() { + return percentDecodeAssumedUtf8(query); + } + + /** + * Returns the query component of this URI in its originally parsed, possibly percent-encoded + * form, without any leading '?' character. + */ + @Nullable + public String getRawQuery() { + return query; + } + + /** + * Returns the percent-decoded "fragment" component of this URI, or null if not present. + * + *

    NB: This method assumes the fragment was encoded as UTF-8, although RFC 3986 doesn't specify + * an encoding. + * + *

    Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in + * the output. Callers who want to detect and handle errors in some other way should call {@link + * #getRawFragment()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves. + */ + @Nullable + public String getFragment() { + return percentDecodeAssumedUtf8(fragment); + } + + /** + * Returns the fragment component of this URI in its original, possibly percent-encoded form, and + * without any leading '#' character. + */ + @Nullable + public String getRawFragment() { + return fragment; + } + + /** + * {@inheritDoc} + * + *

    If this URI was created by {@link #parse(String)} or {@link #create(String)}, then the + * returned string will match that original input exactly. + */ + @Override + public String toString() { + // https://datatracker.ietf.org/doc/html/rfc3986#section-5.3 + StringBuilder sb = new StringBuilder(); + sb.append(scheme).append(':'); + if (hasAuthority()) { + sb.append("//"); + appendAuthority(sb); + } + sb.append(path); + if (query != null) { + sb.append('?').append(query); + } + if (fragment != null) { + sb.append('#').append(fragment); + } + return sb.toString(); + } + + /** + * Returns true iff this URI has a scheme and an authority/path hierarchy, but no fragment. + * + *

    All instances of {@link Uri} are RFC 3986 URIs, not "relative references", so this method is + * equivalent to {@code getFragment() == null}. It mostly exists for compatibility with {@link + * java.net.URI}. + */ + public boolean isAbsolute() { + return scheme != null && fragment == null; + } + + /** + * {@inheritDoc} + * + *

    Two instances of {@link Uri} are equal if and only if they have the same string + * representation, which RFC 3986 calls "Simple String Comparison" (6.2.1). Callers with a higher + * layer expectation of equality (e.g. http://some%2Dhost:80/foo/./bar.txt ~= + * http://some-host/foo/bar.txt) will experience false negatives. + */ + @Override + public boolean equals(Object otherObj) { + if (!(otherObj instanceof Uri)) { + return false; + } + Uri other = (Uri) otherObj; + return Objects.equals(scheme, other.scheme) + && Objects.equals(userInfo, other.userInfo) + && Objects.equals(host, other.host) + && Objects.equals(port, other.port) + && Objects.equals(path, other.path) + && Objects.equals(query, other.query) + && Objects.equals(fragment, other.fragment); + } + + @Override + public int hashCode() { + return Objects.hash(scheme, userInfo, host, port, path, query, fragment); + } + + /** Returns a new Builder initialized with the fields of this URI. */ + public Builder toBuilder() { + return new Builder(this); + } + + /** Creates a new {@link Builder} with all fields uninitialized or set to their default values. */ + public static Builder newBuilder() { + return new Builder(); + } + + /** Builder for {@link Uri}. */ + public static final class Builder { + private String scheme; + private String path = ""; + private String query; + private String fragment; + private String userInfo; + private String host; + private String port; + + private Builder() {} + + Builder(Uri prototype) { + this.scheme = prototype.scheme; + this.userInfo = prototype.userInfo; + this.host = prototype.host; + this.port = prototype.port; + this.path = prototype.path; + this.query = prototype.query; + this.fragment = prototype.fragment; + } + + /** + * Sets the scheme, e.g. "https", "dns" or "xds". + * + *

    This field is required. + * + * @return this, for fluent building + * @throws IllegalArgumentException if the scheme is invalid. + */ + @CanIgnoreReturnValue + public Builder setScheme(String scheme) { + return setRawScheme(scheme.toLowerCase(Locale.ROOT)); + } + + @CanIgnoreReturnValue + Builder setRawScheme(String scheme) { + if (scheme.isEmpty() || !alphaChars.get(scheme.charAt(0))) { + throw new IllegalArgumentException("Scheme must start with an alphabetic char"); + } + for (int i = 0; i < scheme.length(); i++) { + char c = scheme.charAt(i); + if (!schemeChars.get(c)) { + throw new IllegalArgumentException("Invalid character in scheme at index " + i); + } + } + this.scheme = scheme; + return this; + } + + /** + * Specifies the new URI's path component as a string of zero or more '/' delimited segments. + * + *

    Path segments can consist of any string of codepoints. Codepoints that can't be encoded + * literally will be percent-encoded for you. + * + *

    If a URI contains an authority component, then the path component must either be empty or + * begin with a slash ("/") character. If a URI does not contain an authority component, then + * the path cannot begin with two slash characters ("//"). + * + *

    This method interprets all '/' characters in 'path' as segment delimiters. If any of your + * segments contain literal '/' characters, call {@link #setRawPath(String)} instead. + * + *

    See RFC 3986 3.3 + * for more. + * + *

    This field is required but can be empty (its default value). + * + * @param path the new path + * @return this, for fluent building + */ + @CanIgnoreReturnValue + public Builder setPath(String path) { + checkArgument(path != null, "Path can be empty but not null"); + this.path = percentEncode(path, pCharsAndSlash); + return this; + } + + /** + * Specifies the new URI's path component as a string of zero or more '/' delimited segments. + * + *

    Path segments can consist of any string of codepoints but the caller must first percent- + * encode anything other than RFC 3986's "pchar" character class using UTF-8. + * + *

    If a URI contains an authority component, then the path component must either be empty or + * begin with a slash ("/") character. If a URI does not contain an authority component, then + * the path cannot begin with two slash characters ("//"). + * + *

    This method interprets all '/' characters in 'path' as segment delimiters. If any of your + * segments contain literal '/' characters, you must percent-encode them. + * + *

    See RFC 3986 3.3 + * for more. + * + *

    This field is required but can be empty (its default value). + * + * @param path the new path, a string consisting of characters from "pchar" + * @return this, for fluent building + */ + @CanIgnoreReturnValue + public Builder setRawPath(String path) { + checkArgument(path != null, "Path can be empty but not null"); + parseAssumedUtf8PathIntoSegments(path, null); + this.path = path; + return this; + } + + /** + * Specifies the query component of the new URI (not including the leading '?'). + * + *

    Query can contain any string of codepoints. Codepoints that can't be encoded literally + * will be percent-encoded for you as UTF-8. + * + *

    This field is optional. + * + * @param query the new query component, or null to clear this field + * @return this, for fluent building + */ + @CanIgnoreReturnValue + public Builder setQuery(@Nullable String query) { + this.query = percentEncode(query, queryChars); + return this; + } + + @CanIgnoreReturnValue + Builder setRawQuery(String query) { + checkPercentEncodedArg(query, "query", queryChars); + this.query = query; + return this; + } + + /** + * Specifies the fragment component of the new URI (not including the leading '#'). + * + *

    The fragment can contain any string of codepoints. Codepoints that can't be encoded + * literally will be percent-encoded for you as UTF-8. + * + *

    This field is optional. + * + * @param fragment the new fragment component, or null to clear this field + * @return this, for fluent building + */ + @CanIgnoreReturnValue + public Builder setFragment(@Nullable String fragment) { + this.fragment = percentEncode(fragment, fragmentChars); + return this; + } + + @CanIgnoreReturnValue + Builder setRawFragment(String fragment) { + checkPercentEncodedArg(fragment, "fragment", fragmentChars); + this.fragment = fragment; + return this; + } + + /** + * Set the "user info" component of the new URI, e.g. "username:password", not including the + * trailing '@' character. + * + *

    User info can contain any string of codepoints. Codepoints that can't be encoded literally + * will be percent-encoded for you as UTF-8. + * + *

    This field is optional. + * + * @param userInfo the new "user info" component, or null to clear this field + * @return this, for fluent building + */ + @CanIgnoreReturnValue + public Builder setUserInfo(@Nullable String userInfo) { + this.userInfo = percentEncode(userInfo, userInfoChars); + return this; + } + + @CanIgnoreReturnValue + Builder setRawUserInfo(String userInfo) { + checkPercentEncodedArg(userInfo, "userInfo", userInfoChars); + this.userInfo = userInfo; + return this; + } + + /** + * Specifies the "host" component of the new URI in its "registered name" form (usually DNS), + * e.g. "server.com". + * + *

    The registered name can contain any string of codepoints. Codepoints that can't be encoded + * literally will be percent-encoded for you as UTF-8. + * + *

    This field is optional. + * + * @param regName the new host component in "registered name" form, or null to clear this field + * @return this, for fluent building + */ + @CanIgnoreReturnValue + public Builder setHost(@Nullable String regName) { + if (regName != null) { + regName = regName.toLowerCase(Locale.ROOT); + regName = percentEncode(regName, regNameChars); + } + this.host = regName; + return this; + } + + /** + * Specifies the "host" component of the new URI as an IP address. + * + *

    This field is optional. + * + * @param addr the new "host" component in InetAddress form, or null to clear this field + * @return this, for fluent building + */ + @CanIgnoreReturnValue + public Builder setHost(@Nullable InetAddress addr) { + this.host = addr != null ? InetAddresses.toUriString(addr) : null; + return this; + } + + @CanIgnoreReturnValue + Builder setRawHost(String host) { + // IP-literal validation is complicated so we delegate it to Guava. We use this particular + // method of InetAddresses because it doesn't try to match interfaces on the local machine. + // (The validity of a URI should be the same no matter which machine does the parsing.) + // TODO(jdcormie): IPFuture + if (!InetAddresses.isUriInetAddress(host)) { + // Must be a "registered name". + checkPercentEncodedArg(host, "host", regNameChars); + } + this.host = host; + return this; + } + + /** + * Specifies the "port" component of the new URI, e.g. "8080". + * + *

    The port can be any non-negative integer. A negative value represents "no port". + * + *

    This field is optional. + * + * @param port the new "port" component, or -1 to clear this field + * @return this, for fluent building + */ + @CanIgnoreReturnValue + public Builder setPort(int port) { + this.port = port < 0 ? null : Integer.toString(port); + return this; + } + + @CanIgnoreReturnValue + Builder setRawPort(String port) { + try { + Integer.parseInt(port); // Result unused. + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid port", e); + } + this.port = port; + return this; + } + + /** Builds a new instance of {@link Uri} as specified by the setters. */ + public Uri build() { + checkState(scheme != null, "Missing required scheme."); + if (host == null) { + checkState(port == null, "Cannot set port without host."); + checkState(userInfo == null, "Cannot set userInfo without host."); + } + return new Uri(this); + } + } + + /** + * Decodes a string of characters in the range [U+0000, U+007F] to bytes. + * + *

    Each percent-encoded sequence (e.g. "%F0" or "%2a", as defined by RFC 3986 2.1) is decoded + * to the octet it encodes. Other characters are decoded to their code point's single byte value. + * A literal % character must be encoded as %25. + * + * @throws IllegalArgumentException if 's' contains characters out of range or invalid percent + * encoding sequences. + */ + public static ByteBuffer percentDecode(CharSequence s) { + // This is large enough because each input character needs *at most* one byte of output. + ByteBuffer outBuf = ByteBuffer.allocate(s.length()); + percentDecode(s, "input", null, outBuf); + outBuf.flip(); + return outBuf; + } + + private static void percentDecode( + CharSequence s, String what, BitSet allowedChars, ByteBuffer outBuf) { + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '%') { + if (i + 2 >= s.length()) { + throw new IllegalArgumentException( + "Invalid percent-encoding at index " + i + " of " + what + ": " + s); + } + int h1 = Character.digit(s.charAt(i + 1), 16); + int h2 = Character.digit(s.charAt(i + 2), 16); + if (h1 == -1 || h2 == -1) { + throw new IllegalArgumentException( + "Invalid hex digit in " + what + " at index " + i + " of: " + s); + } + if (outBuf != null) { + outBuf.put((byte) (h1 << 4 | h2)); + } + i += 2; + } else if (allowedChars == null || allowedChars.get(c)) { + if (outBuf != null) { + outBuf.put((byte) c); + } + } else { + throw new IllegalArgumentException("Invalid character in " + what + " at index " + i); + } + } + } + + @Nullable + private static String percentDecodeAssumedUtf8(@Nullable String s) { + if (s == null || s.indexOf('%') == -1) { + return s; + } + + ByteBuffer utf8Bytes = percentDecode(s); + try { + return StandardCharsets.UTF_8 + .newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .decode(utf8Bytes) + .toString(); + } catch (CharacterCodingException e) { + throw new VerifyException(e); // Should not happen in REPLACE mode. + } + } + + @Nullable + private static String percentEncode(String s, BitSet allowedCodePoints) { + if (s == null) { + return null; + } + CharsetEncoder encoder = + StandardCharsets.UTF_8 + .newEncoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + ByteBuffer utf8Bytes; + try { + utf8Bytes = encoder.encode(CharBuffer.wrap(s)); + } catch (MalformedInputException e) { + throw new IllegalArgumentException("Malformed input", e); // Must be a broken surrogate pair. + } catch (CharacterCodingException e) { + throw new VerifyException(e); // Should not happen when encoding to UTF-8. + } + + StringBuilder sb = new StringBuilder(); + while (utf8Bytes.hasRemaining()) { + int b = 0xff & utf8Bytes.get(); + if (allowedCodePoints.get(b)) { + sb.append((char) b); + } else { + sb.append('%'); + sb.append(hexDigitsByVal[(b & 0xF0) >> 4]); + sb.append(hexDigitsByVal[b & 0x0F]); + } + } + return sb.toString(); + } + + private static void checkPercentEncodedArg(String s, String what, BitSet allowedChars) { + percentDecode(s, what, allowedChars, null); + } + + // See UriTest for how these were computed from the ABNF constants in RFC 3986. + static final BitSet digitChars = BitSet.valueOf(new long[] {0x3ff000000000000L}); + static final BitSet alphaChars = BitSet.valueOf(new long[] {0L, 0x7fffffe07fffffeL}); + // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + static final BitSet schemeChars = + BitSet.valueOf(new long[] {0x3ff680000000000L, 0x7fffffe07fffffeL}); + // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + static final BitSet unreservedChars = + BitSet.valueOf(new long[] {0x3ff600000000000L, 0x47fffffe87fffffeL}); + // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" + static final BitSet genDelimsChars = + BitSet.valueOf(new long[] {0x8400800800000000L, 0x28000001L}); + // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + static final BitSet subDelimsChars = BitSet.valueOf(new long[] {0x28001fd200000000L}); + // reserved = gen-delims / sub-delims + static final BitSet reservedChars = BitSet.valueOf(new long[] {0xac009fda00000000L, 0x28000001L}); + // reg-name = *( unreserved / pct-encoded / sub-delims ) + static final BitSet regNameChars = + BitSet.valueOf(new long[] {0x2bff7fd200000000L, 0x47fffffe87fffffeL}); + // userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) + static final BitSet userInfoChars = + BitSet.valueOf(new long[] {0x2fff7fd200000000L, 0x47fffffe87fffffeL}); + // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + static final BitSet pChars = + BitSet.valueOf(new long[] {0x2fff7fd200000000L, 0x47fffffe87ffffffL}); + static final BitSet pCharsAndSlash = + BitSet.valueOf(new long[] {0x2fffffd200000000L, 0x47fffffe87ffffffL}); + // query = *( pchar / "/" / "?" ) + static final BitSet queryChars = + BitSet.valueOf(new long[] {0xafffffd200000000L, 0x47fffffe87ffffffL}); + // fragment = *( pchar / "/" / "?" ) + static final BitSet fragmentChars = queryChars; + + private static final char[] hexDigitsByVal = "0123456789ABCDEF".toCharArray(); +} diff --git a/api/src/test/java/io/grpc/UriTest.java b/api/src/test/java/io/grpc/UriTest.java new file mode 100644 index 00000000000..df921e3e487 --- /dev/null +++ b/api/src/test/java/io/grpc/UriTest.java @@ -0,0 +1,601 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.net.InetAddresses; +import com.google.common.testing.EqualsTester; +import java.net.URISyntaxException; +import java.util.BitSet; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class UriTest { + + @Test + public void parse_allComponents() throws URISyntaxException { + Uri uri = Uri.parse("scheme://user@host:0443/path?query#fragment"); + assertThat(uri.getScheme()).isEqualTo("scheme"); + assertThat(uri.getAuthority()).isEqualTo("user@host:0443"); + assertThat(uri.getUserInfo()).isEqualTo("user"); + assertThat(uri.getPort()).isEqualTo(443); + assertThat(uri.getRawPort()).isEqualTo("0443"); + assertThat(uri.getPath()).isEqualTo("/path"); + assertThat(uri.getQuery()).isEqualTo("query"); + assertThat(uri.getFragment()).isEqualTo("fragment"); + assertThat(uri.toString()).isEqualTo("scheme://user@host:0443/path?query#fragment"); + assertThat(uri.isAbsolute()).isFalse(); // Has a fragment. + } + + @Test + public void parse_noAuthority() throws URISyntaxException { + Uri uri = Uri.parse("scheme:/path?query#fragment"); + assertThat(uri.getScheme()).isEqualTo("scheme"); + assertThat(uri.getAuthority()).isNull(); + assertThat(uri.getPath()).isEqualTo("/path"); + assertThat(uri.getQuery()).isEqualTo("query"); + assertThat(uri.getFragment()).isEqualTo("fragment"); + assertThat(uri.toString()).isEqualTo("scheme:/path?query#fragment"); + assertThat(uri.isAbsolute()).isFalse(); // Has a fragment. + } + + @Test + public void parse_ipv6Literal_withPort() throws URISyntaxException { + Uri uri = Uri.parse("scheme://[2001:db8::7]:012345"); + assertThat(uri.getAuthority()).isEqualTo("[2001:db8::7]:012345"); + assertThat(uri.getRawHost()).isEqualTo("[2001:db8::7]"); + assertThat(uri.getHost()).isEqualTo("[2001:db8::7]"); + assertThat(uri.getRawPort()).isEqualTo("012345"); + assertThat(uri.getPort()).isEqualTo(12345); + } + + @Test + public void parse_ipv6Literal_noPort() throws URISyntaxException { + Uri uri = Uri.parse("scheme://[2001:db8::7]"); + assertThat(uri.getAuthority()).isEqualTo("[2001:db8::7]"); + assertThat(uri.getRawHost()).isEqualTo("[2001:db8::7]"); + assertThat(uri.getHost()).isEqualTo("[2001:db8::7]"); + assertThat(uri.getRawPort()).isNull(); + assertThat(uri.getPort()).isLessThan(0); + } + + @Test + public void parse_noQuery() throws URISyntaxException { + Uri uri = Uri.parse("scheme://authority/path#fragment"); + assertThat(uri.getScheme()).isEqualTo("scheme"); + assertThat(uri.getAuthority()).isEqualTo("authority"); + assertThat(uri.getPath()).isEqualTo("/path"); + assertThat(uri.getQuery()).isNull(); + assertThat(uri.getFragment()).isEqualTo("fragment"); + assertThat(uri.toString()).isEqualTo("scheme://authority/path#fragment"); + } + + @Test + public void parse_noFragment() throws URISyntaxException { + Uri uri = Uri.parse("scheme://authority/path?query"); + assertThat(uri.getScheme()).isEqualTo("scheme"); + assertThat(uri.getAuthority()).isEqualTo("authority"); + assertThat(uri.getPath()).isEqualTo("/path"); + assertThat(uri.getQuery()).isEqualTo("query"); + assertThat(uri.getFragment()).isNull(); + assertThat(uri.toString()).isEqualTo("scheme://authority/path?query"); + assertThat(uri.isAbsolute()).isTrue(); + } + + @Test + public void parse_emptyPathWithAuthority() throws URISyntaxException { + Uri uri = Uri.parse("scheme://authority"); + assertThat(uri.getScheme()).isEqualTo("scheme"); + assertThat(uri.getAuthority()).isEqualTo("authority"); + assertThat(uri.getPath()).isEmpty(); + assertThat(uri.getQuery()).isNull(); + assertThat(uri.getFragment()).isNull(); + assertThat(uri.toString()).isEqualTo("scheme://authority"); + assertThat(uri.isAbsolute()).isTrue(); + } + + @Test + public void parse_rootless() throws URISyntaxException { + Uri uri = Uri.parse("mailto:ceo@company.com?subject=raise"); + assertThat(uri.getScheme()).isEqualTo("mailto"); + assertThat(uri.getAuthority()).isNull(); + assertThat(uri.getPath()).isEqualTo("ceo@company.com"); + assertThat(uri.getQuery()).isEqualTo("subject=raise"); + assertThat(uri.getFragment()).isNull(); + assertThat(uri.toString()).isEqualTo("mailto:ceo@company.com?subject=raise"); + assertThat(uri.isAbsolute()).isTrue(); + } + + @Test + public void parse_emptyPath() throws URISyntaxException { + Uri uri = Uri.parse("scheme:"); + assertThat(uri.getScheme()).isEqualTo("scheme"); + assertThat(uri.getAuthority()).isNull(); + assertThat(uri.getPath()).isEmpty(); + assertThat(uri.getQuery()).isNull(); + assertThat(uri.getFragment()).isNull(); + assertThat(uri.toString()).isEqualTo("scheme:"); + assertThat(uri.isAbsolute()).isTrue(); + } + + @Test + public void parse_invalidScheme_throws() { + URISyntaxException e = + assertThrows(URISyntaxException.class, () -> Uri.parse("1scheme://authority/path")); + assertThat(e).hasMessageThat().contains("Scheme must start with an alphabetic char"); + + e = assertThrows(URISyntaxException.class, () -> Uri.parse(":path")); + assertThat(e).hasMessageThat().contains("Scheme must start with an alphabetic char"); + } + + @Test + public void parse_unTerminatedScheme_throws() { + URISyntaxException e = assertThrows(URISyntaxException.class, () -> Uri.parse("scheme/")); + assertThat(e).hasMessageThat().contains("Missing required scheme"); + + e = assertThrows(URISyntaxException.class, () -> Uri.parse("scheme?")); + assertThat(e).hasMessageThat().contains("Missing required scheme"); + + e = assertThrows(URISyntaxException.class, () -> Uri.parse("scheme#")); + assertThat(e).hasMessageThat().contains("Missing required scheme"); + } + + @Test + public void parse_invalidCharactersInScheme_throws() { + URISyntaxException e = + assertThrows(URISyntaxException.class, () -> Uri.parse("schem e://authority/path")); + assertThat(e).hasMessageThat().contains("Invalid character in scheme"); + } + + @Test + public void parse_unTerminatedAuthority_throws() { + Uri uri = Uri.create("s://auth/"); + assertThat(uri.getAuthority()).isEqualTo("auth"); + uri = Uri.create("s://auth?"); + assertThat(uri.getAuthority()).isEqualTo("auth"); + uri = Uri.create("s://auth#"); + assertThat(uri.getAuthority()).isEqualTo("auth"); + } + + @Test + public void parse_invalidCharactersInUserinfo_throws() { + URISyntaxException e = + assertThrows(URISyntaxException.class, () -> Uri.parse("scheme://u ser@host/path")); + assertThat(e).hasMessageThat().contains("Invalid character in userInfo"); + } + + @Test + public void parse_invalidBackslashInUserinfo_throws() { + URISyntaxException e = + assertThrows(URISyntaxException.class, () -> Uri.parse("http://other.com\\@intended.com")); + assertThat(e).hasMessageThat().contains("Invalid character in userInfo"); + } + + @Test + public void parse_invalidCharactersInHost_throws() { + URISyntaxException e = + assertThrows(URISyntaxException.class, () -> Uri.parse("scheme://h ost/path")); + assertThat(e).hasMessageThat().contains("Invalid character in host"); + } + + @Test + public void parse_invalidBackslashInHost_throws() { + URISyntaxException e = + assertThrows(URISyntaxException.class, () -> Uri.parse("http://other.com\\.intended.com")); + assertThat(e).hasMessageThat().contains("Invalid character in host"); + } + + @Test + public void parse_emptyPort_throws() { + URISyntaxException e = + assertThrows(URISyntaxException.class, () -> Uri.parse("scheme://user@host:/path")); + assertThat(e).hasMessageThat().contains("Invalid port"); + } + + @Test + public void parse_invalidCharactersInPort_throws() { + URISyntaxException e = + assertThrows(URISyntaxException.class, () -> Uri.parse("scheme://user@host:8 0/path")); + assertThat(e).hasMessageThat().contains("Invalid character"); + } + + @Test + public void parse_nonAsciiCharacterInPath_throws() throws URISyntaxException { + URISyntaxException e = assertThrows(URISyntaxException.class, () -> Uri.parse("foo:bär")); + assertThat(e).hasMessageThat().contains("Invalid character in path"); + } + + @Test + public void parse_invalidCharactersInPath_throws() { + URISyntaxException e = assertThrows(URISyntaxException.class, () -> Uri.parse("scheme:/p ath")); + assertThat(e).hasMessageThat().contains("Invalid character in path"); + } + + @Test + public void parse_invalidCharactersInQuery_throws() { + URISyntaxException e = + assertThrows(URISyntaxException.class, () -> Uri.parse("scheme://user@host/p?q[]uery")); + assertThat(e).hasMessageThat().contains("Invalid character in query"); + } + + @Test + public void parse_invalidCharactersInFragment_throws() { + URISyntaxException e = + assertThrows(URISyntaxException.class, () -> Uri.parse("scheme://user@host/path#f[]rag")); + assertThat(e).hasMessageThat().contains("Invalid character in fragment"); + } + + @Test + public void parse_nonAsciiCharacterInFragment_throws() throws URISyntaxException { + URISyntaxException e = assertThrows(URISyntaxException.class, () -> Uri.parse("foo:#bär")); + assertThat(e).hasMessageThat().contains("Invalid character in fragment"); + } + + @Test + public void parse_decoding() throws URISyntaxException { + Uri uri = Uri.parse("s://user%2Ename:pass%2Eword@a%2db:1234/p%20ath?q%20uery#f%20ragment"); + assertThat(uri.getAuthority()).isEqualTo("user.name:pass.word@a-b:1234"); + assertThat(uri.getRawAuthority()).isEqualTo("user%2Ename:pass%2Eword@a%2db:1234"); + assertThat(uri.getUserInfo()).isEqualTo("user.name:pass.word"); + assertThat(uri.getRawUserInfo()).isEqualTo("user%2Ename:pass%2Eword"); + assertThat(uri.getHost()).isEqualTo("a-b"); + assertThat(uri.getRawHost()).isEqualTo("a%2db"); + assertThat(uri.getPort()).isEqualTo(1234); + assertThat(uri.getPath()).isEqualTo("/p ath"); + assertThat(uri.getRawPath()).isEqualTo("/p%20ath"); + assertThat(uri.getQuery()).isEqualTo("q uery"); + assertThat(uri.getRawQuery()).isEqualTo("q%20uery"); + assertThat(uri.getFragment()).isEqualTo("f ragment"); + assertThat(uri.getRawFragment()).isEqualTo("f%20ragment"); + } + + @Test + public void parse_decodingNonAscii() throws URISyntaxException { + Uri uri = Uri.parse("s://a/%E2%82%AC"); + assertThat(uri.getPath()).isEqualTo("/€"); + } + + @Test + public void parse_decodingPercent() throws URISyntaxException { + Uri uri = Uri.parse("s://a/p%2520ath?q%25uery#f%25ragment"); + assertThat(uri.getPath()).isEqualTo("/p%20ath"); + assertThat(uri.getQuery()).isEqualTo("q%uery"); + assertThat(uri.getFragment()).isEqualTo("f%ragment"); + } + + @Test + public void parse_invalidPercentEncoding_throws() { + URISyntaxException e = assertThrows(URISyntaxException.class, () -> Uri.parse("s://a/p%2")); + assertThat(e).hasMessageThat().contains("Invalid"); + + e = assertThrows(URISyntaxException.class, () -> Uri.parse("s://a/p%2G")); + assertThat(e).hasMessageThat().contains("Invalid"); + } + + @Test + public void parse_emptyAuthority() { + Uri uri = Uri.create("file:///foo/bar"); + assertThat(uri.getAuthority()).isEmpty(); + assertThat(uri.getHost()).isEmpty(); + assertThat(uri.getUserInfo()).isNull(); + assertThat(uri.getPort()).isEqualTo(-1); + assertThat(uri.getPath()).isEqualTo("/foo/bar"); + } + + @Test + public void parse_pathSegments_empty() throws URISyntaxException { + Uri uri = Uri.create("scheme:"); + assertThat(uri.getPathSegments()).isEmpty(); + } + + @Test + public void parse_pathSegments_root() throws URISyntaxException { + Uri uri = Uri.create("scheme:/"); + assertThat(uri.getPathSegments()).containsExactly(""); + } + + @Test + public void parse_onePathSegment() throws URISyntaxException { + Uri uri = Uri.create("file:/foo"); + assertThat(uri.getPathSegments()).containsExactly("foo"); + } + + @Test + public void parse_onePathSegment_trailingSlash() throws URISyntaxException { + Uri uri = Uri.create("file:/foo/"); + assertThat(uri.getPathSegments()).containsExactly("foo", ""); + } + + @Test + public void parse_twoPathSegments() throws URISyntaxException { + Uri uri = Uri.create("file:/foo/bar"); + assertThat(uri.getPathSegments()).containsExactly("foo", "bar"); + } + + @Test + public void toString_percentEncoding() throws URISyntaxException { + Uri uri = + Uri.newBuilder() + .setScheme("s") + .setHost("a b") + .setPath("/p ath") + .setQuery("q uery") + .setFragment("f ragment") + .build(); + assertThat(uri.toString()).isEqualTo("s://a%20b/p%20ath?q%20uery#f%20ragment"); + } + + @Test + public void parse_transparentRoundTrip_ipLiteral() { + Uri uri = Uri.create("http://[2001:dB8::7]:080/%4a%4B%2f%2F?%4c%4D#%4e%4F").toBuilder().build(); + assertThat(uri.toString()).isEqualTo("http://[2001:dB8::7]:080/%4a%4B%2f%2F?%4c%4D#%4e%4F"); + + // IPv6 host has non-canonical :: zeros and mixed case hex digits. + assertThat(uri.getRawHost()).isEqualTo("[2001:dB8::7]"); + assertThat(uri.getHost()).isEqualTo("[2001:dB8::7]"); + assertThat(uri.getRawPort()).isEqualTo("080"); // Leading zeros. + assertThat(uri.getPort()).isEqualTo(80); + // Unnecessary and mixed case percent encodings. + assertThat(uri.getRawPath()).isEqualTo("/%4a%4B%2f%2F"); + assertThat(uri.getPathSegments()).containsExactly("JK//"); + assertThat(uri.getRawQuery()).isEqualTo("%4c%4D"); + assertThat(uri.getQuery()).isEqualTo("LM"); + assertThat(uri.getRawFragment()).isEqualTo("%4e%4F"); + assertThat(uri.getFragment()).isEqualTo("NO"); + } + + @Test + public void parse_transparentRoundTrip_regName() { + Uri uri = Uri.create("http://aB%4A%4b:080/%4a%4B%2f%2F?%4c%4D#%4e%4F").toBuilder().build(); + assertThat(uri.toString()).isEqualTo("http://aB%4A%4b:080/%4a%4B%2f%2F?%4c%4D#%4e%4F"); + + // Mixed case literal chars and hex digits. + assertThat(uri.getRawHost()).isEqualTo("aB%4A%4b"); + assertThat(uri.getHost()).isEqualTo("aBJK"); + assertThat(uri.getRawPort()).isEqualTo("080"); // Leading zeros. + assertThat(uri.getPort()).isEqualTo(80); + // Unnecessary and mixed case percent encodings. + assertThat(uri.getRawPath()).isEqualTo("/%4a%4B%2f%2F"); + assertThat(uri.getPathSegments()).containsExactly("JK//"); + assertThat(uri.getRawQuery()).isEqualTo("%4c%4D"); + assertThat(uri.getQuery()).isEqualTo("LM"); + assertThat(uri.getRawFragment()).isEqualTo("%4e%4F"); + assertThat(uri.getFragment()).isEqualTo("NO"); + } + + @Test + public void builder_numericPort() throws URISyntaxException { + Uri uri = Uri.newBuilder().setScheme("scheme").setHost("host").setPort(80).build(); + assertThat(uri.toString()).isEqualTo("scheme://host:80"); + } + + @Test + public void builder_ipv6Literal() throws URISyntaxException { + Uri uri = + Uri.newBuilder() + .setScheme("scheme") + .setHost(InetAddresses.forString("2001:4860:4860::8844")) + .build(); + assertThat(uri.toString()).isEqualTo("scheme://[2001:4860:4860::8844]"); + } + + @Test + public void builder_encodingWithAllowedReservedChars() throws URISyntaxException { + Uri uri = + Uri.newBuilder() + .setScheme("s") + .setUserInfo("u@") + .setHost("a[]") + .setPath("/p:/@") + .setQuery("q/?") + .setFragment("f/?") + .build(); + assertThat(uri.toString()).isEqualTo("s://u%40@a%5B%5D/p:/@?q/?#f/?"); + } + + @Test + public void builder_percentEncodingNonAscii() throws URISyntaxException { + Uri uri = Uri.newBuilder().setScheme("s").setHost("a").setPath("/€").build(); + assertThat(uri.toString()).isEqualTo("s://a/%E2%82%AC"); + } + + @Test + public void builder_percentEncodingLoneHighSurrogate_throws() { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> Uri.newBuilder().setPath("\uD83D")); // Lone high surrogate. + assertThat(e.getMessage()).contains("Malformed input"); + } + + @Test + public void builder_hasAuthority_pathStartsWithSlash_throws() throws URISyntaxException { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> Uri.newBuilder().setScheme("s").setHost("a").setPath("path").build()); + assertThat(e.getMessage()).contains("Non-empty path must start with '/'"); + } + + @Test + public void builder_noAuthority_pathStartsWithDoubleSlash_throws() throws URISyntaxException { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> Uri.newBuilder().setScheme("s").setPath("//path").build()); + assertThat(e.getMessage()).contains("Path cannot start with '//'"); + } + + @Test + public void builder_noScheme_throws() { + IllegalStateException e = + assertThrows(IllegalStateException.class, () -> Uri.newBuilder().build()); + assertThat(e.getMessage()).contains("Missing required scheme"); + } + + @Test + public void builder_noHost_hasUserInfo_throws() { + IllegalStateException e = + assertThrows( + IllegalStateException.class, + () -> Uri.newBuilder().setScheme("scheme").setUserInfo("user").build()); + assertThat(e.getMessage()).contains("Cannot set userInfo without host"); + } + + @Test + public void builder_noHost_hasPort_throws() { + IllegalStateException e = + assertThrows( + IllegalStateException.class, + () -> Uri.newBuilder().setScheme("scheme").setPort(1234).build()); + assertThat(e.getMessage()).contains("Cannot set port without host"); + } + + @Test + public void builder_normalizesCaseWhereAppropriate() { + Uri uri = + Uri.newBuilder() + .setScheme("hTtP") // #section-3.1 says producers (Builder) should normalize to lower. + .setHost("aBc") // #section-3.2.2 says producers (Builder) should normalize to lower. + .setPath("/CdE") // #section-6.2.2.1 says the rest are assumed to be case-sensitive + .setQuery("fGh") + .setFragment("IjK") + .build(); + assertThat(uri.toString()).isEqualTo("http://abc/CdE?fGh#IjK"); + } + + @Test + public void builder_normalizesIpv6Literal() { + Uri uri = + Uri.newBuilder().setScheme("scheme").setHost(InetAddresses.forString("ABCD::EFAB")).build(); + assertThat(uri.toString()).isEqualTo("scheme://[abcd::efab]"); + } + + @Test + public void builder_canClearAllOptionalFields() { + Uri uri = + Uri.create("http://user@host:80/path?query#fragment").toBuilder() + .setHost((String) null) + .setPath("") + .setUserInfo(null) + .setPort(-1) + .setQuery(null) + .setFragment(null) + .build(); + assertThat(uri.toString()).isEqualTo("http:"); + } + + @Test + public void toString_percentEncodingMultiChar() throws URISyntaxException { + Uri uri = + Uri.newBuilder() + .setScheme("s") + .setHost("a") + .setPath("/emojis/😊/icon.png") // Smile requires two chars to express in a java String. + .build(); + assertThat(uri.toString()).isEqualTo("s://a/emojis/%F0%9F%98%8A/icon.png"); + } + + @Test + public void toString_percentEncodingLiteralPercent() throws URISyntaxException { + Uri uri = + Uri.newBuilder() + .setScheme("s") + .setHost("a") + .setPath("/p%20ath") + .setQuery("q%uery") + .setFragment("f%ragment") + .build(); + assertThat(uri.toString()).isEqualTo("s://a/p%2520ath?q%25uery#f%25ragment"); + } + + @Test + public void equalsAndHashCode() { + new EqualsTester() + .addEqualityGroup( + Uri.create("scheme://authority/path?query#fragment"), + Uri.create("scheme://authority/path?query#fragment")) + .addEqualityGroup(Uri.create("scheme://authority/path")) + .addEqualityGroup(Uri.create("scheme://authority/path?query")) + .addEqualityGroup(Uri.create("scheme:/path")) + .addEqualityGroup(Uri.create("scheme:/path?query")) + .addEqualityGroup(Uri.create("scheme:/path#fragment")) + .addEqualityGroup(Uri.create("scheme:path")) + .addEqualityGroup(Uri.create("scheme:path?query")) + .addEqualityGroup(Uri.create("scheme:path#fragment")) + .addEqualityGroup(Uri.create("scheme:")) + .testEquals(); + } + + @Test + public void isAbsolute() { + assertThat(Uri.create("scheme://authority/path").isAbsolute()).isTrue(); + assertThat(Uri.create("scheme://authority/path?query").isAbsolute()).isTrue(); + assertThat(Uri.create("scheme://authority/path#fragment").isAbsolute()).isFalse(); + assertThat(Uri.create("scheme://authority/path?query#fragment").isAbsolute()).isFalse(); + } + + @Test + public void serializedCharacterClasses_matchComputed() { + assertThat(Uri.digitChars).isEqualTo(bitSetOfRange('0', '9')); + assertThat(Uri.alphaChars).isEqualTo(or(bitSetOfRange('A', 'Z'), bitSetOfRange('a', 'z'))); + assertThat(Uri.schemeChars) + .isEqualTo(or(Uri.digitChars, Uri.alphaChars, bitSetOf('+', '-', '.'))); + assertThat(Uri.unreservedChars) + .isEqualTo(or(Uri.alphaChars, Uri.digitChars, bitSetOf('-', '.', '_', '~'))); + assertThat(Uri.genDelimsChars).isEqualTo(bitSetOf(':', '/', '?', '#', '[', ']', '@')); + assertThat(Uri.subDelimsChars) + .isEqualTo(bitSetOf('!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=')); + assertThat(Uri.reservedChars).isEqualTo(or(Uri.genDelimsChars, Uri.subDelimsChars)); + assertThat(Uri.regNameChars).isEqualTo(or(Uri.unreservedChars, Uri.subDelimsChars)); + assertThat(Uri.userInfoChars) + .isEqualTo(or(Uri.unreservedChars, Uri.subDelimsChars, bitSetOf(':'))); + assertThat(Uri.pChars) + .isEqualTo(or(Uri.unreservedChars, Uri.subDelimsChars, bitSetOf(':', '@'))); + assertThat(Uri.pCharsAndSlash).isEqualTo(or(Uri.pChars, bitSetOf('/'))); + assertThat(Uri.queryChars).isEqualTo(or(Uri.pChars, bitSetOf('/', '?'))); + assertThat(Uri.fragmentChars).isEqualTo(or(Uri.pChars, bitSetOf('/', '?'))); + } + + private static BitSet bitSetOfRange(char from, char to) { + BitSet bitset = new BitSet(); + for (char c = from; c <= to; c++) { + bitset.set(c); + } + return bitset; + } + + private static BitSet bitSetOf(char... chars) { + BitSet bitset = new BitSet(); + for (char c : chars) { + bitset.set(c); + } + return bitset; + } + + private static BitSet or(BitSet... bitsets) { + BitSet bitset = new BitSet(); + for (BitSet bs : bitsets) { + bitset.or(bs); + } + return bitset; + } +} From 283f1031f7b48ce32a2f91bb92bac93a0ca29bdd Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:36:14 -0800 Subject: [PATCH 481/591] netty: Run handshakeCompleteRunnable in success cases b/458734698 --- .../src/main/java/io/grpc/netty/ProtocolNegotiators.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java index 9323c58aae1..8faf3d0fae8 100644 --- a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java @@ -702,9 +702,6 @@ protected void userEventTriggered0(ChannelHandlerContext ctx, Object evt) throws Exception ex = unavailableException("Failed ALPN negotiation: Unable to find compatible protocol"); logSslEngineDetails(Level.FINE, ctx, "TLS negotiation failed.", ex); - if (handshakeCompleteRunnable.isPresent()) { - handshakeCompleteRunnable.get().run(); - } ctx.fireExceptionCaught(ex); } } else { @@ -719,11 +716,11 @@ protected void userEventTriggered0(ChannelHandlerContext ctx, Object evt) throws .withCause(t) .asRuntimeException(); } - if (handshakeCompleteRunnable.isPresent()) { - handshakeCompleteRunnable.get().run(); - } ctx.fireExceptionCaught(t); } + if (handshakeCompleteRunnable.isPresent()) { + handshakeCompleteRunnable.get().run(); + } } else { super.userEventTriggered0(ctx, evt); } From bdbcaf1795fbb486d970e8a167e0748fae3f8d4c Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 9 Dec 2025 14:14:10 +0530 Subject: [PATCH 482/591] Start 1.79.0 development cycle (#12557) --- MODULE.bazel | 2 +- build.gradle | 2 +- .../src/test/golden/TestDeprecatedService.java.txt | 2 +- compiler/src/test/golden/TestService.java.txt | 2 +- core/src/main/java/io/grpc/internal/GrpcUtil.java | 2 +- examples/MODULE.bazel | 2 +- examples/android/clientcache/app/build.gradle | 10 +++++----- examples/android/helloworld/app/build.gradle | 8 ++++---- examples/android/routeguide/app/build.gradle | 8 ++++---- examples/android/strictmode/app/build.gradle | 8 ++++---- examples/build.gradle | 2 +- examples/example-alts/build.gradle | 2 +- examples/example-debug/build.gradle | 2 +- examples/example-debug/pom.xml | 4 ++-- examples/example-dualstack/build.gradle | 2 +- examples/example-dualstack/pom.xml | 4 ++-- examples/example-gauth/build.gradle | 2 +- examples/example-gauth/pom.xml | 4 ++-- examples/example-gcp-csm-observability/build.gradle | 2 +- examples/example-gcp-observability/build.gradle | 2 +- examples/example-hostname/build.gradle | 2 +- examples/example-hostname/pom.xml | 4 ++-- examples/example-jwt-auth/build.gradle | 2 +- examples/example-jwt-auth/pom.xml | 4 ++-- examples/example-oauth/build.gradle | 2 +- examples/example-oauth/pom.xml | 4 ++-- examples/example-opentelemetry/build.gradle | 2 +- examples/example-orca/build.gradle | 2 +- examples/example-reflection/build.gradle | 2 +- examples/example-servlet/build.gradle | 2 +- examples/example-tls/build.gradle | 2 +- examples/example-tls/pom.xml | 4 ++-- examples/example-xds/build.gradle | 2 +- examples/pom.xml | 4 ++-- 34 files changed, 55 insertions(+), 55 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index f81f287588f..ab3534b3ef8 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "grpc-java", compatibility_level = 0, repo_name = "io_grpc_grpc_java", - version = "1.78.0-SNAPSHOT", # CURRENT_GRPC_VERSION + version = "1.79.0-SNAPSHOT", # CURRENT_GRPC_VERSION ) # GRPC_DEPS_START diff --git a/build.gradle b/build.gradle index 50b7419df64..91690c1e3c3 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.78.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.79.0-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index 3451d0cad09..5289f632637 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.78.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.79.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index e86ad0b8f08..b603c40f09f 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.78.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.79.0-SNAPSHOT)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index 4a9da1d19fe..1b5feeccb4a 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -219,7 +219,7 @@ public byte[] parseAsciiString(byte[] serialized) { public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); - public static final String IMPLEMENTATION_VERSION = "1.78.0-SNAPSHOT"; // CURRENT_GRPC_VERSION + public static final String IMPLEMENTATION_VERSION = "1.79.0-SNAPSHOT"; // CURRENT_GRPC_VERSION /** * The default timeout in nanos for a keepalive ping request. diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index 67c97bbf690..dca8c091f17 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,4 +1,4 @@ -bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.78.0-SNAPSHOT") # CURRENT_GRPC_VERSION +bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.79.0-SNAPSHOT") # CURRENT_GRPC_VERSION bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") bazel_dep(name = "rules_jvm_external", version = "6.0") diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index 0a3cd38b6ed..61d29f253c9 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -34,7 +34,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -54,11 +54,11 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.4.5' - testImplementation 'io.grpc:grpc-testing:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + testImplementation 'io.grpc:grpc-testing:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index 60d32f0360d..5df93b8cd6e 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,7 +52,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index c232389ff3d..ff133450f0e 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,7 +52,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index 6c8209621a9..40155447d7b 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -33,7 +33,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.25.1' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -53,7 +53,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION } diff --git a/examples/build.gradle b/examples/build.gradle index 3c7464ea2d2..cc8f15fd9c9 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index 4796123ef23..97233a56775 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index a21ece5686f..517948dec81 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' dependencies { diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index ddfedaa2e39..5a521e746b3 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -6,13 +6,13 @@ jar - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT example-debug https://github.com/grpc/grpc-java UTF-8 - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index affd18996e6..cd6f2885779 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -23,7 +23,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' dependencies { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index 66e6b481da4..05a873a7c49 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -6,13 +6,13 @@ jar - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT example-dualstack https://github.com/grpc/grpc-java UTF-8 - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 1d305677048..3ec28dd8785 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index 73efa507656..80db41266aa 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,13 +6,13 @@ jar - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index d02e0d76ec4..5ed39227f41 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' def openTelemetryVersion = '1.56.0' def openTelemetryPrometheusVersion = '1.56.0-alpha' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 9ece32e2b78..8ed29af77ec 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -22,7 +22,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 671de65d2ff..ffe52075785 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' dependencies { diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index d228ee3e7c5..c302537146f 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,13 +6,13 @@ jar - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index c2ff9f7a7f2..938eb424c81 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index 5deb3583561..5a0697b2357 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,13 +7,13 @@ jar - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT 3.25.8 3.25.8 diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 447e40f1f3e..7f6e1c425d8 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protobufVersion = '3.25.8' def protocVersion = protobufVersion diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index 85103ec9121..70b9682bb11 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -7,13 +7,13 @@ jar - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT example-oauth https://github.com/grpc/grpc-java UTF-8 - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT 3.25.8 3.25.8 diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index 2c953b3d487..17286dcd546 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' def openTelemetryVersion = '1.56.0' def openTelemetryPrometheusVersion = '1.56.0-alpha' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index e188b4079ef..b4d66282dab 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index 5148eaf7938..472281c4b79 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -16,7 +16,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index 3ee7bb2a592..12458971e30 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -15,7 +15,7 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index b36d21f1b41..d88bced17bf 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index 34819f52a45..6f7243be5a6 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,13 +6,13 @@ jar - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT example-tls https://github.com/grpc/grpc-java UTF-8 - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT 3.25.8 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index f5945320080..fe74bada961 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -21,7 +21,7 @@ java { // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.78.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.79.0-SNAPSHOT' // CURRENT_GRPC_VERSION def protocVersion = '3.25.8' dependencies { diff --git a/examples/pom.xml b/examples/pom.xml index 544f737ceb1..642e0983e03 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,13 +6,13 @@ jar - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT examples https://github.com/grpc/grpc-java UTF-8 - 1.78.0-SNAPSHOT + 1.79.0-SNAPSHOT 3.25.8 3.25.8 From b61a8f49c0e14f98f5315deb2ca89d67d1e2ea4a Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Wed, 10 Dec 2025 09:49:25 -0800 Subject: [PATCH 483/591] compiler: Upgrade Bazel protobuf to 33.1 (#12553) Following up on https://github.com/grpc/grpc-java/commit/58ae5f808cf8e20c5864033c9a8f485b237f9dfc, change Bazel's view of the protobuf version to 33.1 too. This is a bit of an adventure in the bzlmod/WORKSPACE compatibility maze. A few rulesets like `rules_java` must be upgraded too. C++ compilation must happen until C++17 now due to the new abseil version required. Dependency on `rules_proto` is removed because it has become a superfluous wrapper over rules in the upstream protobuf repo. --- .bazelrc | 2 +- .github/workflows/testing.yml | 2 +- MODULE.bazel | 11 ++++------- WORKSPACE | 27 +++++++++++++++++++++++---- alts/BUILD.bazel | 2 +- buildscripts/kokoro/unix.sh | 1 + examples/.bazelrc | 2 +- examples/BUILD.bazel | 3 ++- examples/MODULE.bazel | 3 +-- examples/WORKSPACE | 11 +++++++++-- examples/example-alts/BUILD.bazel | 3 ++- examples/example-hostname/BUILD.bazel | 3 ++- examples/example-tls/BUILD.bazel | 3 ++- java_grpc_library.bzl | 3 ++- repositories.bzl | 6 +++--- testing-proto/BUILD.bazel | 2 +- xds/BUILD.bazel | 4 ++-- 17 files changed, 58 insertions(+), 30 deletions(-) diff --git a/.bazelrc b/.bazelrc index 554440cfe3d..53485cb9743 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1 +1 @@ -build --cxxopt=-std=c++14 --host_cxxopt=-std=c++14 +build --cxxopt=-std=c++17 --host_cxxopt=-std=c++17 diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 4fe75b0be78..ccabd9be79f 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -81,7 +81,7 @@ jobs: matrix: bzlmod: [true, false] env: - USE_BAZEL_VERSION: 7.0.0 + USE_BAZEL_VERSION: 7.7.1 steps: - uses: actions/checkout@v4 diff --git a/MODULE.bazel b/MODULE.bazel index ab3534b3ef8..92749683dde 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -48,17 +48,14 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ bazel_dep(name = "bazel_jar_jar", version = "0.1.7") bazel_dep(name = "bazel_skylib", version = "1.7.1") -bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5") -bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") -# Protobuf 25.5+ is incompatible with Bazel 7 with bzlmod -bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "24.4") +bazel_dep(name = "googleapis", version = "0.0.0-20240326-1c8d509c5", repo_name = "com_google_googleapis") +bazel_dep(name = "grpc-proto", version = "0.0.0-20240627-ec30f58", repo_name = "io_grpc_grpc_proto") +bazel_dep(name = "protobuf", version = "33.1", repo_name = "com_google_protobuf") bazel_dep(name = "rules_cc", version = "0.0.9") -bazel_dep(name = "rules_java", version = "5.3.5") +bazel_dep(name = "rules_java", version = "9.1.0") bazel_dep(name = "rules_jvm_external", version = "6.0") -bazel_dep(name = "rules_proto", version = "5.3.0-21.7") maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") - maven.install( artifacts = IO_GRPC_GRPC_JAVA_ARTIFACTS, repositories = [ diff --git a/WORKSPACE b/WORKSPACE index 227ef332757..e5fa1a185f7 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -2,12 +2,28 @@ workspace(name = "io_grpc_grpc_java") load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +http_archive( + name = "bazel_features", + sha256 = "a660027f5a87f13224ab54b8dc6e191693c554f2692fcca46e8e29ee7dabc43b", + strip_prefix = "bazel_features-1.30.0", + url = "https://github.com/bazel-contrib/bazel_features/releases/download/v1.30.0/bazel_features-v1.30.0.tar.gz", +) + +load("@bazel_features//:deps.bzl", "bazel_features_deps") +bazel_features_deps() + http_archive( name = "rules_java", - url = "https://github.com/bazelbuild/rules_java/releases/download/5.3.5/rules_java-5.3.5.tar.gz", - sha256 = "c73336802d0b4882e40770666ad055212df4ea62cfa6edf9cb0f9d29828a0934", + urls = [ + "https://github.com/bazelbuild/rules_java/releases/download/9.1.0/rules_java-9.1.0.tar.gz", + ], + sha256 = "4e1a28a25c2efa53500c928d22ceffbc505dd95b335a2d025836a293b592212f", ) +load("@rules_java//java:rules_java_deps.bzl", "compatibility_proxy_repo") + +compatibility_proxy_repo() + http_archive( name = "rules_jvm_external", sha256 = "d31e369b854322ca5098ea12c69d7175ded971435e55c18dd9dd5f29cc5249ac", @@ -26,11 +42,14 @@ load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar_repositories") jar_jar_repositories() -load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS") -load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") +load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS", "protobuf_deps") protobuf_deps() +load("@rules_python//python:repositories.bzl", "py_repositories") + +py_repositories() + load("@com_google_googleapis//:repository_rules.bzl", "switched_rules_by_language") switched_rules_by_language( diff --git a/alts/BUILD.bazel b/alts/BUILD.bazel index d2c01449dc3..5cf14504e2c 100644 --- a/alts/BUILD.bazel +++ b/alts/BUILD.bazel @@ -1,6 +1,6 @@ +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@rules_java//java:defs.bzl", "java_library", "java_proto_library") load("@rules_jvm_external//:defs.bzl", "artifact") -load("@rules_proto//proto:defs.bzl", "proto_library") load("//:java_grpc_library.bzl", "java_grpc_library") java_library( diff --git a/buildscripts/kokoro/unix.sh b/buildscripts/kokoro/unix.sh index 455d9c07199..693768a0270 100755 --- a/buildscripts/kokoro/unix.sh +++ b/buildscripts/kokoro/unix.sh @@ -38,6 +38,7 @@ ARCH="$ARCH" buildscripts/make_dependencies.sh # Set properties via flags, do not pollute gradle.properties GRADLE_FLAGS="${GRADLE_FLAGS:-}" +GRADLE_FLAGS+=" --stacktrace" GRADLE_FLAGS+=" -PtargetArch=$ARCH" # For universal binaries on macOS, signal Gradle to use universal flags. diff --git a/examples/.bazelrc b/examples/.bazelrc index 554440cfe3d..53485cb9743 100644 --- a/examples/.bazelrc +++ b/examples/.bazelrc @@ -1 +1 @@ -build --cxxopt=-std=c++14 --host_cxxopt=-std=c++14 +build --cxxopt=-std=c++17 --host_cxxopt=-std=c++17 diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel index 3a0936780a0..24f6966e053 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -1,4 +1,5 @@ -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library") proto_library( diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index dca8c091f17..78c6a42df9b 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,8 +1,7 @@ bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.79.0-SNAPSHOT") # CURRENT_GRPC_VERSION bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") -bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "23.1") +bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "33.1") bazel_dep(name = "rules_jvm_external", version = "6.0") -bazel_dep(name = "rules_proto", version = "5.3.0-21.7") # Do not use this override in your own MODULE.bazel. It is unnecessary when # using a version from BCR. Be aware the gRPC Java team does not update the diff --git a/examples/WORKSPACE b/examples/WORKSPACE index 66a713a1a01..6080a3c3d25 100644 --- a/examples/WORKSPACE +++ b/examples/WORKSPACE @@ -34,11 +34,18 @@ jar_jar_repositories() # Protobuf now requires C++14 or higher, which requires Bazel configuration # outside the WORKSPACE. See .bazelrc in this directory. -load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS") -load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") +load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS", "protobuf_deps") protobuf_deps() +load("@rules_python//python:repositories.bzl", "py_repositories") + +py_repositories() + +load("@rules_java//java:rules_java_deps.bzl", "compatibility_proxy_repo") + +compatibility_proxy_repo() + load("@com_google_googleapis//:repository_rules.bzl", "switched_rules_by_language") switched_rules_by_language( diff --git a/examples/example-alts/BUILD.bazel b/examples/example-alts/BUILD.bazel index 0404dcccf81..2bb0d532fa5 100644 --- a/examples/example-alts/BUILD.bazel +++ b/examples/example-alts/BUILD.bazel @@ -1,4 +1,5 @@ -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library") proto_library( diff --git a/examples/example-hostname/BUILD.bazel b/examples/example-hostname/BUILD.bazel index 8b76f790983..adb50da94f9 100644 --- a/examples/example-hostname/BUILD.bazel +++ b/examples/example-hostname/BUILD.bazel @@ -1,4 +1,5 @@ -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library") proto_library( diff --git a/examples/example-tls/BUILD.bazel b/examples/example-tls/BUILD.bazel index 81913836766..e46f1db9e5a 100644 --- a/examples/example-tls/BUILD.bazel +++ b/examples/example-tls/BUILD.bazel @@ -1,4 +1,5 @@ -load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library") proto_library( diff --git a/java_grpc_library.bzl b/java_grpc_library.bzl index aaebfe3f933..e6afc028883 100644 --- a/java_grpc_library.bzl +++ b/java_grpc_library.bzl @@ -1,5 +1,6 @@ """Build rule for java_grpc_library.""" -load("@rules_proto//proto:defs.bzl", "ProtoInfo") + +load("@com_google_protobuf//bazel/common:proto_info.bzl", "ProtoInfo") load("@rules_java//java:defs.bzl", "JavaInfo", "JavaPluginInfo", "java_common") _JavaRpcToolchainInfo = provider( diff --git a/repositories.bzl b/repositories.bzl index f227b8f72c4..43b7fcfe1ed 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -116,9 +116,9 @@ def com_google_protobuf(): # This statement defines the @com_google_protobuf repo. http_archive( name = "com_google_protobuf", - sha256 = "3cf7d5b17c4ff04fe9f038104e9d0cae6da09b8ce271c13e44f8ac69f51e4e0f", - strip_prefix = "protobuf-25.5", - urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v25.5/protobuf-25.5.tar.gz"], + sha256 = "fda132cb0c86400381c0af1fe98bd0f775cb566cb247cdcc105e344e00acc30e", + strip_prefix = "protobuf-33.1", + urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v33.1/protobuf-33.1.tar.gz"], ) def io_grpc_grpc_proto(): diff --git a/testing-proto/BUILD.bazel b/testing-proto/BUILD.bazel index f02957c6fc3..362cee91463 100644 --- a/testing-proto/BUILD.bazel +++ b/testing-proto/BUILD.bazel @@ -1,5 +1,5 @@ +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@rules_java//java:defs.bzl", "java_proto_library") -load("@rules_proto//proto:defs.bzl", "proto_library") load("//:java_grpc_library.bzl", "java_grpc_library") proto_library( diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index 66c790a654d..be44abf4f39 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -1,6 +1,6 @@ -load("@rules_java//java:defs.bzl", "java_binary", "java_library", "java_proto_library", "java_test") -load("@rules_proto//proto:defs.bzl", "proto_library") load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_java//java:defs.bzl", "java_binary", "java_library", "java_proto_library", "java_test") load("@rules_jvm_external//:defs.bzl", "artifact") load("//:java_grpc_library.bzl", "INTERNAL_java_grpc_library_for_xds", "java_grpc_library", "java_rpc_toolchain") From 226b1e5ecc7ddcbb819cfc893308ac46973d16d5 Mon Sep 17 00:00:00 2001 From: Jeroen Meijer Date: Thu, 11 Dec 2025 08:48:46 +0100 Subject: [PATCH 484/591] api: Add missing @Nullable annotation (#12559) This simple commit adds a missing but necessary @Nullable annotation to `StatusException.getTrailers()`. --- api/src/main/java/io/grpc/StatusException.java | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/main/java/io/grpc/StatusException.java b/api/src/main/java/io/grpc/StatusException.java index 2a235c3aaaf..c0a67a375b2 100644 --- a/api/src/main/java/io/grpc/StatusException.java +++ b/api/src/main/java/io/grpc/StatusException.java @@ -65,6 +65,7 @@ public final Status getStatus() { * * @since 1.0.0 */ + @Nullable public final Metadata getTrailers() { return trailers; } From 24bd540261f0eb077d9686e12c5b9e076ddae97f Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 19 Nov 2025 10:18:39 -0800 Subject: [PATCH 485/591] xds: Fix WRR test flake with TSAN Fixes: ``` org.mockito.exceptions.verification.TooManyActualInvocations: metricSink.addLongCounter( , 1L, [directaddress:///wrr-metrics], [, ] ); Wanted 1 time: -> at io.grpc.xds.WeightedRoundRobinLoadBalancerTest.metricWithRealChannel(WeightedRoundRobinLoadBalancerTest.java:1307) But was 2 times: -> at io.grpc.internal.MetricRecorderImpl.addLongCounter(MetricRecorderImpl.java:103) -> at io.grpc.internal.MetricRecorderImpl.addLongCounter(MetricRecorderImpl.java:103) at io.grpc.xds.WeightedRoundRobinLoadBalancerTest.metricWithRealChannel(WeightedRoundRobinLoadBalancerTest.java:1307) ``` --- .../java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 868bc811a70..9fac46eaf09 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -21,6 +21,7 @@ import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -1303,8 +1304,8 @@ public void metricWithRealChannel() throws Exception { assertThat(recorder.getError()).isNull(); // Make sure at least one metric works. The other tests will make sure other metrics and the - // edge cases are working. - verify(metrics).addLongCounter( + // edge cases are working. Since this is racy, we just care it happened at least once. + verify(metrics, atLeast(1)).addLongCounter( argThat((instr) -> instr.getName().equals("grpc.lb.wrr.rr_fallback")), eq(1L), eq(Arrays.asList("directaddress:///wrr-metrics")), From ed6d175fccf287face885184723bdfb021452123 Mon Sep 17 00:00:00 2001 From: Kim Jin Young Date: Tue, 16 Dec 2025 22:18:03 +0900 Subject: [PATCH 486/591] okhttp: Assert no pending streams before transport READY (#12566) ### What this PR does Based on guidance from maintainers and what I have observed in the OkHttpClientTransport lifecycle, it seems that no pending streams should exist when the transport transitions to READY. This PR adds an assertion to help verify this invariant. --- ### Note on Testing All existing tests pass with this assertion in place. --- This PR corresponds to **Step 2** of the plan discussed in #11985, by adding an explicit assertion to help ensure the transport readiness invariant. Fixes #11985 --- .../src/main/java/io/grpc/okhttp/OkHttpClientTransport.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java index 2b344554644..ce1ff29aa5b 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java @@ -802,7 +802,9 @@ public void run() { } synchronized (lock) { maxConcurrentStreams = Integer.MAX_VALUE; - startPendingStreams(); + checkState(pendingStreams.isEmpty(), + "Pending streams detected during transport start." + + " RPCs should not be started before transport is ready."); } // ClientFrameHandler need to be started after connectionPreface / settings, otherwise it // may send goAway immediately. From 7f398052de7743a32f4231fa6e72ee97051fcdf6 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 16 Dec 2025 10:38:54 -0800 Subject: [PATCH 487/591] api: Fix encoding of IPv6 scopes. (#12564) Both `java.net.Uri` and Guava's `InetAddresses` predate standardization of IPv6 scopes in URIs. They both emit/accept a naked % between the [square bracketed] address. This causes the current implementation to crash in io.grpc.Uri#getHost while percent decoding. RFC 6874 says that % in an IP-literal must be percent-encoded just as is done everywhere else. RFC 3986 & 9844 say not to support scopes at all but I think we should, for feature parity with `java.net.Uri`. (Other contemporary libraries take the same approach, e.g. https://pkg.go.dev/net/url). A future PR will provide a first-class method to convert from `java.net.Uri` handling all the edge cases. --- api/src/main/java/io/grpc/Uri.java | 28 ++++++++++- api/src/test/java/io/grpc/UriTest.java | 65 ++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/io/grpc/Uri.java b/api/src/main/java/io/grpc/Uri.java index 01aaed31c74..0ef6212d35a 100644 --- a/api/src/main/java/io/grpc/Uri.java +++ b/api/src/main/java/io/grpc/Uri.java @@ -142,6 +142,12 @@ * *

    {@link java.net.URI} and {@link Uri} both support IPv6 literals in square brackets as defined * by RFC 2732. + * + *

    {@link java.net.URI} supports IPv6 scope IDs but accepts and emits a non-standard syntax. + * {@link Uri} implements the newer RFC 6874, which percent encodes scope IDs and the % delimiter + * itself. RFC 9844 claims to obsolete RFC 6874 because web browsers would not support it. This + * class implements RFC 6874 anyway, mostly to avoid creating a barrier to migration away from + * {@link java.net.URI}. */ @Internal public final class Uri { @@ -825,12 +831,32 @@ public Builder setHost(@Nullable String regName) { */ @CanIgnoreReturnValue public Builder setHost(@Nullable InetAddress addr) { - this.host = addr != null ? InetAddresses.toUriString(addr) : null; + this.host = addr != null ? toUriString(addr) : null; return this; } + private static String toUriString(InetAddress addr) { + // InetAddresses.toUriString(addr) is almost enough but neglects RFC 6874 percent encoding. + String inetAddrStr = InetAddresses.toUriString(addr); + int percentIndex = inetAddrStr.indexOf('%'); + if (percentIndex < 0) { + return inetAddrStr; + } + + String scope = inetAddrStr.substring(percentIndex, inetAddrStr.length() - 1); + return inetAddrStr.substring(0, percentIndex) + percentEncode(scope, unreservedChars) + "]"; + } + @CanIgnoreReturnValue Builder setRawHost(String host) { + if (host.startsWith("[") && host.endsWith("]")) { + // IP-literal: Guava's isUriInetAddress() is almost enough but it doesn't check the scope. + int percentIndex = host.indexOf('%'); + if (percentIndex > 0) { + String scope = host.substring(percentIndex, host.length() - 1); + checkPercentEncodedArg(scope, "scope", unreservedChars); + } + } // IP-literal validation is complicated so we delegate it to Guava. We use this particular // method of InetAddresses because it doesn't try to match interfaces on the local machine. // (The validity of a URI should be the same no matter which machine does the parsing.) diff --git a/api/src/test/java/io/grpc/UriTest.java b/api/src/test/java/io/grpc/UriTest.java index df921e3e487..12fc9813b60 100644 --- a/api/src/test/java/io/grpc/UriTest.java +++ b/api/src/test/java/io/grpc/UriTest.java @@ -18,10 +18,13 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeNoException; import com.google.common.net.InetAddresses; import com.google.common.testing.EqualsTester; +import java.net.Inet6Address; import java.net.URISyntaxException; +import java.net.UnknownHostException; import java.util.BitSet; import org.junit.Test; import org.junit.runner.RunWith; @@ -77,6 +80,20 @@ public void parse_ipv6Literal_noPort() throws URISyntaxException { assertThat(uri.getPort()).isLessThan(0); } + @Test + public void parse_ipv6ScopedLiteral() throws URISyntaxException { + Uri uri = Uri.parse("http://[fe80::1%25eth0]"); + assertThat(uri.getRawHost()).isEqualTo("[fe80::1%25eth0]"); + assertThat(uri.getHost()).isEqualTo("[fe80::1%eth0]"); + } + + @Test + public void parse_ipv6ScopedPercentEncodedLiteral() throws URISyntaxException { + Uri uri = Uri.parse("http://[fe80::1%25foo-bar%2Fblah]"); + assertThat(uri.getRawHost()).isEqualTo("[fe80::1%25foo-bar%2Fblah]"); + assertThat(uri.getHost()).isEqualTo("[fe80::1%foo-bar/blah]"); + } + @Test public void parse_noQuery() throws URISyntaxException { Uri uri = Uri.parse("scheme://authority/path#fragment"); @@ -203,6 +220,13 @@ public void parse_invalidBackslashInHost_throws() { assertThat(e).hasMessageThat().contains("Invalid character in host"); } + @Test + public void parse_invalidBackslashScope_throws() { + URISyntaxException e = + assertThrows(URISyntaxException.class, () -> Uri.parse("http://[::1%25foo\\bar]")); + assertThat(e).hasMessageThat().contains("Invalid character in scope"); + } + @Test public void parse_emptyPort_throws() { URISyntaxException e = @@ -397,6 +421,47 @@ public void builder_ipv6Literal() throws URISyntaxException { assertThat(uri.toString()).isEqualTo("scheme://[2001:4860:4860::8844]"); } + @Test + public void builder_ipv6ScopedLiteral_numeric() throws UnknownHostException { + Uri uri = + Uri.newBuilder() + .setScheme("http") + // Create an address with a numeric scope_id, which should always be valid. + .setHost( + Inet6Address.getByAddress(null, InetAddresses.forString("fe80::1").getAddress(), 1)) + .build(); + + // We expect the scope ID to be percent encoded. + assertThat(uri.getRawHost()).isEqualTo("[fe80::1%251]"); + assertThat(uri.getHost()).isEqualTo("[fe80::1%1]"); + } + + @Test + public void builder_ipv6ScopedLiteral_named() throws UnknownHostException { + // Unfortunately, there's no Java API to create an Inet6Address with an arbitrary interface- + // scoped name. There's actually no way to hermetically create an Inet6Address with a scope name + // at all! The following address/interface is likely to be present on Linux test runners. + Inet6Address address; + try { + address = (Inet6Address) InetAddresses.forString("::1%lo"); + } catch (IllegalArgumentException e) { + assumeNoException(e); + return; // Not reached. + } + Uri uri = Uri.newBuilder().setScheme("http").setHost(address).build(); + + // We expect the scope ID to be percent encoded. + assertThat(uri.getRawHost()).isEqualTo("[::1%25lo]"); + assertThat(uri.getHost()).isEqualTo("[::1%lo]"); + } + + @Test + public void builder_ipv6PercentEncodedScopedLiteral() { + Uri uri = Uri.newBuilder().setScheme("http").setRawHost("[fe80::1%25foo%2Dbar%2Fblah]").build(); + assertThat(uri.getRawHost()).isEqualTo("[fe80::1%25foo%2Dbar%2Fblah]"); + assertThat(uri.getHost()).isEqualTo("[fe80::1%foo-bar/blah]"); + } + @Test public void builder_encodingWithAllowedReservedChars() throws URISyntaxException { Uri uri = From b3f6adf52622881776ade3a7e59c7ac05f56b56a Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Tue, 16 Dec 2025 18:52:49 -0800 Subject: [PATCH 488/591] Format Bazel files (#12560) This prevents automated transformations of BUILD files from making extraneous diff noise. --- MODULE.bazel | 25 +------------------------ WORKSPACE | 7 +++---- compiler/BUILD.bazel | 2 +- context/BUILD.bazel | 1 + examples/BUILD.bazel | 2 ++ examples/MODULE.bazel | 8 ++++---- examples/WORKSPACE | 4 +--- examples/example-alts/BUILD.bazel | 2 ++ examples/example-gauth/BUILD.bazel | 3 ++- examples/example-hostname/BUILD.bazel | 2 ++ examples/example-tls/BUILD.bazel | 2 ++ s2a/BUILD.bazel | 8 ++++---- 12 files changed, 25 insertions(+), 41 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 92749683dde..42568eef6fd 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,8 +1,8 @@ module( name = "grpc-java", + version = "1.79.0-SNAPSHOT", # CURRENT_GRPC_VERSION compatibility_level = 0, repo_name = "io_grpc_grpc_java", - version = "1.79.0-SNAPSHOT", # CURRENT_GRPC_VERSION ) # GRPC_DEPS_START @@ -63,119 +63,96 @@ maven.install( ], strict_visibility = True, ) - use_repo(maven, "maven") maven.override( coordinates = "com.google.protobuf:protobuf-java", target = "@com_google_protobuf//:protobuf_java", ) - maven.override( coordinates = "com.google.protobuf:protobuf-java-util", target = "@com_google_protobuf//:protobuf_java_util", ) - maven.override( coordinates = "com.google.protobuf:protobuf-javalite", target = "@com_google_protobuf//:protobuf_javalite", ) - maven.override( coordinates = "io.grpc:grpc-alts", target = "@io_grpc_grpc_java//alts", ) - maven.override( coordinates = "io.grpc:grpc-api", target = "@io_grpc_grpc_java//api", ) - maven.override( coordinates = "io.grpc:grpc-auth", target = "@io_grpc_grpc_java//auth", ) - maven.override( coordinates = "io.grpc:grpc-census", target = "@io_grpc_grpc_java//census", ) - maven.override( coordinates = "io.grpc:grpc-context", target = "@io_grpc_grpc_java//context", ) - maven.override( coordinates = "io.grpc:grpc-core", target = "@io_grpc_grpc_java//core:core_maven", ) - maven.override( coordinates = "io.grpc:grpc-googleapis", target = "@io_grpc_grpc_java//googleapis", ) - maven.override( coordinates = "io.grpc:grpc-grpclb", target = "@io_grpc_grpc_java//grpclb", ) - maven.override( coordinates = "io.grpc:grpc-inprocess", target = "@io_grpc_grpc_java//inprocess", ) - maven.override( coordinates = "io.grpc:grpc-netty", target = "@io_grpc_grpc_java//netty", ) - maven.override( coordinates = "io.grpc:grpc-netty-shaded", target = "@io_grpc_grpc_java//netty:shaded_maven", ) - maven.override( coordinates = "io.grpc:grpc-okhttp", target = "@io_grpc_grpc_java//okhttp", ) - maven.override( coordinates = "io.grpc:grpc-protobuf", target = "@io_grpc_grpc_java//protobuf", ) - maven.override( coordinates = "io.grpc:grpc-protobuf-lite", target = "@io_grpc_grpc_java//protobuf-lite", ) - maven.override( coordinates = "io.grpc:grpc-rls", target = "@io_grpc_grpc_java//rls", ) - maven.override( coordinates = "io.grpc:grpc-services", target = "@io_grpc_grpc_java//services:services_maven", ) - maven.override( coordinates = "io.grpc:grpc-stub", target = "@io_grpc_grpc_java//stub", ) - maven.override( coordinates = "io.grpc:grpc-testing", target = "@io_grpc_grpc_java//testing", ) - maven.override( coordinates = "io.grpc:grpc-xds", target = "@io_grpc_grpc_java//xds:xds_maven", ) - maven.override( coordinates = "io.grpc:grpc-util", target = "@io_grpc_grpc_java//util", diff --git a/WORKSPACE b/WORKSPACE index e5fa1a185f7..7d435ec82dc 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -10,14 +10,15 @@ http_archive( ) load("@bazel_features//:deps.bzl", "bazel_features_deps") + bazel_features_deps() http_archive( name = "rules_java", + sha256 = "4e1a28a25c2efa53500c928d22ceffbc505dd95b335a2d025836a293b592212f", urls = [ "https://github.com/bazelbuild/rules_java/releases/download/9.1.0/rules_java-9.1.0.tar.gz", ], - sha256 = "4e1a28a25c2efa53500c928d22ceffbc505dd95b335a2d025836a293b592212f", ) load("@rules_java//java:rules_java_deps.bzl", "compatibility_proxy_repo") @@ -32,9 +33,7 @@ http_archive( ) load("@rules_jvm_external//:defs.bzl", "maven_install") -load("//:repositories.bzl", "IO_GRPC_GRPC_JAVA_ARTIFACTS") -load("//:repositories.bzl", "IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS") -load("//:repositories.bzl", "grpc_java_repositories") +load("//:repositories.bzl", "IO_GRPC_GRPC_JAVA_ARTIFACTS", "IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS", "grpc_java_repositories") grpc_java_repositories() diff --git a/compiler/BUILD.bazel b/compiler/BUILD.bazel index 30ff3ce5dff..e8a0571e134 100644 --- a/compiler/BUILD.bazel +++ b/compiler/BUILD.bazel @@ -1,5 +1,5 @@ -load("@rules_java//java:defs.bzl", "java_library") load("@rules_cc//cc:defs.bzl", "cc_binary") +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") load("//:java_grpc_library.bzl", "java_rpc_toolchain") diff --git a/context/BUILD.bazel b/context/BUILD.bazel index fad8e2ef881..0a51dca24a9 100644 --- a/context/BUILD.bazel +++ b/context/BUILD.bazel @@ -1,4 +1,5 @@ load("@rules_java//java:defs.bzl", "java_library") + java_library( name = "context", visibility = ["//visibility:public"], diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel index 24f6966e053..e3ef8c5ac5d 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -1,6 +1,8 @@ load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library") +load("@rules_java//java:java_binary.bzl", "java_binary") +load("@rules_java//java:java_library.bzl", "java_library") proto_library( name = "helloworld_proto", diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index 78c6a42df9b..36fbdeb8420 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -1,6 +1,7 @@ -bazel_dep(name = "grpc-java", repo_name = "io_grpc_grpc_java", version = "1.79.0-SNAPSHOT") # CURRENT_GRPC_VERSION -bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58") -bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "33.1") +bazel_dep(name = "grpc-java", version = "1.79.0-SNAPSHOT", repo_name = "io_grpc_grpc_java") # CURRENT_GRPC_VERSION +bazel_dep(name = "rules_java", version = "9.3.0") +bazel_dep(name = "grpc-proto", version = "0.0.0-20240627-ec30f58", repo_name = "io_grpc_grpc_proto") +bazel_dep(name = "protobuf", version = "33.1", repo_name = "com_google_protobuf") bazel_dep(name = "rules_jvm_external", version = "6.0") # Do not use this override in your own MODULE.bazel. It is unnecessary when @@ -19,7 +20,6 @@ local_path_override( ) maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") - use_repo(maven, "maven") maven.install( diff --git a/examples/WORKSPACE b/examples/WORKSPACE index 6080a3c3d25..f26e3124562 100644 --- a/examples/WORKSPACE +++ b/examples/WORKSPACE @@ -21,10 +21,8 @@ http_archive( url = "https://github.com/bazelbuild/rules_jvm_external/releases/download/5.3/rules_jvm_external-5.3.tar.gz", ) +load("@io_grpc_grpc_java//:repositories.bzl", "IO_GRPC_GRPC_JAVA_ARTIFACTS", "IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS", "grpc_java_repositories") load("@rules_jvm_external//:defs.bzl", "maven_install") -load("@io_grpc_grpc_java//:repositories.bzl", "IO_GRPC_GRPC_JAVA_ARTIFACTS") -load("@io_grpc_grpc_java//:repositories.bzl", "IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS") -load("@io_grpc_grpc_java//:repositories.bzl", "grpc_java_repositories") grpc_java_repositories() diff --git a/examples/example-alts/BUILD.bazel b/examples/example-alts/BUILD.bazel index 2bb0d532fa5..4d66accfc19 100644 --- a/examples/example-alts/BUILD.bazel +++ b/examples/example-alts/BUILD.bazel @@ -1,6 +1,8 @@ load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library") +load("@rules_java//java:java_binary.bzl", "java_binary") +load("@rules_java//java:java_library.bzl", "java_library") proto_library( name = "helloworld_proto", diff --git a/examples/example-gauth/BUILD.bazel b/examples/example-gauth/BUILD.bazel index edc4a291e27..033c51f8856 100644 --- a/examples/example-gauth/BUILD.bazel +++ b/examples/example-gauth/BUILD.bazel @@ -1,4 +1,5 @@ -load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library") +load("@rules_java//java:java_binary.bzl", "java_binary") +load("@rules_java//java:java_library.bzl", "java_library") java_library( name = "example-gauth", diff --git a/examples/example-hostname/BUILD.bazel b/examples/example-hostname/BUILD.bazel index adb50da94f9..d5bd3aba94c 100644 --- a/examples/example-hostname/BUILD.bazel +++ b/examples/example-hostname/BUILD.bazel @@ -1,6 +1,8 @@ load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library") +load("@rules_java//java:java_binary.bzl", "java_binary") +load("@rules_java//java:java_library.bzl", "java_library") proto_library( name = "helloworld_proto", diff --git a/examples/example-tls/BUILD.bazel b/examples/example-tls/BUILD.bazel index e46f1db9e5a..cb46ef5bb30 100644 --- a/examples/example-tls/BUILD.bazel +++ b/examples/example-tls/BUILD.bazel @@ -1,6 +1,8 @@ load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library") +load("@rules_java//java:java_binary.bzl", "java_binary") +load("@rules_java//java:java_library.bzl", "java_library") proto_library( name = "helloworld_proto", diff --git a/s2a/BUILD.bazel b/s2a/BUILD.bazel index 6f58670534b..34387206ba5 100644 --- a/s2a/BUILD.bazel +++ b/s2a/BUILD.bazel @@ -56,8 +56,8 @@ java_library( "src/main/java/io/grpc/s2a/internal/handshaker/SslContextFactory.java", ], deps = [ - ":token_manager", ":s2a_identity", + ":token_manager", "//api", "//core:internal", "//netty", @@ -87,7 +87,7 @@ java_library( "//netty", artifact("com.google.code.findbugs:jsr305"), artifact("com.google.errorprone:error_prone_annotations"), - artifact("com.google.guava:guava"), - artifact("org.checkerframework:checker-qual"), + artifact("com.google.guava:guava"), + artifact("org.checkerframework:checker-qual"), ], -) \ No newline at end of file +) From 4b41af62a46372904f6bd8e08f9c1cc6d914e30c Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 16 Dec 2025 13:53:17 -0800 Subject: [PATCH 489/591] netty: Replace missed jsr305's GuardedBy with Error Prone's This was missed in 70825adce --- netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index 3f9759ee1d2..7615a96b556 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -46,6 +46,7 @@ import com.google.common.base.Ticker; import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.SettableFuture; +import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; import io.grpc.CallOptions; import io.grpc.ChannelLogger; @@ -122,7 +123,6 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.TrustManager; From 738782fb050ec94b9bf1c795cf2299b90a6ce327 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 22 Dec 2025 11:27:23 -0800 Subject: [PATCH 490/591] core: Delete ReadableBuffer.readBytes(ByteBuffer) (#12580) At the very least it isn't used now. The method is as old as ReadableBuffer itself (05a2b252b), but it appears to have never actually been used. --- .../internal/CompositeReadableBuffer.java | 20 ----------- .../internal/ForwardingReadableBuffer.java | 5 --- .../java/io/grpc/internal/ReadableBuffer.java | 9 ----- .../io/grpc/internal/ReadableBuffers.java | 24 ------------- .../internal/CompositeReadableBufferTest.java | 35 ------------------- .../ForwardingReadableBufferTest.java | 9 ----- .../grpc/internal/ReadableBufferTestBase.java | 25 ------------- .../io/grpc/netty/NettyReadableBuffer.java | 5 --- .../io/grpc/okhttp/OkHttpReadableBuffer.java | 7 ---- .../grpc/okhttp/OkHttpReadableBufferTest.java | 12 ------- 10 files changed, 151 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/CompositeReadableBuffer.java b/core/src/main/java/io/grpc/internal/CompositeReadableBuffer.java index 4c8cb72a7ce..6cedb2caee9 100644 --- a/core/src/main/java/io/grpc/internal/CompositeReadableBuffer.java +++ b/core/src/main/java/io/grpc/internal/CompositeReadableBuffer.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.io.OutputStream; -import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.InvalidMarkException; import java.util.ArrayDeque; @@ -120,20 +119,6 @@ public int read(ReadableBuffer buffer, int length, byte[] dest, int offset) { } }; - private static final NoThrowReadOperation BYTE_BUF_OP = - new NoThrowReadOperation() { - @Override - public int read(ReadableBuffer buffer, int length, ByteBuffer dest, int unused) { - // Change the limit so that only lengthToCopy bytes are available. - int prevLimit = dest.limit(); - ((Buffer) dest).limit(dest.position() + length); - // Write the bytes and restore the original limit. - buffer.readBytes(dest); - ((Buffer) dest).limit(prevLimit); - return 0; - } - }; - private static final ReadOperation STREAM_OP = new ReadOperation() { @Override @@ -149,11 +134,6 @@ public void readBytes(byte[] dest, int destOffset, int length) { executeNoThrow(BYTE_ARRAY_OP, length, dest, destOffset); } - @Override - public void readBytes(ByteBuffer dest) { - executeNoThrow(BYTE_BUF_OP, dest.remaining(), dest, 0); - } - @Override public void readBytes(OutputStream dest, int length) throws IOException { execute(STREAM_OP, length, dest, 0); diff --git a/core/src/main/java/io/grpc/internal/ForwardingReadableBuffer.java b/core/src/main/java/io/grpc/internal/ForwardingReadableBuffer.java index 06d04b6de2d..7e690309647 100644 --- a/core/src/main/java/io/grpc/internal/ForwardingReadableBuffer.java +++ b/core/src/main/java/io/grpc/internal/ForwardingReadableBuffer.java @@ -67,11 +67,6 @@ public void readBytes(byte[] dest, int destOffset, int length) { buf.readBytes(dest, destOffset, length); } - @Override - public void readBytes(ByteBuffer dest) { - buf.readBytes(dest); - } - @Override public void readBytes(OutputStream dest, int length) throws IOException { buf.readBytes(dest, length); diff --git a/core/src/main/java/io/grpc/internal/ReadableBuffer.java b/core/src/main/java/io/grpc/internal/ReadableBuffer.java index 6963c78203e..20f64719875 100644 --- a/core/src/main/java/io/grpc/internal/ReadableBuffer.java +++ b/core/src/main/java/io/grpc/internal/ReadableBuffer.java @@ -71,15 +71,6 @@ public interface ReadableBuffer extends Closeable { */ void readBytes(byte[] dest, int destOffset, int length); - /** - * Reads from this buffer until the destination's position reaches its limit, and increases the - * read position by the number of the transferred bytes. - * - * @param dest the destination buffer to receive the bytes. - * @throws IndexOutOfBoundsException if required bytes are not readable - */ - void readBytes(ByteBuffer dest); - /** * Reads {@code length} bytes from this buffer and writes them to the destination stream. * Increments the read position by {@code length}. If the required bytes are not readable, throws diff --git a/core/src/main/java/io/grpc/internal/ReadableBuffers.java b/core/src/main/java/io/grpc/internal/ReadableBuffers.java index e512c810f84..439745e29b2 100644 --- a/core/src/main/java/io/grpc/internal/ReadableBuffers.java +++ b/core/src/main/java/io/grpc/internal/ReadableBuffers.java @@ -171,15 +171,6 @@ public void readBytes(byte[] dest, int destIndex, int length) { offset += length; } - @Override - public void readBytes(ByteBuffer dest) { - Preconditions.checkNotNull(dest, "dest"); - int length = dest.remaining(); - checkReadable(length); - dest.put(bytes, offset, length); - offset += length; - } - @Override public void readBytes(OutputStream dest, int length) throws IOException { checkReadable(length); @@ -262,21 +253,6 @@ public void readBytes(byte[] dest, int destOffset, int length) { bytes.get(dest, destOffset, length); } - @Override - public void readBytes(ByteBuffer dest) { - Preconditions.checkNotNull(dest, "dest"); - int length = dest.remaining(); - checkReadable(length); - - // Change the limit so that only length bytes are available. - int prevLimit = bytes.limit(); - ((Buffer) bytes).limit(bytes.position() + length); - - // Write the bytes and restore the original limit. - dest.put(bytes); - bytes.limit(prevLimit); - } - @Override public void readBytes(OutputStream dest, int length) throws IOException { checkReadable(length); diff --git a/core/src/test/java/io/grpc/internal/CompositeReadableBufferTest.java b/core/src/test/java/io/grpc/internal/CompositeReadableBufferTest.java index 8d9248a8910..749b71d681e 100644 --- a/core/src/test/java/io/grpc/internal/CompositeReadableBufferTest.java +++ b/core/src/test/java/io/grpc/internal/CompositeReadableBufferTest.java @@ -28,8 +28,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.Buffer; -import java.nio.ByteBuffer; import java.nio.InvalidMarkException; import org.junit.After; import org.junit.Before; @@ -121,27 +119,6 @@ public void readByteArrayShouldSucceed() { assertEquals(EXPECTED_VALUE, new String(bytes, UTF_8)); } - @Test - public void readByteBufferShouldSucceed() { - ByteBuffer byteBuffer = ByteBuffer.allocate(EXPECTED_VALUE.length()); - int remaining = EXPECTED_VALUE.length(); - - ((Buffer) byteBuffer).limit(1); - composite.readBytes(byteBuffer); - remaining--; - assertEquals(remaining, composite.readableBytes()); - - ((Buffer) byteBuffer).limit(byteBuffer.limit() + 5); - composite.readBytes(byteBuffer); - remaining -= 5; - assertEquals(remaining, composite.readableBytes()); - - ((Buffer) byteBuffer).limit(byteBuffer.limit() + remaining); - composite.readBytes(byteBuffer); - assertEquals(0, composite.readableBytes()); - assertEquals(EXPECTED_VALUE, new String(byteBuffer.array(), UTF_8)); - } - @Test public void readStreamShouldSucceed() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -216,18 +193,6 @@ public void markAndResetWithReadByteArrayShouldSucceed() { assertArrayEquals(first, second); } - @Test - public void markAndResetWithReadByteBufferShouldSucceed() { - byte[] first = new byte[EXPECTED_VALUE.length()]; - composite.mark(); - composite.readBytes(ByteBuffer.wrap(first)); - composite.reset(); - byte[] second = new byte[EXPECTED_VALUE.length()]; - assertEquals(EXPECTED_VALUE.length(), composite.readableBytes()); - composite.readBytes(ByteBuffer.wrap(second)); - assertArrayEquals(first, second); - } - @Test public void markAndResetWithReadStreamShouldSucceed() throws IOException { ByteArrayOutputStream first = new ByteArrayOutputStream(); diff --git a/core/src/test/java/io/grpc/internal/ForwardingReadableBufferTest.java b/core/src/test/java/io/grpc/internal/ForwardingReadableBufferTest.java index 8ce45bc77cf..696fb35e379 100644 --- a/core/src/test/java/io/grpc/internal/ForwardingReadableBufferTest.java +++ b/core/src/test/java/io/grpc/internal/ForwardingReadableBufferTest.java @@ -25,7 +25,6 @@ import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Method; -import java.nio.ByteBuffer; import java.util.Collections; import org.junit.Before; import org.junit.Rule; @@ -91,14 +90,6 @@ public void readBytes() { verify(delegate).readBytes(dest, 1, 2); } - @Test - public void readBytes_overload1() { - ByteBuffer dest = ByteBuffer.allocate(0); - buffer.readBytes(dest); - - verify(delegate).readBytes(dest); - } - @Test public void readBytes_overload2() throws IOException { OutputStream dest = mock(OutputStream.class); diff --git a/core/src/testFixtures/java/io/grpc/internal/ReadableBufferTestBase.java b/core/src/testFixtures/java/io/grpc/internal/ReadableBufferTestBase.java index 202fb7ee8a4..2262f0466f7 100644 --- a/core/src/testFixtures/java/io/grpc/internal/ReadableBufferTestBase.java +++ b/core/src/testFixtures/java/io/grpc/internal/ReadableBufferTestBase.java @@ -21,7 +21,6 @@ import static org.junit.Assert.assertEquals; import java.io.ByteArrayOutputStream; -import java.nio.Buffer; import java.nio.ByteBuffer; import java.util.Arrays; import org.junit.Assume; @@ -83,30 +82,6 @@ public void partialReadToStreamShouldSucceed() throws Exception { assertEquals(msg.length() - 2, buffer.readableBytes()); } - @Test - public void readToByteBufferShouldSucceed() { - ReadableBuffer buffer = buffer(); - ByteBuffer byteBuffer = ByteBuffer.allocate(msg.length()); - buffer.readBytes(byteBuffer); - ((Buffer) byteBuffer).flip(); - byte[] array = new byte[msg.length()]; - byteBuffer.get(array); - assertArrayEquals(msg.getBytes(UTF_8), array); - assertEquals(0, buffer.readableBytes()); - } - - @Test - public void partialReadToByteBufferShouldSucceed() { - ReadableBuffer buffer = buffer(); - ByteBuffer byteBuffer = ByteBuffer.allocate(2); - buffer.readBytes(byteBuffer); - ((Buffer) byteBuffer).flip(); - byte[] array = new byte[2]; - byteBuffer.get(array); - assertArrayEquals(new byte[]{'h', 'e'}, array); - assertEquals(msg.length() - 2, buffer.readableBytes()); - } - @Test public void partialReadToReadableBufferShouldSucceed() { ReadableBuffer buffer = buffer(); diff --git a/netty/src/main/java/io/grpc/netty/NettyReadableBuffer.java b/netty/src/main/java/io/grpc/netty/NettyReadableBuffer.java index 7e180544de4..af5ec8d8bad 100644 --- a/netty/src/main/java/io/grpc/netty/NettyReadableBuffer.java +++ b/netty/src/main/java/io/grpc/netty/NettyReadableBuffer.java @@ -60,11 +60,6 @@ public void readBytes(byte[] dest, int index, int length) { buffer.readBytes(dest, index, length); } - @Override - public void readBytes(ByteBuffer dest) { - buffer.readBytes(dest); - } - @Override public void readBytes(OutputStream dest, int length) { try { diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpReadableBuffer.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpReadableBuffer.java index 136ee8954a2..d65453722f0 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpReadableBuffer.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpReadableBuffer.java @@ -21,7 +21,6 @@ import java.io.EOFException; import java.io.IOException; import java.io.OutputStream; -import java.nio.ByteBuffer; /** * A {@link ReadableBuffer} implementation that is backed by an {@link okio.Buffer}. @@ -71,12 +70,6 @@ public void readBytes(byte[] dest, int destOffset, int length) { } } - @Override - public void readBytes(ByteBuffer dest) { - // We are not using it. - throw new UnsupportedOperationException(); - } - @Override public void readBytes(OutputStream dest, int length) throws IOException { buffer.writeTo(dest, length); diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpReadableBufferTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpReadableBufferTest.java index 4aeeae2fa8b..be8dbf0e62b 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpReadableBufferTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpReadableBufferTest.java @@ -44,18 +44,6 @@ public void setup() { } } - @Override - @Test - public void readToByteBufferShouldSucceed() { - // Not supported. - } - - @Override - @Test - public void partialReadToByteBufferShouldSucceed() { - // Not supported. - } - @Override @Test public void markAndResetWithReadShouldSucceed() { From ead532b3910fbb3b005de59a958c6565b1e859aa Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 19 Dec 2025 13:59:42 -0800 Subject: [PATCH 491/591] core: Improve DEADLINE_EXCEEDED message for CallCreds delays DelayedStream is used both by DelayedClientTransport and CallCredentialsApplyingTransport, but it wasn't clear from the error which of the two was the cause of the delay. Now the two will have different messages. b/462499883 --- .../grpc/internal/DelayedClientTransport.java | 1 + .../java/io/grpc/internal/DelayedStream.java | 17 ++++++++++++++--- .../io/grpc/internal/MetadataApplierImpl.java | 2 +- .../internal/DelayedClientTransportTest.java | 4 ++-- .../io/grpc/internal/DelayedStreamTest.java | 6 +++--- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java index eccd8fadc8c..920a0e7006b 100644 --- a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java +++ b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java @@ -363,6 +363,7 @@ private class PendingStream extends DelayedStream { private volatile Status lastPickStatus; private PendingStream(PickSubchannelArgs args, ClientStreamTracer[] tracers) { + super("connecting_and_lb"); this.args = args; this.tracers = tracers; } diff --git a/core/src/main/java/io/grpc/internal/DelayedStream.java b/core/src/main/java/io/grpc/internal/DelayedStream.java index 2ca4630d6a1..a2b1e963ac5 100644 --- a/core/src/main/java/io/grpc/internal/DelayedStream.java +++ b/core/src/main/java/io/grpc/internal/DelayedStream.java @@ -42,6 +42,7 @@ * necessary. */ class DelayedStream implements ClientStream { + private final String bufferContext; /** {@code true} once realStream is valid and all pending calls have been drained. */ private volatile boolean passThrough; /** @@ -64,6 +65,14 @@ class DelayedStream implements ClientStream { // No need to synchronize; start() synchronization provides a happens-before private List preStartPendingCalls = new ArrayList<>(); + /** + * Create a delayed stream with debug context {@code bufferContext}. The context is what this + * stream is delayed by (e.g., "connecting", "call_credentials"). + */ + public DelayedStream(String bufferContext) { + this.bufferContext = checkNotNull(bufferContext, "bufferContext"); + } + @Override public void setMaxInboundMessageSize(final int maxSize) { checkState(listener == null, "May only be called before start"); @@ -104,11 +113,13 @@ public void appendTimeoutInsight(InsightBuilder insight) { return; } if (realStream != null) { - insight.appendKeyValue("buffered_nanos", streamSetTimeNanos - startTimeNanos); + insight.appendKeyValue( + bufferContext + "_delay", "" + (streamSetTimeNanos - startTimeNanos) + "ns"); realStream.appendTimeoutInsight(insight); } else { - insight.appendKeyValue("buffered_nanos", System.nanoTime() - startTimeNanos); - insight.append("waiting_for_connection"); + insight.appendKeyValue( + bufferContext + "_delay", "" + (System.nanoTime() - startTimeNanos) + "ns"); + insight.append("was_still_waiting"); } } } diff --git a/core/src/main/java/io/grpc/internal/MetadataApplierImpl.java b/core/src/main/java/io/grpc/internal/MetadataApplierImpl.java index 09a1018c11c..166f97b78f5 100644 --- a/core/src/main/java/io/grpc/internal/MetadataApplierImpl.java +++ b/core/src/main/java/io/grpc/internal/MetadataApplierImpl.java @@ -120,7 +120,7 @@ ClientStream returnStream() { synchronized (lock) { if (returnedStream == null) { // apply() has not been called, needs to buffer the requests. - delayedStream = new DelayedStream(); + delayedStream = new DelayedStream("call_credentials"); return returnedStream = delayedStream; } else { return returnedStream; diff --git a/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java b/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java index 902c2835a92..3baf8ca7e16 100644 --- a/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java +++ b/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java @@ -742,7 +742,7 @@ public void pendingStream_appendTimeoutInsight_waitForReady() { InsightBuilder insight = new InsightBuilder(); stream.appendTimeoutInsight(insight); assertThat(insight.toString()) - .matches("\\[wait_for_ready, buffered_nanos=[0-9]+\\, waiting_for_connection]"); + .matches("\\[wait_for_ready, connecting_and_lb_delay=[0-9]+ns\\, was_still_waiting]"); } @Test @@ -759,7 +759,7 @@ public void pendingStream_appendTimeoutInsight_waitForReady_withLastPickFailure( assertThat(insight.toString()) .matches("\\[wait_for_ready, " + "Last Pick Failure=Status\\{code=PERMISSION_DENIED, description=null, cause=null\\}," - + " buffered_nanos=[0-9]+, waiting_for_connection]"); + + " connecting_and_lb_delay=[0-9]+ns, was_still_waiting]"); } private static TransportProvider newTransportProvider(final ClientTransport transport) { diff --git a/core/src/test/java/io/grpc/internal/DelayedStreamTest.java b/core/src/test/java/io/grpc/internal/DelayedStreamTest.java index a47bea9f4ab..12c32fcf126 100644 --- a/core/src/test/java/io/grpc/internal/DelayedStreamTest.java +++ b/core/src/test/java/io/grpc/internal/DelayedStreamTest.java @@ -71,7 +71,7 @@ public class DelayedStreamTest { @Mock private ClientStreamListener listener; @Mock private ClientStream realStream; @Captor private ArgumentCaptor listenerCaptor; - private DelayedStream stream = new DelayedStream(); + private DelayedStream stream = new DelayedStream("test_op"); @Test public void setStream_setAuthority() { @@ -450,7 +450,7 @@ public void appendTimeoutInsight_realStreamNotSet() { InsightBuilder insight = new InsightBuilder(); stream.start(listener); stream.appendTimeoutInsight(insight); - assertThat(insight.toString()).matches("\\[buffered_nanos=[0-9]+\\, waiting_for_connection]"); + assertThat(insight.toString()).matches("\\[test_op_delay=[0-9]+ns\\, was_still_waiting]"); } @Test @@ -469,7 +469,7 @@ public Void answer(InvocationOnMock in) { InsightBuilder insight = new InsightBuilder(); stream.appendTimeoutInsight(insight); assertThat(insight.toString()) - .matches("\\[buffered_nanos=[0-9]+, remote_addr=127\\.0\\.0\\.1:443\\]"); + .matches("\\[test_op_delay=[0-9]+ns, remote_addr=127\\.0\\.0\\.1:443\\]"); } private void callMeMaybe(Runnable r) { From d0005b27fe8e397370b0c96c122591a2dbf6fb14 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Tue, 23 Dec 2025 10:02:49 +0530 Subject: [PATCH 492/591] grpclb: pick_first delegation (#12568) **Summary of Changes** This pull request refactors the grpclb load balancer's PICK_FIRST mode to delegate its logic to a standard pick_first load balancing policy. The key changes are as follows: 1. **`grpclb/build.gradle`** Added dependency on `grpc-util` module to access `ForwardingLoadBalancerHelper` 2. **`grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java`** - New imports: LoadBalancer, LoadBalancerProvider, LoadBalancerRegistry, ResolvedAddresses, FixedResultPicker, ForwardingLoadBalancerHelper - New fields for PICK_FIRST delegation: - pickFirstLbProvider - Provider for creating child pick_first LB - pickFirstLb - The child LoadBalancer instance - pickFirstLbState / pickFirstLbPicker - Track child LB's state and picker - currentPickFirstLoadRecorder - Load recorder for token attachment - Key behavioral changes: - updateServerList() PICK_FIRST case: Instead of creating a single subchannel, it now: - Creates the child pick_first LB once and then updates it with new addresses on subsequent updates. - Passes addresses to child LB via acceptResolvedAddresses() - maybeUpdatePicker() PICK_FIRST case: Uses child LB's state and picker wrapped with ChildLbPickerEntry - RoundRobinEntry.picked() signature change: Changed from picked(Metadata) to picked(PickSubchannelArgs) to allow child picker delegation - New ChildLbPickerEntry class: Wraps child LB's picker and attaches TokenAttachingTracerFactory for token propagation - New PickFirstLbHelper class: Forwarding helper that intercepts updateBalancingState() to store child state and trigger grpclb picker updates - Updated shutdown(), requestConnection(), maybeUseFallbackBackends(): Handle the new child LB delegation model 3. **`grpclb/src/test/java/io/grpc/grpclb/GrpclbLoadBalancerTest.java`** - Updated tests to reflect the new delegation behavior: - Initial state is now CONNECTING (not IDLE) since standard pick_first eagerly connects - Tests now verify the child LB is created only once and then handles address updates internally. - Adjusted verification expectations for the new flow - Key Behavioral Changes: - Delegation to child LB: The grpclb state no longer directly manages subchannels for PICK_FIRST mode; the child pick_first LB handles this internally. --- grpclb/BUILD.bazel | 1 + grpclb/build.gradle | 1 + .../main/java/io/grpc/grpclb/GrpclbState.java | 231 +++++++++++------ .../grpc/grpclb/GrpclbLoadBalancerTest.java | 242 ++++++++++-------- 4 files changed, 294 insertions(+), 181 deletions(-) diff --git a/grpclb/BUILD.bazel b/grpclb/BUILD.bazel index 4612968ebcd..ca9975b7ce6 100644 --- a/grpclb/BUILD.bazel +++ b/grpclb/BUILD.bazel @@ -17,6 +17,7 @@ java_library( "//context", "//core:internal", "//stub", + "//util", "@com_google_protobuf//:protobuf_java_util", "@io_grpc_grpc_proto//:grpclb_load_balancer_java_proto", artifact("com.google.code.findbugs:jsr305"), diff --git a/grpclb/build.gradle b/grpclb/build.gradle index f543e0d71fc..e8896604f03 100644 --- a/grpclb/build.gradle +++ b/grpclb/build.gradle @@ -19,6 +19,7 @@ dependencies { implementation project(':grpc-core'), project(':grpc-protobuf'), project(':grpc-stub'), + project(':grpc-util'), libraries.guava, libraries.protobuf.java, libraries.protobuf.java.util diff --git a/grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java b/grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java index 49b74645ec8..7ca20d58bce 100644 --- a/grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java +++ b/grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java @@ -37,13 +37,16 @@ import io.grpc.ConnectivityStateInfo; import io.grpc.Context; import io.grpc.EquivalentAddressGroup; -import io.grpc.LoadBalancer.CreateSubchannelArgs; +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancer.FixedResultPicker; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancer.PickResult; import io.grpc.LoadBalancer.PickSubchannelArgs; +import io.grpc.LoadBalancer.ResolvedAddresses; import io.grpc.LoadBalancer.Subchannel; import io.grpc.LoadBalancer.SubchannelPicker; -import io.grpc.LoadBalancer.SubchannelStateListener; +import io.grpc.LoadBalancerProvider; +import io.grpc.LoadBalancerRegistry; import io.grpc.ManagedChannel; import io.grpc.Metadata; import io.grpc.Status; @@ -62,6 +65,7 @@ import io.grpc.lb.v1.Server; import io.grpc.lb.v1.ServerList; import io.grpc.stub.StreamObserver; +import io.grpc.util.ForwardingLoadBalancerHelper; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; @@ -119,7 +123,7 @@ final class GrpclbState { @VisibleForTesting static final RoundRobinEntry BUFFER_ENTRY = new RoundRobinEntry() { @Override - public PickResult picked(Metadata headers) { + public PickResult picked(PickSubchannelArgs args) { return PickResult.withNoResult(); } @@ -187,6 +191,15 @@ enum Mode { new RoundRobinPicker(Collections.emptyList(), Arrays.asList(BUFFER_ENTRY)); private boolean requestConnectionPending; + // Child LoadBalancer and state for PICK_FIRST mode delegation. + private final LoadBalancerProvider pickFirstLbProvider; + @Nullable + private LoadBalancer pickFirstLb; + private ConnectivityState pickFirstLbState = CONNECTING; + private SubchannelPicker pickFirstLbPicker = new FixedResultPicker(PickResult.withNoResult()); + @Nullable + private GrpclbClientLoadRecorder currentPickFirstLoadRecorder; + GrpclbState( GrpclbConfig config, Helper helper, @@ -212,6 +225,9 @@ public void onSubchannelState( } else { this.subchannelPool = null; } + this.pickFirstLbProvider = checkNotNull( + LoadBalancerRegistry.getDefaultRegistry().getProvider("pick_first"), + "pick_first balancer not available"); this.time = checkNotNull(time, "time provider"); this.stopwatch = checkNotNull(stopwatch, "stopwatch"); this.timerService = checkNotNull(helper.getScheduledExecutorService(), "timerService"); @@ -309,6 +325,12 @@ void handleAddresses( void requestConnection() { requestConnectionPending = true; + // For PICK_FIRST mode with delegation, forward to the child LB. + if (config.getMode() == Mode.PICK_FIRST && pickFirstLb != null) { + pickFirstLb.requestConnection(); + requestConnectionPending = false; + return; + } for (RoundRobinEntry entry : currentPicker.pickList) { if (entry instanceof IdleSubchannelEntry) { ((IdleSubchannelEntry) entry).subchannel.requestConnection(); @@ -323,15 +345,23 @@ private void maybeUseFallbackBackends() { } // Balancer RPC should have either been broken or timed out. checkState(fallbackReason != null, "no reason to fallback"); - for (Subchannel subchannel : subchannels.values()) { - ConnectivityStateInfo stateInfo = subchannel.getAttributes().get(STATE_INFO).get(); - if (stateInfo.getState() == READY) { + // For PICK_FIRST mode with delegation, check the child LB's state. + if (config.getMode() == Mode.PICK_FIRST) { + if (pickFirstLb != null && pickFirstLbState == READY) { return; } - // If we do have balancer-provided backends, use one of its error in the error message if - // fail to fallback. - if (stateInfo.getState() == TRANSIENT_FAILURE) { - fallbackReason = stateInfo.getStatus(); + // For PICK_FIRST, we don't have individual subchannel states to use as fallback reason. + } else { + for (Subchannel subchannel : subchannels.values()) { + ConnectivityStateInfo stateInfo = subchannel.getAttributes().get(STATE_INFO).get(); + if (stateInfo.getState() == READY) { + return; + } + // If we do have balancer-provided backends, use one of its error in the error message if + // fail to fallback. + if (stateInfo.getState() == TRANSIENT_FAILURE) { + fallbackReason = stateInfo.getStatus(); + } } } // Fallback conditions met @@ -438,9 +468,10 @@ void shutdown() { subchannelPool.clear(); break; case PICK_FIRST: - if (!subchannels.isEmpty()) { - checkState(subchannels.size() == 1, "Excessive Subchannels: %s", subchannels); - subchannels.values().iterator().next().shutdown(); + // Shutdown the child pick_first LB which manages its own subchannels. + if (pickFirstLb != null) { + pickFirstLb.shutdown(); + pickFirstLb = null; } break; default: @@ -517,22 +548,17 @@ private void updateServerList( subchannels = Collections.unmodifiableMap(newSubchannelMap); break; case PICK_FIRST: - checkState(subchannels.size() <= 1, "Unexpected Subchannel count: %s", subchannels); - final Subchannel subchannel; + // Delegate to child pick_first LB for address management. + // Shutdown existing child LB if addresses become empty. if (newBackendAddrList.isEmpty()) { - if (subchannels.size() == 1) { - subchannel = subchannels.values().iterator().next(); - subchannel.shutdown(); - subchannels = Collections.emptyMap(); + if (pickFirstLb != null) { + pickFirstLb.shutdown(); + pickFirstLb = null; } break; } List eagList = new ArrayList<>(); - // Because for PICK_FIRST, we create a single Subchannel for all addresses, we have to - // attach the tokens to the EAG attributes and use TokenAttachingLoadRecorder to put them on - // headers. - // - // The PICK_FIRST code path doesn't cache Subchannels. + // Attach tokens to EAG attributes for TokenAttachingTracerFactory to retrieve. for (BackendAddressGroup bag : newBackendAddrList) { EquivalentAddressGroup origEag = bag.getAddresses(); Attributes eagAttrs = origEag.getAttributes(); @@ -542,30 +568,22 @@ private void updateServerList( } eagList.add(new EquivalentAddressGroup(origEag.getAddresses(), eagAttrs)); } - if (subchannels.isEmpty()) { - subchannel = - helper.createSubchannel( - CreateSubchannelArgs.newBuilder() - .setAddresses(eagList) - .setAttributes(createSubchannelAttrs()) - .build()); - subchannel.start(new SubchannelStateListener() { - @Override - public void onSubchannelState(ConnectivityStateInfo newState) { - handleSubchannelState(subchannel, newState); - } - }); - if (requestConnectionPending) { - subchannel.requestConnection(); - requestConnectionPending = false; - } - } else { - subchannel = subchannels.values().iterator().next(); - subchannel.updateAddresses(eagList); + + if (pickFirstLb == null) { + pickFirstLb = pickFirstLbProvider.newLoadBalancer(new PickFirstLbHelper()); } - subchannels = Collections.singletonMap(eagList, subchannel); - newBackendList.add( - new BackendEntry(subchannel, new TokenAttachingTracerFactory(loadRecorder))); + + // Pass addresses to child LB. + pickFirstLb.acceptResolvedAddresses( + ResolvedAddresses.newBuilder() + .setAddresses(eagList) + .build()); + if (requestConnectionPending) { + pickFirstLb.requestConnection(); + requestConnectionPending = false; + } + // Store the load recorder for token attachment. + currentPickFirstLoadRecorder = loadRecorder; break; default: throw new AssertionError("Missing case for " + config.getMode()); @@ -842,7 +860,11 @@ private void cleanUp() { private void maybeUpdatePicker() { List pickList; ConnectivityState state; - if (backendList.isEmpty()) { + // For PICK_FIRST mode with delegation, check if child LB exists instead of backendList. + boolean hasBackends = config.getMode() == Mode.PICK_FIRST + ? pickFirstLb != null + : !backendList.isEmpty(); + if (!hasBackends) { // Note balancer (is working) may enforce using fallback backends, and that fallback may // fail. So we should check if currently in fallback first. if (usingFallbackBackends) { @@ -894,26 +916,12 @@ private void maybeUpdatePicker() { } break; case PICK_FIRST: { - checkState(backendList.size() == 1, "Excessive backend entries: %s", backendList); - BackendEntry onlyEntry = backendList.get(0); - ConnectivityStateInfo stateInfo = - onlyEntry.subchannel.getAttributes().get(STATE_INFO).get(); - state = stateInfo.getState(); - switch (state) { - case READY: - pickList = Collections.singletonList(onlyEntry); - break; - case TRANSIENT_FAILURE: - pickList = - Collections.singletonList(new ErrorEntry(stateInfo.getStatus())); - break; - case CONNECTING: - pickList = Collections.singletonList(BUFFER_ENTRY); - break; - default: - pickList = Collections.singletonList( - new IdleSubchannelEntry(onlyEntry.subchannel, syncContext)); - } + // Use child LB's state and picker. Wrap the picker for token attachment. + state = pickFirstLbState; + TokenAttachingTracerFactory tracerFactory = + new TokenAttachingTracerFactory(currentPickFirstLoadRecorder); + pickList = Collections.singletonList( + new ChildLbPickerEntry(pickFirstLbPicker, tracerFactory)); break; } default: @@ -983,7 +991,7 @@ public boolean equals(Object other) { @VisibleForTesting interface RoundRobinEntry { - PickResult picked(Metadata headers); + PickResult picked(PickSubchannelArgs args); } @VisibleForTesting @@ -1024,7 +1032,8 @@ static final class BackendEntry implements RoundRobinEntry { } @Override - public PickResult picked(Metadata headers) { + public PickResult picked(PickSubchannelArgs args) { + Metadata headers = args.getHeaders(); headers.discardAll(GrpclbConstants.TOKEN_METADATA_KEY); if (token != null) { headers.put(GrpclbConstants.TOKEN_METADATA_KEY, token); @@ -1065,7 +1074,7 @@ static final class IdleSubchannelEntry implements RoundRobinEntry { } @Override - public PickResult picked(Metadata headers) { + public PickResult picked(PickSubchannelArgs args) { if (connectionRequested.compareAndSet(false, true)) { syncContext.execute(new Runnable() { @Override @@ -1108,7 +1117,7 @@ static final class ErrorEntry implements RoundRobinEntry { } @Override - public PickResult picked(Metadata headers) { + public PickResult picked(PickSubchannelArgs args) { return result; } @@ -1132,6 +1141,58 @@ public String toString() { } } + /** + * Entry that wraps a child LB's picker for PICK_FIRST mode delegation. + * Attaches TokenAttachingTracerFactory to the pick result for token propagation. + */ + @VisibleForTesting + static final class ChildLbPickerEntry implements RoundRobinEntry { + private final SubchannelPicker childPicker; + private final TokenAttachingTracerFactory tracerFactory; + + ChildLbPickerEntry(SubchannelPicker childPicker, TokenAttachingTracerFactory tracerFactory) { + this.childPicker = checkNotNull(childPicker, "childPicker"); + this.tracerFactory = checkNotNull(tracerFactory, "tracerFactory"); + } + + @Override + public PickResult picked(PickSubchannelArgs args) { + PickResult childResult = childPicker.pickSubchannel(args); + if (childResult.getSubchannel() == null) { + // No subchannel (e.g., buffer, error), return as-is. + return childResult; + } + // Wrap the pick result to attach tokens via the tracer factory. + return PickResult.withSubchannel( + childResult.getSubchannel(), tracerFactory, childResult.getAuthorityOverride()); + } + + @Override + public int hashCode() { + return Objects.hashCode(childPicker, tracerFactory); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof ChildLbPickerEntry)) { + return false; + } + ChildLbPickerEntry that = (ChildLbPickerEntry) other; + return Objects.equal(childPicker, that.childPicker) + && Objects.equal(tracerFactory, that.tracerFactory); + } + + @Override + public String toString() { + return "ChildLbPickerEntry(" + childPicker + ")"; + } + + @VisibleForTesting + SubchannelPicker getChildPicker() { + return childPicker; + } + } + @VisibleForTesting static final class RoundRobinPicker extends SubchannelPicker { @VisibleForTesting @@ -1174,7 +1235,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { if (pickIndex == pickList.size()) { pickIndex = 0; } - return pick.picked(args.getHeaders()); + return pick.picked(args); } } @@ -1189,4 +1250,28 @@ public String toString() { return MoreObjects.toStringHelper(RoundRobinPicker.class).toString(); } } + + /** + * Helper for the child pick_first LB in PICK_FIRST mode. Intercepts updateBalancingState() + * to store state and trigger the grpclb picker update with drops and token attachment. + */ + private final class PickFirstLbHelper extends ForwardingLoadBalancerHelper { + + @Override + protected Helper delegate() { + return helper; + } + + @Override + public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) { + pickFirstLbState = newState; + pickFirstLbPicker = newPicker; + // Trigger name resolution refresh on TRANSIENT_FAILURE or IDLE, similar to ROUND_ROBIN. + if (newState == TRANSIENT_FAILURE || newState == IDLE) { + helper.refreshNameResolution(); + } + maybeUseFallbackBackends(); + maybeUpdatePicker(); + } + } } diff --git a/grpclb/src/test/java/io/grpc/grpclb/GrpclbLoadBalancerTest.java b/grpclb/src/test/java/io/grpc/grpclb/GrpclbLoadBalancerTest.java index e489129676a..ef31b318cb5 100644 --- a/grpclb/src/test/java/io/grpc/grpclb/GrpclbLoadBalancerTest.java +++ b/grpclb/src/test/java/io/grpc/grpclb/GrpclbLoadBalancerTest.java @@ -72,6 +72,7 @@ import io.grpc.Status.Code; import io.grpc.SynchronizationContext; import io.grpc.grpclb.GrpclbState.BackendEntry; +import io.grpc.grpclb.GrpclbState.ChildLbPickerEntry; import io.grpc.grpclb.GrpclbState.DropEntry; import io.grpc.grpclb.GrpclbState.ErrorEntry; import io.grpc.grpclb.GrpclbState.IdleSubchannelEntry; @@ -779,7 +780,9 @@ public void receiveNoBackendAndBalancerAddress() { verify(helper).updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); RoundRobinPicker picker = (RoundRobinPicker) pickerCaptor.getValue(); assertThat(picker.dropList).isEmpty(); - Status error = Iterables.getOnlyElement(picker.pickList).picked(new Metadata()).getStatus(); + PickSubchannelArgs args = mock(PickSubchannelArgs.class); + when(args.getHeaders()).thenReturn(new Metadata()); + Status error = Iterables.getOnlyElement(picker.pickList).picked(args).getStatus(); assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(error.getDescription()).isEqualTo("No backend or balancer addresses found"); } @@ -1915,6 +1918,7 @@ public void grpclbWorking_pickFirstMode() throws Exception { lbResponseObserver.onNext(buildInitialResponse()); lbResponseObserver.onNext(buildLbResponse(backends1)); + // With delegation, the child pick_first creates the subchannel inOrder.verify(helper).createSubchannel(createSubchannelArgsCaptor.capture()); CreateSubchannelArgs createSubchannelArgs = createSubchannelArgsCaptor.getValue(); assertThat(createSubchannelArgs.getAddresses()) @@ -1922,42 +1926,41 @@ public void grpclbWorking_pickFirstMode() throws Exception { new EquivalentAddressGroup(backends1.get(0).addr, eagAttrsWithToken("token0001")), new EquivalentAddressGroup(backends1.get(1).addr, eagAttrsWithToken("token0002"))); - // Initially IDLE - inOrder.verify(helper).updateBalancingState(eq(IDLE), pickerCaptor.capture()); + // Child pick_first eagerly connects, so we start in CONNECTING + inOrder.verify(helper, atLeast(1)).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); RoundRobinPicker picker0 = (RoundRobinPicker) pickerCaptor.getValue(); - - // Only one subchannel is created + // Only one subchannel is created by the child LB assertThat(mockSubchannels).hasSize(1); Subchannel subchannel = mockSubchannels.poll(); assertThat(picker0.dropList).containsExactly(null, null); - assertThat(picker0.pickList).containsExactly(new IdleSubchannelEntry(subchannel, syncContext)); + assertThat(picker0.pickList).hasSize(1); + assertThat(picker0.pickList.get(0)).isInstanceOf(ChildLbPickerEntry.class); - // PICK_FIRST doesn't eagerly connect - verify(subchannel, never()).requestConnection(); - - // CONNECTING - deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(CONNECTING)); - inOrder.verify(helper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); - RoundRobinPicker picker1 = (RoundRobinPicker) pickerCaptor.getValue(); - assertThat(picker1.dropList).containsExactly(null, null); - assertThat(picker1.pickList).containsExactly(BUFFER_ENTRY); + // Child pick_first eagerly calls requestConnection() + verify(subchannel).requestConnection(); // TRANSIENT_FAILURE Status error = Status.UNAVAILABLE.withDescription("Simulated connection error"); deliverSubchannelState(subchannel, ConnectivityStateInfo.forTransientFailure(error)); - inOrder.verify(helper).updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); - RoundRobinPicker picker2 = (RoundRobinPicker) pickerCaptor.getValue(); - assertThat(picker2.dropList).containsExactly(null, null); - assertThat(picker2.pickList).containsExactly(new ErrorEntry(error)); + // The child LB will notify our helper, which updates grpclb state + inOrder.verify(helper, atLeast(1)) + .updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); + RoundRobinPicker picker1 = (RoundRobinPicker) pickerCaptor.getValue(); + assertThat(picker1.dropList).containsExactly(null, null); + ChildLbPickerEntry failureEntry = (ChildLbPickerEntry) picker1.pickList.get(0); + PickResult failureResult = + failureEntry.getChildPicker().pickSubchannel(mock(PickSubchannelArgs.class)); + assertThat(failureResult.getStatus()).isEqualTo(error); // READY deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY)); - inOrder.verify(helper).updateBalancingState(eq(READY), pickerCaptor.capture()); - RoundRobinPicker picker3 = (RoundRobinPicker) pickerCaptor.getValue(); - assertThat(picker3.dropList).containsExactly(null, null); - assertThat(picker3.pickList).containsExactly( - new BackendEntry(subchannel, new TokenAttachingTracerFactory(getLoadRecorder()))); - + inOrder.verify(helper, atLeast(1)).updateBalancingState(eq(READY), pickerCaptor.capture()); + RoundRobinPicker picker2 = (RoundRobinPicker) pickerCaptor.getValue(); + assertThat(picker2.dropList).containsExactly(null, null); + ChildLbPickerEntry readyEntry = (ChildLbPickerEntry) picker2.pickList.get(0); + PickResult readyResult = + readyEntry.getChildPicker().pickSubchannel(mock(PickSubchannelArgs.class)); + assertThat(readyResult.getSubchannel()).isEqualTo(subchannel); // New server list with drops List backends2 = Arrays.asList( @@ -1968,37 +1971,40 @@ public void grpclbWorking_pickFirstMode() throws Exception { .updateBalancingState(any(ConnectivityState.class), any(SubchannelPicker.class)); lbResponseObserver.onNext(buildLbResponse(backends2)); - // new addresses will be updated to the existing subchannel - // createSubchannel() has ever been called only once + // Verify child LB is updated with new addresses, NOT recreated + inOrder.verify(helper, never()).createSubchannel(any(CreateSubchannelArgs.class)); verify(helper, times(1)).createSubchannel(any(CreateSubchannelArgs.class)); assertThat(mockSubchannels).isEmpty(); + + // The child LB policy internally calls updateAddresses on the subchannel verify(subchannel).updateAddresses( eq(Arrays.asList( new EquivalentAddressGroup(backends2.get(0).addr, eagAttrsWithToken("token0001")), new EquivalentAddressGroup(backends2.get(2).addr, eagAttrsWithToken("token0004"))))); - inOrder.verify(helper).updateBalancingState(eq(READY), pickerCaptor.capture()); - RoundRobinPicker picker4 = (RoundRobinPicker) pickerCaptor.getValue(); - assertThat(picker4.dropList).containsExactly( + inOrder.verify(helper, atLeast(1)).updateBalancingState(eq(READY), pickerCaptor.capture()); + RoundRobinPicker picker3 = (RoundRobinPicker) pickerCaptor.getValue(); + assertThat(picker3.dropList).containsExactly( null, new DropEntry(getLoadRecorder(), "token0003"), null); - assertThat(picker4.pickList).containsExactly( - new BackendEntry(subchannel, new TokenAttachingTracerFactory(getLoadRecorder()))); + ChildLbPickerEntry updatedEntry = (ChildLbPickerEntry) picker3.pickList.get(0); + PickResult updatedResult = + updatedEntry.getChildPicker().pickSubchannel(mock(PickSubchannelArgs.class)); + assertThat(updatedResult.getSubchannel()).isEqualTo(subchannel); - // Subchannel goes IDLE, but PICK_FIRST will not try to reconnect + // Subchannel goes IDLE, grpclb state should follow deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(IDLE)); inOrder.verify(helper).updateBalancingState(eq(IDLE), pickerCaptor.capture()); - RoundRobinPicker picker5 = (RoundRobinPicker) pickerCaptor.getValue(); - verify(subchannel, never()).requestConnection(); + RoundRobinPicker picker4 = (RoundRobinPicker) pickerCaptor.getValue(); - // ... until it's selected + // No new connection request should have happened yet (beyond the first eager one) + verify(subchannel, times(1)).requestConnection(); PickSubchannelArgs args = mock(PickSubchannelArgs.class); - PickResult pick = picker5.pickSubchannel(args); - assertThat(pick).isSameInstanceAs(PickResult.withNoResult()); - verify(subchannel).requestConnection(); - - // ... or requested by application - balancer.requestConnection(); + PickResult pick = picker4.pickSubchannel(args); + // Child pick_first picker returns withNoResult() when IDLE and requests connection + assertThat(pick.getSubchannel()).isNull(); verify(subchannel, times(2)).requestConnection(); + balancer.requestConnection(); + verify(subchannel, times(3)).requestConnection(); // PICK_FIRST doesn't use subchannelPool verify(subchannelPool, never()) @@ -2036,6 +2042,7 @@ public void grpclbWorking_pickFirstMode_lbSendsEmptyAddress() throws Exception { lbResponseObserver.onNext(buildInitialResponse()); lbResponseObserver.onNext(buildLbResponse(backends1)); + // The child pick_first creates the first subchannel inOrder.verify(helper).createSubchannel(createSubchannelArgsCaptor.capture()); CreateSubchannelArgs createSubchannelArgs = createSubchannelArgsCaptor.getValue(); assertThat(createSubchannelArgs.getAddresses()) @@ -2043,56 +2050,43 @@ public void grpclbWorking_pickFirstMode_lbSendsEmptyAddress() throws Exception { new EquivalentAddressGroup(backends1.get(0).addr, eagAttrsWithToken("token0001")), new EquivalentAddressGroup(backends1.get(1).addr, eagAttrsWithToken("token0002"))); - // Initially IDLE - inOrder.verify(helper).updateBalancingState(eq(IDLE), pickerCaptor.capture()); + // Child pick_first eagerly connects, so initial state is CONNECTING + inOrder.verify(helper, atLeast(1)).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); RoundRobinPicker picker0 = (RoundRobinPicker) pickerCaptor.getValue(); - - // Only one subchannel is created + // Verify subchannel creation by child LB assertThat(mockSubchannels).hasSize(1); Subchannel subchannel = mockSubchannels.poll(); assertThat(picker0.dropList).containsExactly(null, null); - assertThat(picker0.pickList).containsExactly(new IdleSubchannelEntry(subchannel, syncContext)); - - // PICK_FIRST doesn't eagerly connect - verify(subchannel, never()).requestConnection(); + assertThat(picker0.pickList).hasSize(1); + assertThat(picker0.pickList.get(0)).isInstanceOf(ChildLbPickerEntry.class); - // CONNECTING - deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(CONNECTING)); - inOrder.verify(helper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); - RoundRobinPicker picker1 = (RoundRobinPicker) pickerCaptor.getValue(); - assertThat(picker1.dropList).containsExactly(null, null); - assertThat(picker1.pickList).containsExactly(BUFFER_ENTRY); - - // TRANSIENT_FAILURE - Status error = Status.UNAVAILABLE.withDescription("Simulated connection error"); - deliverSubchannelState(subchannel, ConnectivityStateInfo.forTransientFailure(error)); - inOrder.verify(helper).updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); - RoundRobinPicker picker2 = (RoundRobinPicker) pickerCaptor.getValue(); - assertThat(picker2.dropList).containsExactly(null, null); - assertThat(picker2.pickList).containsExactly(new ErrorEntry(error)); + // Child pick_first eagerly calls requestConnection() + verify(subchannel).requestConnection(); // READY deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY)); - inOrder.verify(helper).updateBalancingState(eq(READY), pickerCaptor.capture()); - RoundRobinPicker picker3 = (RoundRobinPicker) pickerCaptor.getValue(); - assertThat(picker3.dropList).containsExactly(null, null); - assertThat(picker3.pickList).containsExactly( - new BackendEntry(subchannel, new TokenAttachingTracerFactory(getLoadRecorder()))); - + inOrder.verify(helper, atLeast(1)).updateBalancingState(eq(READY), pickerCaptor.capture()); + RoundRobinPicker pickerReady = (RoundRobinPicker) pickerCaptor.getValue(); + // Verify the subchannel in the delegated picker + ChildLbPickerEntry readyEntry = (ChildLbPickerEntry) pickerReady.pickList.get(0); + assertThat( + readyEntry.getChildPicker().pickSubchannel(mock(PickSubchannelArgs.class)).getSubchannel()) + .isEqualTo(subchannel); inOrder.verify(helper, never()) .updateBalancingState(any(ConnectivityState.class), any(SubchannelPicker.class)); - // Empty addresses from LB + // Empty addresses from LB - child LB is shutdown lbResponseObserver.onNext(buildLbResponse(Collections.emptyList())); - // new addresses will be updated to the existing subchannel + // Child LB is shutdown (which shuts down its subchannel) // createSubchannel() has ever been called only once inOrder.verify(helper, never()).createSubchannel(any(CreateSubchannelArgs.class)); assertThat(mockSubchannels).isEmpty(); verify(subchannel).shutdown(); // RPC error status includes message of no backends provided by balancer - inOrder.verify(helper).updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); + inOrder.verify(helper, atLeast(1)) + .updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); RoundRobinPicker errorPicker = (RoundRobinPicker) pickerCaptor.getValue(); assertThat(errorPicker.pickList) .containsExactly(new ErrorEntry(GrpclbState.NO_AVAILABLE_BACKENDS_STATUS)); @@ -2109,18 +2103,22 @@ public void grpclbWorking_pickFirstMode_lbSendsEmptyAddress() throws Exception { .updateBalancingState(any(ConnectivityState.class), any(SubchannelPicker.class)); lbResponseObserver.onNext(buildLbResponse(backends2)); - // new addresses will be updated to the existing subchannel - inOrder.verify(helper, times(1)).createSubchannel(any(CreateSubchannelArgs.class)); - inOrder.verify(helper).updateBalancingState(eq(IDLE), pickerCaptor.capture()); - subchannel = mockSubchannels.poll(); + // A NEW child LB and NEW subchannel are created upon recovery + inOrder.verify(helper).createSubchannel(any(CreateSubchannelArgs.class)); + assertThat(mockSubchannels).hasSize(1); + Subchannel subchannel2 = mockSubchannels.poll(); + inOrder.verify(helper, atLeast(1)).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); // Subchannel became READY - deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(CONNECTING)); - deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY)); - inOrder.verify(helper).updateBalancingState(eq(READY), pickerCaptor.capture()); - RoundRobinPicker picker4 = (RoundRobinPicker) pickerCaptor.getValue(); - assertThat(picker4.pickList).containsExactly( - new BackendEntry(subchannel, new TokenAttachingTracerFactory(getLoadRecorder()))); + deliverSubchannelState(subchannel2, ConnectivityStateInfo.forNonError(READY)); + inOrder.verify(helper, atLeast(1)).updateBalancingState(eq(READY), pickerCaptor.capture()); + RoundRobinPicker pickerFinal = (RoundRobinPicker) pickerCaptor.getValue(); + assertThat(pickerFinal.dropList).containsExactly( + null, new DropEntry(getLoadRecorder(), "token0003"), null); + ChildLbPickerEntry finalEntry = (ChildLbPickerEntry) pickerFinal.pickList.get(0); + assertThat( + finalEntry.getChildPicker().pickSubchannel(mock(PickSubchannelArgs.class)).getSubchannel()) + .isEqualTo(subchannel2); } @Test @@ -2179,7 +2177,7 @@ private void pickFirstModeFallback(long timeout) throws Exception { // Fallback timer expires with no response fakeClock.forwardTime(timeout, TimeUnit.MILLISECONDS); - // Entering fallback mode + // Entering fallback mode - child LB is created for fallback backends inOrder.verify(helper).createSubchannel(createSubchannelArgsCaptor.capture()); CreateSubchannelArgs createSubchannelArgs = createSubchannelArgsCaptor.getValue(); assertThat(createSubchannelArgs.getAddresses()) @@ -2188,23 +2186,24 @@ private void pickFirstModeFallback(long timeout) throws Exception { assertThat(mockSubchannels).hasSize(1); Subchannel subchannel = mockSubchannels.poll(); - // Initially IDLE - inOrder.verify(helper).updateBalancingState(eq(IDLE), pickerCaptor.capture()); + // child pick_first eagerly connects, so initial state is CONNECTING + inOrder.verify(helper, atLeast(1)).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); RoundRobinPicker picker0 = (RoundRobinPicker) pickerCaptor.getValue(); + assertThat(picker0.pickList.get(0)).isInstanceOf(ChildLbPickerEntry.class); - // READY + // Initial eager connection request + verify(subchannel).requestConnection(); + // READY transition in fallback deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY)); - inOrder.verify(helper).updateBalancingState(eq(READY), pickerCaptor.capture()); + inOrder.verify(helper, atLeast(1)).updateBalancingState(eq(READY), pickerCaptor.capture()); RoundRobinPicker picker1 = (RoundRobinPicker) pickerCaptor.getValue(); assertThat(picker1.dropList).containsExactly(null, null); - assertThat(picker1.pickList).containsExactly( - new BackendEntry(subchannel, new TokenAttachingTracerFactory(null))); + ChildLbPickerEntry readyEntry = (ChildLbPickerEntry) picker1.pickList.get(0); + assertThat( + readyEntry.getChildPicker().pickSubchannel(mock(PickSubchannelArgs.class)).getSubchannel()) + .isEqualTo(subchannel); - assertThat(picker0.dropList).containsExactly(null, null); - assertThat(picker0.pickList).containsExactly(new IdleSubchannelEntry(subchannel, syncContext)); - - - // Finally, an LB response, which brings us out of fallback + // Finally, an LB response arrives, which brings us out of fallback List backends1 = Arrays.asList( new ServerEntry("127.0.0.1", 2000, "token0001"), new ServerEntry("127.0.0.1", 2010, "token0002")); @@ -2213,20 +2212,42 @@ private void pickFirstModeFallback(long timeout) throws Exception { lbResponseObserver.onNext(buildInitialResponse()); lbResponseObserver.onNext(buildLbResponse(backends1)); - // new addresses will be updated to the existing subchannel - // createSubchannel() has ever been called only once + // subchannel should be updated, NOT recreated inOrder.verify(helper, never()).createSubchannel(any(CreateSubchannelArgs.class)); assertThat(mockSubchannels).isEmpty(); + // The child LB internally calls updateAddresses on the existing subchannel verify(subchannel).updateAddresses( eq(Arrays.asList( new EquivalentAddressGroup(backends1.get(0).addr, eagAttrsWithToken("token0001")), new EquivalentAddressGroup(backends1.get(1).addr, eagAttrsWithToken("token0002"))))); - inOrder.verify(helper).updateBalancingState(eq(READY), pickerCaptor.capture()); + inOrder.verify(helper, atLeast(1)).updateBalancingState(eq(READY), pickerCaptor.capture()); RoundRobinPicker picker2 = (RoundRobinPicker) pickerCaptor.getValue(); assertThat(picker2.dropList).containsExactly(null, null); - assertThat(picker2.pickList).containsExactly( - new BackendEntry(subchannel, new TokenAttachingTracerFactory(getLoadRecorder()))); + + // Verify subchannel is still the same via delegated picker + ChildLbPickerEntry updatedEntry = (ChildLbPickerEntry) picker2.pickList.get(0); + assertThat( + updatedEntry.getChildPicker().pickSubchannel(mock(PickSubchannelArgs.class)) + .getSubchannel()) + .isEqualTo(subchannel); + + // Subchannel goes IDLE, grpclb follows + deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(IDLE)); + inOrder.verify(helper).updateBalancingState(eq(IDLE), pickerCaptor.capture()); + RoundRobinPicker pickerIdle = (RoundRobinPicker) pickerCaptor.getValue(); + + // Verify connection is NOT eagerly requested again yet (still only the 1st request from start) + verify(subchannel, times(1)).requestConnection(); + + // Picking while IDLE triggers a new connection request + PickSubchannelArgs args = mock(PickSubchannelArgs.class); + PickResult pick = pickerIdle.pickSubchannel(args); + assertThat(pick.getSubchannel()).isNull(); // BUFFERing while IDLE + verify(subchannel, times(2)).requestConnection(); + + balancer.requestConnection(); + verify(subchannel, times(3)).requestConnection(); // PICK_FIRST doesn't use subchannelPool verify(subchannelPool, never()) @@ -2260,6 +2281,8 @@ public void switchMode() throws Exception { List backends1 = Arrays.asList( new ServerEntry("127.0.0.1", 2000, "token0001"), new ServerEntry("127.0.0.1", 2010, "token0002")); + + // RR Mode: Ensure no updates before initial response inOrder.verify(helper, never()) .updateBalancingState(any(ConnectivityState.class), any(SubchannelPicker.class)); lbResponseObserver.onNext(buildInitialResponse()); @@ -2284,7 +2307,6 @@ public void switchMode() throws Exception { Collections.emptyList(), grpclbBalancerList, GrpclbConfig.create(Mode.PICK_FIRST)); - // GrpclbState will be shutdown, and a new one will be created assertThat(oobChannel.isShutdown()).isTrue(); verify(subchannelPool) @@ -2303,13 +2325,13 @@ public void switchMode() throws Exception { InitialLoadBalanceRequest.newBuilder().setName(SERVICE_AUTHORITY).build()) .build())); - // Simulate receiving LB response + // Simulate receiving LB response for PICK_FIRST inOrder.verify(helper, never()) .updateBalancingState(any(ConnectivityState.class), any(SubchannelPicker.class)); lbResponseObserver.onNext(buildInitialResponse()); lbResponseObserver.onNext(buildLbResponse(backends1)); - // PICK_FIRST Subchannel + // PICK_FIRST Subchannel: child LB creates it inOrder.verify(helper).createSubchannel(createSubchannelArgsCaptor.capture()); CreateSubchannelArgs createSubchannelArgs = createSubchannelArgsCaptor.getValue(); assertThat(createSubchannelArgs.getAddresses()) @@ -2317,7 +2339,9 @@ public void switchMode() throws Exception { new EquivalentAddressGroup(backends1.get(0).addr, eagAttrsWithToken("token0001")), new EquivalentAddressGroup(backends1.get(1).addr, eagAttrsWithToken("token0002"))); - inOrder.verify(helper).updateBalancingState(eq(IDLE), any(SubchannelPicker.class)); + // Child pick_first eagerly connects, so initial state is CONNECTING (not IDLE) + inOrder.verify(helper, atLeast(1)) + .updateBalancingState(eq(CONNECTING), any(SubchannelPicker.class)); } private static Attributes eagAttrsWithToken(String token) { @@ -2344,7 +2368,7 @@ public void switchMode_nullLbPolicy() throws Exception { InitialLoadBalanceRequest.newBuilder().setName(SERVICE_AUTHORITY).build()) .build())); - // Simulate receiving LB response + // Simulate receiving LB response (Initial default mode: ROUND_ROBIN) List backends1 = Arrays.asList( new ServerEntry("127.0.0.1", 2000, "token0001"), new ServerEntry("127.0.0.1", 2010, "token0002")); @@ -2391,13 +2415,13 @@ public void switchMode_nullLbPolicy() throws Exception { InitialLoadBalanceRequest.newBuilder().setName(SERVICE_AUTHORITY).build()) .build())); - // Simulate receiving LB response + // Simulate receiving LB response for PICK_FIRST inOrder.verify(helper, never()) .updateBalancingState(any(ConnectivityState.class), any(SubchannelPicker.class)); lbResponseObserver.onNext(buildInitialResponse()); lbResponseObserver.onNext(buildLbResponse(backends1)); - // PICK_FIRST Subchannel + // PICK_FIRST Subchannel: with delegation, child LB creates the subchannel inOrder.verify(helper).createSubchannel(createSubchannelArgsCaptor.capture()); CreateSubchannelArgs createSubchannelArgs = createSubchannelArgsCaptor.getValue(); assertThat(createSubchannelArgs.getAddresses()) @@ -2405,7 +2429,9 @@ public void switchMode_nullLbPolicy() throws Exception { new EquivalentAddressGroup(backends1.get(0).addr, eagAttrsWithToken("token0001")), new EquivalentAddressGroup(backends1.get(1).addr, eagAttrsWithToken("token0002"))); - inOrder.verify(helper).updateBalancingState(eq(IDLE), any(SubchannelPicker.class)); + // Child pick_first eagerly connects, so state is CONNECTING (not IDLE) + inOrder.verify(helper, atLeast(1)) + .updateBalancingState(eq(CONNECTING), any(SubchannelPicker.class)); } @Test From 6b2f7580cf0f9b0c03d9ffeb335ab95c2e54618d Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Tue, 23 Dec 2025 10:36:21 +0530 Subject: [PATCH 493/591] opentelemetry: plumb subchannel metrics disconnect error (#12342) Finishes the remaining work of [A94](https://github.com/grpc/proposal/pull/485/files) i.e. the plumbing the disconnect error --- .../internal/BinderClientTransportTest.java | 3 +- .../internal/BinderClientTransport.java | 3 +- .../RobolectricBinderTransportTest.java | 6 +- .../grpc/internal/DelayedClientTransport.java | 6 +- .../io/grpc/internal/DisconnectError.java | 34 +++++ .../grpc/internal/GoAwayDisconnectError.java | 64 +++++++++ .../io/grpc/internal/InternalSubchannel.java | 7 +- .../io/grpc/internal/KeepAliveManager.java | 27 +++- .../io/grpc/internal/ManagedChannelImpl.java | 2 +- .../grpc/internal/ManagedClientTransport.java | 3 +- .../java/io/grpc/internal/OobChannel.java | 2 +- .../grpc/internal/SimpleDisconnectError.java | 68 ++++++++++ .../io/grpc/internal/SubchannelMetrics.java | 81 ----------- .../internal/DelayedClientTransportTest.java | 30 +++-- .../grpc/internal/InternalSubchannelTest.java | 126 ++++++++++++------ .../grpc/internal/KeepAliveManagerTest.java | 13 +- .../grpc/internal/ManagedChannelImplTest.java | 65 ++++++--- .../internal/ManagedClientTransportTest.java | 4 +- .../grpc/internal/AbstractTransportTest.java | 24 ++-- .../io/grpc/cronet/CronetClientTransport.java | 3 +- .../cronet/CronetClientTransportTest.java | 4 +- .../io/grpc/inprocess/InProcessTransport.java | 3 +- .../ClientTransportLifecycleManager.java | 13 +- .../io/grpc/netty/NettyClientHandler.java | 25 ++-- .../io/grpc/netty/NettyClientTransport.java | 22 ++- .../io/grpc/netty/NettyClientHandlerTest.java | 3 +- .../grpc/netty/NettyClientTransportTest.java | 3 +- .../io/grpc/netty/NettyTransportTest.java | 3 +- .../grpc/netty/ProtocolNegotiatorsTest.java | 3 +- .../io/grpc/okhttp/OkHttpClientTransport.java | 20 ++- .../okhttp/OkHttpClientTransportTest.java | 94 ++++++++----- 31 files changed, 507 insertions(+), 257 deletions(-) create mode 100644 core/src/main/java/io/grpc/internal/DisconnectError.java create mode 100644 core/src/main/java/io/grpc/internal/GoAwayDisconnectError.java create mode 100644 core/src/main/java/io/grpc/internal/SimpleDisconnectError.java diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java index 17b83a090fe..aa3fb573ab5 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java @@ -49,6 +49,7 @@ import io.grpc.internal.ClientStream; import io.grpc.internal.ClientStreamListener; import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; +import io.grpc.internal.DisconnectError; import io.grpc.internal.FixedObjectPool; import io.grpc.internal.ManagedClientTransport; import io.grpc.internal.ObjectPool; @@ -529,7 +530,7 @@ private static final class TestTransportListener implements ManagedClientTranspo private final SettableFuture isTerminated = SettableFuture.create(); @Override - public void transportShutdown(Status shutdownStatus) { + public void transportShutdown(Status shutdownStatus, DisconnectError disconnectError) { if (!this.shutdownStatus.set(shutdownStatus)) { throw new IllegalStateException("transportShutdown() already called"); } diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 00437956a51..bef1eefd43e 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -56,6 +56,7 @@ import io.grpc.internal.GrpcUtil; import io.grpc.internal.ManagedClientTransport; import io.grpc.internal.ObjectPool; +import io.grpc.internal.SimpleDisconnectError; import io.grpc.internal.StatsTraceContext; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledFuture; @@ -305,7 +306,7 @@ public synchronized void shutdownNow(Status reason) { @Override @GuardedBy("this") void notifyShutdown(Status status) { - clientTransportListener.transportShutdown(status); + clientTransportListener.transportShutdown(status, SimpleDisconnectError.UNKNOWN); } @Override diff --git a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java index 737c5651131..8282f5e1025 100644 --- a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java @@ -26,6 +26,7 @@ import static io.grpc.binder.internal.BinderTransport.WIRE_FORMAT_VERSION; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -58,6 +59,7 @@ import io.grpc.internal.ClientTransport; import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; import io.grpc.internal.ConnectionClientTransport; +import io.grpc.internal.DisconnectError; import io.grpc.internal.GrpcUtil; import io.grpc.internal.InternalServer; import io.grpc.internal.ManagedClientTransport; @@ -357,7 +359,7 @@ public void clientIgnoresTransactionFromNonServerUids() throws Exception { sendShutdownTransportTransactionAsUid(client, serverUid); verify(mockClientTransportListener, timeout(TIMEOUT_MS)) - .transportShutdown(statusCaptor.capture()); + .transportShutdown(statusCaptor.capture(), any(DisconnectError.class)); assertThat(statusCaptor.getValue().getCode()).isEqualTo(Status.Code.UNAVAILABLE); assertThat(statusCaptor.getValue().getDescription()).contains("shutdown"); } @@ -386,7 +388,7 @@ public void clientReportsAuthzErrorToServer() throws Exception { .build(); runIfNotNull(client.start(mockClientTransportListener)); verify(mockClientTransportListener, timeout(TIMEOUT_MS)) - .transportShutdown(statusCaptor.capture()); + .transportShutdown(statusCaptor.capture(), any(DisconnectError.class)); assertThat(statusCaptor.getValue().getCode()).isEqualTo(Status.Code.PERMISSION_DENIED); // Client doesn't tell the server in this case by design -- we don't even want to start it! diff --git a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java index 920a0e7006b..5569e1eecf8 100644 --- a/core/src/main/java/io/grpc/internal/DelayedClientTransport.java +++ b/core/src/main/java/io/grpc/internal/DelayedClientTransport.java @@ -201,8 +201,8 @@ public ListenableFuture getStats() { } /** - * Prevents creating any new streams. Buffered streams are not failed and may still proceed - * when {@link #reprocess} is called. The delayed transport will be terminated when there is no + * Prevents creating any new streams. Buffered streams are not failed and may still proceed + * when {@link #reprocess} is called. The delayed transport will be terminated when there is no * more buffered streams. */ @Override @@ -215,7 +215,7 @@ public final void shutdown(final Status status) { syncContext.executeLater(new Runnable() { @Override public void run() { - listener.transportShutdown(status); + listener.transportShutdown(status, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); } }); if (!hasPendingStreams() && reportTransportTerminated != null) { diff --git a/core/src/main/java/io/grpc/internal/DisconnectError.java b/core/src/main/java/io/grpc/internal/DisconnectError.java new file mode 100644 index 00000000000..771024f106e --- /dev/null +++ b/core/src/main/java/io/grpc/internal/DisconnectError.java @@ -0,0 +1,34 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import javax.annotation.concurrent.Immutable; + +/** + * Represents the reason for a subchannel disconnection. + * Implementations are either the SimpleDisconnectError enum or the GoAwayDisconnectError class for + * dynamic ones. + */ +@Immutable +public interface DisconnectError { + /** + * Returns the string representation suitable for use as an error tag. + * + * @return The formatted error tag string. + */ + String toErrorString(); +} diff --git a/core/src/main/java/io/grpc/internal/GoAwayDisconnectError.java b/core/src/main/java/io/grpc/internal/GoAwayDisconnectError.java new file mode 100644 index 00000000000..20c8c709932 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/GoAwayDisconnectError.java @@ -0,0 +1,64 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + + +import javax.annotation.concurrent.Immutable; + +/** + * Represents a dynamic disconnection due to an HTTP/2 GOAWAY frame. + * This class is immutable and holds the specific error code from the frame. + */ +@Immutable +public final class GoAwayDisconnectError implements DisconnectError { + private static final String ERROR_TAG = "GOAWAY"; + private final GrpcUtil.Http2Error errorCode; + + /** + * Creates a GoAway reason. + * + * @param errorCode The specific, non-null HTTP/2 error code (e.g., "NO_ERROR"). + */ + public GoAwayDisconnectError(GrpcUtil.Http2Error errorCode) { + if (errorCode == null) { + throw new NullPointerException("Http2Error cannot be null for GOAWAY"); + } + this.errorCode = errorCode; + } + + @Override + public String toErrorString() { + return ERROR_TAG + " " + errorCode.name(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GoAwayDisconnectError goAwayDisconnectError = (GoAwayDisconnectError) o; + return errorCode == goAwayDisconnectError.errorCode; + } + + @Override + public int hashCode() { + return errorCode.hashCode(); + } +} diff --git a/core/src/main/java/io/grpc/internal/InternalSubchannel.java b/core/src/main/java/io/grpc/internal/InternalSubchannel.java index 649843c5c03..7a48bf642fe 100644 --- a/core/src/main/java/io/grpc/internal/InternalSubchannel.java +++ b/core/src/main/java/io/grpc/internal/InternalSubchannel.java @@ -326,7 +326,7 @@ public void run() { } /** - * Immediately attempt to reconnect if the current state is TRANSIENT_FAILURE. Otherwise this + * Immediately attempt to reconnect if the current state is TRANSIENT_FAILURE. Otherwise, this * method has no effect. */ void resetConnectBackoff() { @@ -620,7 +620,7 @@ public void transportInUse(boolean inUse) { } @Override - public void transportShutdown(final Status s) { + public void transportShutdown(final Status s, final DisconnectError disconnectError) { channelLogger.log( ChannelLogLevel.INFO, "{0} SHUTDOWN with {1}", transport.getLogId(), printShortStatus(s)); shutdownInitiated = true; @@ -639,8 +639,7 @@ public void run() { NameResolver.ATTR_BACKEND_SERVICE), /* locality= */ getAttributeOrDefault(addressIndex.getCurrentEagAttributes(), EquivalentAddressGroup.ATTR_LOCALITY_NAME), - /* disconnectError= */ SubchannelMetrics.DisconnectError.UNKNOWN - .getErrorString(null), + /* disconnectError= */ disconnectError.toErrorString(), /* securityLevel= */ extractSecurityLevel(addressIndex.getCurrentEagAttributes() .get(GrpcAttributes.ATTR_SECURITY_LEVEL))); } else if (pendingTransport == transport) { diff --git a/core/src/main/java/io/grpc/internal/KeepAliveManager.java b/core/src/main/java/io/grpc/internal/KeepAliveManager.java index d831a096087..535b3a82524 100644 --- a/core/src/main/java/io/grpc/internal/KeepAliveManager.java +++ b/core/src/main/java/io/grpc/internal/KeepAliveManager.java @@ -27,6 +27,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import javax.annotation.concurrent.ThreadSafe; /** * Manages keepalive pings. @@ -262,9 +263,25 @@ public interface KeepAlivePinger { * Default client side {@link KeepAlivePinger}. */ public static final class ClientKeepAlivePinger implements KeepAlivePinger { - private final ConnectionClientTransport transport; - public ClientKeepAlivePinger(ConnectionClientTransport transport) { + + /** + * A {@link ClientTransport} that has life-cycle management. + * + */ + @ThreadSafe + public interface TransportWithDisconnectReason extends ClientTransport { + + /** + * Initiates a forceful shutdown in which preexisting and new calls are closed. Existing calls + * should be closed with the provided {@code reason} and {@code disconnectError}. + */ + void shutdownNow(Status reason, DisconnectError disconnectError); + } + + private final TransportWithDisconnectReason transport; + + public ClientKeepAlivePinger(TransportWithDisconnectReason transport) { this.transport = transport; } @@ -277,7 +294,8 @@ public void onSuccess(long roundTripTimeNanos) {} @Override public void onFailure(Status cause) { transport.shutdownNow(Status.UNAVAILABLE.withDescription( - "Keepalive failed. The connection is likely gone")); + "Keepalive failed. The connection is likely gone"), + SimpleDisconnectError.CONNECTION_TIMED_OUT); } }, MoreExecutors.directExecutor()); } @@ -285,7 +303,8 @@ public void onFailure(Status cause) { @Override public void onPingTimeout() { transport.shutdownNow(Status.UNAVAILABLE.withDescription( - "Keepalive failed. The connection is likely gone")); + "Keepalive failed. The connection is likely gone"), + SimpleDisconnectError.CONNECTION_TIMED_OUT); } } } diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 849e4b8e45c..e9fda4e9ec3 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -2056,7 +2056,7 @@ public String toString() { */ private final class DelayedTransportListener implements ManagedClientTransport.Listener { @Override - public void transportShutdown(Status s) { + public void transportShutdown(Status s, DisconnectError e) { checkState(shutdown.get(), "Channel must have been shut down"); } diff --git a/core/src/main/java/io/grpc/internal/ManagedClientTransport.java b/core/src/main/java/io/grpc/internal/ManagedClientTransport.java index 184a4d98955..8350a005409 100644 --- a/core/src/main/java/io/grpc/internal/ManagedClientTransport.java +++ b/core/src/main/java/io/grpc/internal/ManagedClientTransport.java @@ -77,8 +77,9 @@ interface Listener { *

    This is called exactly once, and must be called prior to {@link #transportTerminated}. * * @param s the reason for the shutdown. + * @param e the disconnect error. */ - void transportShutdown(Status s); + void transportShutdown(Status s, DisconnectError e); /** * The transport completed shutting down. All resources have been released. All streams have diff --git a/core/src/main/java/io/grpc/internal/OobChannel.java b/core/src/main/java/io/grpc/internal/OobChannel.java index 30c9f55e796..71973ed5d64 100644 --- a/core/src/main/java/io/grpc/internal/OobChannel.java +++ b/core/src/main/java/io/grpc/internal/OobChannel.java @@ -117,7 +117,7 @@ public ClientStream newStream(MethodDescriptor method, this.channelz = Preconditions.checkNotNull(channelz); this.delayedTransport.start(new ManagedClientTransport.Listener() { @Override - public void transportShutdown(Status s) { + public void transportShutdown(Status s, DisconnectError e) { // Don't care } diff --git a/core/src/main/java/io/grpc/internal/SimpleDisconnectError.java b/core/src/main/java/io/grpc/internal/SimpleDisconnectError.java new file mode 100644 index 00000000000..addbfbe10a3 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/SimpleDisconnectError.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import javax.annotation.concurrent.Immutable; + +/** + * Represents a fixed, static reason for disconnection. + */ +@Immutable +public enum SimpleDisconnectError implements DisconnectError { + /** + * The subchannel was shut down for various reasons like parent channel shutdown, + * idleness, or load balancing policy changes. + */ + SUBCHANNEL_SHUTDOWN("subchannel shutdown"), + + /** + * Connection was reset (e.g., ECONNRESET, WSAECONNERESET). + */ + CONNECTION_RESET("connection reset"), + + /** + * Connection timed out (e.g., ETIMEDOUT, WSAETIMEDOUT), including closures + * from gRPC keepalives. + */ + CONNECTION_TIMED_OUT("connection timed out"), + + /** + * Connection was aborted (e.g., ECONNABORTED, WSAECONNABORTED). + */ + CONNECTION_ABORTED("connection aborted"), + + /** + * Any socket error not covered by other specific disconnect errors. + */ + SOCKET_ERROR("socket error"), + + /** + * A catch-all for any other unclassified reason. + */ + UNKNOWN("unknown"); + + private final String errorTag; + + SimpleDisconnectError(String errorTag) { + this.errorTag = errorTag; + } + + @Override + public String toErrorString() { + return this.errorTag; + } +} diff --git a/core/src/main/java/io/grpc/internal/SubchannelMetrics.java b/core/src/main/java/io/grpc/internal/SubchannelMetrics.java index 8921f13ebe6..4bc2cf47046 100644 --- a/core/src/main/java/io/grpc/internal/SubchannelMetrics.java +++ b/core/src/main/java/io/grpc/internal/SubchannelMetrics.java @@ -22,7 +22,6 @@ import io.grpc.LongUpDownCounterMetricInstrument; import io.grpc.MetricInstrumentRegistry; import io.grpc.MetricRecorder; -import javax.annotation.Nullable; final class SubchannelMetrics { @@ -106,84 +105,4 @@ public void recordDisconnection(String target, String backendService, String loc ImmutableList.of(target), ImmutableList.of(securityLevel, backendService, locality)); } - - /** - * Represents the reason for a subchannel failure. - */ - public enum DisconnectError { - - /** - * Represents an HTTP/2 GOAWAY frame. The specific error code - * (e.g., "NO_ERROR", "PROTOCOL_ERROR") should be handled separately - * as it is a dynamic part of the error. - * See RFC 9113 for error codes: https://www.rfc-editor.org/rfc/rfc9113.html#name-error-codes - */ - GOAWAY("goaway"), - - /** - * The subchannel was shut down for various reasons like parent channel shutdown, - * idleness, or load balancing policy changes. - */ - SUBCHANNEL_SHUTDOWN("subchannel shutdown"), - - /** - * Connection was reset (e.g., ECONNRESET, WSAECONNERESET). - */ - CONNECTION_RESET("connection reset"), - - /** - * Connection timed out (e.g., ETIMEDOUT, WSAETIMEDOUT), including closures - * from gRPC keepalives. - */ - CONNECTION_TIMED_OUT("connection timed out"), - - /** - * Connection was aborted (e.g., ECONNABORTED, WSAECONNABORTED). - */ - CONNECTION_ABORTED("connection aborted"), - - /** - * Any socket error not covered by other specific disconnect errors. - */ - SOCKET_ERROR("socket error"), - - /** - * A catch-all for any other unclassified reason. - */ - UNKNOWN("unknown"); - - private final String errorTag; - - /** - * Private constructor to associate a description with each enum constant. - * - * @param errorTag The detailed explanation of the error. - */ - DisconnectError(String errorTag) { - this.errorTag = errorTag; - } - - /** - * Gets the error string suitable for use as a metric tag. - * - *

    If the reason is {@code GOAWAY}, this method requires the specific - * HTTP/2 error code to create the complete tag (e.g., "goaway PROTOCOL_ERROR"). - * For all other reasons, the parameter is ignored.

    - * - * @param goawayErrorCode The specific HTTP/2 error code. This is only - * used if the reason is GOAWAY and should not be null in that case. - * @return The formatted error string. - */ - public String getErrorString(@Nullable String goawayErrorCode) { - if (this == GOAWAY) { - if (goawayErrorCode == null || goawayErrorCode.isEmpty()) { - // Return the base tag if the code is missing, or consider throwing an exception - // throw new IllegalArgumentException("goawayErrorCode is required for GOAWAY reason."); - return this.errorTag; - } - return this.errorTag + " " + goawayErrorCode; - } - return this.errorTag; - } - } } diff --git a/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java b/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java index 3baf8ca7e16..d7e1d4ca4f6 100644 --- a/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java +++ b/core/src/test/java/io/grpc/internal/DelayedClientTransportTest.java @@ -175,7 +175,8 @@ public void uncaughtException(Thread t, Throwable e) { delayedTransport.reprocess(mockPicker); assertEquals(0, delayedTransport.getPendingStreamsCount()); delayedTransport.shutdown(SHUTDOWN_STATUS); - verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS)); + verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS), + eq(SimpleDisconnectError.SUBCHANNEL_SHUTDOWN)); verify(transportListener).transportTerminated(); assertEquals(0, fakeExecutor.runDueTasks()); verify(mockRealTransport).newStream( @@ -187,7 +188,8 @@ public void uncaughtException(Thread t, Throwable e) { @Test public void transportTerminatedThenAssignTransport() { delayedTransport.shutdown(SHUTDOWN_STATUS); - verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS)); + verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS), + eq(SimpleDisconnectError.SUBCHANNEL_SHUTDOWN)); verify(transportListener).transportTerminated(); delayedTransport.reprocess(mockPicker); verifyNoMoreInteractions(transportListener); @@ -196,7 +198,8 @@ public void uncaughtException(Thread t, Throwable e) { @Test public void assignTransportThenShutdownThenNewStream() { delayedTransport.reprocess(mockPicker); delayedTransport.shutdown(SHUTDOWN_STATUS); - verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS)); + verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS), + eq(SimpleDisconnectError.SUBCHANNEL_SHUTDOWN)); verify(transportListener).transportTerminated(); ClientStream stream = delayedTransport.newStream( method, headers, callOptions, tracers); @@ -210,7 +213,8 @@ public void uncaughtException(Thread t, Throwable e) { @Test public void assignTransportThenShutdownNowThenNewStream() { delayedTransport.reprocess(mockPicker); delayedTransport.shutdownNow(Status.UNAVAILABLE); - verify(transportListener).transportShutdown(any(Status.class)); + verify(transportListener).transportShutdown(any(Status.class), + eq(SimpleDisconnectError.SUBCHANNEL_SHUTDOWN)); verify(transportListener).transportTerminated(); ClientStream stream = delayedTransport.newStream( method, headers, callOptions, tracers); @@ -241,7 +245,8 @@ public void uncaughtException(Thread t, Throwable e) { delayedTransport.shutdown(SHUTDOWN_STATUS); // Stream is still buffered - verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS)); + verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS), + eq(SimpleDisconnectError.SUBCHANNEL_SHUTDOWN)); verify(transportListener, times(0)).transportTerminated(); assertEquals(1, delayedTransport.getPendingStreamsCount()); @@ -275,7 +280,8 @@ public void uncaughtException(Thread t, Throwable e) { ClientStream stream = delayedTransport.newStream( method, new Metadata(), CallOptions.DEFAULT, tracers); delayedTransport.shutdown(SHUTDOWN_STATUS); - verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS)); + verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS), + eq(SimpleDisconnectError.SUBCHANNEL_SHUTDOWN)); verify(transportListener, times(0)).transportTerminated(); assertEquals(1, delayedTransport.getPendingStreamsCount()); stream.start(streamListener); @@ -288,7 +294,8 @@ public void uncaughtException(Thread t, Throwable e) { @Test public void shutdownThenNewStream() { delayedTransport.shutdown(SHUTDOWN_STATUS); - verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS)); + verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS), + eq(SimpleDisconnectError.SUBCHANNEL_SHUTDOWN)); verify(transportListener).transportTerminated(); ClientStream stream = delayedTransport.newStream( method, new Metadata(), CallOptions.DEFAULT, tracers); @@ -303,7 +310,8 @@ public void uncaughtException(Thread t, Throwable e) { method, new Metadata(), CallOptions.DEFAULT, tracers); stream.start(streamListener); delayedTransport.shutdownNow(Status.UNAVAILABLE); - verify(transportListener).transportShutdown(any(Status.class)); + verify(transportListener).transportShutdown(any(Status.class), + eq(SimpleDisconnectError.SUBCHANNEL_SHUTDOWN)); verify(transportListener).transportTerminated(); verify(streamListener) .closed(statusCaptor.capture(), eq(RpcProgress.REFUSED), any(Metadata.class)); @@ -312,7 +320,8 @@ public void uncaughtException(Thread t, Throwable e) { @Test public void shutdownNowThenNewStream() { delayedTransport.shutdownNow(Status.UNAVAILABLE); - verify(transportListener).transportShutdown(any(Status.class)); + verify(transportListener).transportShutdown(any(Status.class), + eq(SimpleDisconnectError.SUBCHANNEL_SHUTDOWN)); verify(transportListener).transportTerminated(); ClientStream stream = delayedTransport.newStream( method, new Metadata(), CallOptions.DEFAULT, tracers); @@ -487,7 +496,8 @@ public void uncaughtException(Thread t, Throwable e) { // wfr5 will stop delayed transport from terminating delayedTransport.shutdown(SHUTDOWN_STATUS); - verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS)); + verify(transportListener).transportShutdown(same(SHUTDOWN_STATUS), + eq(SimpleDisconnectError.SUBCHANNEL_SHUTDOWN)); verify(transportListener, never()).transportTerminated(); // ... until it's gone picker = mock(SubchannelPicker.class); diff --git a/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java b/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java index 4ac5fbac362..811344da307 100644 --- a/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java +++ b/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java @@ -233,7 +233,8 @@ public void constructor_eagListWithNull_throws() { // Fail this one. Because there is only one address to try, enter TRANSIENT_FAILURE. assertNoCallbackInvoke(); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertEquals(TRANSIENT_FAILURE, internalSubchannel.getState()); assertExactCallbackInvokes("onStateChange:" + UNAVAILABLE_STATE); // Backoff reset and using first back-off value interval @@ -264,7 +265,8 @@ public void constructor_eagListWithNull_throws() { assertNoCallbackInvoke(); // Here we use a different status from the first failure, and verify that it's passed to // the callback. - transports.poll().listener.transportShutdown(Status.RESOURCE_EXHAUSTED); + transports.poll().listener.transportShutdown(Status.RESOURCE_EXHAUSTED, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertEquals(TRANSIENT_FAILURE, internalSubchannel.getState()); assertExactCallbackInvokes("onStateChange:" + RESOURCE_EXHAUSTED_STATE); // Second back-off interval @@ -302,7 +304,8 @@ public void constructor_eagListWithNull_throws() { // Close the READY transport, will enter IDLE state. assertNoCallbackInvoke(); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertEquals(IDLE, internalSubchannel.getState()); assertExactCallbackInvokes("onStateChange:IDLE"); @@ -334,7 +337,8 @@ public void constructor_eagListWithNull_throws() { assertEquals(CONNECTING, internalSubchannel.getState()); verify(mockTransportFactory).newClientTransport(eq(addr1), any(), any()); // Let this one fail without success - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // Still in CONNECTING assertNull(internalSubchannel.obtainActiveTransport()); assertNoCallbackInvoke(); @@ -350,7 +354,8 @@ public void constructor_eagListWithNull_throws() { assertNull(internalSubchannel.obtainActiveTransport()); // Fail this one too assertNoCallbackInvoke(); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // All addresses have failed, but we aren't controlling retries. assertEquals(IDLE, internalSubchannel.getState()); assertExactCallbackInvokes("onStateChange:" + UNAVAILABLE_STATE); @@ -394,7 +399,8 @@ public void constructor_eagListWithNull_throws() { isA(TransportLogger.class)); // Let this one fail without success - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // Still in CONNECTING assertNull(internalSubchannel.obtainActiveTransport()); assertNoCallbackInvoke(); @@ -410,7 +416,8 @@ public void constructor_eagListWithNull_throws() { assertNull(internalSubchannel.obtainActiveTransport()); // Fail this one too assertNoCallbackInvoke(); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // All addresses have failed. Delayed transport will be in back-off interval. assertEquals(TRANSIENT_FAILURE, internalSubchannel.getState()); assertExactCallbackInvokes("onStateChange:" + UNAVAILABLE_STATE); @@ -441,7 +448,8 @@ public void constructor_eagListWithNull_throws() { eq(createClientTransportOptions()), isA(TransportLogger.class)); // Fail this one too - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertEquals(CONNECTING, internalSubchannel.getState()); // Forth attempt will start immediately. Keep back-off policy. @@ -455,7 +463,8 @@ public void constructor_eagListWithNull_throws() { isA(TransportLogger.class)); // Fail this one too assertNoCallbackInvoke(); - transports.poll().listener.transportShutdown(Status.RESOURCE_EXHAUSTED); + transports.poll().listener.transportShutdown(Status.RESOURCE_EXHAUSTED, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // All addresses have failed again. Delayed transport will be in back-off interval. assertExactCallbackInvokes("onStateChange:" + RESOURCE_EXHAUSTED_STATE); assertEquals(TRANSIENT_FAILURE, internalSubchannel.getState()); @@ -492,7 +501,8 @@ public void constructor_eagListWithNull_throws() { ((CallTracingTransport) internalSubchannel.obtainActiveTransport()).delegate()); // Then close it. assertNoCallbackInvoke(); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertExactCallbackInvokes("onStateChange:IDLE"); assertEquals(IDLE, internalSubchannel.getState()); @@ -508,7 +518,8 @@ public void constructor_eagListWithNull_throws() { eq(createClientTransportOptions()), isA(TransportLogger.class)); // Fail the transport - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertEquals(CONNECTING, internalSubchannel.getState()); // Second attempt will start immediately. Still no new back-off policy. @@ -520,7 +531,8 @@ public void constructor_eagListWithNull_throws() { isA(TransportLogger.class)); // Fail this one too assertEquals(CONNECTING, internalSubchannel.getState()); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // All addresses have failed. Enter TRANSIENT_FAILURE. Back-off in effect. assertExactCallbackInvokes("onStateChange:" + UNAVAILABLE_STATE); assertEquals(TRANSIENT_FAILURE, internalSubchannel.getState()); @@ -584,7 +596,8 @@ public void updateAddresses_eagListWithNull_throws() { eq(addr1), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertEquals(CONNECTING, internalSubchannel.getState()); // Second address connects @@ -606,7 +619,8 @@ public void updateAddresses_eagListWithNull_throws() { verify(transports.peek().transport, never()).shutdownNow(any(Status.class)); // And new addresses chosen when re-connecting - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertExactCallbackInvokes("onStateChange:IDLE"); assertNull(internalSubchannel.obtainActiveTransport()); @@ -616,13 +630,15 @@ public void updateAddresses_eagListWithNull_throws() { eq(addr2), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); verify(mockTransportFactory) .newClientTransport( eq(addr3), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); verifyNoMoreInteractions(mockTransportFactory); fakeClock.forwardNanos(10); // Drain retry, but don't care about result @@ -643,7 +659,8 @@ public void updateAddresses_eagListWithNull_throws() { eq(addr1), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertEquals(CONNECTING, internalSubchannel.getState()); // Second address connecting @@ -666,7 +683,8 @@ public void updateAddresses_eagListWithNull_throws() { // And new addresses chosen when re-connecting transports.peek().listener.transportReady(); assertExactCallbackInvokes("onStateChange:READY"); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertExactCallbackInvokes("onStateChange:IDLE"); assertNull(internalSubchannel.obtainActiveTransport()); @@ -676,13 +694,15 @@ public void updateAddresses_eagListWithNull_throws() { eq(addr2), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); verify(mockTransportFactory) .newClientTransport( eq(addr3), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); verifyNoMoreInteractions(mockTransportFactory); fakeClock.forwardNanos(10); // Drain retry, but don't care about result @@ -721,7 +741,8 @@ public void updateAddresses_eagListWithNull_throws() { // And no other addresses attempted assertEquals(0, fakeClock.numPendingTasks()); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertExactCallbackInvokes("onStateChange:" + UNAVAILABLE_STATE); assertEquals(TRANSIENT_FAILURE, internalSubchannel.getState()); verifyNoMoreInteractions(mockTransportFactory); @@ -745,7 +766,8 @@ public void updateAddresses_eagListWithNull_throws() { eq(addr1), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertEquals(CONNECTING, internalSubchannel.getState()); // Second address connects @@ -769,7 +791,8 @@ public void updateAddresses_eagListWithNull_throws() { verify(transports.peek().transport).shutdown(any(Status.class)); // And new addresses chosen when re-connecting - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertNoCallbackInvoke(); assertEquals(IDLE, internalSubchannel.getState()); @@ -780,13 +803,15 @@ public void updateAddresses_eagListWithNull_throws() { eq(addr3), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); verify(mockTransportFactory) .newClientTransport( eq(addr4), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); verifyNoMoreInteractions(mockTransportFactory); fakeClock.forwardNanos(10); // Drain retry, but don't care about result @@ -808,7 +833,8 @@ public void updateAddresses_eagListWithNull_throws() { eq(addr1), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertEquals(CONNECTING, internalSubchannel.getState()); // Second address connecting @@ -838,13 +864,15 @@ public void updateAddresses_eagListWithNull_throws() { eq(addr3), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); verify(mockTransportFactory) .newClientTransport( eq(addr4), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); verifyNoMoreInteractions(mockTransportFactory); fakeClock.forwardNanos(10); // Drain retry, but don't care about result @@ -928,7 +956,8 @@ public void connectIsLazy() { isA(TransportLogger.class)); // Fail this one - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertExactCallbackInvokes("onStateChange:" + UNAVAILABLE_STATE); // Will always reconnect after back-off @@ -944,7 +973,8 @@ public void connectIsLazy() { transports.peek().listener.transportReady(); assertExactCallbackInvokes("onStateChange:READY"); // Then go-away - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertExactCallbackInvokes("onStateChange:IDLE"); // No scheduled tasks that would ever try to reconnect ... @@ -974,7 +1004,8 @@ public void shutdownWhenReady() throws Exception { internalSubchannel.shutdown(SHUTDOWN_REASON); verify(transportInfo.transport).shutdown(same(SHUTDOWN_REASON)); assertExactCallbackInvokes("onStateChange:SHUTDOWN"); - transportInfo.listener.transportShutdown(SHUTDOWN_REASON); + transportInfo.listener.transportShutdown(SHUTDOWN_REASON, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportInfo.listener.transportTerminated(); assertExactCallbackInvokes("onTerminated"); @@ -997,7 +1028,8 @@ public void shutdownBeforeTransportCreated() throws Exception { // Fail this one MockClientTransportInfo transportInfo = transports.poll(); - transportInfo.listener.transportShutdown(Status.UNAVAILABLE); + transportInfo.listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportInfo.listener.transportTerminated(); // Entering TRANSIENT_FAILURE, waiting for back-off @@ -1053,7 +1085,8 @@ public void shutdownBeforeTransportReady() throws Exception { // The transport should've been shut down even though it's not the active transport yet. verify(transportInfo.transport).shutdown(same(SHUTDOWN_REASON)); - transportInfo.listener.transportShutdown(Status.UNAVAILABLE); + transportInfo.listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertNoCallbackInvoke(); transportInfo.listener.transportTerminated(); assertExactCallbackInvokes("onTerminated"); @@ -1069,7 +1102,7 @@ public void shutdownNow() throws Exception { MockClientTransportInfo t1 = transports.poll(); t1.listener.transportReady(); assertExactCallbackInvokes("onStateChange:CONNECTING", "onStateChange:READY"); - t1.listener.transportShutdown(Status.UNAVAILABLE); + t1.listener.transportShutdown(Status.UNAVAILABLE, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertExactCallbackInvokes("onStateChange:IDLE"); internalSubchannel.obtainActiveTransport(); @@ -1126,7 +1159,7 @@ public void inUseState() { t0.listener.transportInUse(true); assertExactCallbackInvokes("onInUse"); - t0.listener.transportShutdown(Status.UNAVAILABLE); + t0.listener.transportShutdown(Status.UNAVAILABLE, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertExactCallbackInvokes("onStateChange:IDLE"); assertNull(internalSubchannel.obtainActiveTransport()); @@ -1159,7 +1192,7 @@ public void transportTerminateWithoutExitingInUse() { t0.listener.transportInUse(true); assertExactCallbackInvokes("onInUse"); - t0.listener.transportShutdown(Status.UNAVAILABLE); + t0.listener.transportShutdown(Status.UNAVAILABLE, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertExactCallbackInvokes("onStateChange:IDLE"); t0.listener.transportTerminated(); assertExactCallbackInvokes("onNotInUse"); @@ -1186,12 +1219,12 @@ public void run() { assertEquals(1, runnableInvokes.get()); MockClientTransportInfo t0 = transports.poll(); - t0.listener.transportShutdown(Status.UNAVAILABLE); + t0.listener.transportShutdown(Status.UNAVAILABLE, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertEquals(2, runnableInvokes.get()); // 2nd address: reconnect immediatly MockClientTransportInfo t1 = transports.poll(); - t1.listener.transportShutdown(Status.UNAVAILABLE); + t1.listener.transportShutdown(Status.UNAVAILABLE, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // Addresses exhausted, waiting for back-off. assertEquals(2, runnableInvokes.get()); @@ -1218,7 +1251,8 @@ public void resetConnectBackoff() throws Exception { eq(addr), eq(createClientTransportOptions()), isA(TransportLogger.class)); - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertExactCallbackInvokes("onStateChange:" + UNAVAILABLE_STATE); // Save the reconnectTask @@ -1254,7 +1288,8 @@ public void resetConnectBackoff() throws Exception { // Fail the reconnect attempt to verify that a fresh reconnect policy is generated after // invoking resetConnectBackoff() - transports.poll().listener.transportShutdown(Status.UNAVAILABLE); + transports.poll().listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertExactCallbackInvokes("onStateChange:" + UNAVAILABLE_STATE); verify(mockBackoffPolicyProvider, times(2)).get(); fakeClock.forwardNanos(10); @@ -1282,7 +1317,8 @@ public void channelzMembership() throws Exception { MockClientTransportInfo t0 = transports.poll(); t0.listener.transportReady(); assertTrue(channelz.containsClientSocket(t0.transport.getLogId())); - t0.listener.transportShutdown(Status.RESOURCE_EXHAUSTED); + t0.listener.transportShutdown(Status.RESOURCE_EXHAUSTED, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); t0.listener.transportTerminated(); assertFalse(channelz.containsClientSocket(t0.transport.getLogId())); } @@ -1502,7 +1538,8 @@ public void subchannelStateChanges_triggersAttemptFailedMetric() { // b. Fail the transport before it can signal `transportReady()`. transportInfo.listener.transportShutdown( - Status.INTERNAL.withDescription("Simulated connect failure")); + Status.INTERNAL.withDescription("Simulated connect failure"), + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); fakeClock.runDueTasks(); // Process the failure event // --- Verification --- @@ -1556,7 +1593,8 @@ public void subchannelStateChanges_triggersSuccessAndDisconnectMetrics() { fakeClock.runDueTasks(); // Process the successful connection // --- Action: Transport is shut down --- - transportInfo.listener.transportShutdown(Status.UNAVAILABLE.withDescription("unknown")); + transportInfo.listener.transportShutdown(Status.UNAVAILABLE.withDescription("unknown"), + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); fakeClock.runDueTasks(); // Process the shutdown // --- Verification --- @@ -1581,7 +1619,7 @@ public void subchannelStateChanges_triggersSuccessAndDisconnectMetrics() { eqMetricInstrumentName("grpc.subchannel.disconnections"), eq(1L), eq(Arrays.asList(AUTHORITY)), - eq(Arrays.asList(BACKEND_SERVICE, LOCALITY, "unknown")) + eq(Arrays.asList(BACKEND_SERVICE, LOCALITY, "subchannel shutdown")) ); inOrder.verify(mockMetricRecorder).addLongUpDownCounter( eqMetricInstrumentName("grpc.subchannel.open_connections"), diff --git a/core/src/test/java/io/grpc/internal/KeepAliveManagerTest.java b/core/src/test/java/io/grpc/internal/KeepAliveManagerTest.java index 3cf7bfcedfe..81e3d1b2638 100644 --- a/core/src/test/java/io/grpc/internal/KeepAliveManagerTest.java +++ b/core/src/test/java/io/grpc/internal/KeepAliveManagerTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -104,13 +105,15 @@ public void keepAlivePingDelayedByIncomingData() { @Test public void clientKeepAlivePinger_pingTimeout() { - ConnectionClientTransport transport = mock(ConnectionClientTransport.class); + ClientKeepAlivePinger.TransportWithDisconnectReason transport = + mock(ClientKeepAlivePinger.TransportWithDisconnectReason.class); ClientKeepAlivePinger pinger = new ClientKeepAlivePinger(transport); pinger.onPingTimeout(); ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); - verify(transport).shutdownNow(statusCaptor.capture()); + verify(transport).shutdownNow(statusCaptor.capture(), + eq(SimpleDisconnectError.CONNECTION_TIMED_OUT)); Status status = statusCaptor.getValue(); assertThat(status.getCode()).isEqualTo(Status.Code.UNAVAILABLE); assertThat(status.getDescription()).isEqualTo( @@ -119,7 +122,8 @@ public void clientKeepAlivePinger_pingTimeout() { @Test public void clientKeepAlivePinger_pingFailure() { - ConnectionClientTransport transport = mock(ConnectionClientTransport.class); + ClientKeepAlivePinger.TransportWithDisconnectReason transport = + mock(ClientKeepAlivePinger.TransportWithDisconnectReason.class); ClientKeepAlivePinger pinger = new ClientKeepAlivePinger(transport); pinger.ping(); ArgumentCaptor pingCallbackCaptor = @@ -130,7 +134,8 @@ public void clientKeepAlivePinger_pingFailure() { pingCallback.onFailure(Status.UNAVAILABLE.withDescription("I must write descriptions")); ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); - verify(transport).shutdownNow(statusCaptor.capture()); + verify(transport).shutdownNow(statusCaptor.capture(), + eq(SimpleDisconnectError.CONNECTION_TIMED_OUT)); Status status = statusCaptor.getValue(); assertThat(status.getCode()).isEqualTo(Status.Code.UNAVAILABLE); assertThat(status.getDescription()).isEqualTo( diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 91a9f506bc8..7b3e725991c 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -798,7 +798,8 @@ public void channelzMembership_subchannel() throws Exception { transportInfo.listener.transportReady(); // terminate transport - transportInfo.listener.transportShutdown(Status.CANCELLED); + transportInfo.listener.transportShutdown(Status.CANCELLED, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportInfo.listener.transportTerminated(); assertFalse(channelz.containsClientSocket(transportInfo.transport.getLogId())); @@ -841,7 +842,8 @@ public void channelzMembership_oob() throws Exception { assertTrue(channelz.containsClientSocket(transportInfo.transport.getLogId())); // terminate transport - transportInfo.listener.transportShutdown(Status.INTERNAL); + transportInfo.listener.transportShutdown(Status.INTERNAL, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportInfo.listener.transportTerminated(); assertFalse(channelz.containsClientSocket(transportInfo.transport.getLogId())); @@ -1002,7 +1004,8 @@ private void subtestCallsAndShutdown(boolean shutdownNow, boolean shutdownNowAft } // Killing the remaining real transport will terminate the channel - transportListener.transportShutdown(Status.UNAVAILABLE); + transportListener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertFalse(channel.isTerminated()); verify(executorPool, never()).returnObject(any()); transportListener.transportTerminated(); @@ -1072,7 +1075,8 @@ public void noMoreCallbackAfterLoadBalancerShutdown() { // Since subchannels are shutdown, SubchannelStateListeners will only get SHUTDOWN regardless of // the transport states. - transportInfo1.listener.transportShutdown(Status.UNAVAILABLE); + transportInfo1.listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportInfo2.listener.transportReady(); verify(stateListener1).onSubchannelState(ConnectivityStateInfo.forNonError(SHUTDOWN)); verify(stateListener2).onSubchannelState(ConnectivityStateInfo.forNonError(SHUTDOWN)); @@ -1141,7 +1145,8 @@ public void noMoreCallbackAfterLoadBalancerShutdown_configError() throws Interru // Since subchannels are shutdown, SubchannelStateListeners will only get SHUTDOWN regardless of // the transport states. - transportInfo1.listener.transportShutdown(Status.UNAVAILABLE); + transportInfo1.listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportInfo2.listener.transportReady(); verify(stateListener1).onSubchannelState(ConnectivityStateInfo.forNonError(SHUTDOWN)); verify(stateListener2).onSubchannelState(ConnectivityStateInfo.forNonError(SHUTDOWN)); @@ -1277,7 +1282,8 @@ public void callOptionsExecutor() { verify(mockCallListener).onClose(same(Status.CANCELLED), same(trailers)); - transportListener.transportShutdown(Status.UNAVAILABLE); + transportListener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportListener.transportTerminated(); // Clean up as much as possible to allow the channel to terminate. @@ -1429,7 +1435,8 @@ public void firstResolvedServerFailedToConnect() throws Exception { MockClientTransportInfo badTransportInfo = transports.poll(); // Which failed to connect - badTransportInfo.listener.transportShutdown(Status.UNAVAILABLE); + badTransportInfo.listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); inOrder.verifyNoMoreInteractions(); // The channel then try the second address (goodAddress) @@ -1579,7 +1586,8 @@ public void allServersFailedToConnect() throws Exception { .newClientTransport( same(addr2), any(ClientTransportOptions.class), any(ChannelLogger.class)); MockClientTransportInfo transportInfo1 = transports.poll(); - transportInfo1.listener.transportShutdown(Status.UNAVAILABLE); + transportInfo1.listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // Connecting to server2, which will fail too verify(mockTransportFactory) @@ -1587,7 +1595,8 @@ public void allServersFailedToConnect() throws Exception { same(addr2), any(ClientTransportOptions.class), any(ChannelLogger.class)); MockClientTransportInfo transportInfo2 = transports.poll(); Status server2Error = Status.UNAVAILABLE.withDescription("Server2 failed to connect"); - transportInfo2.listener.transportShutdown(server2Error); + transportInfo2.listener.transportShutdown(server2Error, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // ... which makes the subchannel enter TRANSIENT_FAILURE. The last error Status is propagated // to LoadBalancer. @@ -1697,9 +1706,11 @@ public void run() { verify(transportInfo2.transport).shutdown(same(ManagedChannelImpl.SHUTDOWN_STATUS)); // Cleanup - transportInfo1.listener.transportShutdown(Status.UNAVAILABLE); + transportInfo1.listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportInfo1.listener.transportTerminated(); - transportInfo2.listener.transportShutdown(Status.UNAVAILABLE); + transportInfo2.listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportInfo2.listener.transportTerminated(); timer.forwardTime(ManagedChannelImpl.SUBCHANNEL_SHUTDOWN_DELAY_SECONDS, TimeUnit.SECONDS); } @@ -1739,8 +1750,10 @@ public void subchannelsWhenChannelShutdownNow() { verify(ti1.transport).shutdownNow(any(Status.class)); verify(ti2.transport).shutdownNow(any(Status.class)); - ti1.listener.transportShutdown(Status.UNAVAILABLE.withDescription("shutdown now")); - ti2.listener.transportShutdown(Status.UNAVAILABLE.withDescription("shutdown now")); + ti1.listener.transportShutdown(Status.UNAVAILABLE.withDescription("shutdown now"), + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); + ti2.listener.transportShutdown(Status.UNAVAILABLE.withDescription("shutdown now"), + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); ti1.listener.transportTerminated(); assertFalse(channel.isTerminated()); @@ -1829,7 +1842,8 @@ public void oobchannels() { ArgumentMatchers.any()); // The transport goes away - transportInfo.listener.transportShutdown(Status.UNAVAILABLE); + transportInfo.listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportInfo.listener.transportTerminated(); // A new call will trigger a new transport @@ -1848,7 +1862,8 @@ public void oobchannels() { // This transport fails Status transportError = Status.UNAVAILABLE.withDescription("Connection refused"); assertEquals(0, balancerRpcExecutor.numPendingTasks()); - transportInfo.listener.transportShutdown(transportError); + transportInfo.listener.transportShutdown(transportError, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); assertTrue(balancerRpcExecutor.runDueTasks() > 0); // Fail-fast RPC will fail, while wait-for-ready RPC will still be pending @@ -2102,8 +2117,10 @@ public void oobChannelsWhenChannelShutdownNow() { verify(ti1.transport).shutdownNow(any(Status.class)); verify(ti2.transport).shutdownNow(any(Status.class)); - ti1.listener.transportShutdown(Status.UNAVAILABLE.withDescription("shutdown now")); - ti2.listener.transportShutdown(Status.UNAVAILABLE.withDescription("shutdown now")); + ti1.listener.transportShutdown(Status.UNAVAILABLE.withDescription("shutdown now"), + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); + ti2.listener.transportShutdown(Status.UNAVAILABLE.withDescription("shutdown now"), + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); ti1.listener.transportTerminated(); assertFalse(channel.isTerminated()); @@ -2331,7 +2348,8 @@ private void subtestNameResolutionRefreshWhenConnectionFailed(boolean isIdle) { // Transport closed when connecting assertEquals(expectedRefreshCount, resolver.refreshCalled); - transportInfo.listener.transportShutdown(Status.UNAVAILABLE); + transportInfo.listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // When channel enters idle, new resolver is created but not started. if (!isIdle) { expectedRefreshCount++; @@ -2346,7 +2364,8 @@ private void subtestNameResolutionRefreshWhenConnectionFailed(boolean isIdle) { // Transport closed when ready assertEquals(expectedRefreshCount, resolver.refreshCalled); - transportInfo.listener.transportShutdown(Status.UNAVAILABLE); + transportInfo.listener.transportShutdown(Status.UNAVAILABLE, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // When channel enters idle, new resolver is created but not started. if (!isIdle) { expectedRefreshCount++; @@ -3917,7 +3936,8 @@ public double nextDouble() { verify(mockLoadBalancer).shutdown(); // simulating the shutdown of load balancer triggers the shutdown of subchannel shutdownSafely(helper, subchannel); - transportInfo.listener.transportShutdown(Status.INTERNAL); + transportInfo.listener.transportShutdown(Status.INTERNAL, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportInfo.listener.transportTerminated(); // simulating transport terminated assertTrue( "channel.isTerminated() is expected to be true but was false", @@ -4022,7 +4042,8 @@ public void hedgingScheduledThenChannelShutdown_hedgeShouldStillHappen_newCallSh // simulating the shutdown of load balancer triggers the shutdown of subchannel shutdownSafely(helper, subchannel); // simulating transport shutdown & terminated - transportInfo.listener.transportShutdown(Status.INTERNAL); + transportInfo.listener.transportShutdown(Status.INTERNAL, + SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportInfo.listener.transportTerminated(); assertTrue( "channel.isTerminated() is expected to be true but was false", @@ -4792,7 +4813,7 @@ public void transportTerminated(Attributes transportAttrs) { assertEquals(1, readyCallbackCalled.get()); assertEquals(0, terminationCallbackCalled.get()); - transportListener.transportShutdown(Status.OK); + transportListener.transportShutdown(Status.OK, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); transportListener.transportTerminated(); assertEquals(1, terminationCallbackCalled.get()); diff --git a/core/src/test/java/io/grpc/internal/ManagedClientTransportTest.java b/core/src/test/java/io/grpc/internal/ManagedClientTransportTest.java index 0af88a62728..5ddea08131b 100644 --- a/core/src/test/java/io/grpc/internal/ManagedClientTransportTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedClientTransportTest.java @@ -32,7 +32,7 @@ public class ManagedClientTransportTest { public void testListener() { ManagedClientTransport.Listener listener = new ManagedClientTransport.Listener() { @Override - public void transportShutdown(Status s) {} + public void transportShutdown(Status s, DisconnectError e) {} @Override public void transportTerminated() {} @@ -45,7 +45,7 @@ public void transportInUse(boolean inUse) {} }; // Test that the listener methods do not throw. - listener.transportShutdown(Status.OK); + listener.transportShutdown(Status.OK, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); listener.transportTerminated(); listener.transportReady(); listener.transportInUse(true); diff --git a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java index c60174b2faf..5d6b88a1392 100644 --- a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java +++ b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java @@ -326,7 +326,8 @@ public void serverNotListening() throws Exception { runIfNotNull(client.start(mockClientTransportListener)); verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportTerminated(); ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); - inOrder.verify(mockClientTransportListener).transportShutdown(statusCaptor.capture()); + inOrder.verify(mockClientTransportListener).transportShutdown(statusCaptor.capture(), + any(DisconnectError.class)); assertCodeEquals(Status.UNAVAILABLE, statusCaptor.getValue()); inOrder.verify(mockClientTransportListener).transportTerminated(); verify(mockClientTransportListener, never()).transportReady(); @@ -342,7 +343,8 @@ public void clientStartStop() throws Exception { Status shutdownReason = Status.UNAVAILABLE.withDescription("shutdown called"); client.shutdown(shutdownReason); verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportTerminated(); - inOrder.verify(mockClientTransportListener).transportShutdown(same(shutdownReason)); + inOrder.verify(mockClientTransportListener).transportShutdown(same(shutdownReason), + any(DisconnectError.class)); inOrder.verify(mockClientTransportListener).transportTerminated(); verify(mockClientTransportListener, never()).transportInUse(anyBoolean()); } @@ -358,7 +360,8 @@ public void clientStartAndStopOnceConnected() throws Exception { = serverListener.takeListenerOrFail(TIMEOUT_MS, TimeUnit.MILLISECONDS); client.shutdown(Status.UNAVAILABLE); verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportTerminated(); - inOrder.verify(mockClientTransportListener).transportShutdown(any(Status.class)); + inOrder.verify(mockClientTransportListener).transportShutdown(any(Status.class), + any(DisconnectError.class)); inOrder.verify(mockClientTransportListener).transportTerminated(); assertTrue(serverTransportListener.waitForTermination(TIMEOUT_MS, TimeUnit.MILLISECONDS)); server.shutdown(); @@ -454,7 +457,8 @@ public void openStreamPreventsTermination() throws Exception { serverTransport.shutdown(); serverTransport = null; - verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportShutdown(any(Status.class)); + verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportShutdown(any(Status.class), + any(DisconnectError.class)); assertTrue(serverListener.waitForShutdown(TIMEOUT_MS, TimeUnit.MILLISECONDS)); // A new server should be able to start listening, since the current server has given up @@ -504,7 +508,8 @@ public void shutdownNowKillsClientStream() throws Exception { client.shutdownNow(status); client = null; - verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportShutdown(any(Status.class)); + verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportShutdown(any(Status.class), + any(DisconnectError.class)); verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportTerminated(); verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportInUse(false); assertTrue(serverTransportListener.waitForTermination(TIMEOUT_MS, TimeUnit.MILLISECONDS)); @@ -543,7 +548,8 @@ public void shutdownNowKillsServerStream() throws Exception { serverTransport.shutdownNow(shutdownStatus); serverTransport = null; - verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportShutdown(any(Status.class)); + verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportShutdown(any(Status.class), + any(DisconnectError.class)); verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportTerminated(); verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportInUse(false); assertTrue(serverTransportListener.waitForTermination(TIMEOUT_MS, TimeUnit.MILLISECONDS)); @@ -591,7 +597,8 @@ public void ping_duringShutdown() throws Exception { ClientStreamListenerBase clientStreamListener = new ClientStreamListenerBase(); stream.start(clientStreamListener); client.shutdown(Status.UNAVAILABLE); - verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportShutdown(any(Status.class)); + verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportShutdown(any(Status.class), + any(DisconnectError.class)); ClientTransport.PingCallback mockPingCallback = mock(ClientTransport.PingCallback.class); try { client.ping(mockPingCallback, MoreExecutors.directExecutor()); @@ -635,7 +642,8 @@ public void newStream_duringShutdown() throws Exception { ClientStreamListenerBase clientStreamListener = new ClientStreamListenerBase(); stream.start(clientStreamListener); client.shutdown(Status.UNAVAILABLE); - verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportShutdown(any(Status.class)); + verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportShutdown(any(Status.class), + any(DisconnectError.class)); ClientStream stream2 = client.newStream( methodDescriptor, new Metadata(), callOptions, tracers); diff --git a/cronet/src/main/java/io/grpc/cronet/CronetClientTransport.java b/cronet/src/main/java/io/grpc/cronet/CronetClientTransport.java index 465df8b2cc9..99eb88737aa 100644 --- a/cronet/src/main/java/io/grpc/cronet/CronetClientTransport.java +++ b/cronet/src/main/java/io/grpc/cronet/CronetClientTransport.java @@ -34,6 +34,7 @@ import io.grpc.internal.ConnectionClientTransport; import io.grpc.internal.GrpcAttributes; import io.grpc.internal.GrpcUtil; +import io.grpc.internal.SimpleDisconnectError; import io.grpc.internal.StatsTraceContext; import io.grpc.internal.TransportTracer; import java.net.InetSocketAddress; @@ -229,7 +230,7 @@ private void startGoAway(Status status) { startedGoAway = true; } - listener.transportShutdown(status); + listener.transportShutdown(status, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); synchronized (lock) { goAway = true; diff --git a/cronet/src/test/java/io/grpc/cronet/CronetClientTransportTest.java b/cronet/src/test/java/io/grpc/cronet/CronetClientTransportTest.java index 03c31f93329..3a79cc0b6a8 100644 --- a/cronet/src/test/java/io/grpc/cronet/CronetClientTransportTest.java +++ b/cronet/src/test/java/io/grpc/cronet/CronetClientTransportTest.java @@ -34,6 +34,7 @@ import io.grpc.Status; import io.grpc.cronet.CronetChannelBuilder.StreamBuilderFactory; import io.grpc.internal.ClientStreamListener; +import io.grpc.internal.DisconnectError; import io.grpc.internal.GrpcAttributes; import io.grpc.internal.ManagedClientTransport; import io.grpc.internal.TransportTracer; @@ -128,7 +129,8 @@ public void shutdownTransport() throws Exception { BidirectionalStream.Callback callback2 = callbackCaptor.getValue(); // Shut down the transport. transportShutdown should be called immediately. transport.shutdown(); - verify(clientTransportListener).transportShutdown(any(Status.class)); + verify(clientTransportListener).transportShutdown(any(Status.class), + any(DisconnectError.class)); // Have two live streams. Transport has not been terminated. verify(clientTransportListener, times(0)).transportTerminated(); diff --git a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java index e31696eb631..a92f10fd5c5 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java @@ -58,6 +58,7 @@ import io.grpc.internal.ServerStreamListener; import io.grpc.internal.ServerTransport; import io.grpc.internal.ServerTransportListener; +import io.grpc.internal.SimpleDisconnectError; import io.grpc.internal.StatsTraceContext; import io.grpc.internal.StreamListener; import java.io.ByteArrayInputStream; @@ -327,7 +328,7 @@ private synchronized void notifyShutdown(Status s) { return; } shutdown = true; - clientTransportListener.transportShutdown(s); + clientTransportListener.transportShutdown(s, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); } private synchronized void notifyTerminated() { diff --git a/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java b/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java index b4e53d5568c..01e7bc3ed12 100644 --- a/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java +++ b/netty/src/main/java/io/grpc/netty/ClientTransportLifecycleManager.java @@ -19,6 +19,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.grpc.Attributes; import io.grpc.Status; +import io.grpc.internal.DisconnectError; import io.grpc.internal.ManagedClientTransport; /** Maintainer of transport lifecycle status. */ @@ -55,18 +56,18 @@ public void notifyReady() { * Marks transport as shutdown, but does not set the error status. This must eventually be * followed by a call to notifyShutdown. */ - public void notifyGracefulShutdown(Status s) { + public void notifyGracefulShutdown(Status s, DisconnectError disconnectError) { if (transportShutdown) { return; } transportShutdown = true; - listener.transportShutdown(s); + listener.transportShutdown(s, disconnectError); } /** Returns {@code true} if was the first shutdown. */ @CanIgnoreReturnValue - public boolean notifyShutdown(Status s) { - notifyGracefulShutdown(s); + public boolean notifyShutdown(Status s, DisconnectError disconnectError) { + notifyGracefulShutdown(s, disconnectError); if (shutdownStatus != null) { return false; } @@ -82,12 +83,12 @@ public void notifyInUse(boolean inUse) { listener.transportInUse(inUse); } - public void notifyTerminated(Status s) { + public void notifyTerminated(Status s, DisconnectError disconnectError) { if (transportTerminated) { return; } transportTerminated = true; - notifyShutdown(s); + notifyShutdown(s, disconnectError); listener.transportTerminated(); } diff --git a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java index d6bb3790433..8ebf89842ad 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java @@ -34,11 +34,14 @@ import io.grpc.StatusException; import io.grpc.internal.ClientStreamListener.RpcProgress; import io.grpc.internal.ClientTransport.PingCallback; +import io.grpc.internal.DisconnectError; +import io.grpc.internal.GoAwayDisconnectError; import io.grpc.internal.GrpcAttributes; import io.grpc.internal.GrpcUtil; import io.grpc.internal.Http2Ping; import io.grpc.internal.InUseStateAggregator; import io.grpc.internal.KeepAliveManager; +import io.grpc.internal.SimpleDisconnectError; import io.grpc.internal.TransportTracer; import io.grpc.netty.GrpcHttp2HeadersUtils.GrpcHttp2ClientHeadersDecoder; import io.netty.buffer.ByteBuf; @@ -478,7 +481,8 @@ public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exce logger.fine("Network channel being closed by the application."); if (ctx.channel().isActive()) { // Ignore notification that the socket was closed lifecycleManager.notifyShutdown( - Status.UNAVAILABLE.withDescription("Transport closed for unknown reason")); + Status.UNAVAILABLE.withDescription("Transport closed for unknown reason"), + SimpleDisconnectError.UNKNOWN); } super.close(ctx, promise); } @@ -491,7 +495,7 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { try { logger.fine("Network channel is closed"); Status status = Status.UNAVAILABLE.withDescription("Network closed for unknown reason"); - lifecycleManager.notifyShutdown(status); + lifecycleManager.notifyShutdown(status, SimpleDisconnectError.UNKNOWN); final Status streamStatus; if (channelInactiveReason != null) { streamStatus = channelInactiveReason; @@ -512,7 +516,7 @@ public boolean visit(Http2Stream stream) throws Http2Exception { } }); } finally { - lifecycleManager.notifyTerminated(status); + lifecycleManager.notifyTerminated(status, SimpleDisconnectError.UNKNOWN); } } finally { // Close any open streams @@ -560,7 +564,8 @@ InternalChannelz.Security getSecurityInfo() { protected void onConnectionError(ChannelHandlerContext ctx, boolean outbound, Throwable cause, Http2Exception http2Ex) { logger.log(Level.FINE, "Caught a connection error", cause); - lifecycleManager.notifyShutdown(Utils.statusFromThrowable(cause)); + lifecycleManager.notifyShutdown(Utils.statusFromThrowable(cause), + SimpleDisconnectError.SOCKET_ERROR); // Parent class will shut down the Channel super.onConnectionError(ctx, outbound, cause, http2Ex); } @@ -667,7 +672,7 @@ private void createStream(CreateStreamCommand command, ChannelPromise promise) if (!connection().goAwaySent()) { logger.fine("Stream IDs have been exhausted for this connection. " + "Initiating graceful shutdown of the connection."); - lifecycleManager.notifyShutdown(e.getStatus()); + lifecycleManager.notifyShutdown(e.getStatus(), SimpleDisconnectError.UNKNOWN); close(ctx(), ctx().newPromise()); } return; @@ -893,7 +898,7 @@ public void operationComplete(ChannelFuture future) throws Exception { private void gracefulClose(ChannelHandlerContext ctx, GracefulCloseCommand msg, ChannelPromise promise) throws Exception { - lifecycleManager.notifyShutdown(msg.getStatus()); + lifecycleManager.notifyShutdown(msg.getStatus(), SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // Explicitly flush to create any buffered streams before sending GOAWAY. // TODO(ejona): determine if the need to flush is a bug in Netty flush(ctx); @@ -929,13 +934,15 @@ public boolean visit(Http2Stream stream) throws Http2Exception { private void goingAway(long errorCode, byte[] debugData) { Status finalStatus = statusFromH2Error( Status.Code.UNAVAILABLE, "GOAWAY shut down transport", errorCode, debugData); - lifecycleManager.notifyGracefulShutdown(finalStatus); + DisconnectError disconnectError = new GoAwayDisconnectError( + GrpcUtil.Http2Error.forCode(errorCode)); + lifecycleManager.notifyGracefulShutdown(finalStatus, disconnectError); abruptGoAwayStatus = statusFromH2Error( Status.Code.UNAVAILABLE, "Abrupt GOAWAY closed unsent stream", errorCode, debugData); // While this _should_ be UNAVAILABLE, Netty uses the wrong stream id in the GOAWAY when it // fails streams due to HPACK failures (e.g., header list too large). To be more conservative, // we assume any sent streams may be related to the GOAWAY. This should rarely impact users - // since the main time servers should use abrupt GOAWAYs is if there is a protocol error, and if + // since the main time servers should use abrupt GOAWAYs if there is a protocol error, and if // there wasn't a protocol error the error code was probably NO_ERROR which is mapped to // UNAVAILABLE. https://github.com/netty/netty/issues/10670 final Status abruptGoAwayStatusConservative = statusFromH2Error( @@ -950,7 +957,7 @@ private void goingAway(long errorCode, byte[] debugData) { // This can cause reentrancy, but should be minor since it is normal to handle writes in // response to a read. Also, the call stack is rather shallow at this point clientWriteQueue.drainNow(); - if (lifecycleManager.notifyShutdown(finalStatus)) { + if (lifecycleManager.notifyShutdown(finalStatus, disconnectError)) { // This is for the only RPCs that are actually covered by the GOAWAY error code. All other // RPCs were not observed by the remote and so should be UNAVAILABLE. channelInactiveReason = statusFromH2Error( diff --git a/netty/src/main/java/io/grpc/netty/NettyClientTransport.java b/netty/src/main/java/io/grpc/netty/NettyClientTransport.java index e03989e9906..53914b3c877 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientTransport.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientTransport.java @@ -37,11 +37,13 @@ import io.grpc.Status; import io.grpc.internal.ClientStream; import io.grpc.internal.ConnectionClientTransport; +import io.grpc.internal.DisconnectError; import io.grpc.internal.FailingClientStream; import io.grpc.internal.GrpcUtil; import io.grpc.internal.Http2Ping; import io.grpc.internal.KeepAliveManager; import io.grpc.internal.KeepAliveManager.ClientKeepAlivePinger; +import io.grpc.internal.SimpleDisconnectError; import io.grpc.internal.StatsTraceContext; import io.grpc.internal.TransportTracer; import io.grpc.netty.NettyChannelBuilder.LocalSocketPicker; @@ -68,7 +70,8 @@ /** * A Netty-based {@link ConnectionClientTransport} implementation. */ -class NettyClientTransport implements ConnectionClientTransport { +class NettyClientTransport implements ConnectionClientTransport, + ClientKeepAlivePinger.TransportWithDisconnectReason { private final InternalLogId logId; private final Map, ?> channelOptions; @@ -231,8 +234,8 @@ public Runnable start(Listener transportListener) { EventLoop eventLoop = group.next(); if (keepAliveTimeNanos != KEEPALIVE_TIME_NANOS_DISABLED) { keepAliveManager = new KeepAliveManager( - new ClientKeepAlivePinger(this), eventLoop, keepAliveTimeNanos, keepAliveTimeoutNanos, - keepAliveWithoutCalls); + new ClientKeepAlivePinger(this), eventLoop, keepAliveTimeNanos, + keepAliveTimeoutNanos, keepAliveWithoutCalls); } handler = NettyClientHandler.newHandler( @@ -291,7 +294,8 @@ public void run() { // could use GlobalEventExecutor (which is what regFuture would use for notifying // listeners in this case), but avoiding on-demand thread creation in an error case seems // a good idea and is probably clearer threading. - lifecycleManager.notifyTerminated(statusExplainingWhyTheChannelIsNull); + lifecycleManager.notifyTerminated(statusExplainingWhyTheChannelIsNull, + SimpleDisconnectError.UNKNOWN); } }; } @@ -323,7 +327,8 @@ public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { // Need to notify of this failure, because NettyClientHandler may not have been added to // the pipeline before the error occurred. - lifecycleManager.notifyTerminated(Utils.statusFromThrowable(future.cause())); + lifecycleManager.notifyTerminated(Utils.statusFromThrowable(future.cause()), + SimpleDisconnectError.UNKNOWN); } } }); @@ -357,12 +362,17 @@ public void shutdown(Status reason) { @Override public void shutdownNow(final Status reason) { + shutdownNow(reason, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); + } + + @Override + public void shutdownNow(final Status reason, DisconnectError disconnectError) { // Notifying of termination is automatically done when the channel closes. if (channel != null && channel.isOpen()) { handler.getWriteQueue().enqueue(new Runnable() { @Override public void run() { - lifecycleManager.notifyShutdown(reason); + lifecycleManager.notifyShutdown(reason, disconnectError); channel.write(new ForcefulCloseCommand(reason)); } }, true); diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java index c5289296ed0..53598727efd 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java @@ -67,6 +67,7 @@ import io.grpc.internal.GrpcUtil; import io.grpc.internal.KeepAliveManager; import io.grpc.internal.ManagedClientTransport; +import io.grpc.internal.SimpleDisconnectError; import io.grpc.internal.StatsTraceContext; import io.grpc.internal.StreamListener; import io.grpc.internal.TransportTracer; @@ -764,7 +765,7 @@ public void exhaustedStreamsShouldFail() throws Exception { public void nonExistentStream() throws Exception { Status status = Status.INTERNAL.withDescription("zz"); - lifecycleManager.notifyShutdown(status); + lifecycleManager.notifyShutdown(status, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); // Stream creation can race with the transport shutting down, with the create command already // enqueued. ChannelFuture future1 = createStream(); diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index 7615a96b556..db44c8f50fd 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -64,6 +64,7 @@ import io.grpc.internal.ClientStream; import io.grpc.internal.ClientStreamListener; import io.grpc.internal.ClientTransport; +import io.grpc.internal.DisconnectError; import io.grpc.internal.FakeClock; import io.grpc.internal.FixedObjectPool; import io.grpc.internal.GrpcUtil; @@ -1462,7 +1463,7 @@ public FakeClientTransportListener(SettableFuture connected) { } @Override - public void transportShutdown(Status s) {} + public void transportShutdown(Status s, DisconnectError e) {} @Override public void transportTerminated() {} diff --git a/netty/src/test/java/io/grpc/netty/NettyTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyTransportTest.java index b1c89e22f93..b779dfbe980 100644 --- a/netty/src/test/java/io/grpc/netty/NettyTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyTransportTest.java @@ -26,6 +26,7 @@ import io.grpc.Status; import io.grpc.internal.AbstractTransportTest; import io.grpc.internal.ClientTransportFactory; +import io.grpc.internal.DisconnectError; import io.grpc.internal.FakeClock; import io.grpc.internal.InternalServer; import io.grpc.internal.ManagedClientTransport; @@ -127,7 +128,7 @@ public void channelHasUnresolvedHostname() throws Exception { .setChannelLogger(logger), logger); Runnable runnable = transport.start(new ManagedClientTransport.Listener() { @Override - public void transportShutdown(Status s) { + public void transportShutdown(Status s, DisconnectError e) { future.set(s); } diff --git a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java index cde33139965..80438532172 100644 --- a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java +++ b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java @@ -55,6 +55,7 @@ import io.grpc.TlsChannelCredentials; import io.grpc.TlsServerCredentials; import io.grpc.internal.ClientTransportFactory; +import io.grpc.internal.DisconnectError; import io.grpc.internal.GrpcAttributes; import io.grpc.internal.InternalServer; import io.grpc.internal.ManagedClientTransport; @@ -409,7 +410,7 @@ private Object expectHandshake( } else { ArgumentCaptor captor = ArgumentCaptor.forClass(Status.class); verify(clientTransportListener, timeout(TIMEOUT_SECONDS * 1000)) - .transportShutdown(captor.capture()); + .transportShutdown(captor.capture(), any(DisconnectError.class)); result = captor.getValue(); } diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java index ce1ff29aa5b..4764a6a1387 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java @@ -48,6 +48,8 @@ import io.grpc.internal.CertificateUtils; import io.grpc.internal.ClientStreamListener.RpcProgress; import io.grpc.internal.ConnectionClientTransport; +import io.grpc.internal.DisconnectError; +import io.grpc.internal.GoAwayDisconnectError; import io.grpc.internal.GrpcAttributes; import io.grpc.internal.GrpcUtil; import io.grpc.internal.Http2Ping; @@ -56,6 +58,7 @@ import io.grpc.internal.KeepAliveManager.ClientKeepAlivePinger; import io.grpc.internal.NoopSslSession; import io.grpc.internal.SerializingExecutor; +import io.grpc.internal.SimpleDisconnectError; import io.grpc.internal.StatsTraceContext; import io.grpc.internal.TransportTracer; import io.grpc.okhttp.ExceptionHandlingFrameWriter.TransportExceptionHandler; @@ -129,7 +132,7 @@ * A okhttp-based {@link ConnectionClientTransport} implementation. */ class OkHttpClientTransport implements ConnectionClientTransport, TransportExceptionHandler, - OutboundFlowController.Transport { + OutboundFlowController.Transport, ClientKeepAlivePinger.TransportWithDisconnectReason { private static final Map ERROR_CODE_TO_STATUS = buildErrorCodeToStatusMap(); private static final Logger log = Logger.getLogger(OkHttpClientTransport.class.getName()); private static final String GRPC_ENABLE_PER_RPC_AUTHORITY_CHECK = @@ -992,13 +995,18 @@ public void shutdown(Status reason) { } goAwayStatus = reason; - listener.transportShutdown(goAwayStatus); + listener.transportShutdown(goAwayStatus, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); stopIfNecessary(); } } @Override public void shutdownNow(Status reason) { + shutdownNow(reason, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); + } + + @Override + public void shutdownNow(Status reason, DisconnectError disconnectError) { shutdown(reason); synchronized (lock) { Iterator> it = streams.entrySet().iterator(); @@ -1088,7 +1096,13 @@ private void startGoAway(int lastKnownStreamId, ErrorCode errorCode, Status stat synchronized (lock) { if (goAwayStatus == null) { goAwayStatus = status; - listener.transportShutdown(status); + GrpcUtil.Http2Error http2Error; + if (errorCode == null) { + http2Error = GrpcUtil.Http2Error.NO_ERROR; + } else { + http2Error = GrpcUtil.Http2Error.forCode(errorCode.httpCode); + } + listener.transportShutdown(status, new GoAwayDisconnectError(http2Error)); } if (errorCode != null && !goAwaySent) { // Send GOAWAY with lastGoodStreamId of 0, since we don't expect any server-initiated diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java index 99f430be009..f87912c44ea 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java @@ -71,9 +71,12 @@ import io.grpc.internal.ClientStream; import io.grpc.internal.ClientStreamListener; import io.grpc.internal.ClientTransport; +import io.grpc.internal.DisconnectError; import io.grpc.internal.FakeClock; +import io.grpc.internal.GoAwayDisconnectError; import io.grpc.internal.GrpcUtil; import io.grpc.internal.ManagedClientTransport; +import io.grpc.internal.SimpleDisconnectError; import io.grpc.okhttp.OkHttpClientTransport.ClientFrameHandler; import io.grpc.okhttp.OkHttpFrameLogger.Direction; import io.grpc.okhttp.internal.Protocol; @@ -280,7 +283,8 @@ public void testTransportExecutorWithTooFewThreads() throws Exception { null); clientTransport.start(transportListener); ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); - verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(statusCaptor.capture()); + verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(statusCaptor.capture(), + eq(new GoAwayDisconnectError(GrpcUtil.Http2Error.INTERNAL_ERROR))); Status capturedStatus = statusCaptor.getValue(); assertEquals("Timed out waiting for second handshake thread. " + "The transport executor pool may have run out of threads", @@ -481,7 +485,8 @@ public void nextFrameThrowIoException() throws Exception { assertEquals(NETWORK_ISSUE_MESSAGE, listener1.status.getCause().getMessage()); assertEquals(Status.INTERNAL.getCode(), listener2.status.getCode()); assertEquals(NETWORK_ISSUE_MESSAGE, listener2.status.getCause().getMessage()); - verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(isA(Status.class)); + verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(isA(Status.class), + any(DisconnectError.class)); verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated(); shutdownAndVerify(); } @@ -507,7 +512,8 @@ public void nextFrameThrowsError() throws Exception { assertEquals(0, activeStreamCount()); assertEquals(Status.INTERNAL.getCode(), listener.status.getCode()); assertEquals(ERROR_MESSAGE, listener.status.getCause().getMessage()); - verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(isA(Status.class)); + verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(isA(Status.class), + any(DisconnectError.class)); verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated(); shutdownAndVerify(); } @@ -523,7 +529,8 @@ public void nextFrameReturnFalse() throws Exception { frameReader.nextFrameAtEndOfStream(); listener.waitUntilStreamClosed(); assertEquals(Status.UNAVAILABLE.getCode(), listener.status.getCode()); - verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(isA(Status.class)); + verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(isA(Status.class), + any(DisconnectError.class)); verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated(); shutdownAndVerify(); } @@ -565,7 +572,8 @@ public void receivedHeadersForInvalidStreamShouldKillConnection() throws Excepti HeadersMode.HTTP_20_HEADERS); verify(frameWriter, timeout(TIME_OUT_MS)) .goAway(eq(0), eq(ErrorCode.PROTOCOL_ERROR), any(byte[].class)); - verify(transportListener).transportShutdown(isA(Status.class)); + verify(transportListener).transportShutdown(isA(Status.class), + any(DisconnectError.class)); verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated(); shutdownAndVerify(); } @@ -577,7 +585,8 @@ public void receivedDataForInvalidStreamShouldKillConnection() throws Exception 1000, 1000); verify(frameWriter, timeout(TIME_OUT_MS)) .goAway(eq(0), eq(ErrorCode.PROTOCOL_ERROR), any(byte[].class)); - verify(transportListener).transportShutdown(isA(Status.class)); + verify(transportListener).transportShutdown(isA(Status.class), + any(DisconnectError.class)); verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated(); shutdownAndVerify(); } @@ -1173,7 +1182,8 @@ public void stopNormally() throws Exception { clientTransport.shutdown(SHUTDOWN_REASON); assertEquals(2, activeStreamCount()); - verify(transportListener).transportShutdown(same(SHUTDOWN_REASON)); + verify(transportListener).transportShutdown(same(SHUTDOWN_REASON), + eq(SimpleDisconnectError.SUBCHANNEL_SHUTDOWN)); stream1.cancel(Status.CANCELLED); stream2.cancel(Status.CANCELLED); @@ -1207,7 +1217,8 @@ public void receiveGoAway() throws Exception { frameHandler().goAway(3, ErrorCode.CANCEL, ByteString.EMPTY); // Transport should be in STOPPING state. - verify(transportListener).transportShutdown(isA(Status.class)); + verify(transportListener).transportShutdown(isA(Status.class), + any(DisconnectError.class)); verify(transportListener, never()).transportTerminated(); // Stream 2 should be closed. @@ -1277,7 +1288,8 @@ public void streamIdExhausted() throws Exception { // Should only have the first message delivered. assertEquals(message, listener.messages.get(0)); verify(frameWriter, timeout(TIME_OUT_MS)).rstStream(eq(startId), eq(ErrorCode.CANCEL)); - verify(transportListener).transportShutdown(isA(Status.class)); + verify(transportListener).transportShutdown(isA(Status.class), + any(DisconnectError.class)); verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated(); shutdownAndVerify(); } @@ -1588,7 +1600,8 @@ public void receiveDataForUnknownStreamUpdateConnectionWindow() throws Exception (int) buffer.size()); verify(frameWriter, timeout(TIME_OUT_MS)) .goAway(eq(0), eq(ErrorCode.PROTOCOL_ERROR), any(byte[].class)); - verify(transportListener).transportShutdown(isA(Status.class)); + verify(transportListener).transportShutdown(isA(Status.class), + any(DisconnectError.class)); verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated(); shutdownAndVerify(); } @@ -1608,7 +1621,8 @@ public void receiveWindowUpdateForUnknownStream() throws Exception { frameHandler().windowUpdate(5, 73); verify(frameWriter, timeout(TIME_OUT_MS)) .goAway(eq(0), eq(ErrorCode.PROTOCOL_ERROR), any(byte[].class)); - verify(transportListener).transportShutdown(isA(Status.class)); + verify(transportListener).transportShutdown(isA(Status.class), + any(DisconnectError.class)); verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated(); shutdownAndVerify(); } @@ -1821,9 +1835,10 @@ public void unreachableServer() throws Exception { ManagedClientTransport.Listener listener = mock(ManagedClientTransport.Listener.class); clientTransport.start(listener); - ArgumentCaptor captor = ArgumentCaptor.forClass(Status.class); - verify(listener, timeout(TIME_OUT_MS)).transportShutdown(captor.capture()); - Status status = captor.getValue(); + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); + verify(listener, timeout(TIME_OUT_MS)).transportShutdown(statusCaptor.capture(), + eq(new GoAwayDisconnectError(GrpcUtil.Http2Error.INTERNAL_ERROR))); + Status status = statusCaptor.getValue(); assertEquals(Status.UNAVAILABLE.getCode(), status.getCode()); assertTrue(status.getCause().toString(), status.getCause() instanceof IOException); @@ -1852,9 +1867,10 @@ public void customSocketFactory() throws Exception { ManagedClientTransport.Listener listener = mock(ManagedClientTransport.Listener.class); clientTransport.start(listener); - ArgumentCaptor captor = ArgumentCaptor.forClass(Status.class); - verify(listener, timeout(TIME_OUT_MS)).transportShutdown(captor.capture()); - Status status = captor.getValue(); + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); + verify(listener, timeout(TIME_OUT_MS)).transportShutdown(statusCaptor.capture(), + eq(new GoAwayDisconnectError(GrpcUtil.Http2Error.INTERNAL_ERROR))); + Status status = statusCaptor.getValue(); assertEquals(Status.UNAVAILABLE.getCode(), status.getCode()); assertSame(exception, status.getCause()); } @@ -1903,7 +1919,8 @@ public void proxy_200() throws Exception { }); sock.getOutputStream().flush(); - verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(isA(Status.class)); + verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(isA(Status.class), + any(DisconnectError.class)); while (sock.getInputStream().read() != -1) {} verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated(); sock.close(); @@ -1943,17 +1960,18 @@ public void proxy_500() throws Exception { assertEquals(-1, sock.getInputStream().read()); - ArgumentCaptor captor = ArgumentCaptor.forClass(Status.class); - verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(captor.capture()); - Status error = captor.getValue(); - assertTrue("Status didn't contain error code: " + captor.getValue(), - error.getDescription().contains("500")); - assertTrue("Status didn't contain error description: " + captor.getValue(), - error.getDescription().contains("OH NO")); - assertTrue("Status didn't contain error text: " + captor.getValue(), - error.getDescription().contains(errorText)); - assertEquals("Not UNAVAILABLE: " + captor.getValue(), - Status.UNAVAILABLE.getCode(), error.getCode()); + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); + verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(statusCaptor.capture(), + eq(new GoAwayDisconnectError(GrpcUtil.Http2Error.INTERNAL_ERROR))); + Status status = statusCaptor.getValue(); + assertTrue("Status didn't contain error code: " + statusCaptor.getValue(), + status.getDescription().contains("500")); + assertTrue("Status didn't contain error description: " + statusCaptor.getValue(), + status.getDescription().contains("OH NO")); + assertTrue("Status didn't contain error text: " + statusCaptor.getValue(), + status.getDescription().contains(errorText)); + assertEquals("Not UNAVAILABLE: " + statusCaptor.getValue(), + Status.UNAVAILABLE.getCode(), status.getCode()); sock.close(); verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated(); } @@ -1980,13 +1998,14 @@ public void proxy_immediateServerClose() throws Exception { serverSocket.close(); sock.close(); - ArgumentCaptor captor = ArgumentCaptor.forClass(Status.class); - verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(captor.capture()); - Status error = captor.getValue(); - assertTrue("Status didn't contain proxy: " + captor.getValue(), - error.getDescription().contains("proxy")); - assertEquals("Not UNAVAILABLE: " + captor.getValue(), - Status.UNAVAILABLE.getCode(), error.getCode()); + ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class); + verify(transportListener, timeout(TIME_OUT_MS)).transportShutdown(statusCaptor.capture(), + eq(new GoAwayDisconnectError(GrpcUtil.Http2Error.INTERNAL_ERROR))); + Status status = statusCaptor.getValue(); + assertTrue("Status didn't contain proxy: " + statusCaptor.getValue(), + status.getDescription().contains("proxy")); + assertEquals("Not UNAVAILABLE: " + statusCaptor.getValue(), + Status.UNAVAILABLE.getCode(), status.getCode()); verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated(); } @@ -2017,7 +2036,8 @@ public void proxy_serverHangs() throws Exception { assertEquals("Host: theservice:80", reader.readLine()); while (!"".equals(reader.readLine())) {} - verify(transportListener, timeout(200)).transportShutdown(any(Status.class)); + verify(transportListener, timeout(200)).transportShutdown(any(Status.class), + any(DisconnectError.class)); verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated(); sock.close(); } From c7f3cdbc3eb83981fbf5574f92e18e1d4c929f57 Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Tue, 16 Dec 2025 18:57:02 -0800 Subject: [PATCH 494/591] bazel: always load java_proto_library from the protobuf repo The java_proto_library rule in rules_java is deprecated. --- BUILD.bazel | 3 ++- alts/BUILD.bazel | 3 ++- testing-proto/BUILD.bazel | 2 +- xds/BUILD.bazel | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/BUILD.bazel b/BUILD.bazel index fee2c0bd12d..27a99fb62eb 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_java//java:defs.bzl", "java_library", "java_plugin", "java_proto_library") +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") +load("@rules_java//java:defs.bzl", "java_library", "java_plugin") load("@rules_jvm_external//:defs.bzl", "artifact") load(":java_grpc_library.bzl", "java_grpc_library") diff --git a/alts/BUILD.bazel b/alts/BUILD.bazel index 5cf14504e2c..f29df303fbe 100644 --- a/alts/BUILD.bazel +++ b/alts/BUILD.bazel @@ -1,5 +1,6 @@ +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") -load("@rules_java//java:defs.bzl", "java_library", "java_proto_library") +load("@rules_java//java:defs.bzl", "java_library") load("@rules_jvm_external//:defs.bzl", "artifact") load("//:java_grpc_library.bzl", "java_grpc_library") diff --git a/testing-proto/BUILD.bazel b/testing-proto/BUILD.bazel index 362cee91463..aa0fc9ee20b 100644 --- a/testing-proto/BUILD.bazel +++ b/testing-proto/BUILD.bazel @@ -1,5 +1,5 @@ +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") -load("@rules_java//java:defs.bzl", "java_proto_library") load("//:java_grpc_library.bzl", "java_grpc_library") proto_library( diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index be44abf4f39..7f2c5f02338 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -1,6 +1,7 @@ load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar") +load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library") load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") -load("@rules_java//java:defs.bzl", "java_binary", "java_library", "java_proto_library", "java_test") +load("@rules_java//java:defs.bzl", "java_binary", "java_library", "java_test") load("@rules_jvm_external//:defs.bzl", "artifact") load("//:java_grpc_library.bzl", "INTERNAL_java_grpc_library_for_xds", "java_grpc_library", "java_rpc_toolchain") From 52f23e435d5f635393900e7c40dda1fa58053728 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Wed, 24 Dec 2025 09:59:45 +0530 Subject: [PATCH 495/591] Update README etc to reference 1.78.0 (#12584) --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 7bd0e67f275..205187b4000 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.77.0/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.77.0/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.78.0/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.78.0/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,34 +56,34 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.77.0 + 1.78.0 runtime io.grpc grpc-protobuf - 1.77.0 + 1.78.0 io.grpc grpc-stub - 1.77.0 + 1.78.0 ``` Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.77.0' -implementation 'io.grpc:grpc-protobuf:1.77.0' -implementation 'io.grpc:grpc-stub:1.77.0' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.78.0' +implementation 'io.grpc:grpc-protobuf:1.78.0' +implementation 'io.grpc:grpc-stub:1.78.0' ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.77.0' -implementation 'io.grpc:grpc-protobuf-lite:1.77.0' -implementation 'io.grpc:grpc-stub:1.77.0' +implementation 'io.grpc:grpc-okhttp:1.78.0' +implementation 'io.grpc:grpc-protobuf-lite:1.78.0' +implementation 'io.grpc:grpc-stub:1.78.0' ``` For [Bazel](https://bazel.build), you can either @@ -91,7 +91,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.77.0 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.78.0 Development snapshots are available in [Sonatypes's snapshot repository](https://central.sonatype.com/repository/maven-snapshots/). @@ -123,7 +123,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use com.google.protobuf:protoc:3.25.8:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.77.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.78.0:exe:${os.detected.classifier} @@ -153,7 +153,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.77.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.78.0' } } generateProtoTasks { @@ -186,7 +186,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.77.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.78.0' } } generateProtoTasks { From 7c87bf49d4499c35d9945175a2ddf5f95642089c Mon Sep 17 00:00:00 2001 From: Yun Peng Date: Tue, 16 Dec 2025 15:50:00 +0100 Subject: [PATCH 496/591] Fix build with Bazel 9 by upgrading bazel_jar_jar and grpc-proto versions --- MODULE.bazel | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 42568eef6fd..5bf2ece6a25 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -46,10 +46,10 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ ] # GRPC_DEPS_END -bazel_dep(name = "bazel_jar_jar", version = "0.1.7") +bazel_dep(name = "bazel_jar_jar", version = "0.1.11.bcr.1") bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "googleapis", version = "0.0.0-20240326-1c8d509c5", repo_name = "com_google_googleapis") -bazel_dep(name = "grpc-proto", version = "0.0.0-20240627-ec30f58", repo_name = "io_grpc_grpc_proto") +bazel_dep(name = "grpc-proto", version = "0.0.0-20240627-ec30f58.bcr.1", repo_name = "io_grpc_grpc_proto") bazel_dep(name = "protobuf", version = "33.1", repo_name = "com_google_protobuf") bazel_dep(name = "rules_cc", version = "0.0.9") bazel_dep(name = "rules_java", version = "9.1.0") From 12f610f8e04f958f65797fdddc3f8064d8a666c1 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 23 Dec 2025 13:26:35 -0800 Subject: [PATCH 497/591] bazel: Fix Bazel 8 WORKSPACE support There's some rearranging happening here, like moving jar_jar later. But the main thing is downgrading rules_java to 8.15.2. rules_java 8.16 and later somehow break protobuf: ``` File ".../external/com_google_protobuf/bazel/private/proto_library_rule.bzl", line 43, column 17, in _get_import_prefix if not paths.is_normalized(import_prefix): Error: 'struct' value has no field or method 'is_normalized' Available attributes: basename, dirname, is_absolute, join, normalize, relativize, replace_extension, split_extension ``` https://github.com/protocolbuffers/protobuf/issues/17687 claims that this is due to not using bazel_skylib 1.7.0, but protobuf_deps is defining bazel_skylib to be 1.7.0 and nothing earlier seems to be defining bazel_skylib. So we'll leave this as a bit of a mystery for later. rules_java 8.15.2 is still newer than protobuf_deps's 8.6.1. --- WORKSPACE | 47 ++++++++++++++++++++-------------------------- examples/WORKSPACE | 43 ++++++++++++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 7d435ec82dc..1efdf2793a8 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1,50 +1,34 @@ workspace(name = "io_grpc_grpc_java") load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("//:repositories.bzl", "IO_GRPC_GRPC_JAVA_ARTIFACTS", "IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS", "grpc_java_repositories") -http_archive( - name = "bazel_features", - sha256 = "a660027f5a87f13224ab54b8dc6e191693c554f2692fcca46e8e29ee7dabc43b", - strip_prefix = "bazel_features-1.30.0", - url = "https://github.com/bazel-contrib/bazel_features/releases/download/v1.30.0/bazel_features-v1.30.0.tar.gz", -) - -load("@bazel_features//:deps.bzl", "bazel_features_deps") - -bazel_features_deps() +grpc_java_repositories() http_archive( name = "rules_java", - sha256 = "4e1a28a25c2efa53500c928d22ceffbc505dd95b335a2d025836a293b592212f", + sha256 = "47632cc506c858011853073449801d648e10483d4b50e080ec2549a4b2398960", urls = [ - "https://github.com/bazelbuild/rules_java/releases/download/9.1.0/rules_java-9.1.0.tar.gz", + "https://github.com/bazelbuild/rules_java/releases/download/8.15.2/rules_java-8.15.2.tar.gz", ], ) -load("@rules_java//java:rules_java_deps.bzl", "compatibility_proxy_repo") +load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS", "protobuf_deps") -compatibility_proxy_repo() +protobuf_deps() -http_archive( - name = "rules_jvm_external", - sha256 = "d31e369b854322ca5098ea12c69d7175ded971435e55c18dd9dd5f29cc5249ac", - strip_prefix = "rules_jvm_external-5.3", - url = "https://github.com/bazelbuild/rules_jvm_external/releases/download/5.3/rules_jvm_external-5.3.tar.gz", -) +load("@rules_java//java:rules_java_deps.bzl", "rules_java_dependencies") -load("@rules_jvm_external//:defs.bzl", "maven_install") -load("//:repositories.bzl", "IO_GRPC_GRPC_JAVA_ARTIFACTS", "IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS", "grpc_java_repositories") +rules_java_dependencies() -grpc_java_repositories() +load("@bazel_features//:deps.bzl", "bazel_features_deps") + +bazel_features_deps() load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar_repositories") jar_jar_repositories() -load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS", "protobuf_deps") - -protobuf_deps() - load("@rules_python//python:repositories.bzl", "py_repositories") py_repositories() @@ -55,6 +39,15 @@ switched_rules_by_language( name = "com_google_googleapis_imports", ) +http_archive( + name = "rules_jvm_external", + sha256 = "d31e369b854322ca5098ea12c69d7175ded971435e55c18dd9dd5f29cc5249ac", + strip_prefix = "rules_jvm_external-5.3", + url = "https://github.com/bazelbuild/rules_jvm_external/releases/download/5.3/rules_jvm_external-5.3.tar.gz", +) + +load("@rules_jvm_external//:defs.bzl", "maven_install") + maven_install( artifacts = IO_GRPC_GRPC_JAVA_ARTIFACTS + PROTOBUF_MAVEN_ARTIFACTS, override_targets = IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS, diff --git a/examples/WORKSPACE b/examples/WORKSPACE index f26e3124562..1387cc4cf12 100644 --- a/examples/WORKSPACE +++ b/examples/WORKSPACE @@ -14,21 +14,17 @@ local_repository( path = "..", ) -http_archive( - name = "rules_jvm_external", - sha256 = "d31e369b854322ca5098ea12c69d7175ded971435e55c18dd9dd5f29cc5249ac", - strip_prefix = "rules_jvm_external-5.3", - url = "https://github.com/bazelbuild/rules_jvm_external/releases/download/5.3/rules_jvm_external-5.3.tar.gz", -) - load("@io_grpc_grpc_java//:repositories.bzl", "IO_GRPC_GRPC_JAVA_ARTIFACTS", "IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS", "grpc_java_repositories") -load("@rules_jvm_external//:defs.bzl", "maven_install") grpc_java_repositories() -load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar_repositories") - -jar_jar_repositories() +http_archive( + name = "rules_java", + sha256 = "47632cc506c858011853073449801d648e10483d4b50e080ec2549a4b2398960", + urls = [ + "https://github.com/bazelbuild/rules_java/releases/download/8.15.2/rules_java-8.15.2.tar.gz", + ], +) # Protobuf now requires C++14 or higher, which requires Bazel configuration # outside the WORKSPACE. See .bazelrc in this directory. @@ -36,20 +32,39 @@ load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS", "pr protobuf_deps() -load("@rules_python//python:repositories.bzl", "py_repositories") +load("@rules_java//java:rules_java_deps.bzl", "compatibility_proxy_repo", "rules_java_dependencies") -py_repositories() +rules_java_dependencies() + +load("@bazel_features//:deps.bzl", "bazel_features_deps") -load("@rules_java//java:rules_java_deps.bzl", "compatibility_proxy_repo") +bazel_features_deps() compatibility_proxy_repo() +load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar_repositories") + +jar_jar_repositories() + +load("@rules_python//python:repositories.bzl", "py_repositories") + +py_repositories() + load("@com_google_googleapis//:repository_rules.bzl", "switched_rules_by_language") switched_rules_by_language( name = "com_google_googleapis_imports", ) +http_archive( + name = "rules_jvm_external", + sha256 = "d31e369b854322ca5098ea12c69d7175ded971435e55c18dd9dd5f29cc5249ac", + strip_prefix = "rules_jvm_external-5.3", + url = "https://github.com/bazelbuild/rules_jvm_external/releases/download/5.3/rules_jvm_external-5.3.tar.gz", +) + +load("@rules_jvm_external//:defs.bzl", "maven_install") + maven_install( artifacts = [ "com.google.api.grpc:grpc-google-cloud-pubsub-v1:0.1.24", From 6422092e3aebb5b29989d4eb0af27b0b46f007a4 Mon Sep 17 00:00:00 2001 From: MV Shiva Date: Wed, 31 Dec 2025 11:42:10 +0530 Subject: [PATCH 498/591] Upgrade dependencies (#12588) --- MODULE.bazel | 32 ++++++++++++------------- SECURITY.md | 3 ++- examples/example-gauth/build.gradle | 2 +- examples/example-oauth/build.gradle | 2 +- gradle/libs.versions.toml | 36 ++++++++++++++--------------- repositories.bzl | 32 ++++++++++++------------- 6 files changed, 54 insertions(+), 53 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 5bf2ece6a25..763897bb383 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,14 +8,14 @@ module( # GRPC_DEPS_START IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.android:annotations:4.1.1.4", - "com.google.api.grpc:proto-google-common-protos:2.63.1", - "com.google.auth:google-auth-library-credentials:1.40.0", - "com.google.auth:google-auth-library-oauth2-http:1.40.0", + "com.google.api.grpc:proto-google-common-protos:2.63.2", + "com.google.auth:google-auth-library-credentials:1.41.0", + "com.google.auth:google-auth-library-oauth2-http:1.41.0", "com.google.auto.value:auto-value-annotations:1.11.0", "com.google.auto.value:auto-value:1.11.0", "com.google.code.findbugs:jsr305:3.0.2", "com.google.code.gson:gson:2.12.1", - "com.google.errorprone:error_prone_annotations:2.44.0", + "com.google.errorprone:error_prone_annotations:2.45.0", "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:33.5.0-android", "com.google.re2j:re2j:1.8", @@ -23,20 +23,20 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.truth:truth:1.4.5", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day - "io.netty:netty-buffer:4.1.127.Final", - "io.netty:netty-codec-http2:4.1.127.Final", - "io.netty:netty-codec-http:4.1.127.Final", - "io.netty:netty-codec-socks:4.1.127.Final", - "io.netty:netty-codec:4.1.127.Final", - "io.netty:netty-common:4.1.127.Final", - "io.netty:netty-handler-proxy:4.1.127.Final", - "io.netty:netty-handler:4.1.127.Final", - "io.netty:netty-resolver:4.1.127.Final", + "io.netty:netty-buffer:4.1.130.Final", + "io.netty:netty-codec-http2:4.1.130.Final", + "io.netty:netty-codec-http:4.1.130.Final", + "io.netty:netty-codec-socks:4.1.130.Final", + "io.netty:netty-codec:4.1.130.Final", + "io.netty:netty-common:4.1.130.Final", + "io.netty:netty-handler-proxy:4.1.130.Final", + "io.netty:netty-handler:4.1.130.Final", + "io.netty:netty-resolver:4.1.130.Final", "io.netty:netty-tcnative-boringssl-static:2.0.74.Final", "io.netty:netty-tcnative-classes:2.0.74.Final", - "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.127.Final", - "io.netty:netty-transport-native-unix-common:4.1.127.Final", - "io.netty:netty-transport:4.1.127.Final", + "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.130.Final", + "io.netty:netty-transport-native-unix-common:4.1.130.Final", + "io.netty:netty-transport:4.1.130.Final", "io.opencensus:opencensus-api:0.31.0", "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", diff --git a/SECURITY.md b/SECURITY.md index c0ef797238f..fa5b85c0e3a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -398,7 +398,8 @@ grpc-netty version | netty-handler version | netty-tcnative-boringssl-static ver 1.67.x-1.70.x | 4.1.110.Final | 2.0.65.Final 1.71.x-1.74.x | 4.1.110.Final | 2.0.70.Final 1.75.x-1.76.x | 4.1.124.Final | 2.0.72.Final -1.77.x- | 4.1.127.Final | 2.0.74.Final +1.77.x-1.78.x | 4.1.127.Final | 2.0.74.Final +1.79.x- | 4.1.130.Final | 2.0.74.Final _(grpc-netty-shaded avoids issues with keeping these versions in sync.)_ diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 3ec28dd8785..ecd03182cfc 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -30,7 +30,7 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-auth:${grpcVersion}" - implementation "com.google.auth:google-auth-library-oauth2-http:1.40.0" + implementation "com.google.auth:google-auth-library-oauth2-http:1.41.0" implementation "com.google.api.grpc:grpc-google-cloud-pubsub-v1:0.1.24" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" } diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 7f6e1c425d8..e8c649f424a 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -29,7 +29,7 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-auth:${grpcVersion}" - implementation "com.google.auth:google-auth-library-oauth2-http:1.40.0" + implementation "com.google.auth:google-auth-library-oauth2-http:1.41.0" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9cf74e270cb..3806fa3404e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,18 +42,18 @@ conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2" cronet-api = "org.chromium.net:cronet-api:119.6045.31" # checkForUpdates: cronet-embedded:119.6045.31 cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31" -errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.44.0" +errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.45.0" # 2.32.0+ requires Java 17+ # checkForUpdates: errorprone-core:2.31.+ errorprone-core = "com.google.errorprone:error_prone_core:2.31.0" # 2.11.0+ requires JDK 11+ (See https://github.com/google/error-prone/releases/tag/v2.11.0) # checkForUpdates: errorprone-corejava8:2.10.+ errorprone-corejava8 = "com.google.errorprone:error_prone_core:2.10.0" -google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.63.1" -google-auth-credentials = "com.google.auth:google-auth-library-credentials:1.40.0" -google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.40.0" +google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.63.2" +google-auth-credentials = "com.google.auth:google-auth-library-credentials:1.41.0" +google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.41.0" # Release notes: https://cloud.google.com/logging/docs/release-notes -google-cloud-logging = "com.google.cloud:google-cloud-logging:3.23.8" +google-cloud-logging = "com.google.cloud:google-cloud-logging:3.23.9" # 2.13.0 requires error_prone_annotations:2.37.0, but we are stuck with 2.36.0 # checkForUpdates: gson:2.12.+ gson = "com.google.code.gson:gson:2.12.1" @@ -71,11 +71,11 @@ javax-servlet-api = "javax.servlet:javax.servlet-api:4.0.1" # 12.0.0+ require Java 17+ # checkForUpdates: jetty-client:11.+ jetty-client = "org.eclipse.jetty:jetty-client:11.0.26" -jetty-http2-server = "org.eclipse.jetty.http2:jetty-http2-server:12.1.4" +jetty-http2-server = "org.eclipse.jetty.http2:jetty-http2-server:12.1.5" # 10.0.25+ uses uses @Deprecated(since=/forRemoval=) from Java 9 # checkForUpdates: jetty-http2-server10:10.0.24 jetty-http2-server10 = "org.eclipse.jetty.http2:http2-server:10.0.24" -jetty-servlet = "org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.4" +jetty-servlet = "org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.5" # checkForUpdates: jetty-servlet10:10.0.24 jetty-servlet10 = "org.eclipse.jetty:jetty-servlet:10.0.24" jsr305 = "com.google.code.findbugs:jsr305:3.0.2" @@ -93,17 +93,17 @@ mockito-android = "org.mockito:mockito-android:4.4.0" mockito-core = "org.mockito:mockito-core:4.4.0" # Need to decide when we require users to absorb the breaking changes in 4.2 # checkForUpdates: netty-codec-http2:4.1.+ -netty-codec-http2 = "io.netty:netty-codec-http2:4.1.127.Final" +netty-codec-http2 = "io.netty:netty-codec-http2:4.1.130.Final" # checkForUpdates: netty-handler-proxy:4.1.+ -netty-handler-proxy = "io.netty:netty-handler-proxy:4.1.127.Final" +netty-handler-proxy = "io.netty:netty-handler-proxy:4.1.130.Final" # Keep the following references of tcnative version in sync whenever it's updated: # SECURITY.md netty-tcnative = "io.netty:netty-tcnative-boringssl-static:2.0.74.Final" netty-tcnative-classes = "io.netty:netty-tcnative-classes:2.0.74.Final" # checkForUpdates: netty-transport-epoll:4.1.+ -netty-transport-epoll = "io.netty:netty-transport-native-epoll:4.1.127.Final" +netty-transport-epoll = "io.netty:netty-transport-native-epoll:4.1.130.Final" # checkForUpdates: netty-unix-common:4.1.+ -netty-unix-common = "io.netty:netty-transport-native-unix-common:4.1.127.Final" +netty-unix-common = "io.netty:netty-transport-native-unix-common:4.1.130.Final" okhttp = "com.squareup.okhttp:okhttp:2.7.5" # okio 3.5+ uses Kotlin 1.9+ which requires Android Gradle Plugin 9+ # checkForUpdates: okio:3.4.+ @@ -113,11 +113,11 @@ opencensus-contrib-grpc-metrics = { module = "io.opencensus:opencensus-contrib-g opencensus-exporter-stats-stackdriver = { module = "io.opencensus:opencensus-exporter-stats-stackdriver", version.ref = "opencensus" } opencensus-exporter-trace-stackdriver = { module = "io.opencensus:opencensus-exporter-trace-stackdriver", version.ref = "opencensus" } opencensus-impl = { module = "io.opencensus:opencensus-impl", version.ref = "opencensus" } -opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.56.0" -opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.56.0-alpha" -opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.51.0-alpha" -opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.56.0" -opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.56.0" +opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.57.0" +opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.57.0-alpha" +opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.52.0-alpha" +opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.57.0" +opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.57.0" perfmark-api = "io.perfmark:perfmark-api:0.27.0" # Not upgrading to 4.x as it is not yet ABI compatible. # https://github.com/protocolbuffers/protobuf/issues/17247 @@ -136,9 +136,9 @@ signature-android = "net.sf.androidscents.signature:android-api-level-21:5.0.1_r signature-java = "org.codehaus.mojo.signature:java18:1.0" # 11.0.0+ require Java 17+ # checkForUpdates: tomcat-embed-core:10.+ -tomcat-embed-core = "org.apache.tomcat.embed:tomcat-embed-core:10.1.49" +tomcat-embed-core = "org.apache.tomcat.embed:tomcat-embed-core:10.1.50" # checkForUpdates: tomcat-embed-core9:9.+ -tomcat-embed-core9 = "org.apache.tomcat.embed:tomcat-embed-core:9.0.112" +tomcat-embed-core9 = "org.apache.tomcat.embed:tomcat-embed-core:9.0.113" truth = "com.google.truth:truth:1.4.5" # checkForUpdates: undertow-servlet22:2.2.+ undertow-servlet22 = "io.undertow:undertow-servlet:2.2.38.Final" diff --git a/repositories.bzl b/repositories.bzl index 43b7fcfe1ed..33efebaf5b3 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -12,14 +12,14 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # GRPC_DEPS_START IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.android:annotations:4.1.1.4", - "com.google.api.grpc:proto-google-common-protos:2.63.1", - "com.google.auth:google-auth-library-credentials:1.40.0", - "com.google.auth:google-auth-library-oauth2-http:1.40.0", + "com.google.api.grpc:proto-google-common-protos:2.63.2", + "com.google.auth:google-auth-library-credentials:1.41.0", + "com.google.auth:google-auth-library-oauth2-http:1.41.0", "com.google.auto.value:auto-value-annotations:1.11.0", "com.google.auto.value:auto-value:1.11.0", "com.google.code.findbugs:jsr305:3.0.2", "com.google.code.gson:gson:2.12.1", - "com.google.errorprone:error_prone_annotations:2.44.0", + "com.google.errorprone:error_prone_annotations:2.45.0", "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:33.5.0-android", "com.google.re2j:re2j:1.8", @@ -27,20 +27,20 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.truth:truth:1.4.5", "com.squareup.okhttp:okhttp:2.7.5", "com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day - "io.netty:netty-buffer:4.1.127.Final", - "io.netty:netty-codec-http2:4.1.127.Final", - "io.netty:netty-codec-http:4.1.127.Final", - "io.netty:netty-codec-socks:4.1.127.Final", - "io.netty:netty-codec:4.1.127.Final", - "io.netty:netty-common:4.1.127.Final", - "io.netty:netty-handler-proxy:4.1.127.Final", - "io.netty:netty-handler:4.1.127.Final", - "io.netty:netty-resolver:4.1.127.Final", + "io.netty:netty-buffer:4.1.130.Final", + "io.netty:netty-codec-http2:4.1.130.Final", + "io.netty:netty-codec-http:4.1.130.Final", + "io.netty:netty-codec-socks:4.1.130.Final", + "io.netty:netty-codec:4.1.130.Final", + "io.netty:netty-common:4.1.130.Final", + "io.netty:netty-handler-proxy:4.1.130.Final", + "io.netty:netty-handler:4.1.130.Final", + "io.netty:netty-resolver:4.1.130.Final", "io.netty:netty-tcnative-boringssl-static:2.0.74.Final", "io.netty:netty-tcnative-classes:2.0.74.Final", - "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.127.Final", - "io.netty:netty-transport-native-unix-common:4.1.127.Final", - "io.netty:netty-transport:4.1.127.Final", + "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.130.Final", + "io.netty:netty-transport-native-unix-common:4.1.130.Final", + "io.netty:netty-transport:4.1.130.Final", "io.opencensus:opencensus-api:0.31.0", "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", From a9f73f4c0aa5617aa2b6ae6ac805693915899b6a Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 30 Dec 2025 13:39:35 -0800 Subject: [PATCH 499/591] opentelemetry: Add Android API checking opentelemetry-android is a thing, so support it with our otel code. opentelemetry-android actually requires desugaring, so some APIs would be available that animalsniffer would believe are not. However, gRPC normally doesn't require desugaring and requiring it in just this case makes it more annoying to verify. --- opentelemetry/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/opentelemetry/build.gradle b/opentelemetry/build.gradle index c856ad8dcb9..c0ec7b73b5a 100644 --- a/opentelemetry/build.gradle +++ b/opentelemetry/build.gradle @@ -29,6 +29,11 @@ dependencies { extension = "signature" } } + signature (libraries.signature.android) { + artifact { + extension = "signature" + } + } } tasks.named("jar").configure { From a1a736339189d81d316c3b81c1c2c7ebaedff384 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 19 Nov 2025 07:58:12 -0800 Subject: [PATCH 500/591] Prefer FixedResultPicker over custom Picker implementations These other implementations pre-date FixedResultPicker, and are no longer needed. gRPC-LB's tests failed when using FixedResultPicker because it didn't transition to READY. This was because GrpclbState.maybeUpdatePicker() didn't consider the ConnectivityState when checking if anything had changed. The old PickFirstLoadBalancer.Picker didn't implement equals() so every update was considered different, ignoring the connectivity state. PickFirstLeafLoadBalancer wasn't impacted because it didn't pick a subchannel when in CONNECTING, and neither did PickFirstLoadBalancer after the first update. This is fixed by gRPC-LB checking the connectivity state. A follow-up will have PickFirstLoadBalancer no longer return the useless subchannel when picking during CONNECTING. --- .../AutoConfiguredLoadBalancerFactory.java | 36 +++-------------- .../java/io/grpc/internal/OobChannel.java | 39 ++----------------- .../internal/PickFirstLeafLoadBalancer.java | 35 +++-------------- .../grpc/internal/PickFirstLoadBalancer.java | 34 +++------------- .../ShufflingPickFirstLoadBalancer.java | 34 +++------------- .../main/java/io/grpc/grpclb/GrpclbState.java | 5 ++- .../java/io/grpc/rls/RlsLoadBalancer.java | 18 +-------- .../io/grpc/rls/CachingRlsLbClientTest.java | 11 ++---- .../OutlierDetectionLoadBalancerTest.java | 8 +--- .../io/grpc/xds/LeastRequestLoadBalancer.java | 4 +- .../xds/LeastRequestLoadBalancerTest.java | 36 ++++++++--------- 11 files changed, 58 insertions(+), 202 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/AutoConfiguredLoadBalancerFactory.java b/core/src/main/java/io/grpc/internal/AutoConfiguredLoadBalancerFactory.java index a257637de22..6b8537fd658 100644 --- a/core/src/main/java/io/grpc/internal/AutoConfiguredLoadBalancerFactory.java +++ b/core/src/main/java/io/grpc/internal/AutoConfiguredLoadBalancerFactory.java @@ -19,17 +19,15 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; import io.grpc.ChannelLogger.ChannelLogLevel; import io.grpc.ConnectivityState; import io.grpc.ConnectivityStateInfo; import io.grpc.LoadBalancer; +import io.grpc.LoadBalancer.FixedResultPicker; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancer.PickResult; -import io.grpc.LoadBalancer.PickSubchannelArgs; import io.grpc.LoadBalancer.ResolvedAddresses; import io.grpc.LoadBalancer.Subchannel; -import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.LoadBalancerProvider; import io.grpc.LoadBalancerRegistry; import io.grpc.NameResolver.ConfigOrError; @@ -110,7 +108,8 @@ Status tryAcceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { defaultProvider = getProviderOrThrow(defaultPolicy, "using default policy"); } catch (PolicyException e) { Status s = Status.INTERNAL.withDescription(e.getMessage()); - helper.updateBalancingState(ConnectivityState.TRANSIENT_FAILURE, new FailingPicker(s)); + helper.updateBalancingState( + ConnectivityState.TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(s))); delegate.shutdown(); delegateProvider = null; delegate = new NoopLoadBalancer(); @@ -122,7 +121,8 @@ Status tryAcceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { if (delegateProvider == null || !policySelection.provider.getPolicyName().equals(delegateProvider.getPolicyName())) { - helper.updateBalancingState(ConnectivityState.CONNECTING, new EmptyPicker()); + helper.updateBalancingState( + ConnectivityState.CONNECTING, new FixedResultPicker(PickResult.withNoResult())); delegate.shutdown(); delegateProvider = policySelection.provider; LoadBalancer old = delegate; @@ -236,30 +236,4 @@ private PolicyException(String msg) { super(msg); } } - - private static final class EmptyPicker extends SubchannelPicker { - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withNoResult(); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(EmptyPicker.class).toString(); - } - } - - private static final class FailingPicker extends SubchannelPicker { - private final Status failure; - - FailingPicker(Status failure) { - this.failure = failure; - } - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withError(failure); - } - } } diff --git a/core/src/main/java/io/grpc/internal/OobChannel.java b/core/src/main/java/io/grpc/internal/OobChannel.java index 71973ed5d64..b2272a89672 100644 --- a/core/src/main/java/io/grpc/internal/OobChannel.java +++ b/core/src/main/java/io/grpc/internal/OobChannel.java @@ -38,8 +38,8 @@ import io.grpc.InternalLogId; import io.grpc.InternalWithLogId; import io.grpc.LoadBalancer; +import io.grpc.LoadBalancer.FixedResultPicker; import io.grpc.LoadBalancer.PickResult; -import io.grpc.LoadBalancer.PickSubchannelArgs; import io.grpc.LoadBalancer.Subchannel; import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.ManagedChannel; @@ -182,23 +182,7 @@ public Object getInternalSubchannel() { } }; - final class OobSubchannelPicker extends SubchannelPicker { - final PickResult result = PickResult.withSubchannel(subchannelImpl); - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return result; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(OobSubchannelPicker.class) - .add("result", result) - .toString(); - } - } - - subchannelPicker = new OobSubchannelPicker(); + subchannelPicker = new FixedResultPicker(PickResult.withSubchannel(subchannelImpl)); delayedTransport.reprocess(subchannelPicker); } @@ -270,23 +254,8 @@ void handleSubchannelStateChange(final ConnectivityStateInfo newState) { delayedTransport.reprocess(subchannelPicker); break; case TRANSIENT_FAILURE: - final class OobErrorPicker extends SubchannelPicker { - final PickResult errorResult = PickResult.withError(newState.getStatus()); - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return errorResult; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(OobErrorPicker.class) - .add("errorResult", errorResult) - .toString(); - } - } - - delayedTransport.reprocess(new OobErrorPicker()); + delayedTransport.reprocess( + new FixedResultPicker(PickResult.withError(newState.getStatus()))); break; default: // Do nothing diff --git a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java index 2689d7d2308..935214a94fd 100644 --- a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java +++ b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java @@ -24,7 +24,6 @@ import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import io.grpc.Attributes; @@ -164,7 +163,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { if (noOldAddrs) { // Make tests happy; they don't properly assume starting in CONNECTING rawConnectivityState = CONNECTING; - updateBalancingState(CONNECTING, new Picker(PickResult.withNoResult())); + updateBalancingState(CONNECTING, new FixedResultPicker(PickResult.withNoResult())); } if (rawConnectivityState == READY) { @@ -237,7 +236,7 @@ public void handleNameResolutionError(Status error) { subchannels.clear(); addressIndex.updateGroups(ImmutableList.of()); rawConnectivityState = TRANSIENT_FAILURE; - updateBalancingState(TRANSIENT_FAILURE, new Picker(PickResult.withError(error))); + updateBalancingState(TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error))); } void processSubchannelState(SubchannelData subchannelData, ConnectivityStateInfo stateInfo) { @@ -290,7 +289,7 @@ void processSubchannelState(SubchannelData subchannelData, ConnectivityStateInfo case CONNECTING: rawConnectivityState = CONNECTING; - updateBalancingState(CONNECTING, new Picker(PickResult.withNoResult())); + updateBalancingState(CONNECTING, new FixedResultPicker(PickResult.withNoResult())); break; case READY: @@ -322,7 +321,7 @@ void processSubchannelState(SubchannelData subchannelData, ConnectivityStateInfo if (isPassComplete()) { rawConnectivityState = TRANSIENT_FAILURE; updateBalancingState(TRANSIENT_FAILURE, - new Picker(PickResult.withError(stateInfo.getStatus()))); + new FixedResultPicker(PickResult.withError(stateInfo.getStatus()))); // Refresh Name Resolution, but only when all 3 conditions are met // * We are at the end of addressIndex @@ -385,11 +384,11 @@ private void updateHealthCheckedState(SubchannelData subchannelData) { updateBalancingState(READY, new FixedResultPicker(PickResult.withSubchannel(subchannelData.subchannel))); } else if (subchannelData.getHealthState() == TRANSIENT_FAILURE) { - updateBalancingState(TRANSIENT_FAILURE, new Picker(PickResult.withError( + updateBalancingState(TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError( subchannelData.healthStateInfo.getStatus()))); } else if (concludedState != TRANSIENT_FAILURE) { updateBalancingState(subchannelData.getHealthState(), - new Picker(PickResult.withNoResult())); + new FixedResultPicker(PickResult.withNoResult())); } } @@ -593,28 +592,6 @@ ConnectivityState getConcludedConnectivityState() { return this.concludedState; } - /** - * No-op picker which doesn't add any custom picking logic. It just passes already known result - * received in constructor. - */ - private static final class Picker extends SubchannelPicker { - private final PickResult result; - - Picker(PickResult result) { - this.result = checkNotNull(result, "result"); - } - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return result; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(Picker.class).add("result", result).toString(); - } - } - /** * Picker that requests connection during the first pick, and returns noResult. */ diff --git a/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java b/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java index a23855e67ec..a41ef03fbda 100644 --- a/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java +++ b/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java @@ -22,7 +22,6 @@ import static io.grpc.ConnectivityState.SHUTDOWN; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import com.google.common.base.MoreObjects; import io.grpc.ConnectivityState; import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; @@ -87,7 +86,8 @@ public void onSubchannelState(ConnectivityStateInfo stateInfo) { // The channel state does not get updated when doing name resolving today, so for the moment // let LB report CONNECTION and call subchannel.requestConnection() immediately. - updateBalancingState(CONNECTING, new Picker(PickResult.withSubchannel(subchannel))); + updateBalancingState( + CONNECTING, new FixedResultPicker(PickResult.withSubchannel(subchannel))); subchannel.requestConnection(); } else { subchannel.updateAddresses(servers); @@ -105,7 +105,7 @@ public void handleNameResolutionError(Status error) { // NB(lukaszx0) Whether we should propagate the error unconditionally is arguable. It's fine // for time being. - updateBalancingState(TRANSIENT_FAILURE, new Picker(PickResult.withError(error))); + updateBalancingState(TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error))); } private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) { @@ -139,13 +139,13 @@ private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo case CONNECTING: // It's safe to use RequestConnectionPicker here, so when coming from IDLE we could leave // the current picker in-place. But ignoring the potential optimization is simpler. - picker = new Picker(PickResult.withNoResult()); + picker = new FixedResultPicker(PickResult.withNoResult()); break; case READY: - picker = new Picker(PickResult.withSubchannel(subchannel)); + picker = new FixedResultPicker(PickResult.withSubchannel(subchannel)); break; case TRANSIENT_FAILURE: - picker = new Picker(PickResult.withError(stateInfo.getStatus())); + picker = new FixedResultPicker(PickResult.withError(stateInfo.getStatus())); break; default: throw new IllegalArgumentException("Unsupported state:" + newState); @@ -173,28 +173,6 @@ public void requestConnection() { } } - /** - * No-op picker which doesn't add any custom picking logic. It just passes already known result - * received in constructor. - */ - private static final class Picker extends SubchannelPicker { - private final PickResult result; - - Picker(PickResult result) { - this.result = checkNotNull(result, "result"); - } - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return result; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(Picker.class).add("result", result).toString(); - } - } - /** Picker that requests connection during the first pick, and returns noResult. */ private final class RequestConnectionPicker extends SubchannelPicker { private final AtomicBoolean connectionRequested = new AtomicBoolean(false); diff --git a/examples/src/main/java/io/grpc/examples/customloadbalance/ShufflingPickFirstLoadBalancer.java b/examples/src/main/java/io/grpc/examples/customloadbalance/ShufflingPickFirstLoadBalancer.java index b49c856c4d0..4715b551524 100644 --- a/examples/src/main/java/io/grpc/examples/customloadbalance/ShufflingPickFirstLoadBalancer.java +++ b/examples/src/main/java/io/grpc/examples/customloadbalance/ShufflingPickFirstLoadBalancer.java @@ -92,7 +92,7 @@ public void onSubchannelState(ConnectivityStateInfo stateInfo) { }); this.subchannel = subchannel; - helper.updateBalancingState(CONNECTING, new Picker(PickResult.withNoResult())); + helper.updateBalancingState(CONNECTING, new FixedResultPicker(PickResult.withNoResult())); subchannel.requestConnection(); } else { subchannel.updateAddresses(servers); @@ -107,7 +107,8 @@ public void handleNameResolutionError(Status error) { subchannel.shutdown(); subchannel = null; } - helper.updateBalancingState(TRANSIENT_FAILURE, new Picker(PickResult.withError(error))); + helper.updateBalancingState( + TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error))); } private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) { @@ -125,13 +126,13 @@ private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo picker = new RequestConnectionPicker(); break; case CONNECTING: - picker = new Picker(PickResult.withNoResult()); + picker = new FixedResultPicker(PickResult.withNoResult()); break; case READY: - picker = new Picker(PickResult.withSubchannel(subchannel)); + picker = new FixedResultPicker(PickResult.withSubchannel(subchannel)); break; case TRANSIENT_FAILURE: - picker = new Picker(PickResult.withError(stateInfo.getStatus())); + picker = new FixedResultPicker(PickResult.withError(stateInfo.getStatus())); break; default: throw new IllegalArgumentException("Unsupported state:" + currentState); @@ -154,29 +155,6 @@ public void requestConnection() { } } - /** - * No-op picker which doesn't add any custom picking logic. It just passes already known result - * received in constructor. - */ - private static final class Picker extends SubchannelPicker { - - private final PickResult result; - - Picker(PickResult result) { - this.result = checkNotNull(result, "result"); - } - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return result; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(Picker.class).add("result", result).toString(); - } - } - /** * Picker that requests connection during the first pick, and returns noResult. */ diff --git a/grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java b/grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java index 7ca20d58bce..2fc2a492ac8 100644 --- a/grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java +++ b/grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java @@ -187,6 +187,7 @@ enum Mode { private List dropList = Collections.emptyList(); // Contains only non-drop, i.e., backends from the round-robin list from the balancer. private List backendList = Collections.emptyList(); + private ConnectivityState currentState = ConnectivityState.CONNECTING; private RoundRobinPicker currentPicker = new RoundRobinPicker(Collections.emptyList(), Arrays.asList(BUFFER_ENTRY)); private boolean requestConnectionPending; @@ -937,10 +938,12 @@ private void maybeUpdatePicker(ConnectivityState state, RoundRobinPicker picker) // Discard the new picker if we are sure it won't make any difference, in order to save // re-processing pending streams, and avoid unnecessary resetting of the pointer in // RoundRobinPicker. - if (picker.dropList.equals(currentPicker.dropList) + if (state.equals(currentState) + && picker.dropList.equals(currentPicker.dropList) && picker.pickList.equals(currentPicker.pickList)) { return; } + currentState = state; currentPicker = picker; helper.updateBalancingState(state, picker); } diff --git a/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java b/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java index 6e59e867e32..848199f50a8 100644 --- a/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java +++ b/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java @@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; import io.grpc.ChannelLogger; import io.grpc.ChannelLogger.ChannelLogLevel; import io.grpc.ConnectivityState; @@ -93,27 +92,14 @@ public void requestConnection() { @Override public void handleNameResolutionError(final Status error) { - class ErrorPicker extends SubchannelPicker { - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withError(error); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("error", error) - .toString(); - } - } - if (routeLookupClient != null) { logger.log(ChannelLogLevel.DEBUG, "closing the routeLookupClient on a name resolution error"); routeLookupClient.close(); routeLookupClient = null; lbPolicyConfiguration = null; } - helper.updateBalancingState(ConnectivityState.TRANSIENT_FAILURE, new ErrorPicker()); + helper.updateBalancingState( + ConnectivityState.TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(error))); } @Override diff --git a/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java b/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java index 12483d60794..b349aecdbf3 100644 --- a/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java +++ b/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java @@ -1025,14 +1025,9 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { @Override public void handleNameResolutionError(final Status error) { - class ErrorPicker extends SubchannelPicker { - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withError(error); - } - } - - helper.updateBalancingState(ConnectivityState.TRANSIENT_FAILURE, new ErrorPicker()); + helper.updateBalancingState( + ConnectivityState.TRANSIENT_FAILURE, + new FixedResultPicker(PickResult.withError(error))); } @Override diff --git a/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java b/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java index c4eb4c7bae5..10436407422 100644 --- a/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java @@ -54,7 +54,6 @@ import io.grpc.SynchronizationContext; import io.grpc.internal.FakeClock; import io.grpc.internal.FakeClock.ScheduledTask; -import io.grpc.internal.PickFirstLoadBalancerProvider; import io.grpc.internal.TestUtils.StandardLoadBalancerProvider; import io.grpc.util.OutlierDetectionLoadBalancer.EndpointTracker; import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig; @@ -568,9 +567,7 @@ public void successRateOneOutlier_configChange() { loadBalancer.acceptResolvedAddresses(buildResolvedAddress(config, servers)); - // The PickFirstLeafLB has an extra level of indirection because of health - int expectedStateChanges = PickFirstLoadBalancerProvider.isEnabledNewPickFirst() ? 8 : 12; - generateLoad(ImmutableMap.of(subchannel2, Status.DEADLINE_EXCEEDED), expectedStateChanges); + generateLoad(ImmutableMap.of(subchannel2, Status.DEADLINE_EXCEEDED), 8); // Move forward in time to a point where the detection timer has fired. forwardTime(config); @@ -604,8 +601,7 @@ public void successRateOneOutlier_unejected() { assertEjectedSubchannels(ImmutableSet.of(ImmutableSet.copyOf(servers.get(0).getAddresses()))); // Now we produce more load, but the subchannel has started working and is no longer an outlier. - int expectedStateChanges = PickFirstLoadBalancerProvider.isEnabledNewPickFirst() ? 8 : 12; - generateLoad(ImmutableMap.of(), expectedStateChanges); + generateLoad(ImmutableMap.of(), 8); // Move forward in time to a point where the detection timer has fired. fakeClock.forwardTime(config.maxEjectionTimeNanos + 1, TimeUnit.NANOSECONDS); diff --git a/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.java b/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.java index dda2ad177e6..1f23f2a4af5 100644 --- a/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.java @@ -54,7 +54,7 @@ final class LeastRequestLoadBalancer extends MultiChildLoadBalancer { private final ThreadSafeRandom random; - private SubchannelPicker currentPicker = new EmptyPicker(); + private SubchannelPicker currentPicker = new FixedResultPicker(PickResult.withNoResult()); private int choiceCount = DEFAULT_CHOICE_COUNT; LeastRequestLoadBalancer(Helper helper) { @@ -113,7 +113,7 @@ protected void updateOverallBalancingState() { } } if (isConnecting) { - updateBalancingState(CONNECTING, new EmptyPicker()); + updateBalancingState(CONNECTING, new FixedResultPicker(PickResult.withNoResult())); } else { // Give it all the failing children and let it randomly pick among them updateBalancingState(TRANSIENT_FAILURE, diff --git a/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java index 6fb6507fa4e..302faed95a4 100644 --- a/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java @@ -22,6 +22,7 @@ import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.SHUTDOWN; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; +import static io.grpc.LoadBalancerMatchers.pickerReturns; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -50,6 +51,7 @@ import io.grpc.EquivalentAddressGroup; import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.CreateSubchannelArgs; +import io.grpc.LoadBalancer.FixedResultPicker; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancer.PickResult; import io.grpc.LoadBalancer.PickSubchannelArgs; @@ -62,7 +64,6 @@ import io.grpc.internal.PickFirstLoadBalancerProvider; import io.grpc.util.AbstractTestHelper; import io.grpc.util.MultiChildLoadBalancer.ChildLbState; -import io.grpc.xds.LeastRequestLoadBalancer.EmptyPicker; import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestLbState; import io.grpc.xds.LeastRequestLoadBalancer.ReadyPicker; @@ -238,7 +239,8 @@ public void pickAfterStateChange() throws Exception { ChildLbState childLbState = loadBalancer.getChildLbStates().iterator().next(); Subchannel subchannel = getSubchannel(servers.get(0)); - inOrder.verify(helper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); + inOrder.verify(helper) + .updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult())); assertThat(childLbState.getCurrentState()).isEqualTo(CONNECTING); deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY)); @@ -251,7 +253,8 @@ public void pickAfterStateChange() throws Exception { assertThat(childLbState.getCurrentState()).isEqualTo(TRANSIENT_FAILURE); assertThat(childLbState.getCurrentPicker().toString()).contains(error.toString()); refreshInvokedAndUpdateBS(inOrder, CONNECTING); - assertThat(pickerCaptor.getValue()).isInstanceOf(EmptyPicker.class); + assertThat(pickerCaptor.getValue().pickSubchannel(mockArgs)) + .isEqualTo(PickResult.withNoResult()); deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(IDLE)); inOrder.verify(helper).refreshNameResolution(); @@ -302,7 +305,8 @@ public void ignoreShutdownSubchannelStateChange() { ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(Attributes.EMPTY) .build()); assertThat(addressesAcceptanceStatus.isOk()).isTrue(); - inOrder.verify(helper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); + inOrder.verify(helper) + .updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult())); List savedSubchannels = new ArrayList<>(subchannels.values()); loadBalancer.shutdown(); @@ -324,7 +328,8 @@ public void stayTransientFailureUntilReady() { .build()); assertThat(addressesAcceptanceStatus.isOk()).isTrue(); - inOrder.verify(helper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); + inOrder.verify(helper) + .updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult())); // Simulate state transitions for each subchannel individually. List children = new ArrayList<>(loadBalancer.getChildLbStates()); @@ -384,7 +389,8 @@ public void refreshNameResolutionWhenSubchannelConnectionBroken() { assertThat(addressesAcceptanceStatus.isOk()).isTrue(); verify(helper, times(3)).createSubchannel(any(CreateSubchannelArgs.class)); - inOrder.verify(helper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); + inOrder.verify(helper) + .updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult())); // Simulate state transitions for each subchannel individually. for (Subchannel sc : subchannels.values()) { @@ -399,7 +405,8 @@ public void refreshNameResolutionWhenSubchannelConnectionBroken() { deliverSubchannelState(sc, ConnectivityStateInfo.forNonError(IDLE)); inOrder.verify(helper).refreshNameResolution(); verify(sc, times(2)).requestConnection(); - inOrder.verify(helper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); + inOrder.verify(helper) + .updateBalancingState(eq(CONNECTING), pickerReturns(PickResult.withNoResult())); } AbstractTestHelper.verifyNoMoreMeaningfulInteractions(helper); @@ -469,14 +476,6 @@ public void pickerLeastRequest() throws Exception { assertEquals(0, ((LeastRequestLbState) childLbStates.get(0)).getActiveRequests()); } - @Test - public void pickerEmptyList() throws Exception { - SubchannelPicker picker = new EmptyPicker(); - - assertNull(picker.pickSubchannel(mockArgs).getSubchannel()); - assertEquals(Status.OK, picker.pickSubchannel(mockArgs).getStatus()); - } - @Test public void nameResolutionErrorWithNoChannels() throws Exception { Status error = Status.NOT_FOUND.withDescription("nameResolutionError"); @@ -554,7 +553,8 @@ public void subchannelStateIsolation() throws Exception { Iterator pickers = pickerCaptor.getAllValues().iterator(); // The picker is incrementally updated as subchannels become READY assertEquals(CONNECTING, stateIterator.next()); - assertThat(pickers.next()).isInstanceOf(EmptyPicker.class); + assertThat(pickers.next().pickSubchannel(mockArgs)) + .isEqualTo(PickResult.withNoResult()); assertEquals(READY, stateIterator.next()); assertThat(getList(pickers.next())).containsExactly(sc1); assertEquals(READY, stateIterator.next()); @@ -585,8 +585,8 @@ public void readyPicker_emptyList() { @Test public void internalPickerComparisons() { - EmptyPicker empty1 = new EmptyPicker(); - EmptyPicker empty2 = new EmptyPicker(); + FixedResultPicker empty1 = new FixedResultPicker(PickResult.withNoResult()); + FixedResultPicker empty2 = new FixedResultPicker(PickResult.withNoResult()); loadBalancer.acceptResolvedAddresses( ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(affinity).build()); From 228fc8ecd924bdad377963b65b55e28eaa56490d Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 5 Jan 2026 10:52:50 -0800 Subject: [PATCH 501/591] core: PickFirstLB should not return a subchannel during CONNECTING This is a follow-up from noticing a breakage in gRPC-LB because it wasn't checking if the connectivity state changed even if the picker was identical. lb_serverStatusCodeConversion() has been misleading since 42e1829b37 ("xds: Do RLS fallback policy eagar start"). At that point, the subchannel it marked as READY was for the default target's policy, not the policy for wilderness. However, since old PF policy provided a subchannel when CONNECTING, everything was "fine", but RLS would mistakenly count toward target_picks. This demonstrates that RLS target_picks has been broken since it was introduced for PF, as PF relied on the caller to avoid the picker when it was CONNECTING. This may have been hard to notice in production, as the metrics become correct as soon as the connection is established, so as long as you use the channel for a while, the duplicate counting would become a small percentage of the overall amount. --- .../io/grpc/internal/PickFirstLoadBalancer.java | 3 +-- .../grpc/internal/PickFirstLoadBalancerTest.java | 11 +++++------ .../java/io/grpc/rls/RlsLoadBalancerTest.java | 16 +++++++--------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java b/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java index a41ef03fbda..aa8b5a7e9a9 100644 --- a/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java +++ b/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java @@ -86,8 +86,7 @@ public void onSubchannelState(ConnectivityStateInfo stateInfo) { // The channel state does not get updated when doing name resolving today, so for the moment // let LB report CONNECTION and call subchannel.requestConnection() immediately. - updateBalancingState( - CONNECTING, new FixedResultPicker(PickResult.withSubchannel(subchannel))); + updateBalancingState(CONNECTING, new FixedResultPicker(PickResult.withNoResult())); subchannel.requestConnection(); } else { subchannel.updateAddresses(servers); diff --git a/core/src/test/java/io/grpc/internal/PickFirstLoadBalancerTest.java b/core/src/test/java/io/grpc/internal/PickFirstLoadBalancerTest.java index 3e0258f2e40..819293e070b 100644 --- a/core/src/test/java/io/grpc/internal/PickFirstLoadBalancerTest.java +++ b/core/src/test/java/io/grpc/internal/PickFirstLoadBalancerTest.java @@ -219,7 +219,7 @@ public void refreshNameResolutionAfterSubchannelConnectionBroken() { inOrder.verify(mockSubchannel).start(stateListenerCaptor.capture()); SubchannelStateListener stateListener = stateListenerCaptor.getValue(); inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); - assertSame(mockSubchannel, pickerCaptor.getValue().pickSubchannel(mockArgs).getSubchannel()); + assertThat(pickerCaptor.getValue().pickSubchannel(mockArgs).hasResult()).isFalse(); inOrder.verify(mockSubchannel).requestConnection(); stateListener.onSubchannelState(ConnectivityStateInfo.forNonError(CONNECTING)); @@ -278,7 +278,7 @@ public void pickAfterResolvedAndChanged() throws Exception { assertThat(args.getAddresses()).isEqualTo(servers); inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); verify(mockSubchannel).requestConnection(); - assertEquals(mockSubchannel, pickerCaptor.getValue().pickSubchannel(mockArgs).getSubchannel()); + assertThat(pickerCaptor.getValue().pickSubchannel(mockArgs).hasResult()).isFalse(); loadBalancer.acceptResolvedAddresses( ResolvedAddresses.newBuilder().setAddresses(newServers).setAttributes(affinity).build()); @@ -300,7 +300,7 @@ public void pickAfterStateChangeAfterResolution() throws Exception { verify(mockSubchannel).start(stateListenerCaptor.capture()); SubchannelStateListener stateListener = stateListenerCaptor.getValue(); verify(mockHelper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); - Subchannel subchannel = pickerCaptor.getValue().pickSubchannel(mockArgs).getSubchannel(); + assertThat(pickerCaptor.getValue().pickSubchannel(mockArgs).hasResult()).isFalse(); reset(mockHelper); when(mockHelper.getSynchronizationContext()).thenReturn(syncContext); @@ -317,7 +317,7 @@ public void pickAfterStateChangeAfterResolution() throws Exception { stateListener.onSubchannelState(ConnectivityStateInfo.forNonError(READY)); inOrder.verify(mockHelper).updateBalancingState(eq(READY), pickerCaptor.capture()); - assertEquals(subchannel, pickerCaptor.getValue().pickSubchannel(mockArgs).getSubchannel()); + assertEquals(mockSubchannel, pickerCaptor.getValue().pickSubchannel(mockArgs).getSubchannel()); verify(mockHelper, atLeast(0)).getSynchronizationContext(); // Don't care verifyNoMoreInteractions(mockHelper); @@ -405,8 +405,7 @@ public void nameResolutionSuccessAfterError() throws Exception { inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); verify(mockSubchannel).requestConnection(); - assertEquals(mockSubchannel, pickerCaptor.getValue().pickSubchannel(mockArgs) - .getSubchannel()); + assertThat(pickerCaptor.getValue().pickSubchannel(mockArgs).hasResult()).isFalse(); assertEquals(pickerCaptor.getValue().pickSubchannel(mockArgs), pickerCaptor.getValue().pickSubchannel(mockArgs)); diff --git a/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java b/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java index 8d16d1bd74c..188c99bcd5a 100644 --- a/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java +++ b/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java @@ -72,7 +72,6 @@ import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.internal.FakeClock; import io.grpc.internal.JsonParser; -import io.grpc.internal.PickFirstLoadBalancerProvider; import io.grpc.internal.PickSubchannelArgsImpl; import io.grpc.internal.testing.StreamRecorder; import io.grpc.lookup.v1.RouteLookupServiceGrpc; @@ -212,12 +211,14 @@ public void lb_serverStatusCodeConversion() throws Exception { throw new RuntimeException(e); } }); + assertThat(subchannels.poll()).isNotNull(); // default target + assertThat(subchannels.poll()).isNull(); + // Warm-up pick; will be queued InOrder inOrder = inOrder(helper); inOrder.verify(helper) .updateBalancingState(eq(ConnectivityState.CONNECTING), pickerCaptor.capture()); SubchannelPicker picker = pickerCaptor.getValue(); PickSubchannelArgs fakeSearchMethodArgs = newPickSubchannelArgs(fakeSearchMethod); - // Warm-up pick; will be queued PickResult res = picker.pickSubchannel(fakeSearchMethodArgs); assertThat(res.getStatus().isOk()).isTrue(); assertThat(res.getSubchannel()).isNull(); @@ -230,8 +231,7 @@ public void lb_serverStatusCodeConversion() throws Exception { subchannel.updateState(ConnectivityStateInfo.forNonError(ConnectivityState.READY)); res = picker.pickSubchannel(fakeSearchMethodArgs); assertThat(res.getStatus().getCode()).isEqualTo(Status.Code.OK); - int expectedTimes = PickFirstLoadBalancerProvider.isEnabledNewPickFirst() ? 1 : 2; - verifyLongCounterAdd("grpc.lb.rls.target_picks", expectedTimes, 1, "wilderness", "complete"); + verifyLongCounterAdd("grpc.lb.rls.target_picks", 1, 1, "wilderness", "complete"); // Check on conversion Throwable cause = new Throwable("cause"); @@ -284,8 +284,7 @@ public void lb_working_withDefaultTarget_rlsResponding() throws Exception { res = picker.pickSubchannel(searchSubchannelArgs); assertThat(subchannelIsReady(res.getSubchannel())).isTrue(); assertThat(res.getSubchannel()).isSameInstanceAs(searchSubchannel); - int expectedTimes = PickFirstLoadBalancerProvider.isEnabledNewPickFirst() ? 1 : 2; - verifyLongCounterAdd("grpc.lb.rls.target_picks", expectedTimes, 1, "wilderness", "complete"); + verifyLongCounterAdd("grpc.lb.rls.target_picks", 1, 1, "wilderness", "complete"); // rescue should be pending status although the overall channel state is READY res = picker.pickSubchannel(rescueSubchannelArgs); @@ -431,7 +430,7 @@ public void lb_working_withDefaultTarget_noRlsResponse() throws Exception { inOrder.verify(helper).getMetricRecorder(); inOrder.verify(helper).getChannelTarget(); inOrder.verifyNoMoreInteractions(); - int times = PickFirstLoadBalancerProvider.isEnabledNewPickFirst() ? 1 : 2; + int times = 1; verifyLongCounterAdd("grpc.lb.rls.default_target_picks", times, 1, "defaultTarget", "complete"); @@ -536,8 +535,7 @@ public void lb_working_withoutDefaultTarget() throws Exception { res = picker.pickSubchannel(newPickSubchannelArgs(fakeSearchMethod)); assertThat(res.getStatus().isOk()).isFalse(); assertThat(subchannelIsReady(res.getSubchannel())).isFalse(); - int expectedTimes = PickFirstLoadBalancerProvider.isEnabledNewPickFirst() ? 1 : 2; - verifyLongCounterAdd("grpc.lb.rls.target_picks", expectedTimes, 1, "wilderness", "complete"); + verifyLongCounterAdd("grpc.lb.rls.target_picks", 1, 1, "wilderness", "complete"); res = picker.pickSubchannel(newPickSubchannelArgs(fakeRescueMethod)); assertThat(subchannelIsReady(res.getSubchannel())).isTrue(); From 3915d029c02efcdcdbd2e57b43e22a3c42435ad0 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 5 Jan 2026 21:27:01 -0800 Subject: [PATCH 502/591] core: Implement oobChannel with resolvingOobChannel The most important part of this change is to ensure that CallCredentials are not propagated to the OOB channel. Because the authority of the OOB channel doesn't match the parent channel, we must ensure that any bearer tokens are not sent to the different server. However, this was not a problem because resolvingOobChannel has the same constraint. (RLS has a different constraint, but we were able to let RLS manage that itself.) This commit does change the behavior of channelz, shutdown, and metrics for the OOB channel. Previously the OOB channel was registered with channelz, but it is only a TODO for resolving channel. Channel shutdown no longer shuts down the OOB channel and it no longer waits for the OOB channel to terminate before becoming terminated itself. That is also a pre-existing TODO. Since ManagedChannelImplBuilder is now being used, global configurators and census are enabled. The proper behavior here is still being determined, but we would want it to be the same for resolving OOB channel and OOB channel. The OOB channel used to refresh the name resolution when the subchannel went IDLE or TF. That is an older behavior from back when regular subchannels would also cause the name resolver to refresh. Now-a-days that goes though the LB tree. gRPC-LB already refreshes name resolution when its RPC closes, so no longer doing it automatically should be fine. balancerRpcExecutorPool no longer has its lifetime managed by the child. It'd be easiest to not use it at all from OOB channel, which wouldn't actually change the regular behavior, as channels already use the same executor by default. However, the tests are making use of the executor being injected, so some propagation needs to be preserved. Lots of OOB channel tests were deleted, but these were either testing OobChannel, which is now gone, or things like channelz, which are known to no longer work like before. --- .../io/grpc/internal/ManagedChannelImpl.java | 153 ++---- .../java/io/grpc/internal/OobChannel.java | 314 ------------- .../internal/OobNameResolverProvider.java | 121 +++++ .../grpc/internal/ManagedChannelImplTest.java | 441 +----------------- .../main/java/io/grpc/grpclb/GrpclbState.java | 5 +- 5 files changed, 173 insertions(+), 861 deletions(-) delete mode 100644 core/src/main/java/io/grpc/internal/OobChannel.java create mode 100644 core/src/main/java/io/grpc/internal/OobNameResolverProvider.java diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index e9fda4e9ec3..bdade61efc6 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -95,6 +95,7 @@ import io.grpc.internal.RetriableStream.ChannelBufferMeter; import io.grpc.internal.RetriableStream.Throttle; import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -167,11 +168,9 @@ public Result selectConfig(PickSubchannelArgs args) { @Nullable private final ChannelCredentials originalChannelCreds; private final ClientTransportFactory transportFactory; - private final ClientTransportFactory oobTransportFactory; private final RestrictedScheduledExecutor scheduledExecutor; private final Executor executor; private final ObjectPool executorPool; - private final ObjectPool balancerRpcExecutorPool; private final ExecutorHolder balancerRpcExecutorHolder; private final ExecutorHolder offloadExecutorHolder; private final TimeProvider timeProvider; @@ -240,9 +239,6 @@ public void uncaughtException(Thread t, Throwable e) { private Collection> pendingCalls; private final Object pendingCallsInUseObject = new Object(); - // Must be mutated from syncContext - private final Set oobChannels = new HashSet<>(1, .75f); - // reprocess() must be run from syncContext private final DelayedClientTransport delayedTransport; private final UncommittedRetriableStreamsRegistry uncommittedRetriableStreamsRegistry @@ -312,9 +308,6 @@ private void maybeShutdownNowSubchannels() { for (InternalSubchannel subchannel : subchannels) { subchannel.shutdownNow(SHUTDOWN_NOW_STATUS); } - for (OobChannel oobChannel : oobChannels) { - oobChannel.getInternalSubchannel().shutdownNow(SHUTDOWN_NOW_STATUS); - } } } @@ -334,7 +327,6 @@ public void run() { builder.setTarget(target).setState(channelStateManager.getState()); List children = new ArrayList<>(); children.addAll(subchannels); - children.addAll(oobChannels); builder.setSubchannels(children); ret.set(builder.build()); } @@ -564,8 +556,6 @@ ClientStream newSubstream( new ExecutorHolder(checkNotNull(builder.offloadExecutorPool, "offloadExecutorPool")); this.transportFactory = new CallCredentialsApplyingTransportFactory( clientTransportFactory, builder.callCredentials, this.offloadExecutorHolder); - this.oobTransportFactory = new CallCredentialsApplyingTransportFactory( - clientTransportFactory, null, this.offloadExecutorHolder); this.scheduledExecutor = new RestrictedScheduledExecutor(transportFactory.getScheduledExecutorService()); maxTraceEvents = builder.maxTraceEvents; @@ -604,8 +594,8 @@ ClientStream newSubstream( this.nameResolverArgs = nameResolverArgsBuilder.build(); this.nameResolver = getNameResolver( targetUri, authorityOverride, nameResolverProvider, nameResolverArgs); - this.balancerRpcExecutorPool = checkNotNull(balancerRpcExecutorPool, "balancerRpcExecutorPool"); - this.balancerRpcExecutorHolder = new ExecutorHolder(balancerRpcExecutorPool); + this.balancerRpcExecutorHolder = new ExecutorHolder( + checkNotNull(balancerRpcExecutorPool, "balancerRpcExecutorPool")); this.delayedTransport = new DelayedClientTransport(this.executor, this.syncContext); this.delayedTransport.start(delayedTransportListener); this.backoffPolicyProvider = backoffPolicyProvider; @@ -1187,7 +1177,7 @@ private void maybeTerminateChannel() { if (terminated) { return; } - if (shutdown.get() && subchannels.isEmpty() && oobChannels.isEmpty()) { + if (shutdown.get() && subchannels.isEmpty()) { channelLogger.log(ChannelLogLevel.INFO, "Terminated"); channelz.removeRootChannel(this); executorPool.returnObject(executor); @@ -1201,13 +1191,6 @@ private void maybeTerminateChannel() { } } - // Must be called from syncContext - private void handleInternalSubchannelState(ConnectivityStateInfo newState) { - if (newState.getState() == TRANSIENT_FAILURE || newState.getState() == IDLE) { - refreshNameResolution(); - } - } - @Override public ConnectivityState getState(boolean requestConnection) { ConnectivityState savedChannelState = channelStateManager.getState(); @@ -1253,9 +1236,6 @@ public void run() { for (InternalSubchannel subchannel : subchannels) { subchannel.resetConnectBackoff(); } - for (OobChannel oobChannel : oobChannels) { - oobChannel.resetConnectBackoff(); - } } } @@ -1413,86 +1393,28 @@ public ManagedChannel createOobChannel(EquivalentAddressGroup addressGroup, Stri @Override public ManagedChannel createOobChannel(List addressGroup, String authority) { - // TODO(ejona): can we be even stricter? Like terminating? - checkState(!terminated, "Channel is terminated"); - long oobChannelCreationTime = timeProvider.currentTimeNanos(); - InternalLogId oobLogId = InternalLogId.allocate("OobChannel", /*details=*/ null); - InternalLogId subchannelLogId = - InternalLogId.allocate("Subchannel-OOB", /*details=*/ authority); - ChannelTracer oobChannelTracer = - new ChannelTracer( - oobLogId, maxTraceEvents, oobChannelCreationTime, - "OobChannel for " + addressGroup); - final OobChannel oobChannel = new OobChannel( - authority, balancerRpcExecutorPool, oobTransportFactory.getScheduledExecutorService(), - syncContext, callTracerFactory.create(), oobChannelTracer, channelz, timeProvider); - channelTracer.reportEvent(new ChannelTrace.Event.Builder() - .setDescription("Child OobChannel created") - .setSeverity(ChannelTrace.Event.Severity.CT_INFO) - .setTimestampNanos(oobChannelCreationTime) - .setChannelRef(oobChannel) - .build()); - ChannelTracer subchannelTracer = - new ChannelTracer(subchannelLogId, maxTraceEvents, oobChannelCreationTime, - "Subchannel for " + addressGroup); - ChannelLogger subchannelLogger = new ChannelLoggerImpl(subchannelTracer, timeProvider); - final class ManagedOobChannelCallback extends InternalSubchannel.Callback { - @Override - void onTerminated(InternalSubchannel is) { - oobChannels.remove(oobChannel); - channelz.removeSubchannel(is); - oobChannel.handleSubchannelTerminated(); - maybeTerminateChannel(); - } - - @Override - void onStateChange(InternalSubchannel is, ConnectivityStateInfo newState) { - // TODO(chengyuanzhang): change to let LB policies explicitly manage OOB channel's - // state and refresh name resolution if necessary. - handleInternalSubchannelState(newState); - oobChannel.handleSubchannelStateChange(newState); - } - } - - final InternalSubchannel internalSubchannel = new InternalSubchannel( - CreateSubchannelArgs.newBuilder().setAddresses(addressGroup).build(), - authority, userAgent, backoffPolicyProvider, oobTransportFactory, - oobTransportFactory.getScheduledExecutorService(), stopwatchSupplier, syncContext, - // All callback methods are run from syncContext - new ManagedOobChannelCallback(), - channelz, - callTracerFactory.create(), - subchannelTracer, - subchannelLogId, - subchannelLogger, - transportFilters, - target, - lbHelper.getMetricRecorder()); - oobChannelTracer.reportEvent(new ChannelTrace.Event.Builder() - .setDescription("Child Subchannel created") - .setSeverity(ChannelTrace.Event.Severity.CT_INFO) - .setTimestampNanos(oobChannelCreationTime) - .setSubchannelRef(internalSubchannel) - .build()); - channelz.addSubchannel(oobChannel); - channelz.addSubchannel(internalSubchannel); - oobChannel.setSubchannel(internalSubchannel); - final class AddOobChannel implements Runnable { - @Override - public void run() { - if (terminating) { - oobChannel.shutdown(); - } - if (!terminated) { - // If channel has not terminated, it will track the subchannel and block termination - // for it. - oobChannels.add(oobChannel); - } - } - } - - syncContext.execute(new AddOobChannel()); - return oobChannel; + NameResolverRegistry nameResolverRegistry = new NameResolverRegistry(); + OobNameResolverProvider resolverProvider = + new OobNameResolverProvider(authority, addressGroup, syncContext); + nameResolverRegistry.register(resolverProvider); + // We could use a hard-coded target, as the name resolver won't actually use this string. + // However, that would make debugging less clear, as we use the target to identify the + // channel. + String target; + try { + target = new URI("oob", "", "/" + authority, null, null).toString(); + } catch (URISyntaxException ex) { + // Any special characters in the path will be percent encoded. So this should be impossible. + throw new AssertionError(ex); + } + ManagedChannel delegate = createResolvingOobChannelBuilder( + target, new DefaultChannelCreds(), nameResolverRegistry) + // TODO(zdapeng): executors should not outlive the parent channel. + .executor(balancerRpcExecutorHolder.getExecutor()) + .idleTimeout(Integer.MAX_VALUE, TimeUnit.SECONDS) + .disableRetry() + .build(); + return new OobChannel(delegate, resolverProvider); } @Deprecated @@ -1504,11 +1426,17 @@ public ManagedChannelBuilder createResolvingOobChannelBuilder(String target) .overrideAuthority(getAuthority()); } - // TODO(creamsoup) prevent main channel to shutdown if oob channel is not terminated - // TODO(zdapeng) register the channel as a subchannel of the parent channel in channelz. @Override public ManagedChannelBuilder createResolvingOobChannelBuilder( final String target, final ChannelCredentials channelCreds) { + return createResolvingOobChannelBuilder(target, channelCreds, nameResolverRegistry); + } + + // TODO(creamsoup) prevent main channel to shutdown if oob channel is not terminated + // TODO(zdapeng) register the channel as a subchannel of the parent channel in channelz. + private ManagedChannelBuilder createResolvingOobChannelBuilder( + final String target, final ChannelCredentials channelCreds, + NameResolverRegistry nameResolverRegistry) { checkNotNull(channelCreds, "channelCreds"); final class ResolvingOobChannelBuilder @@ -1641,6 +1569,19 @@ public ChannelCredentials withoutBearerTokens() { } } + static final class OobChannel extends ForwardingManagedChannel { + private final OobNameResolverProvider resolverProvider; + + public OobChannel(ManagedChannel delegate, OobNameResolverProvider resolverProvider) { + super(delegate); + this.resolverProvider = checkNotNull(resolverProvider, "resolverProvider"); + } + + public void updateAddresses(List eags) { + resolverProvider.updateAddresses(eags); + } + } + final class NameResolverListener extends NameResolver.Listener2 { final LbHelperImpl helper; final NameResolver resolver; diff --git a/core/src/main/java/io/grpc/internal/OobChannel.java b/core/src/main/java/io/grpc/internal/OobChannel.java deleted file mode 100644 index b2272a89672..00000000000 --- a/core/src/main/java/io/grpc/internal/OobChannel.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright 2016 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.internal; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import io.grpc.Attributes; -import io.grpc.CallOptions; -import io.grpc.ClientCall; -import io.grpc.ClientStreamTracer; -import io.grpc.ConnectivityState; -import io.grpc.ConnectivityStateInfo; -import io.grpc.Context; -import io.grpc.EquivalentAddressGroup; -import io.grpc.InternalChannelz; -import io.grpc.InternalChannelz.ChannelStats; -import io.grpc.InternalChannelz.ChannelTrace; -import io.grpc.InternalInstrumented; -import io.grpc.InternalLogId; -import io.grpc.InternalWithLogId; -import io.grpc.LoadBalancer; -import io.grpc.LoadBalancer.FixedResultPicker; -import io.grpc.LoadBalancer.PickResult; -import io.grpc.LoadBalancer.Subchannel; -import io.grpc.LoadBalancer.SubchannelPicker; -import io.grpc.ManagedChannel; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.Status; -import io.grpc.SynchronizationContext; -import io.grpc.internal.ClientCallImpl.ClientStreamProvider; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.concurrent.ThreadSafe; - -/** - * A ManagedChannel backed by a single {@link InternalSubchannel} and used for {@link LoadBalancer} - * to its own RPC needs. - */ -@ThreadSafe -final class OobChannel extends ManagedChannel implements InternalInstrumented { - private static final Logger log = Logger.getLogger(OobChannel.class.getName()); - - private InternalSubchannel subchannel; - private AbstractSubchannel subchannelImpl; - private SubchannelPicker subchannelPicker; - - private final InternalLogId logId; - private final String authority; - private final DelayedClientTransport delayedTransport; - private final InternalChannelz channelz; - private final ObjectPool executorPool; - private final Executor executor; - private final ScheduledExecutorService deadlineCancellationExecutor; - private final CountDownLatch terminatedLatch = new CountDownLatch(1); - private volatile boolean shutdown; - private final CallTracer channelCallsTracer; - private final ChannelTracer channelTracer; - private final TimeProvider timeProvider; - - private final ClientStreamProvider transportProvider = new ClientStreamProvider() { - @Override - public ClientStream newStream(MethodDescriptor method, - CallOptions callOptions, Metadata headers, Context context) { - ClientStreamTracer[] tracers = GrpcUtil.getClientStreamTracers( - callOptions, headers, 0, /* isTransparentRetry= */ false, - /* isHedging= */ false); - Context origContext = context.attach(); - // delayed transport's newStream() always acquires a lock, but concurrent performance doesn't - // matter here because OOB communication should be sparse, and it's not on application RPC's - // critical path. - try { - return delayedTransport.newStream(method, headers, callOptions, tracers); - } finally { - context.detach(origContext); - } - } - }; - - OobChannel( - String authority, ObjectPool executorPool, - ScheduledExecutorService deadlineCancellationExecutor, SynchronizationContext syncContext, - CallTracer callsTracer, ChannelTracer channelTracer, InternalChannelz channelz, - TimeProvider timeProvider) { - this.authority = checkNotNull(authority, "authority"); - this.logId = InternalLogId.allocate(getClass(), authority); - this.executorPool = checkNotNull(executorPool, "executorPool"); - this.executor = checkNotNull(executorPool.getObject(), "executor"); - this.deadlineCancellationExecutor = checkNotNull( - deadlineCancellationExecutor, "deadlineCancellationExecutor"); - this.delayedTransport = new DelayedClientTransport(executor, syncContext); - this.channelz = Preconditions.checkNotNull(channelz); - this.delayedTransport.start(new ManagedClientTransport.Listener() { - @Override - public void transportShutdown(Status s, DisconnectError e) { - // Don't care - } - - @Override - public void transportTerminated() { - subchannelImpl.shutdown(); - } - - @Override - public void transportReady() { - // Don't care - } - - @Override - public Attributes filterTransport(Attributes attributes) { - return attributes; - } - - @Override - public void transportInUse(boolean inUse) { - // Don't care - } - }); - this.channelCallsTracer = callsTracer; - this.channelTracer = checkNotNull(channelTracer, "channelTracer"); - this.timeProvider = checkNotNull(timeProvider, "timeProvider"); - } - - // Must be called only once, right after the OobChannel is created. - void setSubchannel(final InternalSubchannel subchannel) { - log.log(Level.FINE, "[{0}] Created with [{1}]", new Object[] {this, subchannel}); - this.subchannel = subchannel; - subchannelImpl = new AbstractSubchannel() { - @Override - public void shutdown() { - subchannel.shutdown(Status.UNAVAILABLE.withDescription("OobChannel is shutdown")); - } - - @Override - InternalInstrumented getInstrumentedInternalSubchannel() { - return subchannel; - } - - @Override - public void requestConnection() { - subchannel.obtainActiveTransport(); - } - - @Override - public List getAllAddresses() { - return subchannel.getAddressGroups(); - } - - @Override - public Attributes getAttributes() { - return Attributes.EMPTY; - } - - @Override - public Object getInternalSubchannel() { - return subchannel; - } - }; - - subchannelPicker = new FixedResultPicker(PickResult.withSubchannel(subchannelImpl)); - delayedTransport.reprocess(subchannelPicker); - } - - void updateAddresses(List eag) { - subchannel.updateAddresses(eag); - } - - @Override - public ClientCall newCall( - MethodDescriptor methodDescriptor, CallOptions callOptions) { - return new ClientCallImpl<>(methodDescriptor, - callOptions.getExecutor() == null ? executor : callOptions.getExecutor(), - callOptions, transportProvider, deadlineCancellationExecutor, channelCallsTracer, null); - } - - @Override - public String authority() { - return authority; - } - - @Override - public boolean isTerminated() { - return terminatedLatch.getCount() == 0; - } - - @Override - public boolean awaitTermination(long time, TimeUnit unit) throws InterruptedException { - return terminatedLatch.await(time, unit); - } - - @Override - public ConnectivityState getState(boolean requestConnectionIgnored) { - if (subchannel == null) { - return ConnectivityState.IDLE; - } - return subchannel.getState(); - } - - @Override - public ManagedChannel shutdown() { - shutdown = true; - delayedTransport.shutdown(Status.UNAVAILABLE.withDescription("OobChannel.shutdown() called")); - return this; - } - - @Override - public boolean isShutdown() { - return shutdown; - } - - @Override - public ManagedChannel shutdownNow() { - shutdown = true; - delayedTransport.shutdownNow( - Status.UNAVAILABLE.withDescription("OobChannel.shutdownNow() called")); - return this; - } - - void handleSubchannelStateChange(final ConnectivityStateInfo newState) { - channelTracer.reportEvent( - new ChannelTrace.Event.Builder() - .setDescription("Entering " + newState.getState() + " state") - .setSeverity(ChannelTrace.Event.Severity.CT_INFO) - .setTimestampNanos(timeProvider.currentTimeNanos()) - .build()); - switch (newState.getState()) { - case READY: - case IDLE: - delayedTransport.reprocess(subchannelPicker); - break; - case TRANSIENT_FAILURE: - delayedTransport.reprocess( - new FixedResultPicker(PickResult.withError(newState.getStatus()))); - break; - default: - // Do nothing - } - } - - // must be run from channel executor - void handleSubchannelTerminated() { - channelz.removeSubchannel(this); - // When delayedTransport is terminated, it shuts down subchannel. Therefore, at this point - // both delayedTransport and subchannel have terminated. - executorPool.returnObject(executor); - terminatedLatch.countDown(); - } - - @VisibleForTesting - Subchannel getSubchannel() { - return subchannelImpl; - } - - InternalSubchannel getInternalSubchannel() { - return subchannel; - } - - @Override - public ListenableFuture getStats() { - final SettableFuture ret = SettableFuture.create(); - final ChannelStats.Builder builder = new ChannelStats.Builder(); - channelCallsTracer.updateBuilder(builder); - channelTracer.updateBuilder(builder); - builder - .setTarget(authority) - .setState(subchannel.getState()) - .setSubchannels(Collections.singletonList(subchannel)); - ret.set(builder.build()); - return ret; - } - - @Override - public InternalLogId getLogId() { - return logId; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("logId", logId.getId()) - .add("authority", authority) - .toString(); - } - - @Override - public void resetConnectBackoff() { - subchannel.resetConnectBackoff(); - } -} diff --git a/core/src/main/java/io/grpc/internal/OobNameResolverProvider.java b/core/src/main/java/io/grpc/internal/OobNameResolverProvider.java new file mode 100644 index 00000000000..408b92e0c84 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/OobNameResolverProvider.java @@ -0,0 +1,121 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import static java.util.Objects.requireNonNull; + +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import io.grpc.NameResolverProvider; +import io.grpc.StatusOr; +import io.grpc.SynchronizationContext; +import java.net.URI; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +/** + * A provider that is passed addresses and relays those addresses to its created resolvers. + */ +final class OobNameResolverProvider extends NameResolverProvider { + private final String authority; + private final SynchronizationContext parentSyncContext; + // Only accessed from parentSyncContext + @SuppressWarnings("JdkObsolete") // LinkedList uses O(n) memory, including after deletions + private final Collection resolvers = new LinkedList<>(); + // Only accessed from parentSyncContext + private List lastEags; + + public OobNameResolverProvider( + String authority, List eags, SynchronizationContext syncContext) { + this.authority = requireNonNull(authority, "authority"); + this.lastEags = requireNonNull(eags, "eags"); + this.parentSyncContext = requireNonNull(syncContext, "syncContext"); + } + + @Override + public String getDefaultScheme() { + return "oob"; + } + + @Override + protected boolean isAvailable() { + return true; + } + + @Override + protected int priority() { + return 5; // Doesn't matter, as we expect only one provider in the registry + } + + public void updateAddresses(List eags) { + requireNonNull(eags, "eags"); + parentSyncContext.execute(() -> { + this.lastEags = eags; + for (OobNameResolver resolver : resolvers) { + resolver.updateAddresses(eags); + } + }); + } + + @Override + public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) { + return new OobNameResolver(args.getSynchronizationContext()); + } + + final class OobNameResolver extends NameResolver { + private final SynchronizationContext syncContext; + // Null before started, and after shutdown. Only accessed from syncContext + private Listener2 listener; + + public OobNameResolver(SynchronizationContext syncContext) { + this.syncContext = requireNonNull(syncContext, "syncContext"); + } + + @Override + public String getServiceAuthority() { + return authority; + } + + @Override + public void start(Listener2 listener) { + this.listener = requireNonNull(listener, "listener"); + parentSyncContext.execute(() -> { + resolvers.add(this); + updateAddresses(lastEags); + }); + } + + void updateAddresses(List eags) { + parentSyncContext.throwIfNotInThisSynchronizationContext(); + syncContext.execute(() -> { + if (listener == null) { + return; + } + listener.onResult2(ResolutionResult.newBuilder() + .setAddressesOrError(StatusOr.fromValue(lastEags)) + .build()); + }); + } + + @Override + public void shutdown() { + this.listener = null; + parentSyncContext.execute(() -> resolvers.remove(this)); + } + } +} diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 7b3e725991c..731fac94044 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -285,10 +285,6 @@ public String getPolicyName() { @Mock private ClientCall.Listener mockCallListener3; @Mock - private ClientCall.Listener mockCallListener4; - @Mock - private ClientCall.Listener mockCallListener5; - @Mock private ObjectPool executorPool; @Mock private ObjectPool balancerRpcExecutorPool; @@ -817,47 +813,6 @@ public void channelzMembership_subchannel() throws Exception { assertNotNull(channelz.getRootChannel(channel.getLogId().getId())); } - @Test - public void channelzMembership_oob() throws Exception { - createChannel(); - OobChannel oob = (OobChannel) helper.createOobChannel( - Collections.singletonList(addressGroup), AUTHORITY); - // oob channels are not root channels - assertNull(channelz.getRootChannel(oob.getLogId().getId())); - assertTrue(channelz.containsSubchannel(oob.getLogId())); - assertThat(getStats(channel).subchannels).containsExactly(oob); - assertTrue(channelz.containsSubchannel(oob.getLogId())); - - AbstractSubchannel subchannel = (AbstractSubchannel) oob.getSubchannel(); - assertTrue( - channelz.containsSubchannel(subchannel.getInstrumentedInternalSubchannel().getLogId())); - assertThat(getStats(oob).subchannels) - .containsExactly(subchannel.getInstrumentedInternalSubchannel()); - assertTrue( - channelz.containsSubchannel(subchannel.getInstrumentedInternalSubchannel().getLogId())); - - oob.getSubchannel().requestConnection(); - MockClientTransportInfo transportInfo = transports.poll(); - assertNotNull(transportInfo); - assertTrue(channelz.containsClientSocket(transportInfo.transport.getLogId())); - - // terminate transport - transportInfo.listener.transportShutdown(Status.INTERNAL, - SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); - transportInfo.listener.transportTerminated(); - assertFalse(channelz.containsClientSocket(transportInfo.transport.getLogId())); - - // terminate oobchannel - oob.shutdown(); - assertFalse(channelz.containsSubchannel(oob.getLogId())); - assertThat(getStats(channel).subchannels).isEmpty(); - assertFalse( - channelz.containsSubchannel(subchannel.getInstrumentedInternalSubchannel().getLogId())); - - // channel still appears - assertNotNull(channelz.getRootChannel(channel.getLogId().getId())); - } - @Test public void callsAndShutdown() { subtestCallsAndShutdown(false, false); @@ -1801,7 +1756,7 @@ public void subchannelsNoConnectionShutdownNow() { channel.shutdownNow(); verify(mockLoadBalancer).shutdown(); - // Channel's shutdownNow() will call shutdownNow() on all subchannels and oobchannels. + // Channel's shutdownNow() will call shutdownNow() on all subchannels. // Therefore, channel is terminated without relying on LoadBalancer to shutdown subchannels. assertTrue(channel.isTerminated()); verify(mockTransportFactory, never()) @@ -1809,114 +1764,6 @@ public void subchannelsNoConnectionShutdownNow() { any(SocketAddress.class), any(ClientTransportOptions.class), any(ChannelLogger.class)); } - @Test - public void oobchannels() { - createChannel(); - - ManagedChannel oob1 = helper.createOobChannel( - Collections.singletonList(addressGroup), "oob1authority"); - ManagedChannel oob2 = helper.createOobChannel( - Collections.singletonList(addressGroup), "oob2authority"); - verify(balancerRpcExecutorPool, times(2)).getObject(); - - assertEquals("oob1authority", oob1.authority()); - assertEquals("oob2authority", oob2.authority()); - - // OOB channels create connections lazily. A new call will initiate the connection. - Metadata headers = new Metadata(); - ClientCall call = oob1.newCall(method, CallOptions.DEFAULT); - call.start(mockCallListener, headers); - verify(mockTransportFactory) - .newClientTransport( - eq(socketAddress), - eq(new ClientTransportOptions().setAuthority("oob1authority").setUserAgent(USER_AGENT)), - isA(ChannelLogger.class)); - MockClientTransportInfo transportInfo = transports.poll(); - assertNotNull(transportInfo); - - assertEquals(0, balancerRpcExecutor.numPendingTasks()); - transportInfo.listener.transportReady(); - assertEquals(1, balancerRpcExecutor.runDueTasks()); - verify(transportInfo.transport).newStream( - same(method), same(headers), same(CallOptions.DEFAULT), - ArgumentMatchers.any()); - - // The transport goes away - transportInfo.listener.transportShutdown(Status.UNAVAILABLE, - SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); - transportInfo.listener.transportTerminated(); - - // A new call will trigger a new transport - ClientCall call2 = oob1.newCall(method, CallOptions.DEFAULT); - call2.start(mockCallListener2, headers); - ClientCall call3 = - oob1.newCall(method, CallOptions.DEFAULT.withWaitForReady()); - call3.start(mockCallListener3, headers); - verify(mockTransportFactory, times(2)).newClientTransport( - eq(socketAddress), - eq(new ClientTransportOptions().setAuthority("oob1authority").setUserAgent(USER_AGENT)), - isA(ChannelLogger.class)); - transportInfo = transports.poll(); - assertNotNull(transportInfo); - - // This transport fails - Status transportError = Status.UNAVAILABLE.withDescription("Connection refused"); - assertEquals(0, balancerRpcExecutor.numPendingTasks()); - transportInfo.listener.transportShutdown(transportError, - SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); - assertTrue(balancerRpcExecutor.runDueTasks() > 0); - - // Fail-fast RPC will fail, while wait-for-ready RPC will still be pending - verify(mockCallListener2).onClose(same(transportError), any(Metadata.class)); - verify(mockCallListener3, never()).onClose(any(Status.class), any(Metadata.class)); - - // Shutdown - assertFalse(oob1.isShutdown()); - assertFalse(oob2.isShutdown()); - oob1.shutdown(); - oob2.shutdownNow(); - assertTrue(oob1.isShutdown()); - assertTrue(oob2.isShutdown()); - assertTrue(oob2.isTerminated()); - verify(balancerRpcExecutorPool).returnObject(balancerRpcExecutor.getScheduledExecutorService()); - - // New RPCs will be rejected. - assertEquals(0, balancerRpcExecutor.numPendingTasks()); - ClientCall call4 = oob1.newCall(method, CallOptions.DEFAULT); - ClientCall call5 = oob2.newCall(method, CallOptions.DEFAULT); - call4.start(mockCallListener4, headers); - call5.start(mockCallListener5, headers); - assertTrue(balancerRpcExecutor.runDueTasks() > 0); - verify(mockCallListener4).onClose(statusCaptor.capture(), any(Metadata.class)); - Status status4 = statusCaptor.getValue(); - assertEquals(Status.Code.UNAVAILABLE, status4.getCode()); - verify(mockCallListener5).onClose(statusCaptor.capture(), any(Metadata.class)); - Status status5 = statusCaptor.getValue(); - assertEquals(Status.Code.UNAVAILABLE, status5.getCode()); - - // The pending RPC will still be pending - verify(mockCallListener3, never()).onClose(any(Status.class), any(Metadata.class)); - - // This will shutdownNow() the delayed transport, terminating the pending RPC - assertEquals(0, balancerRpcExecutor.numPendingTasks()); - oob1.shutdownNow(); - assertTrue(balancerRpcExecutor.runDueTasks() > 0); - verify(mockCallListener3).onClose(any(Status.class), any(Metadata.class)); - - // Shut down the channel, and it will not terminated because OOB channel has not. - channel.shutdown(); - assertFalse(channel.isTerminated()); - // Delayed transport has already terminated. Terminating the transport terminates the - // subchannel, which in turn terimates the OOB channel, which terminates the channel. - assertFalse(oob1.isTerminated()); - verify(balancerRpcExecutorPool).returnObject(balancerRpcExecutor.getScheduledExecutorService()); - transportInfo.listener.transportTerminated(); - assertTrue(oob1.isTerminated()); - assertTrue(channel.isTerminated()); - verify(balancerRpcExecutorPool, times(2)) - .returnObject(balancerRpcExecutor.getScheduledExecutorService()); - } - @Test public void oobChannelHasNoChannelCallCredentials() { Metadata.Key metadataKey = @@ -1968,7 +1815,7 @@ public void oobChannelHasNoChannelCallCredentials() { balancerRpcExecutor.runDueTasks(); verify(transportInfo.transport).newStream( - same(method), same(headers), same(callOptions), + same(method), same(headers), ArgumentMatchers.any(), ArgumentMatchers.any()); assertThat(headers.getAll(metadataKey)).containsExactly(callCredValue); oob.shutdownNow(); @@ -2095,76 +1942,6 @@ public SwapChannelCredentialsResult answer(InvocationOnMock invocation) { oob.shutdownNow(); } - @Test - public void oobChannelsWhenChannelShutdownNow() { - createChannel(); - ManagedChannel oob1 = helper.createOobChannel( - Collections.singletonList(addressGroup), "oob1Authority"); - ManagedChannel oob2 = helper.createOobChannel( - Collections.singletonList(addressGroup), "oob2Authority"); - - oob1.newCall(method, CallOptions.DEFAULT).start(mockCallListener, new Metadata()); - oob2.newCall(method, CallOptions.DEFAULT).start(mockCallListener2, new Metadata()); - - assertThat(transports).hasSize(2); - MockClientTransportInfo ti1 = transports.poll(); - MockClientTransportInfo ti2 = transports.poll(); - - ti1.listener.transportReady(); - ti2.listener.transportReady(); - - channel.shutdownNow(); - verify(ti1.transport).shutdownNow(any(Status.class)); - verify(ti2.transport).shutdownNow(any(Status.class)); - - ti1.listener.transportShutdown(Status.UNAVAILABLE.withDescription("shutdown now"), - SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); - ti2.listener.transportShutdown(Status.UNAVAILABLE.withDescription("shutdown now"), - SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); - ti1.listener.transportTerminated(); - - assertFalse(channel.isTerminated()); - ti2.listener.transportTerminated(); - assertTrue(channel.isTerminated()); - } - - @Test - public void oobChannelsNoConnectionShutdown() { - createChannel(); - ManagedChannel oob1 = helper.createOobChannel( - Collections.singletonList(addressGroup), "oob1Authority"); - ManagedChannel oob2 = helper.createOobChannel( - Collections.singletonList(addressGroup), "oob2Authority"); - channel.shutdown(); - - verify(mockLoadBalancer).shutdown(); - oob1.shutdown(); - assertTrue(oob1.isTerminated()); - assertFalse(channel.isTerminated()); - oob2.shutdown(); - assertTrue(oob2.isTerminated()); - assertTrue(channel.isTerminated()); - verify(mockTransportFactory, never()) - .newClientTransport( - any(SocketAddress.class), any(ClientTransportOptions.class), any(ChannelLogger.class)); - } - - @Test - public void oobChannelsNoConnectionShutdownNow() { - createChannel(); - helper.createOobChannel(Collections.singletonList(addressGroup), "oob1Authority"); - helper.createOobChannel(Collections.singletonList(addressGroup), "oob2Authority"); - channel.shutdownNow(); - - verify(mockLoadBalancer).shutdown(); - assertTrue(channel.isTerminated()); - // Channel's shutdownNow() will call shutdownNow() on all subchannels and oobchannels. - // Therefore, channel is terminated without relying on LoadBalancer to shutdown oobchannels. - verify(mockTransportFactory, never()) - .newClientTransport( - any(SocketAddress.class), any(ClientTransportOptions.class), any(ChannelLogger.class)); - } - @Test public void subchannelChannel_normalUsage() { createChannel(); @@ -2310,69 +2087,6 @@ public void lbHelper_getNonDefaultNameResolverRegistry() { .isNotSameInstanceAs(NameResolverRegistry.getDefaultRegistry()); } - @Test - public void refreshNameResolution_whenOobChannelConnectionFailed_notIdle() { - subtestNameResolutionRefreshWhenConnectionFailed(false); - } - - @Test - public void notRefreshNameResolution_whenOobChannelConnectionFailed_idle() { - subtestNameResolutionRefreshWhenConnectionFailed(true); - } - - private void subtestNameResolutionRefreshWhenConnectionFailed(boolean isIdle) { - FakeNameResolverFactory nameResolverFactory = - new FakeNameResolverFactory.Builder(expectedUri) - .setServers(Collections.singletonList(new EquivalentAddressGroup(socketAddress))) - .build(); - channelBuilder.nameResolverFactory(nameResolverFactory); - createChannel(); - OobChannel oobChannel = (OobChannel) helper.createOobChannel( - Collections.singletonList(addressGroup), "oobAuthority"); - oobChannel.getSubchannel().requestConnection(); - - MockClientTransportInfo transportInfo = transports.poll(); - assertNotNull(transportInfo); - - FakeNameResolverFactory.FakeNameResolver resolver = nameResolverFactory.resolvers.remove(0); - - if (isIdle) { - channel.enterIdle(); - // Entering idle mode will result in a new resolver - resolver = nameResolverFactory.resolvers.remove(0); - } - - assertEquals(0, nameResolverFactory.resolvers.size()); - - int expectedRefreshCount = 0; - - // Transport closed when connecting - assertEquals(expectedRefreshCount, resolver.refreshCalled); - transportInfo.listener.transportShutdown(Status.UNAVAILABLE, - SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); - // When channel enters idle, new resolver is created but not started. - if (!isIdle) { - expectedRefreshCount++; - } - assertEquals(expectedRefreshCount, resolver.refreshCalled); - - timer.forwardNanos(RECONNECT_BACKOFF_INTERVAL_NANOS); - transportInfo = transports.poll(); - assertNotNull(transportInfo); - - transportInfo.listener.transportReady(); - - // Transport closed when ready - assertEquals(expectedRefreshCount, resolver.refreshCalled); - transportInfo.listener.transportShutdown(Status.UNAVAILABLE, - SimpleDisconnectError.SUBCHANNEL_SHUTDOWN); - // When channel enters idle, new resolver is created but not started. - if (!isIdle) { - expectedRefreshCount++; - } - assertEquals(expectedRefreshCount, resolver.refreshCalled); - } - /** * Test that information such as the Call's context, MethodDescriptor, authority, executor are * propagated to newStream() and applyRequestMetadata(). @@ -3525,48 +3239,6 @@ public void channelTracing_subchannelStateChangeEvent() throws Exception { .build()); } - @Test - public void channelTracing_oobChannelStateChangeEvent() throws Exception { - channelBuilder.maxTraceEvents(10); - createChannel(); - OobChannel oobChannel = (OobChannel) helper.createOobChannel( - Collections.singletonList(addressGroup), "authority"); - timer.forwardNanos(1234); - oobChannel.handleSubchannelStateChange( - ConnectivityStateInfo.forNonError(ConnectivityState.CONNECTING)); - assertThat(getStats(oobChannel).channelTrace.events).contains(new ChannelTrace.Event.Builder() - .setDescription("Entering CONNECTING state") - .setSeverity(ChannelTrace.Event.Severity.CT_INFO) - .setTimestampNanos(timer.getTicker().read()) - .build()); - } - - @Test - public void channelTracing_oobChannelCreationEvents() throws Exception { - channelBuilder.maxTraceEvents(10); - createChannel(); - timer.forwardNanos(1234); - OobChannel oobChannel = (OobChannel) helper.createOobChannel( - Collections.singletonList(addressGroup), "authority"); - assertThat(getStats(channel).channelTrace.events).contains(new ChannelTrace.Event.Builder() - .setDescription("Child OobChannel created") - .setSeverity(ChannelTrace.Event.Severity.CT_INFO) - .setTimestampNanos(timer.getTicker().read()) - .setChannelRef(oobChannel) - .build()); - assertThat(getStats(oobChannel).channelTrace.events).contains(new ChannelTrace.Event.Builder() - .setDescription("OobChannel for [[[test-addr]/{}]] created") - .setSeverity(ChannelTrace.Event.Severity.CT_INFO) - .setTimestampNanos(timer.getTicker().read()) - .build()); - assertThat(getStats(oobChannel.getInternalSubchannel()).channelTrace.events).contains( - new ChannelTrace.Event.Builder() - .setDescription("Subchannel for [[[test-addr]/{}]] created") - .setSeverity(ChannelTrace.Event.Severity.CT_INFO) - .setTimestampNanos(timer.getTicker().read()) - .build()); - } - @Test public void channelsAndSubchannels_instrumented_state() throws Exception { createChannel(); @@ -3682,115 +3354,6 @@ private void channelsAndSubchannels_instrumented0(boolean success) throws Except } } - @Test - public void channelsAndSubchannels_oob_instrumented_success() throws Exception { - channelsAndSubchannels_oob_instrumented0(true); - } - - @Test - public void channelsAndSubchannels_oob_instrumented_fail() throws Exception { - channelsAndSubchannels_oob_instrumented0(false); - } - - private void channelsAndSubchannels_oob_instrumented0(boolean success) throws Exception { - // set up - ClientStream mockStream = mock(ClientStream.class); - createChannel(); - - OobChannel oobChannel = (OobChannel) helper.createOobChannel( - Collections.singletonList(addressGroup), "oobauthority"); - AbstractSubchannel oobSubchannel = (AbstractSubchannel) oobChannel.getSubchannel(); - FakeClock callExecutor = new FakeClock(); - CallOptions options = - CallOptions.DEFAULT.withExecutor(callExecutor.getScheduledExecutorService()); - ClientCall call = oobChannel.newCall(method, options); - Metadata headers = new Metadata(); - - // Channel stat bumped when ClientCall.start() called - assertEquals(0, getStats(oobChannel).callsStarted); - call.start(mockCallListener, headers); - assertEquals(1, getStats(oobChannel).callsStarted); - - MockClientTransportInfo transportInfo = transports.poll(); - ConnectionClientTransport mockTransport = transportInfo.transport; - ManagedClientTransport.Listener transportListener = transportInfo.listener; - when(mockTransport.newStream( - same(method), same(headers), any(CallOptions.class), - ArgumentMatchers.any())) - .thenReturn(mockStream); - - // subchannel stat bumped when call gets assigned to it - assertEquals(0, getStats(oobSubchannel).callsStarted); - transportListener.transportReady(); - callExecutor.runDueTasks(); - verify(mockStream).start(streamListenerCaptor.capture()); - assertEquals(1, getStats(oobSubchannel).callsStarted); - - ClientStreamListener streamListener = streamListenerCaptor.getValue(); - call.halfClose(); - - // closing stream listener affects subchannel stats immediately - assertEquals(0, getStats(oobSubchannel).callsSucceeded); - assertEquals(0, getStats(oobSubchannel).callsFailed); - streamListener.closed(success ? Status.OK : Status.UNKNOWN, PROCESSED, new Metadata()); - if (success) { - assertEquals(1, getStats(oobSubchannel).callsSucceeded); - assertEquals(0, getStats(oobSubchannel).callsFailed); - } else { - assertEquals(0, getStats(oobSubchannel).callsSucceeded); - assertEquals(1, getStats(oobSubchannel).callsFailed); - } - - // channel stats bumped when the ClientCall.Listener is notified - assertEquals(0, getStats(oobChannel).callsSucceeded); - assertEquals(0, getStats(oobChannel).callsFailed); - callExecutor.runDueTasks(); - if (success) { - assertEquals(1, getStats(oobChannel).callsSucceeded); - assertEquals(0, getStats(oobChannel).callsFailed); - } else { - assertEquals(0, getStats(oobChannel).callsSucceeded); - assertEquals(1, getStats(oobChannel).callsFailed); - } - // oob channel is separate from the original channel - assertEquals(0, getStats(channel).callsSucceeded); - assertEquals(0, getStats(channel).callsFailed); - } - - @Test - public void channelsAndSubchannels_oob_instrumented_name() throws Exception { - createChannel(); - - String authority = "oobauthority"; - OobChannel oobChannel = (OobChannel) helper.createOobChannel( - Collections.singletonList(addressGroup), authority); - assertEquals(authority, getStats(oobChannel).target); - } - - @Test - public void channelsAndSubchannels_oob_instrumented_state() throws Exception { - createChannel(); - - OobChannel oobChannel = (OobChannel) helper.createOobChannel( - Collections.singletonList(addressGroup), "oobauthority"); - assertEquals(IDLE, getStats(oobChannel).state); - - oobChannel.getSubchannel().requestConnection(); - assertEquals(CONNECTING, getStats(oobChannel).state); - - MockClientTransportInfo transportInfo = transports.poll(); - ManagedClientTransport.Listener transportListener = transportInfo.listener; - - transportListener.transportReady(); - assertEquals(READY, getStats(oobChannel).state); - - // oobchannel state is separate from the ManagedChannel - assertEquals(CONNECTING, getStats(channel).state); - channel.shutdownNow(); - assertEquals(SHUTDOWN, getStats(channel).state); - assertEquals(SHUTDOWN, getStats(oobChannel).state); - } - @Test public void binaryLogInstalled() throws Exception { final SettableFuture intercepted = SettableFuture.create(); diff --git a/grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java b/grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java index 2fc2a492ac8..5ed84ade2f8 100644 --- a/grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java +++ b/grpclb/src/main/java/io/grpc/grpclb/GrpclbState.java @@ -386,11 +386,12 @@ private void useFallbackBackends() { } private void shutdownLbComm() { + shutdownLbRpc(); if (lbCommChannel != null) { - lbCommChannel.shutdown(); + // The channel should have no RPCs at this point + lbCommChannel.shutdownNow(); lbCommChannel = null; } - shutdownLbRpc(); } private void shutdownLbRpc() { From 4bbf8eee5069ed45624a601c046bb3ee81798e43 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Mon, 5 Jan 2026 21:30:53 -0800 Subject: [PATCH 503/591] core: Convert AutoConfiguredLB to an actual LB AutoConfiguredLB wasn't able to be a LB because it needed to be able to reject configuration to cause the NameResolver to refresh. Since 4b4cb0bd3b, and especially 9888a54abd, the LB API now is able to do this directly. The real end-goal of this work is to replace (much of) AutoConfiguredLB with GracefulSwitchLB. The AutoConfiguredLBFactory will still be needed for config handling, but the LB itself could just become an instance of GracefulSwitchLB. Using GracefulSwitchLB will let us reuse more of the config parsing logic, avoids a latency hit when the top-level policy changes, and gets rid of the last usage of ServiceConfigUtil.selectLbPolicyFromList() outside of GracefulSwitchLB. Go and C are already using GracefulSwitchLB for the top-level policy. Moving the defaultProvider creation earlier was to allow parseLoadBalancingPolicyConfig() to never return null. However, that ran into some simple but annoying test failures because the service config was now being detected as changed. That's solveable, but turns out to be more involved than this change itself, so that's left for later. Since the error handling is nicer now and the earlier creation will be needed eventually anyway, I left the earlier creation in-place even though it technically doesn't have to be done as part of this commit. --- .../AutoConfiguredLoadBalancerFactory.java | 107 +++++++----------- .../FixedPickerLoadBalancerProvider.java | 80 +++++++++++++ .../io/grpc/internal/ManagedChannelImpl.java | 8 +- .../main/java/io/grpc/internal/ScParser.java | 11 +- ...AutoConfiguredLoadBalancerFactoryTest.java | 82 +++++++------- .../grpc/internal/ManagedChannelImplTest.java | 4 +- 6 files changed, 178 insertions(+), 114 deletions(-) create mode 100644 core/src/main/java/io/grpc/internal/FixedPickerLoadBalancerProvider.java diff --git a/core/src/main/java/io/grpc/internal/AutoConfiguredLoadBalancerFactory.java b/core/src/main/java/io/grpc/internal/AutoConfiguredLoadBalancerFactory.java index 6b8537fd658..dcefa8f8351 100644 --- a/core/src/main/java/io/grpc/internal/AutoConfiguredLoadBalancerFactory.java +++ b/core/src/main/java/io/grpc/internal/AutoConfiguredLoadBalancerFactory.java @@ -38,10 +38,10 @@ import java.util.Map; import javax.annotation.Nullable; -public final class AutoConfiguredLoadBalancerFactory { +public final class AutoConfiguredLoadBalancerFactory extends LoadBalancerProvider { private final LoadBalancerRegistry registry; - private final String defaultPolicy; + private final LoadBalancerProvider defaultProvider; public AutoConfiguredLoadBalancerFactory(String defaultPolicy) { this(LoadBalancerRegistry.getDefaultRegistry(), defaultPolicy); @@ -50,47 +50,34 @@ public AutoConfiguredLoadBalancerFactory(String defaultPolicy) { @VisibleForTesting AutoConfiguredLoadBalancerFactory(LoadBalancerRegistry registry, String defaultPolicy) { this.registry = checkNotNull(registry, "registry"); - this.defaultPolicy = checkNotNull(defaultPolicy, "defaultPolicy"); + LoadBalancerProvider provider = + registry.getProvider(checkNotNull(defaultPolicy, "defaultPolicy")); + if (provider == null) { + Status status = Status.INTERNAL.withDescription("Could not find policy '" + defaultPolicy + + "'. Make sure its implementation is either registered to LoadBalancerRegistry or" + + " included in META-INF/services/io.grpc.LoadBalancerProvider from your jar files."); + provider = new FixedPickerLoadBalancerProvider( + ConnectivityState.TRANSIENT_FAILURE, + new LoadBalancer.FixedResultPicker(PickResult.withError(status)), + status); + } + this.defaultProvider = provider; } + @Override public AutoConfiguredLoadBalancer newLoadBalancer(Helper helper) { return new AutoConfiguredLoadBalancer(helper); } - private static final class NoopLoadBalancer extends LoadBalancer { - - @Override - @Deprecated - @SuppressWarnings("InlineMeSuggester") - public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { - } - - @Override - public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - return Status.OK; - } - - @Override - public void handleNameResolutionError(Status error) {} - - @Override - public void shutdown() {} - } - @VisibleForTesting - public final class AutoConfiguredLoadBalancer { + public final class AutoConfiguredLoadBalancer extends LoadBalancer { private final Helper helper; private LoadBalancer delegate; private LoadBalancerProvider delegateProvider; AutoConfiguredLoadBalancer(Helper helper) { this.helper = helper; - delegateProvider = registry.getProvider(defaultPolicy); - if (delegateProvider == null) { - throw new IllegalStateException("Could not find policy '" + defaultPolicy - + "'. Make sure its implementation is either registered to LoadBalancerRegistry or" - + " included in META-INF/services/io.grpc.LoadBalancerProvider from your jar files."); - } + this.delegateProvider = defaultProvider; delegate = delegateProvider.newLoadBalancer(helper); } @@ -98,23 +85,12 @@ public final class AutoConfiguredLoadBalancer { * Returns non-OK status if the delegate rejects the resolvedAddresses (e.g. if it does not * support an empty list). */ - Status tryAcceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + @Override + public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { PolicySelection policySelection = (PolicySelection) resolvedAddresses.getLoadBalancingPolicyConfig(); if (policySelection == null) { - LoadBalancerProvider defaultProvider; - try { - defaultProvider = getProviderOrThrow(defaultPolicy, "using default policy"); - } catch (PolicyException e) { - Status s = Status.INTERNAL.withDescription(e.getMessage()); - helper.updateBalancingState( - ConnectivityState.TRANSIENT_FAILURE, new FixedResultPicker(PickResult.withError(s))); - delegate.shutdown(); - delegateProvider = null; - delegate = new NoopLoadBalancer(); - return Status.OK; - } policySelection = new PolicySelection(defaultProvider, /* config= */ null); } @@ -145,20 +121,24 @@ Status tryAcceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { .build()); } - void handleNameResolutionError(Status error) { + @Override + public void handleNameResolutionError(Status error) { getDelegate().handleNameResolutionError(error); } + @Override @Deprecated - void handleSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) { + public void handleSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) { getDelegate().handleSubchannelState(subchannel, stateInfo); } - void requestConnection() { + @Override + public void requestConnection() { getDelegate().requestConnection(); } - void shutdown() { + @Override + public void shutdown() { delegate.shutdown(); delegate = null; } @@ -179,16 +159,6 @@ LoadBalancerProvider getDelegateProvider() { } } - private LoadBalancerProvider getProviderOrThrow(String policy, String choiceReason) - throws PolicyException { - LoadBalancerProvider provider = registry.getProvider(policy); - if (provider == null) { - throw new PolicyException( - "Trying to load '" + policy + "' because " + choiceReason + ", but it's unavailable"); - } - return provider; - } - /** * Parses first available LoadBalancer policy from service config. Available LoadBalancer should * be registered to {@link LoadBalancerRegistry}. If the first available LoadBalancer policy is @@ -209,8 +179,11 @@ private LoadBalancerProvider getProviderOrThrow(String policy, String choiceReas * * @return the parsed {@link PolicySelection}, or {@code null} if no selection could be made. */ + // TODO(ejona): The Provider API doesn't allow null, but ScParser can handle this and it will need + // tweaking to ManagedChannelImpl.defaultServiceConfig to fix. @Nullable - ConfigOrError parseLoadBalancerPolicy(Map serviceConfig) { + @Override + public ConfigOrError parseLoadBalancingPolicyConfig(Map serviceConfig) { try { List loadBalancerConfigs = null; if (serviceConfig != null) { @@ -228,12 +201,18 @@ ConfigOrError parseLoadBalancerPolicy(Map serviceConfig) { } } - @VisibleForTesting - static final class PolicyException extends Exception { - private static final long serialVersionUID = 1L; + @Override + public boolean isAvailable() { + return true; + } - private PolicyException(String msg) { - super(msg); - } + @Override + public int getPriority() { + return 5; + } + + @Override + public String getPolicyName() { + return "auto_configured_internal"; } } diff --git a/core/src/main/java/io/grpc/internal/FixedPickerLoadBalancerProvider.java b/core/src/main/java/io/grpc/internal/FixedPickerLoadBalancerProvider.java new file mode 100644 index 00000000000..a632948bdb9 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/FixedPickerLoadBalancerProvider.java @@ -0,0 +1,80 @@ +/* + * Copyright 2026 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.internal; + +import static java.util.Objects.requireNonNull; + +import io.grpc.ConnectivityState; +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancerProvider; +import io.grpc.Status; + +/** A LB provider whose LB always uses the same picker. */ +final class FixedPickerLoadBalancerProvider extends LoadBalancerProvider { + private final ConnectivityState state; + private final LoadBalancer.SubchannelPicker picker; + private final Status acceptAddressesStatus; + + public FixedPickerLoadBalancerProvider( + ConnectivityState state, LoadBalancer.SubchannelPicker picker, Status acceptAddressesStatus) { + this.state = requireNonNull(state, "state"); + this.picker = requireNonNull(picker, "picker"); + this.acceptAddressesStatus = requireNonNull(acceptAddressesStatus, "acceptAddressesStatus"); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public int getPriority() { + return 5; + } + + @Override + public String getPolicyName() { + return "fixed_picker_lb_internal"; + } + + @Override + public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) { + return new FixedPickerLoadBalancer(helper); + } + + private final class FixedPickerLoadBalancer extends LoadBalancer { + private final Helper helper; + + public FixedPickerLoadBalancer(Helper helper) { + this.helper = requireNonNull(helper, "helper"); + } + + @Override + public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + helper.updateBalancingState(state, picker); + return acceptAddressesStatus; + } + + @Override + public void handleNameResolutionError(Status error) { + helper.updateBalancingState(state, picker); + } + + @Override + public void shutdown() {} + } +} diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index bdade61efc6..5c3afbf8de0 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -69,6 +69,7 @@ import io.grpc.LoadBalancer.ResolvedAddresses; import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.LoadBalancer.SubchannelStateListener; +import io.grpc.LoadBalancerProvider; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.Metadata; @@ -85,7 +86,6 @@ import io.grpc.StatusOr; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; -import io.grpc.internal.AutoConfiguredLoadBalancerFactory.AutoConfiguredLoadBalancer; import io.grpc.internal.ClientCallImpl.ClientStreamProvider; import io.grpc.internal.ClientTransportFactory.SwapChannelCredentialsResult; import io.grpc.internal.ManagedChannelImplBuilder.ClientTransportFactoryBuilder; @@ -163,7 +163,7 @@ public Result selectConfig(PickSubchannelArgs args) { private final URI targetUri; private final NameResolverProvider nameResolverProvider; private final NameResolver.Args nameResolverArgs; - private final AutoConfiguredLoadBalancerFactory loadBalancerFactory; + private final LoadBalancerProvider loadBalancerFactory; private final ClientTransportFactory originalTransportFactory; @Nullable private final ChannelCredentials originalChannelCreds; @@ -1342,7 +1342,7 @@ void remove(RetriableStream retriableStream) { } private final class LbHelperImpl extends LoadBalancer.Helper { - AutoConfiguredLoadBalancer lb; + LoadBalancer lb; @Override public AbstractSubchannel createSubchannel(CreateSubchannelArgs args) { @@ -1727,7 +1727,7 @@ public Status onResult2(final ResolutionResult resolutionResult) { .setAddresses(serversOrError.getValue()) .setAttributes(attributes) .setLoadBalancingPolicyConfig(effectiveServiceConfig.getLoadBalancingConfig()); - Status addressAcceptanceStatus = helper.lb.tryAcceptResolvedAddresses( + Status addressAcceptanceStatus = helper.lb.acceptResolvedAddresses( resolvedAddresses.build()); return addressAcceptanceStatus; } diff --git a/core/src/main/java/io/grpc/internal/ScParser.java b/core/src/main/java/io/grpc/internal/ScParser.java index f94449f7c7b..16a241f41f6 100644 --- a/core/src/main/java/io/grpc/internal/ScParser.java +++ b/core/src/main/java/io/grpc/internal/ScParser.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; +import io.grpc.LoadBalancerProvider; import io.grpc.NameResolver; import io.grpc.NameResolver.ConfigOrError; import io.grpc.Status; @@ -31,18 +32,18 @@ public final class ScParser extends NameResolver.ServiceConfigParser { private final boolean retryEnabled; private final int maxRetryAttemptsLimit; private final int maxHedgedAttemptsLimit; - private final AutoConfiguredLoadBalancerFactory autoLoadBalancerFactory; + private final LoadBalancerProvider parser; /** Creates a parse with global retry settings and an auto configured lb factory. */ public ScParser( boolean retryEnabled, int maxRetryAttemptsLimit, int maxHedgedAttemptsLimit, - AutoConfiguredLoadBalancerFactory autoLoadBalancerFactory) { + LoadBalancerProvider parser) { this.retryEnabled = retryEnabled; this.maxRetryAttemptsLimit = maxRetryAttemptsLimit; this.maxHedgedAttemptsLimit = maxHedgedAttemptsLimit; - this.autoLoadBalancerFactory = checkNotNull(autoLoadBalancerFactory, "autoLoadBalancerFactory"); + this.parser = checkNotNull(parser, "parser"); } @Override @@ -50,7 +51,9 @@ public ConfigOrError parseServiceConfig(Map rawServiceConfig) { try { Object loadBalancingPolicySelection; ConfigOrError choiceFromLoadBalancer = - autoLoadBalancerFactory.parseLoadBalancerPolicy(rawServiceConfig); + parser.parseLoadBalancingPolicyConfig(rawServiceConfig); + // TODO(ejona): The Provider API doesn't allow null, but AutoConfiguredLoadBalancerFactory can + // return null and it will need tweaking to ManagedChannelImpl.defaultServiceConfig to fix. if (choiceFromLoadBalancer == null) { loadBalancingPolicySelection = null; } else if (choiceFromLoadBalancer.getError() != null) { diff --git a/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java b/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java index 8d56968737b..07d19d41a86 100644 --- a/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java +++ b/core/src/test/java/io/grpc/internal/AutoConfiguredLoadBalancerFactoryTest.java @@ -193,7 +193,7 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { AutoConfiguredLoadBalancer lb = lbf.newLoadBalancer(helper); LoadBalancer oldDelegate = lb.getDelegate(); - Status addressAcceptanceStatus = lb.tryAcceptResolvedAddresses( + Status addressAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(servers) .setAttributes(Attributes.EMPTY) @@ -208,7 +208,7 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { public void acceptResolvedAddresses_shutsDownOldBalancer() throws Exception { Map serviceConfig = parseConfig("{\"loadBalancingConfig\": [ {\"round_robin\": { } } ] }"); - ConfigOrError lbConfigs = lbf.parseLoadBalancerPolicy(serviceConfig); + ConfigOrError lbConfigs = lbf.parseLoadBalancingPolicyConfig(serviceConfig); final List servers = Collections.singletonList(new EquivalentAddressGroup(new SocketAddress(){})); @@ -235,7 +235,7 @@ public void shutdown() { }; lb.setDelegate(testlb); - Status addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + Status addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(servers) .setLoadBalancingPolicyConfig(lbConfigs.getConfig()) @@ -252,7 +252,7 @@ public void shutdown() { public void acceptResolvedAddresses_propagateLbConfigToDelegate() throws Exception { Map rawServiceConfig = parseConfig("{\"loadBalancingConfig\": [ {\"test_lb\": { \"setting1\": \"high\" } } ] }"); - ConfigOrError lbConfigs = lbf.parseLoadBalancerPolicy(rawServiceConfig); + ConfigOrError lbConfigs = lbf.parseLoadBalancingPolicyConfig(rawServiceConfig); assertThat(lbConfigs.getConfig()).isNotNull(); final List servers = @@ -260,7 +260,7 @@ public void acceptResolvedAddresses_propagateLbConfigToDelegate() throws Excepti Helper helper = new TestHelper(); AutoConfiguredLoadBalancer lb = lbf.newLoadBalancer(helper); - Status addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + Status addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(servers) .setLoadBalancingPolicyConfig(lbConfigs.getConfig()) @@ -280,9 +280,9 @@ public void acceptResolvedAddresses_propagateLbConfigToDelegate() throws Excepti rawServiceConfig = parseConfig("{\"loadBalancingConfig\": [ {\"test_lb\": { \"setting1\": \"low\" } } ] }"); - lbConfigs = lbf.parseLoadBalancerPolicy(rawServiceConfig); + lbConfigs = lbf.parseLoadBalancingPolicyConfig(rawServiceConfig); - addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(servers) .setLoadBalancingPolicyConfig(lbConfigs.getConfig()) @@ -305,7 +305,7 @@ public void acceptResolvedAddresses_propagateLbConfigToDelegate() throws Excepti public void acceptResolvedAddresses_propagateAddrsToDelegate() throws Exception { Map rawServiceConfig = parseConfig("{\"loadBalancingConfig\": [ {\"test_lb\": { \"setting1\": \"high\" } } ] }"); - ConfigOrError lbConfigs = lbf.parseLoadBalancerPolicy(rawServiceConfig); + ConfigOrError lbConfigs = lbf.parseLoadBalancingPolicyConfig(rawServiceConfig); assertThat(lbConfigs.getConfig()).isNotNull(); Helper helper = new TestHelper(); @@ -313,7 +313,7 @@ public void acceptResolvedAddresses_propagateAddrsToDelegate() throws Exception List servers = Collections.singletonList(new EquivalentAddressGroup(new InetSocketAddress(8080){})); - Status addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + Status addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(servers) .setLoadBalancingPolicyConfig(lbConfigs.getConfig()) @@ -329,7 +329,7 @@ public void acceptResolvedAddresses_propagateAddrsToDelegate() throws Exception servers = Collections.singletonList(new EquivalentAddressGroup(new InetSocketAddress(9090){})); - addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(servers) .setLoadBalancingPolicyConfig(lbConfigs.getConfig()) @@ -353,8 +353,8 @@ public void acceptResolvedAddresses_delegateDoNotAcceptEmptyAddressList_nothing( Map serviceConfig = parseConfig("{\"loadBalancingConfig\": [ {\"test_lb\": { \"setting1\": \"high\" } } ] }"); - ConfigOrError lbConfig = lbf.parseLoadBalancerPolicy(serviceConfig); - Status addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + ConfigOrError lbConfig = lbf.parseLoadBalancingPolicyConfig(serviceConfig); + Status addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(Collections.emptyList()) .setLoadBalancingPolicyConfig(lbConfig.getConfig()) @@ -373,8 +373,8 @@ public void acceptResolvedAddresses_delegateAcceptsEmptyAddressList() Map rawServiceConfig = parseConfig("{\"loadBalancingConfig\": [ {\"test_lb2\": { \"setting1\": \"high\" } } ] }"); ConfigOrError lbConfigs = - lbf.parseLoadBalancerPolicy(rawServiceConfig); - Status addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + lbf.parseLoadBalancingPolicyConfig(rawServiceConfig); + Status addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(Collections.emptyList()) .setLoadBalancingPolicyConfig(lbConfigs.getConfig()) @@ -394,7 +394,7 @@ public void acceptResolvedAddresses_delegateAcceptsEmptyAddressList() public void acceptResolvedAddresses_useSelectedLbPolicy() throws Exception { Map rawServiceConfig = parseConfig("{\"loadBalancingConfig\": [{\"round_robin\": {}}]}"); - ConfigOrError lbConfigs = lbf.parseLoadBalancerPolicy(rawServiceConfig); + ConfigOrError lbConfigs = lbf.parseLoadBalancingPolicyConfig(rawServiceConfig); assertThat(lbConfigs.getConfig()).isNotNull(); assertThat(((PolicySelection) lbConfigs.getConfig()).provider.getClass().getName()) .isEqualTo("io.grpc.util.SecretRoundRobinLoadBalancerProvider$Provider"); @@ -409,7 +409,7 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { } }; AutoConfiguredLoadBalancer lb = lbf.newLoadBalancer(helper); - Status addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + Status addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(servers) .setLoadBalancingPolicyConfig(lbConfigs.getConfig()) @@ -431,7 +431,7 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { } }; AutoConfiguredLoadBalancer lb = lbf.newLoadBalancer(helper); - Status addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + Status addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(servers) .setLoadBalancingPolicyConfig(null) @@ -446,7 +446,7 @@ public void acceptResolvedAddresses_noLbPolicySelected_defaultToCustomDefault() .newLoadBalancer(new TestHelper()); List servers = Collections.singletonList(new EquivalentAddressGroup(new SocketAddress(){})); - Status addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + Status addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(servers) .setLoadBalancingPolicyConfig(null) @@ -468,7 +468,7 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { AutoConfiguredLoadBalancer lb = new AutoConfiguredLoadBalancerFactory(GrpcUtil.DEFAULT_LB_POLICY).newLoadBalancer(helper); - Status addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + Status addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(servers) .setAttributes(Attributes.EMPTY) @@ -481,8 +481,8 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { nextParsedConfigOrError.set(testLbParsedConfig); Map serviceConfig = parseConfig("{\"loadBalancingConfig\": [ {\"test_lb\": { } } ] }"); - ConfigOrError lbConfigs = lbf.parseLoadBalancerPolicy(serviceConfig); - addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + ConfigOrError lbConfigs = lbf.parseLoadBalancingPolicyConfig(serviceConfig); + addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(servers) .setLoadBalancingPolicyConfig(lbConfigs.getConfig()) @@ -504,8 +504,8 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { testLbParsedConfig = ConfigOrError.fromConfig("bar"); nextParsedConfigOrError.set(testLbParsedConfig); serviceConfig = parseConfig("{\"loadBalancingConfig\": [ {\"test_lb\": { } } ] }"); - lbConfigs = lbf.parseLoadBalancerPolicy(serviceConfig); - addressesAcceptanceStatus = lb.tryAcceptResolvedAddresses( + lbConfigs = lbf.parseLoadBalancingPolicyConfig(serviceConfig); + addressesAcceptanceStatus = lb.acceptResolvedAddresses( ResolvedAddresses.newBuilder() .setAddresses(servers) .setLoadBalancingPolicyConfig(lbConfigs.getConfig()) @@ -519,33 +519,33 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) { } @Test - public void parseLoadBalancerConfig_failedOnUnknown() throws Exception { + public void parseLoadBalancingConfig_failedOnUnknown() throws Exception { Map serviceConfig = parseConfig("{\"loadBalancingConfig\": [ {\"magic_balancer\": {} } ] }"); - ConfigOrError parsed = lbf.parseLoadBalancerPolicy(serviceConfig); + ConfigOrError parsed = lbf.parseLoadBalancingPolicyConfig(serviceConfig); assertThat(parsed.getError()).isNotNull(); assertThat(parsed.getError().getDescription()) .isEqualTo("None of [magic_balancer] specified by Service Config are available."); } @Test - public void parseLoadBalancerPolicy_failedOnUnknown() throws Exception { + public void parseLoadBalancingPolicy_failedOnUnknown() throws Exception { Map serviceConfig = parseConfig("{\"loadBalancingPolicy\": \"magic_balancer\"}"); - ConfigOrError parsed = lbf.parseLoadBalancerPolicy(serviceConfig); + ConfigOrError parsed = lbf.parseLoadBalancingPolicyConfig(serviceConfig); assertThat(parsed.getError()).isNotNull(); assertThat(parsed.getError().getDescription()) .isEqualTo("None of [magic_balancer] specified by Service Config are available."); } @Test - public void parseLoadBalancerConfig_multipleValidPolicies() throws Exception { + public void parseLoadBalancingConfig_multipleValidPolicies() throws Exception { Map serviceConfig = parseConfig( "{\"loadBalancingConfig\": [" + "{\"round_robin\": {}}," + "{\"test_lb\": {} } ] }"); - ConfigOrError parsed = lbf.parseLoadBalancerPolicy(serviceConfig); + ConfigOrError parsed = lbf.parseLoadBalancingPolicyConfig(serviceConfig); assertThat(parsed).isNotNull(); assertThat(parsed.getError()).isNull(); assertThat(parsed.getConfig()).isInstanceOf(PolicySelection.class); @@ -554,12 +554,12 @@ public void parseLoadBalancerConfig_multipleValidPolicies() throws Exception { } @Test - public void parseLoadBalancerConfig_policyShouldBeIgnoredIfConfigExists() throws Exception { + public void parseLoadBalancingConfig_policyShouldBeIgnoredIfConfigExists() throws Exception { Map serviceConfig = parseConfig( "{\"loadBalancingConfig\": [{\"round_robin\": {} } ]," + "\"loadBalancingPolicy\": \"pick_first\" }"); - ConfigOrError parsed = lbf.parseLoadBalancerPolicy(serviceConfig); + ConfigOrError parsed = lbf.parseLoadBalancingPolicyConfig(serviceConfig); assertThat(parsed).isNotNull(); assertThat(parsed.getError()).isNull(); assertThat(parsed.getConfig()).isInstanceOf(PolicySelection.class); @@ -568,13 +568,13 @@ public void parseLoadBalancerConfig_policyShouldBeIgnoredIfConfigExists() throws } @Test - public void parseLoadBalancerConfig_policyShouldBeIgnoredEvenIfUnknownPolicyExists() + public void parseLoadBalancingConfig_policyShouldBeIgnoredEvenIfUnknownPolicyExists() throws Exception { Map serviceConfig = parseConfig( "{\"loadBalancingConfig\": [{\"magic_balancer\": {} } ]," + "\"loadBalancingPolicy\": \"round_robin\" }"); - ConfigOrError parsed = lbf.parseLoadBalancerPolicy(serviceConfig); + ConfigOrError parsed = lbf.parseLoadBalancingPolicyConfig(serviceConfig); assertThat(parsed.getError()).isNotNull(); assertThat(parsed.getError().getDescription()) .isEqualTo("None of [magic_balancer] specified by Service Config are available."); @@ -582,7 +582,7 @@ public void parseLoadBalancerConfig_policyShouldBeIgnoredEvenIfUnknownPolicyExis @Test @SuppressWarnings("unchecked") - public void parseLoadBalancerConfig_firstInvalidPolicy() throws Exception { + public void parseLoadBalancingConfig_firstInvalidPolicy() throws Exception { when(testLbBalancerProvider.parseLoadBalancingPolicyConfig(any(Map.class))) .thenReturn(ConfigOrError.fromError(Status.UNKNOWN)); Map serviceConfig = @@ -590,7 +590,7 @@ public void parseLoadBalancerConfig_firstInvalidPolicy() throws Exception { "{\"loadBalancingConfig\": [" + "{\"test_lb\": {}}," + "{\"round_robin\": {} } ] }"); - ConfigOrError parsed = lbf.parseLoadBalancerPolicy(serviceConfig); + ConfigOrError parsed = lbf.parseLoadBalancingPolicyConfig(serviceConfig); assertThat(parsed).isNotNull(); assertThat(parsed.getConfig()).isNull(); assertThat(parsed.getError()).isEqualTo(Status.UNKNOWN); @@ -598,7 +598,7 @@ public void parseLoadBalancerConfig_firstInvalidPolicy() throws Exception { @Test @SuppressWarnings("unchecked") - public void parseLoadBalancerConfig_firstValidSecondInvalidPolicy() throws Exception { + public void parseLoadBalancingConfig_firstValidSecondInvalidPolicy() throws Exception { when(testLbBalancerProvider.parseLoadBalancingPolicyConfig(any(Map.class))) .thenReturn(ConfigOrError.fromError(Status.UNKNOWN)); Map serviceConfig = @@ -606,32 +606,32 @@ public void parseLoadBalancerConfig_firstValidSecondInvalidPolicy() throws Excep "{\"loadBalancingConfig\": [" + "{\"round_robin\": {}}," + "{\"test_lb\": {} } ] }"); - ConfigOrError parsed = lbf.parseLoadBalancerPolicy(serviceConfig); + ConfigOrError parsed = lbf.parseLoadBalancingPolicyConfig(serviceConfig); assertThat(parsed).isNotNull(); assertThat(parsed.getConfig()).isNotNull(); assertThat(((PolicySelection) parsed.getConfig()).config).isNotNull(); } @Test - public void parseLoadBalancerConfig_someProvidesAreNotAvailable() throws Exception { + public void parseLoadBalancingConfig_someProvidesAreNotAvailable() throws Exception { Map serviceConfig = parseConfig("{\"loadBalancingConfig\": [ " + "{\"magic_balancer\": {} }," + "{\"round_robin\": {}} ] }"); - ConfigOrError parsed = lbf.parseLoadBalancerPolicy(serviceConfig); + ConfigOrError parsed = lbf.parseLoadBalancingPolicyConfig(serviceConfig); assertThat(parsed).isNotNull(); assertThat(parsed.getConfig()).isNotNull(); assertThat(((PolicySelection) parsed.getConfig()).config).isNotNull(); } @Test - public void parseLoadBalancerConfig_lbConfigPropagated() throws Exception { + public void parseLoadBalancingConfig_lbConfigPropagated() throws Exception { Map rawServiceConfig = parseConfig( "{\"loadBalancingConfig\": [" + "{\"pick_first\": {\"shuffleAddressList\": true } }" + "] }"); - ConfigOrError parsed = lbf.parseLoadBalancerPolicy(rawServiceConfig); + ConfigOrError parsed = lbf.parseLoadBalancingPolicyConfig(rawServiceConfig); assertThat(parsed).isNotNull(); assertThat(parsed.getConfig()).isNotNull(); PolicySelection policySelection = (PolicySelection) parsed.getConfig(); diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 731fac94044..24ea4fa03d9 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -3946,8 +3946,10 @@ public void nameResolverHelper_noConfigChosen() { boolean retryEnabled = false; int maxRetryAttemptsLimit = 2; int maxHedgedAttemptsLimit = 3; + LoadBalancerRegistry registry = new LoadBalancerRegistry(); + registry.register(mockLoadBalancerProvider); AutoConfiguredLoadBalancerFactory autoConfiguredLoadBalancerFactory = - new AutoConfiguredLoadBalancerFactory("pick_first"); + new AutoConfiguredLoadBalancerFactory(registry, MOCK_POLICY_NAME); ScParser parser = new ScParser( retryEnabled, From 172f650f1f55abfce980742441235eb0c6fe51d4 Mon Sep 17 00:00:00 2001 From: Kim Jin Young Date: Wed, 7 Jan 2026 03:03:08 +0900 Subject: [PATCH 504/591] xds: Implement proactive connection in RingHashLoadBalancer Implement proactive connection logic in RingHashLoadBalancer as outlined in gRFC A61. This address the missing logic where the balancer should initialize the first IDLE child when a child balancer reports TRANSIENT_FAILURE and no other children are connecting. This behavior, which was previously present before #10610, ensures that a backup subchannel starts connecting immediately outside of the picker flow, reducing failover latency. Fixes #12024 --- .../io/grpc/xds/RingHashLoadBalancer.java | 20 +++++++ .../io/grpc/xds/RingHashLoadBalancerTest.java | 60 +++++++++++++++---- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java index 21ee914ff8f..513f4d643ea 100644 --- a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java @@ -214,12 +214,32 @@ protected void updateOverallBalancingState() { overallState = TRANSIENT_FAILURE; } + // gRFC A61: if the aggregated connectivity state is TRANSIENT_FAILURE or CONNECTING and + // there are no endpoints in CONNECTING state, the ring_hash policy will choose one of + // the endpoints in IDLE state (if any) to trigger a connection attempt on + if (numReady == 0 && numTF > 0 && numConnecting == 0 && numIdle > 0) { + triggerIdleChildConnection(); + } + RingHashPicker picker = new RingHashPicker(syncContext, ring, getChildLbStates(), requestHashHeaderKey, random); getHelper().updateBalancingState(overallState, picker); this.currentConnectivityState = overallState; } + + /** + * Triggers a connection attempt for the first IDLE child load balancer. + */ + private void triggerIdleChildConnection() { + for (ChildLbState child : getChildLbStates()) { + if (child.getCurrentState() == ConnectivityState.IDLE) { + child.getLb().requestConnection(); + return; + } + } + } + @Override protected ChildLbState createChildLbState(Object key) { return new ChildLbState(key, lazyLbFactory); diff --git a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java index d65cf96c00d..b515ed81158 100644 --- a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java @@ -255,7 +255,7 @@ public void aggregateSubchannelStates_connectingReadyIdleFailure() { inOrder.verify(helper).refreshNameResolution(); inOrder.verify(helper).updateBalancingState(eq(CONNECTING), any()); } - verifyConnection(0); + verifyConnection(1); } private void verifyConnection(int times) { @@ -537,7 +537,7 @@ public void pickWithRandomHash_firstSubchannelInTransientFailure_remainingSubcha // Bring one subchannel to TRANSIENT_FAILURE. deliverSubchannelUnreachable(getSubChannel(servers.get(0))); verify(helper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); - verifyConnection(0); + verifyConnection(1); // Pick subchannel with random hash does trigger connection by walking the ring // and choosing the first (at most one) IDLE subchannel along the way. @@ -583,7 +583,7 @@ public void skipFailingHosts_pickNextNonFailingHost() { getSubChannel(servers.get(0)), ConnectivityStateInfo.forTransientFailure( Status.UNAVAILABLE.withDescription("unreachable"))); - verify(helper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); + verify(helper, atLeastOnce()).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); PickResult result = pickerCaptor.getValue().pickSubchannel(args); assertThat(result.getStatus().isOk()).isTrue(); @@ -649,7 +649,7 @@ public void skipFailingHosts_firstTwoHostsFailed_pickNextFirstReady() { ConnectivityStateInfo.forTransientFailure( Status.PERMISSION_DENIED.withDescription("permission denied"))); verify(helper).updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); - verifyConnection(0); + verifyConnection(2); PickResult result = pickerCaptor.getValue().pickSubchannel(args); // activate last subchannel assertThat(result.getStatus().isOk()).isTrue(); int expectedCount = PickFirstLoadBalancerProvider.isEnabledNewPickFirst() ? 0 : 1; @@ -721,7 +721,7 @@ public void allSubchannelsInTransientFailure() { } verify(helper, atLeastOnce()) .updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); - verifyConnection(0); + verifyConnection(2); // Picking subchannel triggers connection. RPC hash hits server0. PickSubchannelArgs args = getDefaultPickSubchannelArgsForServer(0); @@ -740,12 +740,13 @@ public void firstSubchannelIdle() { List servers = createWeightedServerAddrs(1, 1, 1); initializeLbSubchannels(config, servers); - // Go to TF does nothing, though PF will try to reconnect after backoff + // As per gRFC A61, entering TF triggers a proactive connection attempt + // on an IDLE subchannel because no other subchannel is currently CONNECTING. deliverSubchannelState(getSubchannel(servers, 1), ConnectivityStateInfo.forTransientFailure( Status.UNAVAILABLE.withDescription("unreachable"))); verify(helper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); - verifyConnection(0); + verifyConnection(1); // Picking subchannel triggers connection. RPC hash hits server0. PickSubchannelArgs args = getDefaultPickSubchannelArgs(hashFunc.hashVoid()); @@ -796,7 +797,7 @@ public void firstSubchannelFailure() { ConnectivityStateInfo.forTransientFailure( Status.UNAVAILABLE.withDescription("unreachable"))); verify(helper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); - verifyConnection(0); + verifyConnection(1); // Per GRFC A61 Picking subchannel should no longer request connections that were failing PickSubchannelArgs args = getDefaultPickSubchannelArgs(hashFunc.hashVoid()); @@ -824,7 +825,7 @@ public void secondSubchannelConnecting() { Subchannel firstSubchannel = getSubchannel(servers, 0); deliverSubchannelUnreachable(firstSubchannel); - verifyConnection(0); + verifyConnection(1); deliverSubchannelState(getSubchannel(servers, 2), CSI_CONNECTING); verify(helper, times(2)).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); @@ -833,7 +834,7 @@ public void secondSubchannelConnecting() { // Picking subchannel when idle triggers connection. deliverSubchannelState(getSubchannel(servers, 2), ConnectivityStateInfo.forNonError(IDLE)); - verifyConnection(0); + verifyConnection(1); PickSubchannelArgs args = getDefaultPickSubchannelArgs(hashFunc.hashVoid()); PickResult result = pickerCaptor.getValue().pickSubchannel(args); assertThat(result.getStatus().isOk()).isTrue(); @@ -857,7 +858,7 @@ public void secondSubchannelFailure() { deliverSubchannelUnreachable(firstSubchannel); deliverSubchannelUnreachable(getSubchannel(servers, 2)); verify(helper).updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); - verifyConnection(0); + verifyConnection(2); // Picking subchannel triggers connection. PickSubchannelArgs args = getDefaultPickSubchannelArgs(hashFunc.hashVoid()); @@ -887,7 +888,7 @@ public void thirdSubchannelConnecting() { deliverSubchannelState(getSubchannel(servers, 1), CSI_CONNECTING); verify(helper, atLeastOnce()) .updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); - verifyConnection(0); + verifyConnection(2); // Picking subchannel should not trigger connection per gRFC A61. PickSubchannelArgs args = getDefaultPickSubchannelArgs(hashFunc.hashVoid()); @@ -909,7 +910,7 @@ public void stickyTransientFailure() { deliverSubchannelUnreachable(firstSubchannel); verify(helper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); - verifyConnection(0); + verifyConnection(1); reset(helper); deliverSubchannelState(firstSubchannel, ConnectivityStateInfo.forNonError(IDLE)); @@ -1127,6 +1128,39 @@ public void config_equalsTester() { .testEquals(); } + @Test + public void tfWithoutConnectingChild_triggersIdleChildConnection() { + RingHashConfig config = new RingHashConfig(10, 100, ""); + List servers = createWeightedServerAddrs(1, 1); + + initializeLbSubchannels(config, servers); + + Subchannel tfSubchannel = getSubchannel(servers, 0); + Subchannel idleSubchannel = getSubchannel(servers, 1); + + deliverSubchannelUnreachable(tfSubchannel); + + Subchannel requested = connectionRequestedQueue.poll(); + assertThat(requested).isSameInstanceAs(idleSubchannel); + assertThat(connectionRequestedQueue.poll()).isNull(); + } + + @Test + public void tfWithReadyChild_doesNotTriggerIdleChildConnection() { + RingHashConfig config = new RingHashConfig(10, 100, ""); + List servers = createWeightedServerAddrs(1, 1, 1); + + initializeLbSubchannels(config, servers); + + Subchannel tfSubchannel = getSubchannel(servers, 0); + Subchannel readySubchannel = getSubchannel(servers, 1); + + deliverSubchannelState(readySubchannel, ConnectivityStateInfo.forNonError(READY)); + deliverSubchannelUnreachable(tfSubchannel); + + assertThat(connectionRequestedQueue.poll()).isNull(); + } + private List initializeLbSubchannels(RingHashConfig config, List servers, InitializationFlags... initFlags) { From 2e48dcddd49f055df2adc50b6b5b54e00cfcb654 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Wed, 17 Dec 2025 01:26:59 -0800 Subject: [PATCH 505/591] api: Add newNameResolver() overload and default, best-effort impl. --- api/src/main/java/io/grpc/NameResolver.java | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java index 0e8315e812c..53dbc5d6888 100644 --- a/api/src/main/java/io/grpc/NameResolver.java +++ b/api/src/main/java/io/grpc/NameResolver.java @@ -158,6 +158,10 @@ public abstract static class Factory { * cannot be resolved by this factory. The decision should be solely based on the scheme of the * URI. * + *

    This method will eventually be deprecated and removed as part of a migration from {@code + * java.net.URI} to {@code io.grpc.Uri}. Implementations will override {@link + * #newNameResolver(Uri, Args)} instead. + * * @param targetUri the target URI to be resolved, whose scheme must not be {@code null} * @param args other information that may be useful * @@ -165,6 +169,37 @@ public abstract static class Factory { */ public abstract NameResolver newNameResolver(URI targetUri, final Args args); + /** + * Creates a {@link NameResolver} for the given target URI. + * + *

    Implementations return {@code null} if 'targetUri' cannot be resolved by this factory. The + * decision should be solely based on the target's scheme. + * + *

    All {@link NameResolver.Factory} implementations should override this method, as it will + * eventually replace {@link #newNameResolver(URI, Args)}. For backwards compatibility, this + * default implementation delegates to {@link #newNameResolver(URI, Args)} if 'targetUri' can be + * converted to a java.net.URI. + * + *

    NB: Conversion is not always possible, for example {@code scheme:#frag} is a valid {@link + * Uri} but not a valid {@link URI} because its path is empty. The default implementation throws + * IllegalArgumentException in these cases. + * + * @param targetUri the target URI to be resolved + * @param args other information that may be useful + * @throws IllegalArgumentException if targetUri does not have the expected form + * @since 1.79 + */ + public NameResolver newNameResolver(Uri targetUri, final Args args) { + // Not every io.grpc.Uri can be converted but in the ordinary ManagedChannel creation flow, + // any IllegalArgumentException thrown here would happened anyway, just earlier. That's + // because parse/toString is transparent so java.net.URI#create here sees the original target + // string just like it did before the io.grpc.Uri migration. + // + // Throwing IAE shouldn't surprise non-framework callers either. After all, many existing + // Factory impls are picky about targetUri and throw IAE when it doesn't look how they expect. + return newNameResolver(URI.create(targetUri.toString()), args); + } + /** * Returns the default scheme, which will be used to construct a URI when {@link * ManagedChannelBuilder#forTarget(String)} is given an authority string instead of a compliant From 5aeb7143ac3605c03573bd5c3d3ea494ef892c53 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 18 Dec 2025 15:44:26 -0800 Subject: [PATCH 506/591] core: Create a UriWrapper abstraction for the migration # Conflicts: # core/src/main/java/io/grpc/internal/ManagedChannelImpl.java --- .../io/grpc/internal/ManagedChannelImpl.java | 8 +- .../internal/ManagedChannelImplBuilder.java | 7 +- .../java/io/grpc/internal/UriWrapper.java | 139 ++++++++++++++++++ ...ManagedChannelImplGetNameResolverTest.java | 3 +- .../ManagedChannelImplIdlenessTest.java | 3 +- .../grpc/internal/ManagedChannelImplTest.java | 13 +- .../ServiceConfigErrorHandlingTest.java | 3 +- 7 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 core/src/main/java/io/grpc/internal/UriWrapper.java diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 5c3afbf8de0..e423220e3ad 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -160,7 +160,7 @@ public Result selectConfig(PickSubchannelArgs args) { @Nullable private final String authorityOverride; private final NameResolverRegistry nameResolverRegistry; - private final URI targetUri; + private final UriWrapper targetUri; private final NameResolverProvider nameResolverProvider; private final NameResolver.Args nameResolverArgs; private final LoadBalancerProvider loadBalancerFactory; @@ -538,7 +538,7 @@ ClientStream newSubstream( ManagedChannelImpl( ManagedChannelImplBuilder builder, ClientTransportFactory clientTransportFactory, - URI targetUri, + UriWrapper targetUri, NameResolverProvider nameResolverProvider, BackoffPolicy.Provider backoffPolicyProvider, ObjectPool balancerRpcExecutorPool, @@ -667,9 +667,9 @@ public CallTracer create() { @VisibleForTesting static NameResolver getNameResolver( - URI targetUri, @Nullable final String overrideAuthority, + UriWrapper targetUri, @Nullable final String overrideAuthority, NameResolverProvider provider, NameResolver.Args nameResolverArgs) { - NameResolver resolver = provider.newNameResolver(targetUri, nameResolverArgs); + NameResolver resolver = targetUri.newNameResolver(provider, nameResolverArgs); if (resolver == null) { throw new IllegalArgumentException("cannot create a NameResolver for " + targetUri); } diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java index 1773c04388d..944fea9c512 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.internal.UriWrapper.wrap; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -814,10 +815,10 @@ int getDefaultPort() { @VisibleForTesting static class ResolvedNameResolver { - public final URI targetUri; + public final UriWrapper targetUri; public final NameResolverProvider provider; - public ResolvedNameResolver(URI targetUri, NameResolverProvider provider) { + public ResolvedNameResolver(UriWrapper targetUri, NameResolverProvider provider) { this.targetUri = checkNotNull(targetUri, "targetUri"); this.provider = checkNotNull(provider, "provider"); } @@ -872,7 +873,7 @@ static ResolvedNameResolver getNameResolverProvider( } } - return new ResolvedNameResolver(targetUri, provider); + return new ResolvedNameResolver(wrap(targetUri), provider); } private static class DirectAddressNameResolverProvider extends NameResolverProvider { diff --git a/core/src/main/java/io/grpc/internal/UriWrapper.java b/core/src/main/java/io/grpc/internal/UriWrapper.java new file mode 100644 index 00000000000..ca5835cabd8 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/UriWrapper.java @@ -0,0 +1,139 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import io.grpc.NameResolver; +import io.grpc.Uri; +import java.net.URI; +import javax.annotation.Nullable; + +/** Temporary wrapper for a URI-like object to ease the migration to io.grpc.Uri. */ +interface UriWrapper { + + static UriWrapper wrap(URI uri) { + return new JavaNetUriWrapper(uri); + } + + static UriWrapper wrap(Uri uri) { + return new IoGrpcUriWrapper(uri); + } + + /** Uses the given factory and args to create a {@link NameResolver} for this URI. */ + NameResolver newNameResolver(NameResolver.Factory factory, NameResolver.Args args); + + /** Returns the scheme component of this URI, e.g. "http", "mailto" or "dns". */ + String getScheme(); + + /** + * Returns the authority component of this URI, e.g. "google.com", "127.0.0.1:8080", or null if + * not present. + */ + @Nullable + String getAuthority(); + + /** Wraps an instance of java.net.URI. */ + final class JavaNetUriWrapper implements UriWrapper { + private final URI uri; + + private JavaNetUriWrapper(URI uri) { + this.uri = checkNotNull(uri); + } + + @Override + public NameResolver newNameResolver(NameResolver.Factory factory, NameResolver.Args args) { + return factory.newNameResolver(uri, args); + } + + @Override + public String getScheme() { + return uri.getScheme(); + } + + @Override + public String getAuthority() { + return uri.getAuthority(); + } + + @Override + public String toString() { + return uri.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof JavaNetUriWrapper)) { + return false; + } + return uri.equals(((JavaNetUriWrapper) other).uri); + } + + @Override + public int hashCode() { + return uri.hashCode(); + } + } + + /** Wraps an instance of io.grpc.Uri. */ + final class IoGrpcUriWrapper implements UriWrapper { + private final Uri uri; + + private IoGrpcUriWrapper(Uri uri) { + this.uri = checkNotNull(uri); + } + + @Override + public NameResolver newNameResolver(NameResolver.Factory factory, NameResolver.Args args) { + return factory.newNameResolver(uri, args); + } + + @Override + public String getScheme() { + return uri.getScheme(); + } + + @Override + public String getAuthority() { + return uri.getAuthority(); + } + + @Override + public String toString() { + return uri.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof IoGrpcUriWrapper)) { + return false; + } + return uri.equals(((IoGrpcUriWrapper) other).uri); + } + + @Override + public int hashCode() { + return uri.hashCode(); + } + } +} diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplGetNameResolverTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplGetNameResolverTest.java index a0bd388b1b6..8ec522260ae 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplGetNameResolverTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplGetNameResolverTest.java @@ -17,6 +17,7 @@ package io.grpc.internal; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.internal.UriWrapper.wrap; import static org.junit.Assert.fail; import io.grpc.NameResolver; @@ -125,7 +126,7 @@ private void testValidTarget(String target, String expectedUriString, URI expect ManagedChannelImplBuilder.getNameResolverProvider( target, nameResolverRegistry, Collections.singleton(InetSocketAddress.class)); assertThat(resolved.provider).isInstanceOf(FakeNameResolverProvider.class); - assertThat(resolved.targetUri).isEqualTo(expectedUri); + assertThat(resolved.targetUri).isEqualTo(wrap(expectedUri)); assertThat(resolved.targetUri.toString()).isEqualTo(expectedUriString); } diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java index 293d0e70961..97e92be7fdd 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplIdlenessTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; +import static io.grpc.internal.UriWrapper.wrap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -185,7 +186,7 @@ public void setUp() { NameResolverProvider nameResolverProvider = builder.nameResolverRegistry.getProviderForScheme(targetUri.getScheme()); channel = new ManagedChannelImpl( - builder, mockTransportFactory, targetUri, nameResolverProvider, + builder, mockTransportFactory, wrap(targetUri), nameResolverProvider, new FakeBackoffPolicyProvider(), oobExecutorPool, timer.getStopwatchSupplier(), Collections.emptyList(), diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 24ea4fa03d9..30b53e99946 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -28,6 +28,7 @@ import static io.grpc.EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE; import static io.grpc.PickSubchannelArgsMatcher.eqPickSubchannelArgs; import static io.grpc.internal.ClientStreamListener.RpcProgress.PROCESSED; +import static io.grpc.internal.UriWrapper.wrap; import static junit.framework.TestCase.assertNotSame; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -315,7 +316,7 @@ private void createChannel(boolean nameResolutionExpectedToFail, NameResolverProvider nameResolverProvider = channelBuilder.nameResolverRegistry.getProviderForScheme(expectedUri.getScheme()); channel = new ManagedChannelImpl( - channelBuilder, mockTransportFactory, expectedUri, nameResolverProvider, + channelBuilder, mockTransportFactory, wrap(expectedUri), nameResolverProvider, new FakeBackoffPolicyProvider(), balancerRpcExecutorPool, timer.getStopwatchSupplier(), Arrays.asList(interceptors), timer.getTimeProvider()); @@ -504,7 +505,7 @@ public void startCallBeforeNameResolution() throws Exception { when(mockTransportFactory.getSupportedSocketAddressTypes()).thenReturn(Collections.singleton( InetSocketAddress.class)); channel = new ManagedChannelImpl( - channelBuilder, mockTransportFactory, expectedUri, nameResolverFactory, + channelBuilder, mockTransportFactory, wrap(expectedUri), nameResolverFactory, new FakeBackoffPolicyProvider(), balancerRpcExecutorPool, timer.getStopwatchSupplier(), Collections.emptyList(), timer.getTimeProvider()); @@ -569,7 +570,7 @@ public void newCallWithConfigSelector() { when(mockTransportFactory.getSupportedSocketAddressTypes()).thenReturn(Collections.singleton( InetSocketAddress.class)); channel = new ManagedChannelImpl( - channelBuilder, mockTransportFactory, expectedUri, nameResolverFactory, + channelBuilder, mockTransportFactory, wrap(expectedUri), nameResolverFactory, new FakeBackoffPolicyProvider(), balancerRpcExecutorPool, timer.getStopwatchSupplier(), Collections.emptyList(), timer.getTimeProvider()); @@ -4416,11 +4417,11 @@ public void validAuthorityTarget_overrideAuthority() throws Exception { URI targetUri = new URI("defaultscheme", "", "/foo.googleapis.com:8080", null); NameResolver nameResolver = ManagedChannelImpl.getNameResolver( - targetUri, null, nameResolverProvider, NAMERESOLVER_ARGS); + wrap(targetUri), null, nameResolverProvider, NAMERESOLVER_ARGS); assertThat(nameResolver.getServiceAuthority()).isEqualTo(serviceAuthority); nameResolver = ManagedChannelImpl.getNameResolver( - targetUri, overrideAuthority, nameResolverProvider, NAMERESOLVER_ARGS); + wrap(targetUri), overrideAuthority, nameResolverProvider, NAMERESOLVER_ARGS); assertThat(nameResolver.getServiceAuthority()).isEqualTo(overrideAuthority); } @@ -4449,7 +4450,7 @@ public String getDefaultScheme() { }; try { ManagedChannelImpl.getNameResolver( - URI.create("defaultscheme:///foo.gogoleapis.com:8080"), + wrap(URI.create("defaultscheme:///foo.gogoleapis.com:8080")), null, nameResolverProvider, NAMERESOLVER_ARGS); fail("Should fail"); } catch (IllegalArgumentException e) { diff --git a/core/src/test/java/io/grpc/internal/ServiceConfigErrorHandlingTest.java b/core/src/test/java/io/grpc/internal/ServiceConfigErrorHandlingTest.java index 6f255763d30..0daee676b82 100644 --- a/core/src/test/java/io/grpc/internal/ServiceConfigErrorHandlingTest.java +++ b/core/src/test/java/io/grpc/internal/ServiceConfigErrorHandlingTest.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static io.grpc.internal.UriWrapper.wrap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.AdditionalAnswers.delegatesTo; @@ -168,7 +169,7 @@ private void createChannel(ClientInterceptor... interceptors) { new ManagedChannelImpl( channelBuilder, mockTransportFactory, - expectedUri, + wrap(expectedUri), nameResolverProvider, new FakeBackoffPolicyProvider(), balancerRpcExecutorPool, From fc5efee745e8f66ba9ad16973d076c9327eeee35 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Mon, 5 Jan 2026 23:06:48 -0800 Subject: [PATCH 507/591] core: add a FlagResetRule test fixture --- .../java/io/grpc/internal/FlagResetRule.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 core/src/testFixtures/java/io/grpc/internal/FlagResetRule.java diff --git a/core/src/testFixtures/java/io/grpc/internal/FlagResetRule.java b/core/src/testFixtures/java/io/grpc/internal/FlagResetRule.java new file mode 100644 index 00000000000..dbcaca79358 --- /dev/null +++ b/core/src/testFixtures/java/io/grpc/internal/FlagResetRule.java @@ -0,0 +1,66 @@ +/* + * Copyright 2026 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.internal; + +import java.util.ArrayDeque; +import java.util.Deque; +import org.junit.rules.ExternalResource; + +/** + * A {@link org.junit.rules.TestRule} that lets you set one or more feature flags just for + * the duration of the current test case. + * + *

    Flags and other global variables must be reset to ensure no state leaks across tests. + */ +public final class FlagResetRule extends ExternalResource { + + /** A functional interface representing a standard gRPC feature flag setter. */ + public interface SetterMethod { + /** Sets a flag for testing and returns its previous value. */ + T set(T val); + } + + private final Deque toRunAfter = new ArrayDeque<>(); + + /** + * Sets a global feature flag to 'value' using 'setter' and arranges for its previous value to be + * unconditionally restored when the test completes. + */ + public void setFlagForTest(SetterMethod setter, T value) { + final T oldValue = setter.set(value); + toRunAfter.push(() -> setter.set(oldValue)); + } + + @Override + protected void after() { + RuntimeException toThrow = null; + while (!toRunAfter.isEmpty()) { + try { + toRunAfter.pop().run(); + } catch (RuntimeException e) { + if (toThrow == null) { + toThrow = e; + } else { + toThrow.addSuppressed(e); + } + } + } + if (toThrow != null) { + throw toThrow; + } + } +} From 7ef40a98b6321a99ce8fb742f54604c4a5a68677 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Mon, 5 Jan 2026 23:07:58 -0800 Subject: [PATCH 508/591] core: Use io.grpc.Uri to parse the target, if flagged --- .../internal/ManagedChannelImplBuilder.java | 83 +++++- .../ManagedChannelImplBuilderTest.java | 24 +- ...ChannelImplGetNameResolverRfc3986Test.java | 243 ++++++++++++++++++ ...ManagedChannelImplGetNameResolverTest.java | 36 ++- 4 files changed, 361 insertions(+), 25 deletions(-) create mode 100644 core/src/test/java/io/grpc/internal/ManagedChannelImplGetNameResolverRfc3986Test.java diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java index 944fea9c512..628224b826e 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java @@ -47,6 +47,7 @@ import io.grpc.NameResolverRegistry; import io.grpc.ProxyDetector; import io.grpc.StatusOr; +import io.grpc.Uri; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.SocketAddress; @@ -105,6 +106,16 @@ public static ManagedChannelBuilder forTarget(String target) { */ static final long IDLE_MODE_MIN_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(1); + private static boolean enableRfc3986Uris = GrpcUtil.getFlag("GRPC_ENABLE_RFC3986_URIS", false); + + /** Whether to parse targets as RFC 3986 URIs (true), or use {@link java.net.URI} (false). */ + @VisibleForTesting + static boolean setRfc3986UrisEnabled(boolean value) { + boolean prevValue = ManagedChannelImplBuilder.enableRfc3986Uris; + ManagedChannelImplBuilder.enableRfc3986Uris = value; + return prevValue; + } + private static final ObjectPool DEFAULT_EXECUTOR_POOL = SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR); @@ -719,8 +730,11 @@ protected ManagedChannelImplBuilder addMetricSink(MetricSink metricSink) { public ManagedChannel build() { ClientTransportFactory clientTransportFactory = clientTransportFactoryBuilder.buildClientTransportFactory(); - ResolvedNameResolver resolvedResolver = getNameResolverProvider( - target, nameResolverRegistry, clientTransportFactory.getSupportedSocketAddressTypes()); + ResolvedNameResolver resolvedResolver = + enableRfc3986Uris + ? getNameResolverProviderRfc3986(target, nameResolverRegistry) + : getNameResolverProvider(target, nameResolverRegistry); + resolvedResolver.checkAddressTypes(clientTransportFactory.getSupportedSocketAddressTypes()); return new ManagedChannelOrphanWrapper(new ManagedChannelImpl( this, clientTransportFactory, @@ -822,12 +836,25 @@ public ResolvedNameResolver(UriWrapper targetUri, NameResolverProvider provider) this.targetUri = checkNotNull(targetUri, "targetUri"); this.provider = checkNotNull(provider, "provider"); } + + void checkAddressTypes( + Collection> channelTransportSocketAddressTypes) { + if (channelTransportSocketAddressTypes != null) { + Collection> nameResolverSocketAddressTypes = + provider.getProducedSocketAddressTypes(); + if (!channelTransportSocketAddressTypes.containsAll(nameResolverSocketAddressTypes)) { + throw new IllegalArgumentException( + String.format( + "Address types of NameResolver '%s' for '%s' not supported by transport", + provider.getDefaultScheme(), targetUri)); + } + } + } } @VisibleForTesting static ResolvedNameResolver getNameResolverProvider( - String target, NameResolverRegistry nameResolverRegistry, - Collection> channelTransportSocketAddressTypes) { + String target, NameResolverRegistry nameResolverRegistry) { // Finding a NameResolver. Try using the target string as the URI. If that fails, try prepending // "dns:///". NameResolverProvider provider = null; @@ -863,14 +890,46 @@ static ResolvedNameResolver getNameResolverProvider( target, uriSyntaxErrors.length() > 0 ? " (" + uriSyntaxErrors + ")" : "")); } - if (channelTransportSocketAddressTypes != null) { - Collection> nameResolverSocketAddressTypes - = provider.getProducedSocketAddressTypes(); - if (!channelTransportSocketAddressTypes.containsAll(nameResolverSocketAddressTypes)) { - throw new IllegalArgumentException(String.format( - "Address types of NameResolver '%s' for '%s' not supported by transport", - targetUri.getScheme(), target)); - } + return new ResolvedNameResolver(wrap(targetUri), provider); + } + + @VisibleForTesting + static ResolvedNameResolver getNameResolverProviderRfc3986( + String target, NameResolverRegistry nameResolverRegistry) { + // Finding a NameResolver. Try using the target string as the URI. If that fails, try prepending + // "dns:///". + NameResolverProvider provider = null; + Uri targetUri = null; + StringBuilder uriSyntaxErrors = new StringBuilder(); + try { + targetUri = Uri.parse(target); + } catch (URISyntaxException e) { + // Can happen with ip addresses like "[::1]:1234" or 127.0.0.1:1234. + uriSyntaxErrors.append(e.getMessage()); + } + if (targetUri != null) { + // For "localhost:8080" this would likely cause provider to be null, because "localhost" is + // parsed as the scheme. Will hit the next case and try "dns:///localhost:8080". + provider = nameResolverRegistry.getProviderForScheme(targetUri.getScheme()); + } + + if (provider == null && !URI_PATTERN.matcher(target).matches()) { + // It doesn't look like a URI target. Maybe it's an authority string. Try with the default + // scheme from the registry. + targetUri = + Uri.newBuilder() + .setScheme(nameResolverRegistry.getDefaultScheme()) + .setHost("") + .setPath("/" + target) + .build(); + provider = nameResolverRegistry.getProviderForScheme(targetUri.getScheme()); + } + + if (provider == null) { + throw new IllegalArgumentException( + String.format( + "Could not find a NameResolverProvider for %s%s", + target, uriSyntaxErrors.length() > 0 ? " (" + uriSyntaxErrors + ")" : "")); } return new ResolvedNameResolver(wrap(targetUri), provider); diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java index a054e65a6e8..8bf10de3949 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplBuilderTest.java @@ -69,13 +69,15 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; /** Unit tests for {@link ManagedChannelImplBuilder}. */ -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class ManagedChannelImplBuilderTest { private static final int DUMMY_PORT = 42; private static final String DUMMY_TARGET = "fake-target"; @@ -98,8 +100,16 @@ public ClientCall interceptCall( } }; + @Parameters(name = "enableRfc3986UrisParam={0}") + public static Iterable data() { + return Arrays.asList(new Object[][] {{true}, {false}}); + } + + @Parameter public boolean enableRfc3986UrisParam; + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @Rule public final GrpcCleanupRule grpcCleanupRule = new GrpcCleanupRule(); + @Rule public final FlagResetRule flagResetRule = new FlagResetRule(); @Mock private ClientTransportFactory mockClientTransportFactory; @Mock private ClientTransportFactoryBuilder mockClientTransportFactoryBuilder; @@ -117,6 +127,9 @@ public ClientCall interceptCall( @Before public void setUp() throws Exception { + flagResetRule.setFlagForTest( + ManagedChannelImplBuilder::setRfc3986UrisEnabled, enableRfc3986UrisParam); + builder = new ManagedChannelImplBuilder( DUMMY_TARGET, new UnsupportedClientTransportFactoryBuilder(), @@ -373,8 +386,11 @@ public void transportDoesNotSupportAddressTypes() { ManagedChannel unused = grpcCleanupRule.register(builder.build()); fail("Should fail"); } catch (IllegalArgumentException e) { - assertThat(e).hasMessageThat().isEqualTo( - "Address types of NameResolver 'dns' for 'valid:1234' not supported by transport"); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Address types of NameResolver 'dns' for 'dns:///valid:1234' not supported by" + + " transport"); } } diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplGetNameResolverRfc3986Test.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplGetNameResolverRfc3986Test.java new file mode 100644 index 00000000000..5bcf24a30e2 --- /dev/null +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplGetNameResolverRfc3986Test.java @@ -0,0 +1,243 @@ +/* + * Copyright 2015 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.internal; + +import static com.google.common.truth.Truth.assertThat; +import static io.grpc.internal.UriWrapper.wrap; +import static org.junit.Assert.fail; + +import io.grpc.NameResolver; +import io.grpc.NameResolverProvider; +import io.grpc.NameResolverRegistry; +import io.grpc.Uri; +import java.net.SocketAddress; +import java.net.URI; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for ManagedChannelImplBuilder#getNameResolverProviderNew(). */ +@RunWith(JUnit4.class) +public class ManagedChannelImplGetNameResolverRfc3986Test { + @Test + public void invalidUriTarget() { + testInvalidTarget("defaultscheme:///[invalid]"); + } + + @Test + public void invalidUnescapedSquareBracketsInRfc3986UriFragment() { + testInvalidTarget("defaultscheme://8.8.8.8/host#section[1]"); + } + + @Test + public void invalidUnescapedSquareBracketsInRfc3986UriQuery() { + testInvalidTarget("dns://8.8.8.8/path?section=[1]"); + } + + @Test + public void validTargetWithInvalidDnsName() throws Exception { + testValidTarget( + "[valid]", + "defaultscheme:///%5Bvalid%5D", + Uri.newBuilder().setScheme("defaultscheme").setHost("").setPath("/[valid]").build()); + } + + @Test + public void validAuthorityTarget() throws Exception { + testValidTarget( + "foo.googleapis.com:8080", + "defaultscheme:///foo.googleapis.com:8080", + Uri.newBuilder() + .setScheme("defaultscheme") + .setHost("") + .setPath("/foo.googleapis.com:8080") + .build()); + } + + @Test + public void validUriTarget() throws Exception { + testValidTarget( + "scheme:///foo.googleapis.com:8080", + "scheme:///foo.googleapis.com:8080", + Uri.newBuilder() + .setScheme("scheme") + .setHost("") + .setPath("/foo.googleapis.com:8080") + .build()); + } + + @Test + public void validIpv4AuthorityTarget() throws Exception { + testValidTarget( + "127.0.0.1:1234", + "defaultscheme:///127.0.0.1:1234", + Uri.newBuilder().setScheme("defaultscheme").setHost("").setPath("/127.0.0.1:1234").build()); + } + + @Test + public void validIpv4UriTarget() throws Exception { + testValidTarget( + "dns:///127.0.0.1:1234", + "dns:///127.0.0.1:1234", + Uri.newBuilder().setScheme("dns").setHost("").setPath("/127.0.0.1:1234").build()); + } + + @Test + public void validIpv6AuthorityTarget() throws Exception { + testValidTarget( + "[::1]:1234", + "defaultscheme:///%5B::1%5D:1234", + Uri.newBuilder().setScheme("defaultscheme").setHost("").setPath("/[::1]:1234").build()); + } + + @Test + public void invalidIpv6UriTarget() throws Exception { + testInvalidTarget("dns:///[::1]:1234"); + } + + @Test + public void invalidIpv6UriWithUnescapedScope() { + testInvalidTarget("dns://[::1%eth0]:53/host"); + } + + @Test + public void validIpv6UriTarget() throws Exception { + testValidTarget( + "dns:///%5B::1%5D:1234", + "dns:///%5B::1%5D:1234", + Uri.newBuilder().setScheme("dns").setHost("").setPath("/[::1]:1234").build()); + } + + @Test + public void validTargetStartingWithSlash() throws Exception { + testValidTarget( + "/target", + "defaultscheme:////target", + Uri.newBuilder().setScheme("defaultscheme").setHost("").setPath("//target").build()); + } + + @Test + public void validTargetNoProvider() { + NameResolverRegistry nameResolverRegistry = new NameResolverRegistry(); + try { + ManagedChannelImplBuilder.getNameResolverProviderRfc3986( + "foo.googleapis.com:8080", nameResolverRegistry); + fail("Should fail"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void validTargetProviderAddrTypesNotSupported() { + NameResolverRegistry nameResolverRegistry = getTestRegistry("testscheme"); + try { + ManagedChannelImplBuilder.getNameResolverProviderRfc3986( + "testscheme:///foo.googleapis.com:8080", nameResolverRegistry) + .checkAddressTypes(Collections.singleton(CustomSocketAddress.class)); + fail("Should fail"); + } catch (IllegalArgumentException e) { + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Address types of NameResolver 'testscheme' for " + + "'testscheme:///foo.googleapis.com:8080' not supported by transport"); + } + } + + private void testValidTarget(String target, String expectedUriString, Uri expectedUri) { + NameResolverRegistry nameResolverRegistry = getTestRegistry(expectedUri.getScheme()); + ManagedChannelImplBuilder.ResolvedNameResolver resolved = + ManagedChannelImplBuilder.getNameResolverProviderRfc3986(target, nameResolverRegistry); + assertThat(resolved.provider).isInstanceOf(FakeNameResolverProvider.class); + assertThat(resolved.targetUri).isEqualTo(wrap(expectedUri)); + assertThat(resolved.targetUri.toString()).isEqualTo(expectedUriString); + } + + private void testInvalidTarget(String target) { + NameResolverRegistry nameResolverRegistry = getTestRegistry("dns"); + + try { + ManagedChannelImplBuilder.ResolvedNameResolver resolved = + ManagedChannelImplBuilder.getNameResolverProviderRfc3986(target, nameResolverRegistry); + FakeNameResolverProvider nameResolverProvider = (FakeNameResolverProvider) resolved.provider; + fail("Should have failed, but got resolver provider " + nameResolverProvider); + } catch (IllegalArgumentException e) { + // expected + } + } + + private static NameResolverRegistry getTestRegistry(String expectedScheme) { + NameResolverRegistry nameResolverRegistry = new NameResolverRegistry(); + FakeNameResolverProvider nameResolverProvider = new FakeNameResolverProvider(expectedScheme); + nameResolverRegistry.register(nameResolverProvider); + return nameResolverRegistry; + } + + private static class FakeNameResolverProvider extends NameResolverProvider { + final String expectedScheme; + + FakeNameResolverProvider(String expectedScheme) { + this.expectedScheme = expectedScheme; + } + + @Override + public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) { + if (expectedScheme.equals(targetUri.getScheme())) { + return new FakeNameResolver(targetUri); + } + return null; + } + + @Override + public String getDefaultScheme() { + return expectedScheme; + } + + @Override + protected boolean isAvailable() { + return true; + } + + @Override + protected int priority() { + return 5; + } + } + + private static class FakeNameResolver extends NameResolver { + final URI uri; + + FakeNameResolver(URI uri) { + this.uri = uri; + } + + @Override + public String getServiceAuthority() { + return uri.getAuthority(); + } + + @Override + public void start(final Listener2 listener) {} + + @Override + public void shutdown() {} + } + + private static class CustomSocketAddress extends SocketAddress {} +} diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplGetNameResolverTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplGetNameResolverTest.java index 8ec522260ae..792f4daca4e 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplGetNameResolverTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplGetNameResolverTest.java @@ -23,7 +23,6 @@ import io.grpc.NameResolver; import io.grpc.NameResolverProvider; import io.grpc.NameResolverRegistry; -import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; import java.util.Collections; @@ -39,6 +38,21 @@ public void invalidUriTarget() { testInvalidTarget("defaultscheme:///[invalid]"); } + @Test + public void validSquareBracketsInRfc2396UriFragment() throws Exception { + testValidTarget("dns://8.8.8.8/host#section[1]", + "dns://8.8.8.8/host#section[1]", + new URI("dns", "8.8.8.8", "/host", null, "section[1]")); + } + + + @Test + public void validSquareBracketsInRfc2396UriQuery() throws Exception { + testValidTarget("dns://8.8.8.8/host?section=[1]", + "dns://8.8.8.8/host?section=[1]", + new URI("dns", "8.8.8.8", "/host", "section=[1]", null)); + } + @Test public void validTargetWithInvalidDnsName() throws Exception { testValidTarget("[valid]", "defaultscheme:///%5Bvalid%5D", @@ -75,6 +89,13 @@ public void validIpv6AuthorityTarget() throws Exception { new URI("defaultscheme", "", "/[::1]:1234", null)); } + @Test + public void validIpv6UriWithJavaNetUriScopeName() throws Exception { + testValidTarget("dns://[::1%eth0]:53/host", + "dns://[::1%eth0]:53/host", + new URI("dns", "[::1%eth0]:53", "/host", null, null)); + } + @Test public void invalidIpv6UriTarget() throws Exception { testInvalidTarget("dns:///[::1]:1234"); @@ -97,8 +118,7 @@ public void validTargetNoProvider() { NameResolverRegistry nameResolverRegistry = new NameResolverRegistry(); try { ManagedChannelImplBuilder.getNameResolverProvider( - "foo.googleapis.com:8080", nameResolverRegistry, - Collections.singleton(InetSocketAddress.class)); + "foo.googleapis.com:8080", nameResolverRegistry); fail("Should fail"); } catch (IllegalArgumentException e) { // expected @@ -110,8 +130,8 @@ public void validTargetProviderAddrTypesNotSupported() { NameResolverRegistry nameResolverRegistry = getTestRegistry("testscheme"); try { ManagedChannelImplBuilder.getNameResolverProvider( - "testscheme:///foo.googleapis.com:8080", nameResolverRegistry, - Collections.singleton(CustomSocketAddress.class)); + "testscheme:///foo.googleapis.com:8080", nameResolverRegistry) + .checkAddressTypes(Collections.singleton(CustomSocketAddress.class)); fail("Should fail"); } catch (IllegalArgumentException e) { assertThat(e).hasMessageThat().isEqualTo( @@ -123,8 +143,7 @@ public void validTargetProviderAddrTypesNotSupported() { private void testValidTarget(String target, String expectedUriString, URI expectedUri) { NameResolverRegistry nameResolverRegistry = getTestRegistry(expectedUri.getScheme()); ManagedChannelImplBuilder.ResolvedNameResolver resolved = - ManagedChannelImplBuilder.getNameResolverProvider( - target, nameResolverRegistry, Collections.singleton(InetSocketAddress.class)); + ManagedChannelImplBuilder.getNameResolverProvider(target, nameResolverRegistry); assertThat(resolved.provider).isInstanceOf(FakeNameResolverProvider.class); assertThat(resolved.targetUri).isEqualTo(wrap(expectedUri)); assertThat(resolved.targetUri.toString()).isEqualTo(expectedUriString); @@ -135,8 +154,7 @@ private void testInvalidTarget(String target) { try { ManagedChannelImplBuilder.ResolvedNameResolver resolved = - ManagedChannelImplBuilder.getNameResolverProvider( - target, nameResolverRegistry, Collections.singleton(InetSocketAddress.class)); + ManagedChannelImplBuilder.getNameResolverProvider(target, nameResolverRegistry); FakeNameResolverProvider nameResolverProvider = (FakeNameResolverProvider) resolved.provider; fail("Should have failed, but got resolver provider " + nameResolverProvider); } catch (IllegalArgumentException e) { From 57e858945dd9c1eb310e7f05e167bd16714295dc Mon Sep 17 00:00:00 2001 From: John Cormie Date: Mon, 5 Jan 2026 17:04:45 -0800 Subject: [PATCH 509/591] binder: Migrate IntentNameResolverProvider to io.grpc.Uri --- .../internal/IntentNameResolverProvider.java | 17 ++++++++++++++--- .../IntentNameResolverProviderTest.java | 19 +++++++++++++++++-- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/IntentNameResolverProvider.java b/binder/src/main/java/io/grpc/binder/internal/IntentNameResolverProvider.java index 67859045dba..5a3c9fcc986 100644 --- a/binder/src/main/java/io/grpc/binder/internal/IntentNameResolverProvider.java +++ b/binder/src/main/java/io/grpc/binder/internal/IntentNameResolverProvider.java @@ -20,6 +20,7 @@ import android.content.Intent; import com.google.common.collect.ImmutableSet; import io.grpc.NameResolver; +import io.grpc.Uri; import io.grpc.NameResolver.Args; import io.grpc.NameResolverProvider; import io.grpc.binder.AndroidComponentAddress; @@ -46,7 +47,17 @@ public String getDefaultScheme() { @Override public NameResolver newNameResolver(URI targetUri, final Args args) { if (Objects.equals(targetUri.getScheme(), ANDROID_INTENT_SCHEME)) { - return new IntentNameResolver(parseUriArg(targetUri), args); + return new IntentNameResolver(parseUriArg(targetUri.toString()), args); + } else { + return null; + } + } + + @Nullable + @Override + public NameResolver newNameResolver(Uri targetUri, final Args args) { + if (Objects.equals(targetUri.getScheme(), ANDROID_INTENT_SCHEME)) { + return new IntentNameResolver(parseUriArg(targetUri.toString()), args); } else { return null; } @@ -67,9 +78,9 @@ public ImmutableSet> getProducedSocketAddressType return ImmutableSet.of(AndroidComponentAddress.class); } - private static Intent parseUriArg(URI targetUri) { + private static Intent parseUriArg(String targetUri) { try { - return Intent.parseUri(targetUri.toString(), URI_INTENT_SCHEME); + return Intent.parseUri(targetUri, URI_INTENT_SCHEME); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } diff --git a/binder/src/test/java/io/grpc/binder/internal/IntentNameResolverProviderTest.java b/binder/src/test/java/io/grpc/binder/internal/IntentNameResolverProviderTest.java index aa75ba84b09..2809a72fee1 100644 --- a/binder/src/test/java/io/grpc/binder/internal/IntentNameResolverProviderTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/IntentNameResolverProviderTest.java @@ -30,6 +30,7 @@ import io.grpc.NameResolver.ServiceConfigParser; import io.grpc.NameResolverProvider; import io.grpc.SynchronizationContext; +import io.grpc.Uri; import io.grpc.binder.ApiConstants; import java.net.URI; import org.junit.Before; @@ -70,18 +71,32 @@ public void testProviderScheme_returnsIntentScheme() throws Exception { @Test public void testNoResolverForUnknownScheme_returnsNull() throws Exception { - assertThat(provider.newNameResolver(new URI("random://uri"), args)).isNull(); + assertThat(provider.newNameResolver(Uri.create("random://uri"), args)).isNull(); } @Test public void testResolutionWithBadUri_throwsIllegalArg() throws Exception { assertThrows( IllegalArgumentException.class, - () -> provider.newNameResolver(new URI("intent:xxx#Intent;e.x=1;end;"), args)); + () -> provider.newNameResolver(Uri.create("intent:xxx#Intent;e.x=1;end;"), args)); } @Test public void testResolverForIntentScheme_returnsResolver() throws Exception { + Uri uri = Uri.create("intent:#Intent;action=action;end"); + NameResolver resolver = provider.newNameResolver(uri, args); + assertThat(resolver).isNotNull(); + assertThat(resolver.getServiceAuthority()).isEqualTo("localhost"); + syncContext.execute(() -> resolver.start(mockListener)); + shadowOf(getMainLooper()).idle(); + verify(mockListener).onResult2(resultCaptor.capture()); + assertThat(resultCaptor.getValue().getAddressesOrError()).isNotNull(); + syncContext.execute(resolver::shutdown); + shadowOf(getMainLooper()).idle(); + } + + @Test + public void testResolverForIntentScheme_returnsResolver_javaNetUri() throws Exception { URI uri = new URI("intent://authority/path#Intent;action=action;scheme=scheme;end"); NameResolver resolver = provider.newNameResolver(uri, args); assertThat(resolver).isNotNull(); From ebb9420948e4c5ea380f9da2e1c6c0d8cf991bbe Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 30 Dec 2025 13:31:48 -0800 Subject: [PATCH 510/591] xds: Merge ClusterResolverLB into CdsLB2 This is a cleanup from gRFC A74: > The xds_cluster_resolver LB policy will be removed completely, as > obtaining endpoint addresses will now be done in the xds resolver. The > code for generating the child policy configs for the priority policy > will now be done in the cds LB policy. Since XdsDependencyManager handles the actual resolution, ClusterResolverLB was only handling config conversion. We just do that in CdsLB2 now. The large blob of code from ClusterResolverLB was moved into CdsLB2 unchanged. The cluster resolver tests were left in-place, though, as they did need changes and it'd be very hard to see the changes if moved at the same time. --- .../java/io/grpc/xds/CdsLoadBalancer2.java | 417 ++++++++++++++-- .../grpc/xds/ClusterResolverLoadBalancer.java | 455 ------------------ .../ClusterResolverLoadBalancerProvider.java | 245 ---------- .../main/java/io/grpc/xds/XdsLbPolicies.java | 1 - .../services/io.grpc.LoadBalancerProvider | 1 - .../io/grpc/xds/CdsLoadBalancer2Test.java | 121 +---- ...usterResolverLoadBalancerProviderTest.java | 79 --- .../xds/ClusterResolverLoadBalancerTest.java | 98 +--- 8 files changed, 425 insertions(+), 992 deletions(-) delete mode 100644 xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java delete mode 100644 xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java delete mode 100644 xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerProviderTest.java diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java index 688626d9b3d..67c5626aff5 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java @@ -19,20 +19,31 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; import static io.grpc.xds.XdsLbPolicies.CDS_POLICY_NAME; -import static io.grpc.xds.XdsLbPolicies.CLUSTER_RESOLVER_POLICY_NAME; import static io.grpc.xds.XdsLbPolicies.PRIORITY_POLICY_NAME; +import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.CheckReturnValue; +import io.grpc.Attributes; +import io.grpc.EquivalentAddressGroup; +import io.grpc.HttpConnectProxiedSocketAddress; import io.grpc.InternalLogId; import io.grpc.LoadBalancer; +import io.grpc.LoadBalancerProvider; import io.grpc.LoadBalancerRegistry; import io.grpc.NameResolver; import io.grpc.Status; import io.grpc.StatusOr; import io.grpc.util.GracefulSwitchLoadBalancer; +import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig; import io.grpc.xds.CdsLoadBalancerProvider.CdsConfig; -import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; -import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; +import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; +import io.grpc.xds.Endpoints.DropOverload; +import io.grpc.xds.Endpoints.LbEndpoint; +import io.grpc.xds.Endpoints.LocalityLbEndpoints; +import io.grpc.xds.EnvoyServerProtoData.FailurePercentageEjection; +import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; +import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection; +import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; import io.grpc.xds.XdsClusterResource.CdsUpdate; import io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType; @@ -40,13 +51,22 @@ import io.grpc.xds.XdsConfig.XdsClusterConfig; import io.grpc.xds.XdsConfig.XdsClusterConfig.AggregateConfig; import io.grpc.xds.XdsConfig.XdsClusterConfig.EndpointConfig; +import io.grpc.xds.XdsEndpointResource.EdsUpdate; +import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger.XdsLogLevel; +import io.grpc.xds.internal.XdsInternalAttributes; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeMap; /** * Load balancer for cds_experimental LB policy. One instance per top-level cluster. @@ -57,6 +77,7 @@ final class CdsLoadBalancer2 extends LoadBalancer { private final XdsLogger logger; private final Helper helper; private final LoadBalancerRegistry lbRegistry; + private final ClusterState clusterState = new ClusterState(); private GracefulSwitchLoadBalancer delegate; // Following fields are effectively final. private String clusterName; @@ -100,7 +121,6 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { XdsClusterConfig clusterConfig = clusterConfigOr.getValue(); NameResolver.ConfigOrError configOrError; - Object gracefulConfig; if (clusterConfig.getChildren() instanceof EndpointConfig) { // The LB policy config is provided in service_config.proto/JSON format. configOrError = @@ -112,34 +132,38 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { return fail(Status.INTERNAL.withDescription( errorPrefix() + "Unable to parse the LB config: " + configOrError.getError())); } - CdsUpdate result = clusterConfig.getClusterResource(); - DiscoveryMechanism instance; - if (result.clusterType() == ClusterType.EDS) { - instance = DiscoveryMechanism.forEds( - clusterName, - result.edsServiceName(), - result.lrsServerInfo(), - result.maxConcurrentRequests(), - result.upstreamTlsContext(), - result.filterMetadata(), - result.outlierDetection(), - result.backendMetricPropagation()); - } else { - instance = DiscoveryMechanism.forLogicalDns( - clusterName, - result.dnsHostName(), - result.lrsServerInfo(), - result.maxConcurrentRequests(), - result.upstreamTlsContext(), - result.filterMetadata(), - result.backendMetricPropagation()); - } - gracefulConfig = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - lbRegistry.getProvider(CLUSTER_RESOLVER_POLICY_NAME), - new ClusterResolverConfig( - instance, - configOrError.getConfig(), - clusterConfig.getClusterResource().isHttp11ProxyAvailable())); + + StatusOr edsUpdate = getEdsUpdate(xdsConfig, clusterName); + StatusOr statusOrResult = clusterState.edsUpdateToResult( + clusterName, + clusterConfig.getClusterResource(), + configOrError.getConfig(), + edsUpdate); + if (!statusOrResult.hasValue()) { + Status status = Status.UNAVAILABLE + .withDescription(statusOrResult.getStatus().getDescription()) + .withCause(statusOrResult.getStatus().getCause()); + delegate.handleNameResolutionError(status); + return status; + } + ClusterResolutionResult result = statusOrResult.getValue(); + List addresses = result.addresses; + if (addresses.isEmpty()) { + Status status = Status.UNAVAILABLE + .withDescription("No usable endpoint from cluster: " + clusterName); + delegate.handleNameResolutionError(status); + return status; + } + Object gracefulConfig = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + lbRegistry.getProvider(PRIORITY_POLICY_NAME), + new PriorityLbConfig( + Collections.unmodifiableMap(result.priorityChildConfigs), + Collections.unmodifiableList(result.priorities))); + return delegate.acceptResolvedAddresses( + resolvedAddresses.toBuilder() + .setLoadBalancingPolicyConfig(gracefulConfig) + .setAddresses(Collections.unmodifiableList(addresses)) + .build()); } else if (clusterConfig.getChildren() instanceof AggregateConfig) { Map priorityChildConfigs = new HashMap<>(); List leafClusters = ((AggregateConfig) clusterConfig.getChildren()).getLeafNames(); @@ -151,18 +175,17 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { new CdsConfig(childCluster)), false)); } - gracefulConfig = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + Object gracefulConfig = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( lbRegistry.getProvider(PRIORITY_POLICY_NAME), new PriorityLoadBalancerProvider.PriorityLbConfig( Collections.unmodifiableMap(priorityChildConfigs), leafClusters)); + return delegate.acceptResolvedAddresses( + resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(gracefulConfig).build()); } else { return fail(Status.INTERNAL.withDescription( errorPrefix() + "Unexpected cluster children type: " + clusterConfig.getChildren().getClass())); } - - return delegate.acceptResolvedAddresses( - resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(gracefulConfig).build()); } @Override @@ -198,4 +221,326 @@ private Status fail(Status error) { private String errorPrefix() { return "CdsLb for " + clusterName + ": "; } + + private static StatusOr getEdsUpdate(XdsConfig xdsConfig, String cluster) { + StatusOr clusterConfig = xdsConfig.getClusters().get(cluster); + if (clusterConfig == null) { + return StatusOr.fromStatus(Status.INTERNAL + .withDescription("BUG: cluster resolver could not find cluster in xdsConfig")); + } + if (!clusterConfig.hasValue()) { + return StatusOr.fromStatus(clusterConfig.getStatus()); + } + if (!(clusterConfig.getValue().getChildren() instanceof XdsClusterConfig.EndpointConfig)) { + return StatusOr.fromStatus(Status.INTERNAL + .withDescription("BUG: cluster resolver cluster with children of unknown type")); + } + XdsClusterConfig.EndpointConfig endpointConfig = + (XdsClusterConfig.EndpointConfig) clusterConfig.getValue().getChildren(); + return endpointConfig.getEndpoint(); + } + + /** + * Generates a string that represents the priority in the LB policy config. The string is unique + * across priorities in all clusters and priorityName(c, p1) < priorityName(c, p2) iff p1 < p2. + * The ordering is undefined for priorities in different clusters. + */ + private static String priorityName(String cluster, int priority) { + return cluster + "[child" + priority + "]"; + } + + /** + * Generates a string that represents the locality in the LB policy config. The string is unique + * across all localities in all clusters. + */ + private static String localityName(Locality locality) { + return "{region=\"" + locality.region() + + "\", zone=\"" + locality.zone() + + "\", sub_zone=\"" + locality.subZone() + + "\"}"; + } + + private final class ClusterState { + private Map localityPriorityNames = Collections.emptyMap(); + int priorityNameGenId = 1; + + StatusOr edsUpdateToResult( + String clusterName, + CdsUpdate discovery, + Object lbConfig, + StatusOr updateOr) { + if (!updateOr.hasValue()) { + return StatusOr.fromStatus(updateOr.getStatus()); + } + EdsUpdate update = updateOr.getValue(); + logger.log(XdsLogLevel.DEBUG, "Received endpoint update {0}", update); + if (logger.isLoggable(XdsLogLevel.INFO)) { + logger.log(XdsLogLevel.INFO, "Cluster {0}: {1} localities, {2} drop categories", + clusterName, update.localityLbEndpointsMap.size(), + update.dropPolicies.size()); + } + Map localityLbEndpoints = + update.localityLbEndpointsMap; + List dropOverloads = update.dropPolicies; + List addresses = new ArrayList<>(); + Map> prioritizedLocalityWeights = new HashMap<>(); + List sortedPriorityNames = + generatePriorityNames(clusterName, localityLbEndpoints); + for (Locality locality : localityLbEndpoints.keySet()) { + LocalityLbEndpoints localityLbInfo = localityLbEndpoints.get(locality); + String priorityName = localityPriorityNames.get(locality); + boolean discard = true; + for (LbEndpoint endpoint : localityLbInfo.endpoints()) { + if (endpoint.isHealthy()) { + discard = false; + long weight = localityLbInfo.localityWeight(); + if (endpoint.loadBalancingWeight() != 0) { + weight *= endpoint.loadBalancingWeight(); + } + String localityName = localityName(locality); + Attributes attr = + endpoint.eag().getAttributes().toBuilder() + .set(io.grpc.xds.XdsAttributes.ATTR_LOCALITY, locality) + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, localityName) + .set(io.grpc.xds.XdsAttributes.ATTR_LOCALITY_WEIGHT, + localityLbInfo.localityWeight()) + .set(io.grpc.xds.XdsAttributes.ATTR_SERVER_WEIGHT, weight) + .set(XdsInternalAttributes.ATTR_ADDRESS_NAME, endpoint.hostname()) + .build(); + EquivalentAddressGroup eag; + if (discovery.isHttp11ProxyAvailable()) { + List rewrittenAddresses = new ArrayList<>(); + for (SocketAddress addr : endpoint.eag().getAddresses()) { + rewrittenAddresses.add(rewriteAddress( + addr, endpoint.endpointMetadata(), localityLbInfo.localityMetadata())); + } + eag = new EquivalentAddressGroup(rewrittenAddresses, attr); + } else { + eag = new EquivalentAddressGroup(endpoint.eag().getAddresses(), attr); + } + eag = AddressFilter.setPathFilter(eag, Arrays.asList(priorityName, localityName)); + addresses.add(eag); + } + } + if (discard) { + logger.log(XdsLogLevel.INFO, + "Discard locality {0} with 0 healthy endpoints", locality); + continue; + } + if (!prioritizedLocalityWeights.containsKey(priorityName)) { + prioritizedLocalityWeights.put(priorityName, new HashMap()); + } + prioritizedLocalityWeights.get(priorityName).put( + locality, localityLbInfo.localityWeight()); + } + if (prioritizedLocalityWeights.isEmpty()) { + // Will still update the result, as if the cluster resource is revoked. + logger.log(XdsLogLevel.INFO, + "Cluster {0} has no usable priority/locality/endpoint", clusterName); + } + sortedPriorityNames.retainAll(prioritizedLocalityWeights.keySet()); + Map priorityChildConfigs = + generatePriorityChildConfigs( + clusterName, discovery, lbConfig, lbRegistry, + prioritizedLocalityWeights, dropOverloads); + return StatusOr.fromValue(new ClusterResolutionResult(addresses, priorityChildConfigs, + sortedPriorityNames)); + } + + private SocketAddress rewriteAddress(SocketAddress addr, + ImmutableMap endpointMetadata, + ImmutableMap localityMetadata) { + if (!(addr instanceof InetSocketAddress)) { + return addr; + } + + SocketAddress proxyAddress; + try { + proxyAddress = (SocketAddress) endpointMetadata.get( + "envoy.http11_proxy_transport_socket.proxy_address"); + if (proxyAddress == null) { + proxyAddress = (SocketAddress) localityMetadata.get( + "envoy.http11_proxy_transport_socket.proxy_address"); + } + } catch (ClassCastException e) { + return addr; + } + + if (proxyAddress == null) { + return addr; + } + + return HttpConnectProxiedSocketAddress.newBuilder() + .setTargetAddress((InetSocketAddress) addr) + .setProxyAddress(proxyAddress) + .build(); + } + + private List generatePriorityNames(String name, + Map localityLbEndpoints) { + TreeMap> todo = new TreeMap<>(); + for (Locality locality : localityLbEndpoints.keySet()) { + int priority = localityLbEndpoints.get(locality).priority(); + if (!todo.containsKey(priority)) { + todo.put(priority, new ArrayList<>()); + } + todo.get(priority).add(locality); + } + Map newNames = new HashMap<>(); + Set usedNames = new HashSet<>(); + List ret = new ArrayList<>(); + for (Integer priority: todo.keySet()) { + String foundName = ""; + for (Locality locality : todo.get(priority)) { + if (localityPriorityNames.containsKey(locality) + && usedNames.add(localityPriorityNames.get(locality))) { + foundName = localityPriorityNames.get(locality); + break; + } + } + if ("".equals(foundName)) { + foundName = priorityName(name, priorityNameGenId++); + } + for (Locality locality : todo.get(priority)) { + newNames.put(locality, foundName); + } + ret.add(foundName); + } + localityPriorityNames = newNames; + return ret; + } + } + + private static class ClusterResolutionResult { + // Endpoint addresses. + private final List addresses; + // Config (include load balancing policy/config) for each priority in the cluster. + private final Map priorityChildConfigs; + // List of priority names ordered in descending priorities. + private final List priorities; + + ClusterResolutionResult(List addresses, + Map configs, List priorities) { + this.addresses = addresses; + this.priorityChildConfigs = configs; + this.priorities = priorities; + } + } + + /** + * Generates configs to be used in the priority LB policy for priorities in a cluster. + * + *

    priority LB -> cluster_impl LB (one per priority) -> (weighted_target LB + * -> round_robin / least_request_experimental (one per locality)) / ring_hash_experimental + */ + private static Map generatePriorityChildConfigs( + String clusterName, + CdsUpdate discovery, + Object endpointLbConfig, + LoadBalancerRegistry lbRegistry, + Map> prioritizedLocalityWeights, + List dropOverloads) { + Map configs = new HashMap<>(); + for (String priority : prioritizedLocalityWeights.keySet()) { + ClusterImplConfig clusterImplConfig = + new ClusterImplConfig( + clusterName, discovery.edsServiceName(), discovery.lrsServerInfo(), + discovery.maxConcurrentRequests(), dropOverloads, endpointLbConfig, + discovery.upstreamTlsContext(), discovery.filterMetadata(), + discovery.backendMetricPropagation()); + LoadBalancerProvider clusterImplLbProvider = + lbRegistry.getProvider(XdsLbPolicies.CLUSTER_IMPL_POLICY_NAME); + Object priorityChildPolicy = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + clusterImplLbProvider, clusterImplConfig); + + // If outlier detection has been configured we wrap the child policy in the outlier detection + // load balancer. + if (discovery.outlierDetection() != null) { + LoadBalancerProvider outlierDetectionProvider = lbRegistry.getProvider( + "outlier_detection_experimental"); + priorityChildPolicy = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( + outlierDetectionProvider, + buildOutlierDetectionLbConfig(discovery.outlierDetection(), priorityChildPolicy)); + } + + boolean isEds = discovery.clusterType() == ClusterType.EDS; + PriorityChildConfig priorityChildConfig = + new PriorityChildConfig(priorityChildPolicy, isEds /* ignoreReresolution */); + configs.put(priority, priorityChildConfig); + } + return configs; + } + + /** + * Converts {@link OutlierDetection} that represents the xDS configuration to {@link + * OutlierDetectionLoadBalancerConfig} that the {@link io.grpc.util.OutlierDetectionLoadBalancer} + * understands. + */ + private static OutlierDetectionLoadBalancerConfig buildOutlierDetectionLbConfig( + OutlierDetection outlierDetection, Object childConfig) { + OutlierDetectionLoadBalancerConfig.Builder configBuilder + = new OutlierDetectionLoadBalancerConfig.Builder(); + + configBuilder.setChildConfig(childConfig); + + if (outlierDetection.intervalNanos() != null) { + configBuilder.setIntervalNanos(outlierDetection.intervalNanos()); + } + if (outlierDetection.baseEjectionTimeNanos() != null) { + configBuilder.setBaseEjectionTimeNanos(outlierDetection.baseEjectionTimeNanos()); + } + if (outlierDetection.maxEjectionTimeNanos() != null) { + configBuilder.setMaxEjectionTimeNanos(outlierDetection.maxEjectionTimeNanos()); + } + if (outlierDetection.maxEjectionPercent() != null) { + configBuilder.setMaxEjectionPercent(outlierDetection.maxEjectionPercent()); + } + + SuccessRateEjection successRate = outlierDetection.successRateEjection(); + if (successRate != null) { + OutlierDetectionLoadBalancerConfig.SuccessRateEjection.Builder + successRateConfigBuilder = new OutlierDetectionLoadBalancerConfig + .SuccessRateEjection.Builder(); + + if (successRate.stdevFactor() != null) { + successRateConfigBuilder.setStdevFactor(successRate.stdevFactor()); + } + if (successRate.enforcementPercentage() != null) { + successRateConfigBuilder.setEnforcementPercentage(successRate.enforcementPercentage()); + } + if (successRate.minimumHosts() != null) { + successRateConfigBuilder.setMinimumHosts(successRate.minimumHosts()); + } + if (successRate.requestVolume() != null) { + successRateConfigBuilder.setRequestVolume(successRate.requestVolume()); + } + + configBuilder.setSuccessRateEjection(successRateConfigBuilder.build()); + } + + FailurePercentageEjection failurePercentage = outlierDetection.failurePercentageEjection(); + if (failurePercentage != null) { + OutlierDetectionLoadBalancerConfig.FailurePercentageEjection.Builder + failurePercentageConfigBuilder = new OutlierDetectionLoadBalancerConfig + .FailurePercentageEjection.Builder(); + + if (failurePercentage.threshold() != null) { + failurePercentageConfigBuilder.setThreshold(failurePercentage.threshold()); + } + if (failurePercentage.enforcementPercentage() != null) { + failurePercentageConfigBuilder.setEnforcementPercentage( + failurePercentage.enforcementPercentage()); + } + if (failurePercentage.minimumHosts() != null) { + failurePercentageConfigBuilder.setMinimumHosts(failurePercentage.minimumHosts()); + } + if (failurePercentage.requestVolume() != null) { + failurePercentageConfigBuilder.setRequestVolume(failurePercentage.requestVolume()); + } + + configBuilder.setFailurePercentageEjection(failurePercentageConfigBuilder.build()); + } + + return configBuilder.build(); + } } diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java deleted file mode 100644 index 61bfd3c0839..00000000000 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ /dev/null @@ -1,455 +0,0 @@ -/* - * 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 io.grpc.xds.XdsLbPolicies.PRIORITY_POLICY_NAME; - -import com.google.common.collect.ImmutableMap; -import io.grpc.Attributes; -import io.grpc.EquivalentAddressGroup; -import io.grpc.HttpConnectProxiedSocketAddress; -import io.grpc.InternalLogId; -import io.grpc.LoadBalancer; -import io.grpc.LoadBalancerProvider; -import io.grpc.LoadBalancerRegistry; -import io.grpc.Status; -import io.grpc.StatusOr; -import io.grpc.util.GracefulSwitchLoadBalancer; -import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig; -import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; -import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; -import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; -import io.grpc.xds.Endpoints.DropOverload; -import io.grpc.xds.Endpoints.LbEndpoint; -import io.grpc.xds.Endpoints.LocalityLbEndpoints; -import io.grpc.xds.EnvoyServerProtoData.FailurePercentageEjection; -import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; -import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection; -import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig; -import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; -import io.grpc.xds.XdsConfig.XdsClusterConfig; -import io.grpc.xds.XdsEndpointResource.EdsUpdate; -import io.grpc.xds.client.Locality; -import io.grpc.xds.client.XdsLogger; -import io.grpc.xds.client.XdsLogger.XdsLogLevel; -import io.grpc.xds.internal.XdsInternalAttributes; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; - -/** - * Load balancer for cluster_resolver_experimental LB policy. This LB policy is the child LB policy - * of the cds_experimental LB policy and the parent LB policy of the priority_experimental LB - * policy in the xDS load balancing hierarchy. This policy converts endpoints of non-aggregate - * clusters (e.g., EDS or Logical DNS) and groups endpoints in priorities and localities to be - * used in the downstream LB policies for fine-grained load balancing purposes. - */ -final class ClusterResolverLoadBalancer extends LoadBalancer { - private final XdsLogger logger; - private final LoadBalancerRegistry lbRegistry; - private final LoadBalancer delegate; - private ClusterState clusterState; - - ClusterResolverLoadBalancer(Helper helper, LoadBalancerRegistry lbRegistry) { - this.delegate = lbRegistry.getProvider(PRIORITY_POLICY_NAME).newLoadBalancer(helper); - this.lbRegistry = checkNotNull(lbRegistry, "lbRegistry"); - logger = XdsLogger.withLogId( - InternalLogId.allocate("cluster-resolver-lb", helper.getAuthority())); - logger.log(XdsLogLevel.INFO, "Created"); - } - - @Override - public void handleNameResolutionError(Status error) { - logger.log(XdsLogLevel.WARNING, "Received name resolution error: {0}", error); - delegate.handleNameResolutionError(error); - } - - @Override - public void shutdown() { - logger.log(XdsLogLevel.INFO, "Shutdown"); - delegate.shutdown(); - } - - @Override - public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses); - ClusterResolverConfig config = - (ClusterResolverConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); - XdsConfig xdsConfig = resolvedAddresses.getAttributes().get( - io.grpc.xds.XdsAttributes.XDS_CONFIG); - - DiscoveryMechanism instance = config.discoveryMechanism; - String cluster = instance.cluster; - if (clusterState == null) { - clusterState = new ClusterState(); - } - - StatusOr edsUpdate = getEdsUpdate(xdsConfig, cluster); - StatusOr statusOrResult = - clusterState.edsUpdateToResult(config, instance, edsUpdate); - if (!statusOrResult.hasValue()) { - Status status = Status.UNAVAILABLE - .withDescription(statusOrResult.getStatus().getDescription()) - .withCause(statusOrResult.getStatus().getCause()); - delegate.handleNameResolutionError(status); - return status; - } - ClusterResolutionResult result = statusOrResult.getValue(); - List addresses = result.addresses; - if (addresses.isEmpty()) { - Status status = Status.UNAVAILABLE - .withDescription("No usable endpoint from cluster: " + cluster); - delegate.handleNameResolutionError(status); - return status; - } - PriorityLbConfig childConfig = - new PriorityLbConfig( - Collections.unmodifiableMap(result.priorityChildConfigs), - Collections.unmodifiableList(result.priorities)); - return delegate.acceptResolvedAddresses( - resolvedAddresses.toBuilder() - .setLoadBalancingPolicyConfig(childConfig) - .setAddresses(Collections.unmodifiableList(addresses)) - .build()); - } - - private static StatusOr getEdsUpdate(XdsConfig xdsConfig, String cluster) { - StatusOr clusterConfig = xdsConfig.getClusters().get(cluster); - if (clusterConfig == null) { - return StatusOr.fromStatus(Status.INTERNAL - .withDescription("BUG: cluster resolver could not find cluster in xdsConfig")); - } - if (!clusterConfig.hasValue()) { - return StatusOr.fromStatus(clusterConfig.getStatus()); - } - if (!(clusterConfig.getValue().getChildren() instanceof XdsClusterConfig.EndpointConfig)) { - return StatusOr.fromStatus(Status.INTERNAL - .withDescription("BUG: cluster resolver cluster with children of unknown type")); - } - XdsClusterConfig.EndpointConfig endpointConfig = - (XdsClusterConfig.EndpointConfig) clusterConfig.getValue().getChildren(); - return endpointConfig.getEndpoint(); - } - - private final class ClusterState { - private Map localityPriorityNames = Collections.emptyMap(); - int priorityNameGenId = 1; - - StatusOr edsUpdateToResult( - ClusterResolverConfig config, DiscoveryMechanism discovery, StatusOr updateOr) { - if (!updateOr.hasValue()) { - return StatusOr.fromStatus(updateOr.getStatus()); - } - EdsUpdate update = updateOr.getValue(); - logger.log(XdsLogLevel.DEBUG, "Received endpoint update {0}", update); - if (logger.isLoggable(XdsLogLevel.INFO)) { - logger.log(XdsLogLevel.INFO, "Cluster {0}: {1} localities, {2} drop categories", - discovery.cluster, update.localityLbEndpointsMap.size(), - update.dropPolicies.size()); - } - Map localityLbEndpoints = - update.localityLbEndpointsMap; - List dropOverloads = update.dropPolicies; - List addresses = new ArrayList<>(); - Map> prioritizedLocalityWeights = new HashMap<>(); - List sortedPriorityNames = - generatePriorityNames(discovery.cluster, localityLbEndpoints); - for (Locality locality : localityLbEndpoints.keySet()) { - LocalityLbEndpoints localityLbInfo = localityLbEndpoints.get(locality); - String priorityName = localityPriorityNames.get(locality); - boolean discard = true; - for (LbEndpoint endpoint : localityLbInfo.endpoints()) { - if (endpoint.isHealthy()) { - discard = false; - long weight = localityLbInfo.localityWeight(); - if (endpoint.loadBalancingWeight() != 0) { - weight *= endpoint.loadBalancingWeight(); - } - String localityName = localityName(locality); - Attributes attr = - endpoint.eag().getAttributes().toBuilder() - .set(io.grpc.xds.XdsAttributes.ATTR_LOCALITY, locality) - .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, localityName) - .set(io.grpc.xds.XdsAttributes.ATTR_LOCALITY_WEIGHT, - localityLbInfo.localityWeight()) - .set(io.grpc.xds.XdsAttributes.ATTR_SERVER_WEIGHT, weight) - .set(XdsInternalAttributes.ATTR_ADDRESS_NAME, endpoint.hostname()) - .build(); - EquivalentAddressGroup eag; - if (config.isHttp11ProxyAvailable()) { - List rewrittenAddresses = new ArrayList<>(); - for (SocketAddress addr : endpoint.eag().getAddresses()) { - rewrittenAddresses.add(rewriteAddress( - addr, endpoint.endpointMetadata(), localityLbInfo.localityMetadata())); - } - eag = new EquivalentAddressGroup(rewrittenAddresses, attr); - } else { - eag = new EquivalentAddressGroup(endpoint.eag().getAddresses(), attr); - } - eag = AddressFilter.setPathFilter(eag, Arrays.asList(priorityName, localityName)); - addresses.add(eag); - } - } - if (discard) { - logger.log(XdsLogLevel.INFO, - "Discard locality {0} with 0 healthy endpoints", locality); - continue; - } - if (!prioritizedLocalityWeights.containsKey(priorityName)) { - prioritizedLocalityWeights.put(priorityName, new HashMap()); - } - prioritizedLocalityWeights.get(priorityName).put( - locality, localityLbInfo.localityWeight()); - } - if (prioritizedLocalityWeights.isEmpty()) { - // Will still update the result, as if the cluster resource is revoked. - logger.log(XdsLogLevel.INFO, - "Cluster {0} has no usable priority/locality/endpoint", discovery.cluster); - } - sortedPriorityNames.retainAll(prioritizedLocalityWeights.keySet()); - Map priorityChildConfigs = - generatePriorityChildConfigs( - discovery, config.lbConfig, lbRegistry, - prioritizedLocalityWeights, dropOverloads); - return StatusOr.fromValue(new ClusterResolutionResult(addresses, priorityChildConfigs, - sortedPriorityNames)); - } - - private SocketAddress rewriteAddress(SocketAddress addr, - ImmutableMap endpointMetadata, - ImmutableMap localityMetadata) { - if (!(addr instanceof InetSocketAddress)) { - return addr; - } - - SocketAddress proxyAddress; - try { - proxyAddress = (SocketAddress) endpointMetadata.get( - "envoy.http11_proxy_transport_socket.proxy_address"); - if (proxyAddress == null) { - proxyAddress = (SocketAddress) localityMetadata.get( - "envoy.http11_proxy_transport_socket.proxy_address"); - } - } catch (ClassCastException e) { - return addr; - } - - if (proxyAddress == null) { - return addr; - } - - return HttpConnectProxiedSocketAddress.newBuilder() - .setTargetAddress((InetSocketAddress) addr) - .setProxyAddress(proxyAddress) - .build(); - } - - private List generatePriorityNames(String name, - Map localityLbEndpoints) { - TreeMap> todo = new TreeMap<>(); - for (Locality locality : localityLbEndpoints.keySet()) { - int priority = localityLbEndpoints.get(locality).priority(); - if (!todo.containsKey(priority)) { - todo.put(priority, new ArrayList<>()); - } - todo.get(priority).add(locality); - } - Map newNames = new HashMap<>(); - Set usedNames = new HashSet<>(); - List ret = new ArrayList<>(); - for (Integer priority: todo.keySet()) { - String foundName = ""; - for (Locality locality : todo.get(priority)) { - if (localityPriorityNames.containsKey(locality) - && usedNames.add(localityPriorityNames.get(locality))) { - foundName = localityPriorityNames.get(locality); - break; - } - } - if ("".equals(foundName)) { - foundName = priorityName(name, priorityNameGenId++); - } - for (Locality locality : todo.get(priority)) { - newNames.put(locality, foundName); - } - ret.add(foundName); - } - localityPriorityNames = newNames; - return ret; - } - } - - private static class ClusterResolutionResult { - // Endpoint addresses. - private final List addresses; - // Config (include load balancing policy/config) for each priority in the cluster. - private final Map priorityChildConfigs; - // List of priority names ordered in descending priorities. - private final List priorities; - - ClusterResolutionResult(List addresses, - Map configs, List priorities) { - this.addresses = addresses; - this.priorityChildConfigs = configs; - this.priorities = priorities; - } - } - - /** - * Generates configs to be used in the priority LB policy for priorities in a cluster. - * - *

    priority LB -> cluster_impl LB (one per priority) -> (weighted_target LB - * -> round_robin / least_request_experimental (one per locality)) / ring_hash_experimental - */ - private static Map generatePriorityChildConfigs( - DiscoveryMechanism discovery, - Object endpointLbConfig, - LoadBalancerRegistry lbRegistry, - Map> prioritizedLocalityWeights, - List dropOverloads) { - Map configs = new HashMap<>(); - for (String priority : prioritizedLocalityWeights.keySet()) { - ClusterImplConfig clusterImplConfig = - new ClusterImplConfig( - discovery.cluster, discovery.edsServiceName, discovery.lrsServerInfo, - discovery.maxConcurrentRequests, dropOverloads, endpointLbConfig, - discovery.tlsContext, discovery.filterMetadata, discovery.backendMetricPropagation); - LoadBalancerProvider clusterImplLbProvider = - lbRegistry.getProvider(XdsLbPolicies.CLUSTER_IMPL_POLICY_NAME); - Object priorityChildPolicy = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - clusterImplLbProvider, clusterImplConfig); - - // If outlier detection has been configured we wrap the child policy in the outlier detection - // load balancer. - if (discovery.outlierDetection != null) { - LoadBalancerProvider outlierDetectionProvider = lbRegistry.getProvider( - "outlier_detection_experimental"); - priorityChildPolicy = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - outlierDetectionProvider, - buildOutlierDetectionLbConfig(discovery.outlierDetection, priorityChildPolicy)); - } - - boolean isEds = discovery.type == DiscoveryMechanism.Type.EDS; - PriorityChildConfig priorityChildConfig = - new PriorityChildConfig(priorityChildPolicy, isEds /* ignoreReresolution */); - configs.put(priority, priorityChildConfig); - } - return configs; - } - - /** - * Converts {@link OutlierDetection} that represents the xDS configuration to {@link - * OutlierDetectionLoadBalancerConfig} that the {@link io.grpc.util.OutlierDetectionLoadBalancer} - * understands. - */ - private static OutlierDetectionLoadBalancerConfig buildOutlierDetectionLbConfig( - OutlierDetection outlierDetection, Object childConfig) { - OutlierDetectionLoadBalancerConfig.Builder configBuilder - = new OutlierDetectionLoadBalancerConfig.Builder(); - - configBuilder.setChildConfig(childConfig); - - if (outlierDetection.intervalNanos() != null) { - configBuilder.setIntervalNanos(outlierDetection.intervalNanos()); - } - if (outlierDetection.baseEjectionTimeNanos() != null) { - configBuilder.setBaseEjectionTimeNanos(outlierDetection.baseEjectionTimeNanos()); - } - if (outlierDetection.maxEjectionTimeNanos() != null) { - configBuilder.setMaxEjectionTimeNanos(outlierDetection.maxEjectionTimeNanos()); - } - if (outlierDetection.maxEjectionPercent() != null) { - configBuilder.setMaxEjectionPercent(outlierDetection.maxEjectionPercent()); - } - - SuccessRateEjection successRate = outlierDetection.successRateEjection(); - if (successRate != null) { - OutlierDetectionLoadBalancerConfig.SuccessRateEjection.Builder - successRateConfigBuilder = new OutlierDetectionLoadBalancerConfig - .SuccessRateEjection.Builder(); - - if (successRate.stdevFactor() != null) { - successRateConfigBuilder.setStdevFactor(successRate.stdevFactor()); - } - if (successRate.enforcementPercentage() != null) { - successRateConfigBuilder.setEnforcementPercentage(successRate.enforcementPercentage()); - } - if (successRate.minimumHosts() != null) { - successRateConfigBuilder.setMinimumHosts(successRate.minimumHosts()); - } - if (successRate.requestVolume() != null) { - successRateConfigBuilder.setRequestVolume(successRate.requestVolume()); - } - - configBuilder.setSuccessRateEjection(successRateConfigBuilder.build()); - } - - FailurePercentageEjection failurePercentage = outlierDetection.failurePercentageEjection(); - if (failurePercentage != null) { - OutlierDetectionLoadBalancerConfig.FailurePercentageEjection.Builder - failurePercentageConfigBuilder = new OutlierDetectionLoadBalancerConfig - .FailurePercentageEjection.Builder(); - - if (failurePercentage.threshold() != null) { - failurePercentageConfigBuilder.setThreshold(failurePercentage.threshold()); - } - if (failurePercentage.enforcementPercentage() != null) { - failurePercentageConfigBuilder.setEnforcementPercentage( - failurePercentage.enforcementPercentage()); - } - if (failurePercentage.minimumHosts() != null) { - failurePercentageConfigBuilder.setMinimumHosts(failurePercentage.minimumHosts()); - } - if (failurePercentage.requestVolume() != null) { - failurePercentageConfigBuilder.setRequestVolume(failurePercentage.requestVolume()); - } - - configBuilder.setFailurePercentageEjection(failurePercentageConfigBuilder.build()); - } - - return configBuilder.build(); - } - - /** - * Generates a string that represents the priority in the LB policy config. The string is unique - * across priorities in all clusters and priorityName(c, p1) < priorityName(c, p2) iff p1 < p2. - * The ordering is undefined for priorities in different clusters. - */ - private static String priorityName(String cluster, int priority) { - return cluster + "[child" + priority + "]"; - } - - /** - * Generates a string that represents the locality in the LB policy config. The string is unique - * across all localities in all clusters. - */ - private static String localityName(Locality locality) { - return "{region=\"" + locality.region() - + "\", zone=\"" + locality.zone() - + "\", sub_zone=\"" + locality.subZone() - + "\"}"; - } -} diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java deleted file mode 100644 index f6f92a7a9a7..00000000000 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * 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 com.google.common.base.MoreObjects; -import com.google.common.collect.ImmutableMap; -import com.google.protobuf.Struct; -import io.grpc.Internal; -import io.grpc.LoadBalancer; -import io.grpc.LoadBalancer.Helper; -import io.grpc.LoadBalancerProvider; -import io.grpc.LoadBalancerRegistry; -import io.grpc.NameResolver.ConfigOrError; -import io.grpc.Status; -import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; -import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; -import io.grpc.xds.client.BackendMetricPropagation; -import io.grpc.xds.client.Bootstrapper.ServerInfo; -import java.util.Map; -import java.util.Objects; -import javax.annotation.Nullable; - -/** - * The provider for the cluster_resolver load balancing policy. This class should not be directly - * referenced in code. The policy should be accessed through - * {@link io.grpc.LoadBalancerRegistry#getProvider} with the name "cluster_resolver_experimental". - */ -@Internal -public final class ClusterResolverLoadBalancerProvider extends LoadBalancerProvider { - private final LoadBalancerRegistry lbRegistry; - - public ClusterResolverLoadBalancerProvider() { - this.lbRegistry = null; - } - - ClusterResolverLoadBalancerProvider(LoadBalancerRegistry lbRegistry) { - this.lbRegistry = checkNotNull(lbRegistry, "lbRegistry"); - } - - @Override - public boolean isAvailable() { - return true; - } - - @Override - public int getPriority() { - return 5; - } - - @Override - public String getPolicyName() { - return XdsLbPolicies.CLUSTER_RESOLVER_POLICY_NAME; - } - - @Override - public ConfigOrError parseLoadBalancingPolicyConfig(Map rawLoadBalancingPolicyConfig) { - return ConfigOrError.fromError( - Status.INTERNAL.withDescription(getPolicyName() + " cannot be used from service config")); - } - - @Override - public LoadBalancer newLoadBalancer(Helper helper) { - LoadBalancerRegistry lbRegistry = this.lbRegistry; - if (lbRegistry == null) { - lbRegistry = LoadBalancerRegistry.getDefaultRegistry(); - } - return new ClusterResolverLoadBalancer(helper, lbRegistry); - } - - static final class ClusterResolverConfig { - // Cluster to be resolved. - final DiscoveryMechanism discoveryMechanism; - // GracefulSwitch configuration - final Object lbConfig; - private final boolean isHttp11ProxyAvailable; - - ClusterResolverConfig(DiscoveryMechanism discoveryMechanism, Object lbConfig, - boolean isHttp11ProxyAvailable) { - this.discoveryMechanism = checkNotNull(discoveryMechanism, "discoveryMechanism"); - this.lbConfig = checkNotNull(lbConfig, "lbConfig"); - this.isHttp11ProxyAvailable = isHttp11ProxyAvailable; - } - - boolean isHttp11ProxyAvailable() { - return isHttp11ProxyAvailable; - } - - @Override - public int hashCode() { - return Objects.hash(discoveryMechanism, lbConfig, isHttp11ProxyAvailable); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ClusterResolverConfig that = (ClusterResolverConfig) o; - return discoveryMechanism.equals(that.discoveryMechanism) - && lbConfig.equals(that.lbConfig) - && isHttp11ProxyAvailable == that.isHttp11ProxyAvailable; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("discoveryMechanism", discoveryMechanism) - .add("lbConfig", lbConfig) - .add("isHttp11ProxyAvailable", isHttp11ProxyAvailable) - .toString(); - } - - // Describes the mechanism for a specific cluster. - static final class DiscoveryMechanism { - // Name of the cluster to resolve. - final String cluster; - // Type of the cluster. - final Type type; - // Load reporting server info. Null if not enabled. - @Nullable - final ServerInfo lrsServerInfo; - // Cluster-level max concurrent request threshold. Null if not specified. - @Nullable - final Long maxConcurrentRequests; - // TLS context for connections to endpoints in the cluster. - @Nullable - final UpstreamTlsContext tlsContext; - // Resource name for resolving endpoints via EDS. Only valid for EDS clusters. - @Nullable - final String edsServiceName; - // Hostname for resolving endpoints via DNS. Only valid for LOGICAL_DNS clusters. - @Nullable - final String dnsHostName; - @Nullable - final OutlierDetection outlierDetection; - final Map filterMetadata; - @Nullable - final BackendMetricPropagation backendMetricPropagation; - - enum Type { - EDS, - LOGICAL_DNS, - } - - private DiscoveryMechanism(String cluster, Type type, @Nullable String edsServiceName, - @Nullable String dnsHostName, @Nullable ServerInfo lrsServerInfo, - @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext tlsContext, - Map filterMetadata, @Nullable OutlierDetection outlierDetection, - @Nullable BackendMetricPropagation backendMetricPropagation) { - this.cluster = checkNotNull(cluster, "cluster"); - this.type = checkNotNull(type, "type"); - this.edsServiceName = edsServiceName; - this.dnsHostName = dnsHostName; - this.lrsServerInfo = lrsServerInfo; - this.maxConcurrentRequests = maxConcurrentRequests; - this.tlsContext = tlsContext; - this.filterMetadata = ImmutableMap.copyOf(checkNotNull(filterMetadata, "filterMetadata")); - this.outlierDetection = outlierDetection; - this.backendMetricPropagation = backendMetricPropagation; - } - - static DiscoveryMechanism forEds(String cluster, @Nullable String edsServiceName, - @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, - @Nullable UpstreamTlsContext tlsContext, Map filterMetadata, - OutlierDetection outlierDetection, - @Nullable BackendMetricPropagation backendMetricPropagation) { - return new DiscoveryMechanism(cluster, Type.EDS, edsServiceName, - null, lrsServerInfo, maxConcurrentRequests, tlsContext, - filterMetadata, outlierDetection, backendMetricPropagation); - } - - static DiscoveryMechanism forLogicalDns(String cluster, String dnsHostName, - @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, - @Nullable UpstreamTlsContext tlsContext, Map filterMetadata, - @Nullable BackendMetricPropagation backendMetricPropagation) { - return new DiscoveryMechanism(cluster, Type.LOGICAL_DNS, null, dnsHostName, - lrsServerInfo, maxConcurrentRequests, tlsContext, filterMetadata, null, - backendMetricPropagation); - } - - @Override - public int hashCode() { - return Objects.hash(cluster, type, lrsServerInfo, maxConcurrentRequests, tlsContext, - edsServiceName, dnsHostName, filterMetadata, - outlierDetection, backendMetricPropagation); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DiscoveryMechanism that = (DiscoveryMechanism) o; - return cluster.equals(that.cluster) - && type == that.type - && Objects.equals(edsServiceName, that.edsServiceName) - && Objects.equals(dnsHostName, that.dnsHostName) - && Objects.equals(lrsServerInfo, that.lrsServerInfo) - && Objects.equals(maxConcurrentRequests, that.maxConcurrentRequests) - && Objects.equals(tlsContext, that.tlsContext) - && Objects.equals(filterMetadata, that.filterMetadata) - && Objects.equals(outlierDetection, that.outlierDetection); - } - - @Override - public String toString() { - MoreObjects.ToStringHelper toStringHelper = - MoreObjects.toStringHelper(this) - .add("cluster", cluster) - .add("type", type) - .add("edsServiceName", edsServiceName) - .add("dnsHostName", dnsHostName) - .add("lrsServerInfo", lrsServerInfo) - // Exclude tlsContext as its string representation is cumbersome. - .add("maxConcurrentRequests", maxConcurrentRequests) - .add("filterMetadata", filterMetadata) - // Exclude outlierDetection as its string representation is long. - ; - return toStringHelper.toString(); - } - } - } -} diff --git a/xds/src/main/java/io/grpc/xds/XdsLbPolicies.java b/xds/src/main/java/io/grpc/xds/XdsLbPolicies.java index dcca2fbfff3..ae5ac38b471 100644 --- a/xds/src/main/java/io/grpc/xds/XdsLbPolicies.java +++ b/xds/src/main/java/io/grpc/xds/XdsLbPolicies.java @@ -19,7 +19,6 @@ final class XdsLbPolicies { static final String CLUSTER_MANAGER_POLICY_NAME = "cluster_manager_experimental"; static final String CDS_POLICY_NAME = "cds_experimental"; - static final String CLUSTER_RESOLVER_POLICY_NAME = "cluster_resolver_experimental"; static final String PRIORITY_POLICY_NAME = "priority_experimental"; static final String CLUSTER_IMPL_POLICY_NAME = "cluster_impl_experimental"; static final String WEIGHTED_TARGET_POLICY_NAME = "weighted_target_experimental"; diff --git a/xds/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider b/xds/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider index e1c4d4aa427..04a2d9cf7a8 100644 --- a/xds/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider +++ b/xds/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider @@ -2,7 +2,6 @@ io.grpc.xds.CdsLoadBalancerProvider io.grpc.xds.PriorityLoadBalancerProvider io.grpc.xds.WeightedTargetLoadBalancerProvider io.grpc.xds.ClusterManagerLoadBalancerProvider -io.grpc.xds.ClusterResolverLoadBalancerProvider io.grpc.xds.ClusterImplLoadBalancerProvider io.grpc.xds.LeastRequestLoadBalancerProvider io.grpc.xds.RingHashLoadBalancerProvider diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java index c1d3322faa2..928520aded7 100644 --- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java +++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java @@ -17,7 +17,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; -import static io.grpc.xds.XdsLbPolicies.CLUSTER_RESOLVER_POLICY_NAME; +import static io.grpc.xds.XdsLbPolicies.CLUSTER_IMPL_POLICY_NAME; import static io.grpc.xds.XdsLbPolicies.PRIORITY_POLICY_NAME; import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS; import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_EDS; @@ -78,11 +78,7 @@ import io.grpc.testing.GrpcCleanupRule; import io.grpc.util.GracefulSwitchLoadBalancerAccessor; import io.grpc.xds.CdsLoadBalancerProvider.CdsConfig; -import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; -import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; -import io.grpc.xds.EnvoyServerProtoData.FailurePercentageEjection; -import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection; -import io.grpc.xds.client.Bootstrapper.ServerInfo; +import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; import io.grpc.xds.client.XdsClient; import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import java.util.ArrayList; @@ -139,7 +135,6 @@ public class CdsLoadBalancer2Test { .directExecutor() .build()), fakeClock); - private final ServerInfo lrsServerInfo = xdsClient.getBootstrapInfo().servers().get(0); private XdsDependencyManager xdsDepManager; @Mock @@ -151,8 +146,10 @@ public class CdsLoadBalancer2Test { @Before public void setUp() throws Exception { - lbRegistry.register(new FakeLoadBalancerProvider(CLUSTER_RESOLVER_POLICY_NAME)); + lbRegistry.register(new FakeLoadBalancerProvider(PRIORITY_POLICY_NAME)); + lbRegistry.register(new FakeLoadBalancerProvider(CLUSTER_IMPL_POLICY_NAME)); lbRegistry.register(new FakeLoadBalancerProvider("round_robin")); + lbRegistry.register(new FakeLoadBalancerProvider("outlier_detection_experimental")); lbRegistry.register( new FakeLoadBalancerProvider("ring_hash_experimental", new RingHashLoadBalancerProvider())); lbRegistry.register(new FakeLoadBalancerProvider("least_request_experimental", @@ -222,7 +219,7 @@ private void shutdownLoadBalancer() { } @Test - public void discoverTopLevelEdsCluster() { + public void discoverTopLevelCluster() { Cluster cluster = Cluster.newBuilder() .setName(CLUSTER) .setType(Cluster.DiscoveryType.EDS) @@ -250,64 +247,7 @@ public void discoverTopLevelEdsCluster() { verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(childBalancer.name).isEqualTo(CLUSTER_RESOLVER_POLICY_NAME); - ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanism).isEqualTo( - DiscoveryMechanism.forEds( - CLUSTER, EDS_SERVICE_NAME, lrsServerInfo, 100L, upstreamTlsContext, - Collections.emptyMap(), io.grpc.xds.EnvoyServerProtoData.OutlierDetection.create( - null, null, null, null, SuccessRateEjection.create(null, null, null, null), - FailurePercentageEjection.create(null, null, null, null)), null)); - assertThat( - GracefulSwitchLoadBalancerAccessor.getChildProvider(childLbConfig.lbConfig).getPolicyName()) - .isEqualTo("wrr_locality_experimental"); - } - - @Test - public void discoverTopLevelLogicalDnsCluster() { - Cluster cluster = Cluster.newBuilder() - .setName(CLUSTER) - .setType(Cluster.DiscoveryType.LOGICAL_DNS) - .setLoadAssignment(ClusterLoadAssignment.newBuilder() - .addEndpoints(LocalityLbEndpoints.newBuilder() - .addLbEndpoints(LbEndpoint.newBuilder() - .setEndpoint(Endpoint.newBuilder() - .setAddress(Address.newBuilder() - .setSocketAddress(SocketAddress.newBuilder() - .setAddress("dns.example.com") - .setPortValue(1111))))))) - .setEdsClusterConfig(Cluster.EdsClusterConfig.newBuilder() - .setServiceName(EDS_SERVICE_NAME) - .setEdsConfig(ConfigSource.newBuilder() - .setAds(AggregatedConfigSource.newBuilder()))) - .setLbPolicy(Cluster.LbPolicy.LEAST_REQUEST) - .setLrsServer(ConfigSource.newBuilder() - .setSelf(SelfConfigSource.getDefaultInstance())) - .setCircuitBreakers(CircuitBreakers.newBuilder() - .addThresholds(CircuitBreakers.Thresholds.newBuilder() - .setPriority(RoutingPriority.DEFAULT) - .setMaxRequests(UInt32Value.newBuilder().setValue(100)))) - .setTransportSocket(TransportSocket.newBuilder() - .setName("envoy.transport_sockets.tls") - .setTypedConfig(Any.pack(UpstreamTlsContext.newBuilder() - .setCommonTlsContext(upstreamTlsContext.getCommonTlsContext()) - .build()))) - .build(); - controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of(CLUSTER, cluster)); - startXdsDepManager(); - - verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); - assertThat(childBalancers).hasSize(1); - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(childBalancer.name).isEqualTo(CLUSTER_RESOLVER_POLICY_NAME); - ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanism).isEqualTo( - DiscoveryMechanism.forLogicalDns( - CLUSTER, "dns.example.com:1111", lrsServerInfo, 100L, upstreamTlsContext, - Collections.emptyMap(), null)); - assertThat( - GracefulSwitchLoadBalancerAccessor.getChildProvider(childLbConfig.lbConfig).getPolicyName()) - .isEqualTo("wrr_locality_experimental"); + assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); } @Test @@ -325,6 +265,7 @@ public void nonAggregateCluster_resourceNotExist_returnErrorPicker() { @Test public void nonAggregateCluster_resourceUpdate() { + lbRegistry.register(new PriorityLoadBalancerProvider()); Cluster cluster = EDS_CLUSTER.toBuilder() .setCircuitBreakers(CircuitBreakers.newBuilder() .addThresholds(CircuitBreakers.Thresholds.newBuilder() @@ -337,10 +278,9 @@ public void nonAggregateCluster_resourceUpdate() { verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanism).isEqualTo( - DiscoveryMechanism.forEds( - CLUSTER, EDS_SERVICE_NAME, null, 100L, null, Collections.emptyMap(), null, null)); + ClusterImplConfig childLbConfig = (ClusterImplConfig) childBalancer.config; + assertThat(childLbConfig.cluster).isEqualTo(CLUSTER); + assertThat(childLbConfig.maxConcurrentRequests).isEqualTo(100L); cluster = EDS_CLUSTER.toBuilder() .setCircuitBreakers(CircuitBreakers.newBuilder() @@ -352,24 +292,21 @@ public void nonAggregateCluster_resourceUpdate() { verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); childBalancer = Iterables.getOnlyElement(childBalancers); - childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanism).isEqualTo( - DiscoveryMechanism.forEds( - CLUSTER, EDS_SERVICE_NAME, null, 200L, null, Collections.emptyMap(), null, null)); + childLbConfig = (ClusterImplConfig) childBalancer.config; + assertThat(childLbConfig.maxConcurrentRequests).isEqualTo(200L); } @Test public void nonAggregateCluster_resourceRevoked() { + lbRegistry.register(new PriorityLoadBalancerProvider()); controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of(CLUSTER, EDS_CLUSTER)); startXdsDepManager(); verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanism).isEqualTo( - DiscoveryMechanism.forEds( - CLUSTER, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null, null)); + ClusterImplConfig childLbConfig = (ClusterImplConfig) childBalancer.config; + assertThat(childLbConfig.cluster).isEqualTo(CLUSTER); controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of()); @@ -398,10 +335,7 @@ public void dynamicCluster() { verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; - assertThat(childLbConfig.discoveryMechanism).isEqualTo( - DiscoveryMechanism.forEds( - clusterName, EDS_SERVICE_NAME, null, null, null, Collections.emptyMap(), null, null)); + assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); assertThat(this.lastXdsConfig.getClusters()).containsKey(clusterName); shutdownLoadBalancer(); @@ -410,7 +344,6 @@ public void dynamicCluster() { @Test public void discoverAggregateCluster_createsPriorityLbPolicy() { - lbRegistry.register(new FakeLoadBalancerProvider(PRIORITY_POLICY_NAME)); CdsLoadBalancerProvider cdsLoadBalancerProvider = new CdsLoadBalancerProvider(lbRegistry); lbRegistry.register(cdsLoadBalancerProvider); loadBalancer = (CdsLoadBalancer2) cdsLoadBalancerProvider.newLoadBalancer(helper); @@ -522,21 +455,17 @@ public void discoverAggregateCluster_testChildCdsLbPolicyParsing() { verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); assertThat(childBalancers).hasSize(2); - ClusterResolverConfig cluster1ResolverConfig = - (ClusterResolverConfig) childBalancers.get(0).config; - assertThat(cluster1ResolverConfig.discoveryMechanism.cluster) + ClusterImplConfig cluster1ImplConfig = + (ClusterImplConfig) childBalancers.get(0).config; + assertThat(cluster1ImplConfig.cluster) .isEqualTo("cluster-01.googleapis.com"); - assertThat(cluster1ResolverConfig.discoveryMechanism.type) - .isEqualTo(DiscoveryMechanism.Type.EDS); - assertThat(cluster1ResolverConfig.discoveryMechanism.edsServiceName) + assertThat(cluster1ImplConfig.edsServiceName) .isEqualTo("backend-service-1.googleapis.com"); - ClusterResolverConfig cluster2ResolverConfig = - (ClusterResolverConfig) childBalancers.get(1).config; - assertThat(cluster2ResolverConfig.discoveryMechanism.cluster) + ClusterImplConfig cluster2ImplConfig = + (ClusterImplConfig) childBalancers.get(1).config; + assertThat(cluster2ImplConfig.cluster) .isEqualTo("cluster-02.googleapis.com"); - assertThat(cluster2ResolverConfig.discoveryMechanism.type) - .isEqualTo(DiscoveryMechanism.Type.EDS); - assertThat(cluster2ResolverConfig.discoveryMechanism.edsServiceName) + assertThat(cluster2ImplConfig.edsServiceName) .isEqualTo("backend-service-1.googleapis.com"); } diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerProviderTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerProviderTest.java deleted file mode 100644 index a201ecfaa4b..00000000000 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerProviderTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.truth.Truth.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import io.grpc.ChannelLogger; -import io.grpc.LoadBalancer; -import io.grpc.LoadBalancer.Helper; -import io.grpc.LoadBalancerProvider; -import io.grpc.LoadBalancerRegistry; -import io.grpc.NameResolver; -import io.grpc.NameResolver.ServiceConfigParser; -import io.grpc.NameResolverRegistry; -import io.grpc.SynchronizationContext; -import io.grpc.internal.FakeClock; -import io.grpc.internal.GrpcUtil; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Tests for {@link ClusterResolverLoadBalancerProvider}. */ -@RunWith(JUnit4.class) -public class ClusterResolverLoadBalancerProviderTest { - - @Test - public void provided() { - LoadBalancerProvider provider = - LoadBalancerRegistry.getDefaultRegistry().getProvider( - XdsLbPolicies.CLUSTER_RESOLVER_POLICY_NAME); - assertThat(provider).isInstanceOf(ClusterResolverLoadBalancerProvider.class); - } - - @Test - public void providesLoadBalancer() { - Helper helper = mock(Helper.class); - - SynchronizationContext syncContext = new SynchronizationContext( - new Thread.UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread t, Throwable e) { - throw new AssertionError(e); - } - }); - FakeClock fakeClock = new FakeClock(); - NameResolverRegistry nsRegistry = new NameResolverRegistry(); - NameResolver.Args args = NameResolver.Args.newBuilder() - .setDefaultPort(8080) - .setProxyDetector(GrpcUtil.NOOP_PROXY_DETECTOR) - .setSynchronizationContext(syncContext) - .setServiceConfigParser(mock(ServiceConfigParser.class)) - .setChannelLogger(mock(ChannelLogger.class)) - .build(); - when(helper.getNameResolverRegistry()).thenReturn(nsRegistry); - when(helper.getNameResolverArgs()).thenReturn(args); - when(helper.getSynchronizationContext()).thenReturn(syncContext); - when(helper.getScheduledExecutorService()).thenReturn(fakeClock.getScheduledExecutorService()); - when(helper.getAuthority()).thenReturn("api.google.com"); - LoadBalancerProvider provider = new ClusterResolverLoadBalancerProvider(); - LoadBalancer loadBalancer = provider.newLoadBalancer(helper); - assertThat(loadBalancer).isInstanceOf(ClusterResolverLoadBalancer.class); - } -} diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index 5c710699a96..5e524f79596 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -36,7 +36,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; -import com.google.common.testing.EqualsTester; import com.google.protobuf.Any; import com.google.protobuf.Duration; import com.google.protobuf.UInt32Value; @@ -61,7 +60,6 @@ import io.grpc.ConnectivityState; import io.grpc.EquivalentAddressGroup; import io.grpc.HttpConnectProxiedSocketAddress; -import io.grpc.InsecureChannelCredentials; import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancer.PickResult; @@ -83,19 +81,13 @@ import io.grpc.internal.FakeClock; import io.grpc.internal.GrpcUtil; import io.grpc.testing.GrpcCleanupRule; -import io.grpc.util.GracefulSwitchLoadBalancer; import io.grpc.util.GracefulSwitchLoadBalancerAccessor; import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig; import io.grpc.util.OutlierDetectionLoadBalancerProvider; import io.grpc.xds.CdsLoadBalancerProvider.CdsConfig; import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; -import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; -import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; import io.grpc.xds.Endpoints.DropOverload; -import io.grpc.xds.EnvoyServerProtoData.FailurePercentageEjection; -import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; -import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; @@ -105,7 +97,6 @@ import io.grpc.xds.client.LoadStatsManager2; import io.grpc.xds.client.XdsClient; import io.grpc.xds.internal.XdsInternalAttributes; -import io.grpc.xds.internal.security.CommonTlsContextTestsUtil; import java.net.InetSocketAddress; import java.net.URI; import java.util.ArrayList; @@ -202,7 +193,6 @@ public void uncaughtException(Thread t, Throwable e) { @Before public void setUp() throws Exception { - lbRegistry.register(new ClusterResolverLoadBalancerProvider(lbRegistry)); lbRegistry.register(new RingHashLoadBalancerProvider()); lbRegistry.register(new WrrLocalityLoadBalancerProvider()); lbRegistry.register(new FakeLoadBalancerProvider(PRIORITY_POLICY_NAME)); @@ -750,37 +740,34 @@ public void cdsRevoked_handledDirectly() { } @Test - public void edsMissing_handledByChildPolicy() { + public void edsMissing_failsRpcs() { controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, ImmutableMap.of()); startXdsDepManager(); - assertThat(childBalancers).hasSize(1); // child LB policy created - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(childBalancer.upstreamError).isNotNull(); - assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(childBalancers).hasSize(0); // Graceful switch handles it, so no child policies yet + verify(helper).updateBalancingState( + eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); String expectedDescription = "Error retrieving EDS resource " + EDS_SERVICE_NAME + ": NOT_FOUND. Details: Timed out waiting for resource " + EDS_SERVICE_NAME + " from xDS server nodeID: node-id"; - assertThat(childBalancer.upstreamError.getDescription()).isEqualTo(expectedDescription); - assertThat(childBalancer.shutdown).isFalse(); + Status expectedError = Status.UNAVAILABLE.withDescription(expectedDescription); + assertPicker(pickerCaptor.getValue(), expectedError, null); } @Test - public void logicalDnsLookupFailed_handledByChildPolicy() { + public void logicalDnsLookupFailed_failsRpcs() { controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of( CLUSTER, LOGICAL_DNS_CLUSTER)); startXdsDepManager(new CdsConfig(CLUSTER), /* forwardTime= */ false); FakeNameResolver resolver = assertResolverCreated("/" + DNS_HOST_NAME + ":9000"); assertThat(childBalancers).isEmpty(); - resolver.deliverError(Status.UNAVAILABLE.withDescription("OH NO! Who would have guessed?")); + Status status = Status.UNAVAILABLE.withDescription("OH NO! Who would have guessed?"); + resolver.deliverError(status); - assertThat(childBalancers).hasSize(1); // child LB policy created - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(childBalancer.upstreamError).isNotNull(); - assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Status.Code.UNAVAILABLE); - assertThat(childBalancer.upstreamError.getDescription()) - .isEqualTo("OH NO! Who would have guessed?"); - assertThat(childBalancer.shutdown).isFalse(); + assertThat(childBalancers).hasSize(0); // Graceful switch handles it, so no child policies yet + verify(helper).updateBalancingState( + eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); + assertPicker(pickerCaptor.getValue(), status, null); } @Test @@ -873,13 +860,12 @@ public void handleEdsResource_noHealthyEndpoint() { EDS_SERVICE_NAME, clusterLoadAssignment)); startXdsDepManager(); - verify(helper, never()).updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), any()); - assertThat(childBalancers).hasSize(1); - FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); - assertThat(childBalancer.upstreamError).isNotNull(); - assertThat(childBalancer.upstreamError.getCode()).isEqualTo(Status.Code.UNAVAILABLE); - assertThat(childBalancer.upstreamError.getDescription()) - .isEqualTo("No usable endpoint from cluster: " + CLUSTER); + assertThat(childBalancers).hasSize(0); // Graceful switch handles it, so no child policies yet + verify(helper).updateBalancingState( + eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture()); + Status expectedStatus = Status.UNAVAILABLE + .withDescription("No usable endpoint from cluster: " + CLUSTER); + assertPicker(pickerCaptor.getValue(), expectedStatus, null); } @Test @@ -1016,50 +1002,6 @@ public void outlierDetection_fullConfig() { "wrr_locality_experimental"); } - @Test - public void config_equalsTester() { - ServerInfo lrsServerInfo = - ServerInfo.create("lrs.googleapis.com", InsecureChannelCredentials.create()); - UpstreamTlsContext tlsContext = - CommonTlsContextTestsUtil.buildUpstreamTlsContext( - "google_cloud_private_spiffe", true); - DiscoveryMechanism edsDiscoveryMechanism1 = - DiscoveryMechanism.forEds(CLUSTER, EDS_SERVICE_NAME, lrsServerInfo, 100L, tlsContext, - Collections.emptyMap(), null, null); - io.grpc.xds.EnvoyServerProtoData.OutlierDetection outlierDetection = - io.grpc.xds.EnvoyServerProtoData.OutlierDetection.create( - 100L, 100L, 100L, 100, SuccessRateEjection.create(100, 100, 100, 100), - FailurePercentageEjection.create(100, 100, 100, 100)); - DiscoveryMechanism edsDiscoveryMechanismWithOutlierDetection = - DiscoveryMechanism.forEds(CLUSTER, EDS_SERVICE_NAME, lrsServerInfo, 100L, tlsContext, - Collections.emptyMap(), outlierDetection, null); - Object roundRobin = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - new FakeLoadBalancerProvider("wrr_locality_experimental"), new WrrLocalityConfig( - GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - new FakeLoadBalancerProvider("round_robin"), null))); - Object leastRequest = GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - new FakeLoadBalancerProvider("wrr_locality_experimental"), new WrrLocalityConfig( - GracefulSwitchLoadBalancer.createLoadBalancingPolicyConfig( - new FakeLoadBalancerProvider("least_request_experimental"), - new LeastRequestConfig(3)))); - - new EqualsTester() - .addEqualityGroup( - new ClusterResolverConfig( - edsDiscoveryMechanism1, leastRequest, false), - new ClusterResolverConfig( - edsDiscoveryMechanism1, leastRequest, false)) - .addEqualityGroup(new ClusterResolverConfig( - edsDiscoveryMechanism1, roundRobin, false)) - .addEqualityGroup(new ClusterResolverConfig( - edsDiscoveryMechanism1, leastRequest, true)) - .addEqualityGroup(new ClusterResolverConfig( - edsDiscoveryMechanismWithOutlierDetection, - leastRequest, - false)) - .testEquals(); - } - private void startXdsDepManager() { startXdsDepManager(new CdsConfig(CLUSTER)); } @@ -1262,7 +1204,6 @@ private final class FakeLoadBalancer extends LoadBalancer { private final Helper helper; private List addresses; private Object config; - private Status upstreamError; private boolean shutdown; FakeLoadBalancer(String name, Helper helper) { @@ -1279,7 +1220,6 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { @Override public void handleNameResolutionError(Status error) { - upstreamError = error; } @Override From a535ed799387e41b1edb44b59907af99e770259b Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Tue, 6 Jan 2026 16:50:22 -0800 Subject: [PATCH 511/591] Catch Errors when calling complex parsing code When we have involved parsing/validation logic, it would be easy to have bugs. There's little reason we can't handle those bugs, as the logic shouldn't be modifying global state. While the current config is bad or trigger a bug, we want to keep working until we get good/working config again. For XDS, we do have the problem of losing the exception's stack trace. We could consider increasing the log level, but we could also consider propagating the error to the listener. Let's skip figuring out that exact behavior for now, and just move a step in the right direction. --- .../main/java/io/grpc/internal/ScParser.java | 11 ++ .../grpc/internal/ManagedChannelImplTest.java | 22 ++++ .../io/grpc/xds/client/XdsResourceType.java | 14 ++ .../grpc/xds/GrpcXdsClientImplTestBase.java | 124 ++++++++++++++++++ 4 files changed, 171 insertions(+) diff --git a/core/src/main/java/io/grpc/internal/ScParser.java b/core/src/main/java/io/grpc/internal/ScParser.java index 16a241f41f6..71d6d33877f 100644 --- a/core/src/main/java/io/grpc/internal/ScParser.java +++ b/core/src/main/java/io/grpc/internal/ScParser.java @@ -69,8 +69,19 @@ public ConfigOrError parseServiceConfig(Map rawServiceConfig) { maxHedgedAttemptsLimit, loadBalancingPolicySelection)); } catch (RuntimeException e) { + // TODO(ejona): We really don't want parsers throwing exceptions; they should return an error. + // However, right now ManagedChannelServiceConfig itself uses exceptions like + // ClassCastException. We should handle those with a graceful return within + // ManagedChannelServiceConfig and then get rid of this case. Then all exceptions are + // "unexpected" and the INTERNAL status code makes it clear a bug needs to be fixed. return ConfigOrError.fromError( Status.UNKNOWN.withDescription("failed to parse service config").withCause(e)); + } catch (Throwable t) { + // Even catch Errors, since broken config parsing could trigger AssertionError, + // StackOverflowError, and other errors we can reasonably safely recover. Since the config + // could be untrusted, we want to error on the side of recovering. + return ConfigOrError.fromError( + Status.INTERNAL.withDescription("Unexpected error parsing service config").withCause(t)); } } } diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 30b53e99946..ae224af27e1 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -3942,6 +3942,28 @@ public void nameResolverHelper_badConfigFails() { assertThat(coe.getError().getCause()).isInstanceOf(ClassCastException.class); } + @Test + public void nameResolverHelper_badParser_failsGracefully() { + boolean retryEnabled = false; + int maxRetryAttemptsLimit = 2; + int maxHedgedAttemptsLimit = 3; + + Throwable t = new Error("really poor config parser"); + when(mockLoadBalancerProvider.parseLoadBalancingPolicyConfig(any())).thenThrow(t); + ScParser parser = new ScParser( + retryEnabled, + maxRetryAttemptsLimit, + maxHedgedAttemptsLimit, + mockLoadBalancerProvider); + + ConfigOrError coe = parser.parseServiceConfig(ImmutableMap.of()); + + assertThat(coe.getError()).isNotNull(); + assertThat(coe.getError().getCode()).isEqualTo(Code.INTERNAL); + assertThat(coe.getError().getDescription()).contains("Unexpected error parsing service config"); + assertThat(coe.getError().getCause()).isSameInstanceAs(t); + } + @Test public void nameResolverHelper_noConfigChosen() { boolean retryEnabled = false; diff --git a/xds/src/main/java/io/grpc/xds/client/XdsResourceType.java b/xds/src/main/java/io/grpc/xds/client/XdsResourceType.java index ccb622ff168..4d6e75b1809 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsResourceType.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsResourceType.java @@ -33,10 +33,14 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.Nullable; @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10847") public abstract class XdsResourceType { + private static final Logger log = Logger.getLogger(XdsResourceType.class.getName()); + static final String TYPE_URL_RESOURCE = "type.googleapis.com/envoy.service.discovery.v3.Resource"; protected static final String TRANSPORT_SOCKET_NAME_TLS = "envoy.transport_sockets.tls"; @@ -176,6 +180,16 @@ ValidatedResourceUpdate parse(Args args, List resources) { typeName(), unpackedClassName().getSimpleName(), cname, e.getMessage())); invalidResources.add(cname); continue; + } catch (Throwable t) { + log.log(Level.FINE, "Unexpected error in doParse()", t); + String errorMessage = t.getClass().getSimpleName(); + if (t.getMessage() != null) { + errorMessage = errorMessage + ": " + t.getMessage(); + } + errors.add(String.format("%s response '%s' unexpected error: %s", + typeName(), cname, errorMessage)); + invalidResources.add(cname); + continue; } // Resource parsed successfully. diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 887923b169b..af55e572811 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static io.grpc.StatusMatcher.statusHasCode; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -42,6 +43,7 @@ import com.google.protobuf.Duration; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; +import com.google.protobuf.StringValue; import com.google.protobuf.UInt32Value; import com.google.protobuf.util.Durations; import io.envoyproxy.envoy.config.cluster.v3.OutlierDetection; @@ -59,6 +61,7 @@ import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.StatusOr; +import io.grpc.StatusOrMatcher; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.internal.BackoffPolicy; @@ -111,6 +114,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Queue; import java.util.concurrent.BlockingDeque; import java.util.concurrent.CountDownLatch; @@ -278,6 +282,8 @@ public long currentTimeNanos() { @Mock private ResourceWatcher edsResourceWatcher; @Mock + private ResourceWatcher stringResourceWatcher; + @Mock private XdsClientMetricReporter xdsClientMetricReporter; @Mock private ServerConnectionCallback serverConnectionCallback; @@ -667,6 +673,58 @@ private void verifyServerConnection(int times, boolean isConnected, String xdsSe eq(xdsServer)); } + @Test + public void doParse_returnsSuccessfully() { + XdsStringResource resourceType = new XdsStringResource(); + xdsClient.watchXdsResource( + resourceType, "resource1", stringResourceWatcher, MoreExecutors.directExecutor()); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + + Any resource = Any.pack(StringValue.newBuilder().setValue("resource1").build()); + call.sendResponse(resourceType, resource, VERSION_1, "0000"); + verify(stringResourceWatcher).onResourceChanged(argThat(StatusOrMatcher.hasValue( + (StringUpdate arg) -> new StringUpdate("resource1").equals(arg)))); + } + + @Test + public void doParse_throwsResourceInvalidException_resourceInvalid() { + XdsStringResource resourceType = new XdsStringResource() { + @Override + protected StringUpdate doParse(Args args, Message unpackedMessage) + throws ResourceInvalidException { + throw new ResourceInvalidException("some bad input"); + } + }; + xdsClient.watchXdsResource( + resourceType, "resource1", stringResourceWatcher, MoreExecutors.directExecutor()); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + + Any resource = Any.pack(StringValue.newBuilder().setValue("resource1").build()); + call.sendResponse(resourceType, resource, VERSION_1, "0000"); + verify(stringResourceWatcher).onResourceChanged(argThat(StatusOrMatcher.hasStatus( + statusHasCode(Status.Code.UNAVAILABLE) + .andDescriptionContains("validation error: some bad input")))); + } + + @Test + public void doParse_throwsError_resourceInvalid() throws Exception { + XdsStringResource resourceType = new XdsStringResource() { + @Override + protected StringUpdate doParse(Args args, Message unpackedMessage) { + throw new AssertionError("something bad happened"); + } + }; + xdsClient.watchXdsResource( + resourceType, "resource1", stringResourceWatcher, MoreExecutors.directExecutor()); + DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); + + Any resource = Any.pack(StringValue.newBuilder().setValue("resource1").build()); + call.sendResponse(resourceType, resource, VERSION_1, "0000"); + verify(stringResourceWatcher).onResourceChanged(argThat(StatusOrMatcher.hasStatus( + statusHasCode(Status.Code.UNAVAILABLE) + .andDescriptionContains("unexpected error: AssertionError: something bad happened")))); + } + @Test public void ldsResourceNotFound() { DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE, @@ -5318,4 +5376,70 @@ protected abstract Message buildHttpConnectionManagerFilter( protected abstract Message buildTerminalFilter(); } + + private static class XdsStringResource extends XdsResourceType { + @Override + @SuppressWarnings("unchecked") + protected Class unpackedClassName() { + return StringValue.class; + } + + @Override + public String typeName() { + return "EMPTY"; + } + + @Override + public String typeUrl() { + return "type.googleapis.com/google.protobuf.StringValue"; + } + + @Override + public boolean shouldRetrieveResourceKeysForArgs() { + return false; + } + + @Override + protected boolean isFullStateOfTheWorld() { + return false; + } + + @Override + @Nullable + protected String extractResourceName(Message unpackedResource) { + if (!(unpackedResource instanceof StringValue)) { + return null; + } + return ((StringValue) unpackedResource).getValue(); + } + + @Override + protected StringUpdate doParse(Args args, Message unpackedMessage) + throws ResourceInvalidException { + return new StringUpdate(((StringValue) unpackedMessage).getValue()); + } + } + + private static final class StringUpdate implements ResourceUpdate { + @SuppressWarnings("UnusedVariable") + public final String value; + + public StringUpdate(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof StringUpdate)) { + return false; + } + StringUpdate that = (StringUpdate) o; + return Objects.equals(this.value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + } } From f65127cf7c861a1b7cf7411ef48df6f9116d6bc3 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Fri, 9 Jan 2026 05:58:11 -0800 Subject: [PATCH 512/591] api: Add RFC 3986 support to DnsNameResolverProvider (#12602) Accept both absolute (e.g. `dns:///hostname`) and rootless (e.g. `dns:hostname`) paths as specified by https://github.com/grpc/grpc/blob/master/doc/naming.md and matching the behavior of grpc core and grpc-go. --- api/src/main/java/io/grpc/Uri.java | 47 +++++++++++++ api/src/test/java/io/grpc/UriTest.java | 30 +++++++++ .../internal/DnsNameResolverProvider.java | 33 ++++++++-- .../internal/DnsNameResolverProviderTest.java | 66 ++++++++++++++++--- 4 files changed, 163 insertions(+), 13 deletions(-) diff --git a/api/src/main/java/io/grpc/Uri.java b/api/src/main/java/io/grpc/Uri.java index 0ef6212d35a..3034211752b 100644 --- a/api/src/main/java/io/grpc/Uri.java +++ b/api/src/main/java/io/grpc/Uri.java @@ -481,6 +481,15 @@ public String getPath() { *

    Prefer this method over {@link #getPath()} because it preserves the distinction between * segment separators and literal '/'s within a path segment. * + *

    A trailing '/' delimiter in the path results in the empty string as the last element in the + * returned list. For example, file://localhost/foo/bar/ has path segments + * ["foo", "bar", ""] + * + *

    A leading '/' delimiter cannot be detected using this method. For example, both + * dns:example.com and dns:///example.com have the same list of path segments: + * ["example.com"]. Use {@link #isPathAbsolute()} or {@link #isPathRootless()} to + * distinguish these cases. + * *

    The returned list is immutable. */ public List getPathSegments() { @@ -490,6 +499,44 @@ public List getPathSegments() { return segmentsBuilder.build(); } + /** + * Returns true iff this URI's path component starts with a path segment (rather than the '/' + * segment delimiter). + * + *

    The path of an RFC 3986 URI is either empty, absolute (starts with the '/' segment + * delimiter) or rootless (starts with a path segment). For example, tel:+1-206-555-1212 + * , mailto:me@example.com and urn:isbn:978-1492082798 all have + * rootless paths. mailto:%2Fdev%2Fnull@example.com is also rootless because its + * percent-encoded slashes are not segment delimiters but rather part of the first and only path + * segment. + * + *

    Contrast rootless paths with absolute ones (see {@link #isPathAbsolute()}. + */ + public boolean isPathRootless() { + return !path.isEmpty() && !path.startsWith("/"); + } + + /** + * Returns true iff this URI's path component starts with the '/' segment delimiter (rather than a + * path segment). + * + *

    The path of an RFC 3986 URI is either empty, absolute (starts with the '/' segment + * delimiter) or rootless (starts with a path segment). For example, file:///resume.txt + * , file:/resume.txt and file://localhost/ all have absolute + * paths while tel:+1-206-555-1212's path is not absolute. + * mailto:%2Fdev%2Fnull@example.com is also not absolute because its percent-encoded + * slashes are not segment delimiters but rather part of the first and only path segment. + * + *

    Contrast absolute paths with rootless ones (see {@link #isPathRootless()}. + * + *

    NB: The term "absolute" has two different meanings in RFC 3986 which are easily confused. + * This method tests for a property of this URI's path component. Contrast with {@link + * #isAbsolute()} which tests the URI itself for a different property. + */ + public boolean isPathAbsolute() { + return path.startsWith("/"); + } + /** * Returns the path component of this URI in its originally parsed, possibly percent-encoded form. */ diff --git a/api/src/test/java/io/grpc/UriTest.java b/api/src/test/java/io/grpc/UriTest.java index 12fc9813b60..e34319e8910 100644 --- a/api/src/test/java/io/grpc/UriTest.java +++ b/api/src/test/java/io/grpc/UriTest.java @@ -46,6 +46,8 @@ public void parse_allComponents() throws URISyntaxException { assertThat(uri.getFragment()).isEqualTo("fragment"); assertThat(uri.toString()).isEqualTo("scheme://user@host:0443/path?query#fragment"); assertThat(uri.isAbsolute()).isFalse(); // Has a fragment. + assertThat(uri.isPathAbsolute()).isTrue(); + assertThat(uri.isPathRootless()).isFalse(); } @Test @@ -127,6 +129,8 @@ public void parse_emptyPathWithAuthority() throws URISyntaxException { assertThat(uri.getFragment()).isNull(); assertThat(uri.toString()).isEqualTo("scheme://authority"); assertThat(uri.isAbsolute()).isTrue(); + assertThat(uri.isPathAbsolute()).isFalse(); + assertThat(uri.isPathRootless()).isFalse(); } @Test @@ -139,6 +143,8 @@ public void parse_rootless() throws URISyntaxException { assertThat(uri.getFragment()).isNull(); assertThat(uri.toString()).isEqualTo("mailto:ceo@company.com?subject=raise"); assertThat(uri.isAbsolute()).isTrue(); + assertThat(uri.isPathAbsolute()).isFalse(); + assertThat(uri.isPathRootless()).isTrue(); } @Test @@ -151,6 +157,8 @@ public void parse_emptyPath() throws URISyntaxException { assertThat(uri.getFragment()).isNull(); assertThat(uri.toString()).isEqualTo("scheme:"); assertThat(uri.isAbsolute()).isTrue(); + assertThat(uri.isPathAbsolute()).isFalse(); + assertThat(uri.isPathRootless()).isFalse(); } @Test @@ -348,12 +356,34 @@ public void parse_onePathSegment_trailingSlash() throws URISyntaxException { assertThat(uri.getPathSegments()).containsExactly("foo", ""); } + @Test + public void parse_onePathSegment_rootless() throws URISyntaxException { + Uri uri = Uri.create("dns:www.example.com"); + assertThat(uri.getPathSegments()).containsExactly("www.example.com"); + assertThat(uri.isPathAbsolute()).isFalse(); + assertThat(uri.isPathRootless()).isTrue(); + } + @Test public void parse_twoPathSegments() throws URISyntaxException { Uri uri = Uri.create("file:/foo/bar"); assertThat(uri.getPathSegments()).containsExactly("foo", "bar"); } + @Test + public void parse_twoPathSegments_rootless() throws URISyntaxException { + Uri uri = Uri.create("file:foo/bar"); + assertThat(uri.getPathSegments()).containsExactly("foo", "bar"); + } + + @Test + public void parse_percentEncodedPathSegment_rootless() throws URISyntaxException { + Uri uri = Uri.create("mailto:%2Fdev%2Fnull@example.com"); + assertThat(uri.getPathSegments()).containsExactly("/dev/null@example.com"); + assertThat(uri.isPathAbsolute()).isFalse(); + assertThat(uri.isPathRootless()).isTrue(); + } + @Test public void toString_percentEncoding() throws URISyntaxException { Uri uri = diff --git a/core/src/main/java/io/grpc/internal/DnsNameResolverProvider.java b/core/src/main/java/io/grpc/internal/DnsNameResolverProvider.java index c977fbb0cca..16edf767901 100644 --- a/core/src/main/java/io/grpc/internal/DnsNameResolverProvider.java +++ b/core/src/main/java/io/grpc/internal/DnsNameResolverProvider.java @@ -21,25 +21,30 @@ import io.grpc.InternalServiceProviders; import io.grpc.NameResolver; import io.grpc.NameResolverProvider; +import io.grpc.Uri; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; import java.util.Collection; import java.util.Collections; +import java.util.List; /** * A provider for {@link DnsNameResolver}. * *

    It resolves a target URI whose scheme is {@code "dns"}. The (optional) authority of the target - * URI is reserved for the address of alternative DNS server (not implemented yet). The path of the - * target URI, excluding the leading slash {@code '/'}, is treated as the host name and the optional - * port to be resolved by DNS. Example target URIs: + * URI is reserved for the address of alternative DNS server (not implemented yet). The first path + * segment of the hierarchical target URI is interpreted as an RFC 2396 "server-based" authority and + * used as the "service authority" of the resulting {@link NameResolver}. The "host" part of this + * authority is the name to be resolved by DNS. The "port" part of this authority (if present) will + * become the port number for all {@link InetSocketAddress} produced by this resolver. For example: * *

      *
    • {@code "dns:///foo.googleapis.com:8080"} (using default DNS)
    • *
    • {@code "dns://8.8.8.8/foo.googleapis.com:8080"} (using alternative DNS (not implemented * yet))
    • - *
    • {@code "dns:///foo.googleapis.com"} (without port)
    • + *
    • {@code "dns:///foo.googleapis.com"} (output addresses will have port {@link + * NameResolver.Args#getDefaultPort()})
    • *
    */ public final class DnsNameResolverProvider extends NameResolverProvider { @@ -51,6 +56,7 @@ public final class DnsNameResolverProvider extends NameResolverProvider { @Override public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) { + // TODO(jdcormie): Remove once RFC 3986 migration is complete. if (SCHEME.equals(targetUri.getScheme())) { String targetPath = Preconditions.checkNotNull(targetUri.getPath(), "targetPath"); Preconditions.checkArgument(targetPath.startsWith("/"), @@ -68,6 +74,25 @@ public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) { } } + @Override + public NameResolver newNameResolver(Uri targetUri, final NameResolver.Args args) { + if (SCHEME.equals(targetUri.getScheme())) { + List pathSegments = targetUri.getPathSegments(); + Preconditions.checkArgument(!pathSegments.isEmpty(), + "expected 1 path segment in target %s but found %s", targetUri, pathSegments); + String domainNameToResolve = pathSegments.get(0); + return new DnsNameResolver( + targetUri.getAuthority(), + domainNameToResolve, + args, + GrpcUtil.SHARED_CHANNEL_EXECUTOR, + Stopwatch.createUnstarted(), + IS_ANDROID); + } else { + return null; + } + } + @Override public String getDefaultScheme() { return SCHEME; diff --git a/core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java b/core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java index aff10ce9337..fabecea0bad 100644 --- a/core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java +++ b/core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java @@ -16,8 +16,8 @@ package io.grpc.internal; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -25,16 +25,27 @@ import io.grpc.NameResolver; import io.grpc.NameResolver.ServiceConfigParser; import io.grpc.SynchronizationContext; +import io.grpc.Uri; import java.net.URI; +import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; /** Unit tests for {@link DnsNameResolverProvider}. */ -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class DnsNameResolverProviderTest { private final FakeClock fakeClock = new FakeClock(); + @Parameters(name = "enableRfc3986UrisParam={0}") + public static Iterable data() { + return Arrays.asList(new Object[][] {{true}, {false}}); + } + + @Parameter public boolean enableRfc3986UrisParam; + private final SynchronizationContext syncContext = new SynchronizationContext( new Thread.UncaughtExceptionHandler() { @Override @@ -59,10 +70,47 @@ public void isAvailable() { } @Test - public void newNameResolver() { - assertSame(DnsNameResolver.class, - provider.newNameResolver(URI.create("dns:///localhost:443"), args).getClass()); - assertNull( - provider.newNameResolver(URI.create("notdns:///localhost:443"), args)); + public void newNameResolver_acceptsHostAndPort() { + NameResolver nameResolver = newNameResolver("dns:///localhost:443", args); + assertThat(nameResolver).isNotNull(); + assertThat(nameResolver.getClass()).isSameInstanceAs(DnsNameResolver.class); + assertThat(nameResolver.getServiceAuthority()).isEqualTo("localhost:443"); + } + + @Test + public void newNameResolver_acceptsRootless() { + assume().that(enableRfc3986UrisParam).isTrue(); + NameResolver nameResolver = newNameResolver("dns:localhost:443", args); + assertThat(nameResolver).isNotNull(); + assertThat(nameResolver.getClass()).isSameInstanceAs(DnsNameResolver.class); + assertThat(nameResolver.getServiceAuthority()).isEqualTo("localhost:443"); + } + + @Test + public void newNameResolver_rejectsNonDnsScheme() { + NameResolver nameResolver = newNameResolver("notdns:///localhost:443", args); + assertThat(nameResolver).isNull(); + } + + @Test + public void newNameResolver_toleratesTrailingPathSegments() { + NameResolver nameResolver = newNameResolver("dns:///foo.googleapis.com/ig/nor/ed", args); + assertThat(nameResolver).isNotNull(); + assertThat(nameResolver.getClass()).isSameInstanceAs(DnsNameResolver.class); + assertThat(nameResolver.getServiceAuthority()).isEqualTo("foo.googleapis.com"); + } + + @Test + public void newNameResolver_toleratesAuthority() { + NameResolver nameResolver = newNameResolver("dns://8.8.8.8/foo.googleapis.com", args); + assertThat(nameResolver).isNotNull(); + assertThat(nameResolver.getClass()).isSameInstanceAs(DnsNameResolver.class); + assertThat(nameResolver.getServiceAuthority()).isEqualTo("foo.googleapis.com"); + } + + private NameResolver newNameResolver(String uriString, NameResolver.Args args) { + return enableRfc3986UrisParam + ? provider.newNameResolver(Uri.create(uriString), args) + : provider.newNameResolver(URI.create(uriString), args); } } From c5f5ee0e994f76d4edac48ea17b52a0f918af822 Mon Sep 17 00:00:00 2001 From: Kim Jin Young Date: Fri, 9 Jan 2026 23:16:41 +0900 Subject: [PATCH 513/591] opentelemetry: Add target attribute filter for metrics (#12587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce an optional Predicate targetAttributeFilter to control how grpc.target is recorded in OpenTelemetry client metrics. When a filter is provided, targets rejected by the predicate are normalized to "other" to reduce grpc.target metric cardinality, while accepted targets are recorded as-is. If no filter is set, existing behavior is preserved. This change adds a new Builder API on GrpcOpenTelemetry to allow applications to configure the filter. Tests verify both the Builder wiring and the target normalization behavior. This is an optional API; annotation (e.g., experimental) can be added per maintainer guidance. Refs #12322 Related: gRFC A109 – Target Attribute Filter for OpenTelemetry Metrics https://github.com/grpc/proposal/pull/528 --- opentelemetry/build.gradle | 3 +- .../grpc/opentelemetry/GrpcOpenTelemetry.java | 40 ++++- .../OpenTelemetryMetricsModule.java | 30 +++- .../opentelemetry/GrpcOpenTelemetryTest.java | 13 ++ .../OpenTelemetryMetricsModuleTest.java | 138 ++++++++++++++++++ 5 files changed, 221 insertions(+), 3 deletions(-) diff --git a/opentelemetry/build.gradle b/opentelemetry/build.gradle index c0ec7b73b5a..594686294f0 100644 --- a/opentelemetry/build.gradle +++ b/opentelemetry/build.gradle @@ -12,7 +12,8 @@ dependencies { implementation libraries.guava, project(':grpc-core'), libraries.opentelemetry.api, - libraries.auto.value.annotations + libraries.auto.value.annotations, + libraries.animalsniffer.annotations testImplementation project(':grpc-testing'), project(':grpc-testing-proto'), diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java index 4341b27daa4..6904340ac74 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/GrpcOpenTelemetry.java @@ -48,6 +48,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Predicate; +import javax.annotation.Nullable; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; /** * The entrypoint for OpenTelemetry metrics functionality in gRPC. @@ -97,7 +100,8 @@ private GrpcOpenTelemetry(Builder builder) { this.resource = createMetricInstruments(meter, enableMetrics, disableDefault); this.optionalLabels = ImmutableList.copyOf(builder.optionalLabels); this.openTelemetryMetricsModule = new OpenTelemetryMetricsModule( - STOPWATCH_SUPPLIER, resource, optionalLabels, builder.plugins); + STOPWATCH_SUPPLIER, resource, optionalLabels, builder.plugins, + builder.targetFilter); this.openTelemetryTracingModule = new OpenTelemetryTracingModule(openTelemetrySdk); this.sink = new OpenTelemetryMetricSink(meter, enableMetrics, disableDefault, optionalLabels); } @@ -141,6 +145,11 @@ Tracer getTracer() { return this.openTelemetryTracingModule.getTracer(); } + @VisibleForTesting + TargetFilter getTargetAttributeFilter() { + return this.openTelemetryMetricsModule.getTargetAttributeFilter(); + } + /** * Registers GrpcOpenTelemetry globally, applying its configuration to all subsequently created * gRPC channels and servers. @@ -349,6 +358,13 @@ static boolean isMetricEnabled(String metricName, Map enableMet && !disableDefault; } + /** + * Internal interface to avoid storing a {@link java.util.function.Predicate} directly, ensuring + * compatibility with Android devices (API level < 24) that do not use library desugaring. + */ + interface TargetFilter { + boolean test(String target); + } /** * Builder for configuring {@link GrpcOpenTelemetry}. @@ -359,6 +375,8 @@ public static class Builder { private final Collection optionalLabels = new ArrayList<>(); private final Map enableMetrics = new HashMap<>(); private boolean disableAll; + @Nullable + private TargetFilter targetFilter; private Builder() {} @@ -421,6 +439,26 @@ Builder enableTracing(boolean enable) { return this; } + /** + * Sets an optional filter to control recording of the {@code grpc.target} metric + * attribute. + * + *

    If the predicate returns {@code true}, the original target is recorded. Otherwise, + * the target is recorded as {@code "other"} to limit metric cardinality. + * + *

    If unset, all targets are recorded as-is. + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12595") + @IgnoreJRERequirement + public Builder targetAttributeFilter(@Nullable Predicate filter) { + if (filter == null) { + this.targetFilter = null; + } else { + this.targetFilter = filter::test; + } + return this; + } + /** * Returns a new {@link GrpcOpenTelemetry} built with the configuration of this {@link * Builder}. diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java index 3e5137e0034..b05884305dc 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricsModule.java @@ -45,6 +45,7 @@ import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.StreamTracer; +import io.grpc.opentelemetry.GrpcOpenTelemetry.TargetFilter; import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; @@ -68,6 +69,10 @@ * tracer. It's the tracer that reports per-attempt stats, and the factory that reports the stats * of the overall RPC, such as RETRIES_PER_CALL, to OpenTelemetry. * + *

    This module optionally applies a target attribute filter to limit the cardinality of + * the {@code grpc.target} attribute in client-side metrics by mapping disallowed targets + * to a stable placeholder value. + * *

    On the server-side, there is only one ServerStream per each ServerCall, and ServerStream * starts earlier than the ServerCall. Therefore, only one tracer is created per stream/call, and * it's the tracer that reports the summary to OpenTelemetry. @@ -95,15 +100,30 @@ final class OpenTelemetryMetricsModule { private final boolean localityEnabled; private final boolean backendServiceEnabled; private final ImmutableList plugins; + @Nullable + private final TargetFilter targetAttributeFilter; OpenTelemetryMetricsModule(Supplier stopwatchSupplier, OpenTelemetryMetricsResource resource, Collection optionalLabels, List plugins) { + this(stopwatchSupplier, resource, optionalLabels, plugins, null); + } + + OpenTelemetryMetricsModule(Supplier stopwatchSupplier, + OpenTelemetryMetricsResource resource, + Collection optionalLabels, List plugins, + @Nullable TargetFilter targetAttributeFilter) { this.resource = checkNotNull(resource, "resource"); this.stopwatchSupplier = checkNotNull(stopwatchSupplier, "stopwatchSupplier"); this.localityEnabled = optionalLabels.contains(LOCALITY_KEY.getKey()); this.backendServiceEnabled = optionalLabels.contains(BACKEND_SERVICE_KEY.getKey()); this.plugins = ImmutableList.copyOf(plugins); + this.targetAttributeFilter = targetAttributeFilter; + } + + @VisibleForTesting + TargetFilter getTargetAttributeFilter() { + return targetAttributeFilter; } /** @@ -124,7 +144,15 @@ ClientInterceptor getClientInterceptor(String target) { pluginBuilder.add(plugin); } } - return new MetricsClientInterceptor(target, pluginBuilder.build()); + String filteredTarget = recordTarget(target); + return new MetricsClientInterceptor(filteredTarget, pluginBuilder.build()); + } + + String recordTarget(String target) { + if (targetAttributeFilter == null || target == null) { + return target; + } + return targetAttributeFilter.test(target) ? target : "other"; } static String recordMethodName(String fullMethodName, boolean isGeneratedMethod) { diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/GrpcOpenTelemetryTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/GrpcOpenTelemetryTest.java index 1ae7b755a48..16cb02c61c2 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/GrpcOpenTelemetryTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/GrpcOpenTelemetryTest.java @@ -29,6 +29,7 @@ import io.grpc.MetricSink; import io.grpc.ServerBuilder; import io.grpc.internal.GrpcUtil; +import io.grpc.opentelemetry.GrpcOpenTelemetry.TargetFilter; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.metrics.SdkMeterProvider; @@ -130,6 +131,18 @@ public void builderDefaults() { ); } + @Test + public void builderTargetAttributeFilter() { + GrpcOpenTelemetry module = GrpcOpenTelemetry.newBuilder() + .targetAttributeFilter(t -> t.contains("allowed.com")) + .build(); + + TargetFilter internalFilter = module.getTargetAttributeFilter(); + + assertThat(internalFilter.test("allowed.com")).isTrue(); + assertThat(internalFilter.test("example.com")).isFalse(); + } + @Test public void enableDisableMetrics() { GrpcOpenTelemetry.Builder builder = GrpcOpenTelemetry.newBuilder(); diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java index 58759294fca..391f94cefea 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricsModuleTest.java @@ -51,6 +51,7 @@ import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.internal.FakeClock; +import io.grpc.opentelemetry.GrpcOpenTelemetry.TargetFilter; import io.grpc.opentelemetry.OpenTelemetryMetricsModule.CallAttemptsTracerFactory; import io.grpc.opentelemetry.internal.OpenTelemetryConstants; import io.grpc.stub.MetadataUtils; @@ -1667,12 +1668,149 @@ public void serverBaggagePropagationToMetrics() { assertEquals("67", capturedBaggage.getEntryValue("user-id")); } + @Test + public void targetAttributeFilter_notSet_usesOriginalTarget() { + // Test that when no filter is set, the original target is used + String target = "dns:///example.com"; + OpenTelemetryMetricsResource resource = GrpcOpenTelemetry.createMetricInstruments(testMeter, + enabledMetricsMap, disableDefaultMetrics); + OpenTelemetryMetricsModule module = newOpenTelemetryMetricsModule(resource); + + Channel interceptedChannel = + ClientInterceptors.intercept( + grpcServerRule.getChannel(), module.getClientInterceptor(target)); + + ClientCall call = interceptedChannel.newCall(method, CALL_OPTIONS); + + // Make the call + Metadata headers = new Metadata(); + call.start(mockClientCallListener, headers); + + // End the call + call.halfClose(); + call.request(1); + + io.opentelemetry.api.common.Attributes attributes = io.opentelemetry.api.common.Attributes.of( + TARGET_KEY, target, + METHOD_KEY, method.getFullMethodName()); + + assertThat(openTelemetryTesting.getMetrics()) + .anySatisfy( + metric -> + assertThat(metric) + .hasInstrumentationScope(InstrumentationScopeInfo.create( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .hasName(CLIENT_ATTEMPT_COUNT_INSTRUMENT_NAME) + .hasUnit("{attempt}") + .hasLongSumSatisfying( + longSum -> + longSum + .hasPointsSatisfying( + point -> + point + .hasAttributes(attributes)))); + } + + @Test + public void targetAttributeFilter_allowsTarget_usesOriginalTarget() { + // Test that when filter allows the target, the original target is used + String target = "dns:///example.com"; + OpenTelemetryMetricsResource resource = GrpcOpenTelemetry.createMetricInstruments(testMeter, + enabledMetricsMap, disableDefaultMetrics); + OpenTelemetryMetricsModule module = newOpenTelemetryMetricsModule(resource, + t -> t.contains("example.com")); + + Channel interceptedChannel = + ClientInterceptors.intercept( + grpcServerRule.getChannel(), module.getClientInterceptor(target)); + + ClientCall call = interceptedChannel.newCall(method, CALL_OPTIONS); + + // Make the call + Metadata headers = new Metadata(); + call.start(mockClientCallListener, headers); + + // End the call + call.halfClose(); + call.request(1); + + io.opentelemetry.api.common.Attributes attributes = io.opentelemetry.api.common.Attributes.of( + TARGET_KEY, target, + METHOD_KEY, method.getFullMethodName()); + + assertThat(openTelemetryTesting.getMetrics()) + .anySatisfy( + metric -> + assertThat(metric) + .hasInstrumentationScope(InstrumentationScopeInfo.create( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .hasName(CLIENT_ATTEMPT_COUNT_INSTRUMENT_NAME) + .hasUnit("{attempt}") + .hasLongSumSatisfying( + longSum -> + longSum + .hasPointsSatisfying( + point -> + point + .hasAttributes(attributes)))); + } + + @Test + public void targetAttributeFilter_rejectsTarget_mapsToOther() { + // Test that when filter rejects the target, it is mapped to "other" + String target = "dns:///example.com"; + OpenTelemetryMetricsResource resource = GrpcOpenTelemetry.createMetricInstruments(testMeter, + enabledMetricsMap, disableDefaultMetrics); + OpenTelemetryMetricsModule module = newOpenTelemetryMetricsModule(resource, + t -> t.contains("allowed.com")); + + Channel interceptedChannel = + ClientInterceptors.intercept( + grpcServerRule.getChannel(), module.getClientInterceptor(target)); + + ClientCall call = interceptedChannel.newCall(method, CALL_OPTIONS); + + // Make the call + Metadata headers = new Metadata(); + call.start(mockClientCallListener, headers); + + // End the call + call.halfClose(); + call.request(1); + + io.opentelemetry.api.common.Attributes attributes = io.opentelemetry.api.common.Attributes.of( + TARGET_KEY, "other", + METHOD_KEY, method.getFullMethodName()); + + assertThat(openTelemetryTesting.getMetrics()) + .anySatisfy( + metric -> + assertThat(metric) + .hasInstrumentationScope(InstrumentationScopeInfo.create( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .hasName(CLIENT_ATTEMPT_COUNT_INSTRUMENT_NAME) + .hasUnit("{attempt}") + .hasLongSumSatisfying( + longSum -> + longSum + .hasPointsSatisfying( + point -> + point + .hasAttributes(attributes)))); + } + private OpenTelemetryMetricsModule newOpenTelemetryMetricsModule( OpenTelemetryMetricsResource resource) { return new OpenTelemetryMetricsModule( fakeClock.getStopwatchSupplier(), resource, emptyList(), emptyList()); } + private OpenTelemetryMetricsModule newOpenTelemetryMetricsModule( + OpenTelemetryMetricsResource resource, TargetFilter filter) { + return new OpenTelemetryMetricsModule( + fakeClock.getStopwatchSupplier(), resource, emptyList(), emptyList(), filter); + } + static class CallInfo extends ServerCallInfo { private final MethodDescriptor methodDescriptor; private final Attributes attributes; From 59a64f0b685f32b70b9bf73c7e15a5b0f847f767 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Mon, 12 Jan 2026 05:10:33 -0800 Subject: [PATCH 514/591] core: Use FlagResetRule to set/restore system properties in DnsNameResolverTest (#12604) --- .../io/grpc/internal/DnsNameResolverTest.java | 35 ++++++------------- .../java/io/grpc/internal/FlagResetRule.java | 30 ++++++++++++++++ 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java b/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java index f5be078f83a..b50dd5bcca3 100644 --- a/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java +++ b/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java @@ -17,6 +17,7 @@ package io.grpc.internal; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.internal.DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -78,8 +79,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; -import javax.annotation.Nullable; -import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -100,6 +99,7 @@ public class DnsNameResolverTest { @Rule public final TestRule globalTimeout = new DisableOnDebug(Timeout.seconds(10)); @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + @Rule public final FlagResetRule flagResetRule = new FlagResetRule(); private final Map serviceConfig = new LinkedHashMap<>(); @@ -152,8 +152,6 @@ public void close(Executor instance) {} private NameResolver.Listener2 mockListener; @Captor private ArgumentCaptor resultCaptor; - @Nullable - private String networkaddressCacheTtlPropertyValue; @Mock private RecordFetcher recordFetcher; @Mock private ProxyDetector mockProxyDetector; @@ -213,24 +211,11 @@ private RetryingNameResolver newResolver( @Before public void setUp() { DnsNameResolver.enableJndi = true; - networkaddressCacheTtlPropertyValue = - System.getProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY); // By default the mock listener processes the result successfully. when(mockListener.onResult2(isA(ResolutionResult.class))).thenReturn(Status.OK); } - @After - public void restoreSystemProperty() { - if (networkaddressCacheTtlPropertyValue == null) { - System.clearProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY); - } else { - System.setProperty( - DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, - networkaddressCacheTtlPropertyValue); - } - } - @Test public void invalidDnsName() throws Exception { testInvalidUri(new URI("dns", null, "/[invalid]", null)); @@ -275,19 +260,19 @@ public void invalidDnsName_containsUnderscore() { @Test public void resolve_androidIgnoresPropertyValue() throws Exception { - System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, Long.toString(2)); + flagResetRule.setSystemPropertyForTest(NETWORKADDRESS_CACHE_TTL_PROPERTY, "2"); resolveNeverCache(true); } @Test public void resolve_androidIgnoresPropertyValueCacheForever() throws Exception { - System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, Long.toString(-1)); + flagResetRule.setSystemPropertyForTest(NETWORKADDRESS_CACHE_TTL_PROPERTY, "-1"); resolveNeverCache(true); } @Test public void resolve_neverCache() throws Exception { - System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, "0"); + flagResetRule.setSystemPropertyForTest(NETWORKADDRESS_CACHE_TTL_PROPERTY, "0"); resolveNeverCache(false); } @@ -387,7 +372,7 @@ public void execute(Runnable command) { @Test public void resolve_cacheForever() throws Exception { - System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, "-1"); + flagResetRule.setSystemPropertyForTest(NETWORKADDRESS_CACHE_TTL_PROPERTY, "-1"); final List answer1 = createAddressList(2); String name = "foo.googleapis.com"; FakeTicker fakeTicker = new FakeTicker(); @@ -421,7 +406,7 @@ public void resolve_cacheForever() throws Exception { @Test public void resolve_usingCache() throws Exception { long ttl = 60; - System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, Long.toString(ttl)); + flagResetRule.setSystemPropertyForTest(NETWORKADDRESS_CACHE_TTL_PROPERTY, Long.toString(ttl)); final List answer = createAddressList(2); String name = "foo.googleapis.com"; FakeTicker fakeTicker = new FakeTicker(); @@ -456,7 +441,7 @@ public void resolve_usingCache() throws Exception { @Test public void resolve_cacheExpired() throws Exception { long ttl = 60; - System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, Long.toString(ttl)); + flagResetRule.setSystemPropertyForTest(NETWORKADDRESS_CACHE_TTL_PROPERTY, Long.toString(ttl)); final List answer1 = createAddressList(2); final List answer2 = createAddressList(1); String name = "foo.googleapis.com"; @@ -491,13 +476,13 @@ public void resolve_cacheExpired() throws Exception { @Test public void resolve_invalidTtlPropertyValue() throws Exception { - System.setProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY, "not_a_number"); + flagResetRule.setSystemPropertyForTest(NETWORKADDRESS_CACHE_TTL_PROPERTY, "not_a_number"); resolveDefaultValue(); } @Test public void resolve_noPropertyValue() throws Exception { - System.clearProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY); + flagResetRule.clearSystemPropertyForTest(NETWORKADDRESS_CACHE_TTL_PROPERTY); resolveDefaultValue(); } diff --git a/core/src/testFixtures/java/io/grpc/internal/FlagResetRule.java b/core/src/testFixtures/java/io/grpc/internal/FlagResetRule.java index dbcaca79358..96125cd0c8a 100644 --- a/core/src/testFixtures/java/io/grpc/internal/FlagResetRule.java +++ b/core/src/testFixtures/java/io/grpc/internal/FlagResetRule.java @@ -18,6 +18,7 @@ import java.util.ArrayDeque; import java.util.Deque; +import javax.annotation.Nullable; import org.junit.rules.ExternalResource; /** @@ -45,6 +46,35 @@ public void setFlagForTest(SetterMethod setter, T value) { toRunAfter.push(() -> setter.set(oldValue)); } + /** + * Sets java system property 'key' to 'value' and arranges for its previous value to be + * unconditionally restored when the test completes. + */ + public void setSystemPropertyForTest(String key, String value) { + String oldValue = System.setProperty(key, value); + restoreSystemPropertyAfterTest(key, oldValue); + } + + /** + * Clears java system property 'key' and arranges for its previous value to be unconditionally + * restored when the test completes. + */ + public void clearSystemPropertyForTest(String key) { + String oldValue = System.clearProperty(key); + restoreSystemPropertyAfterTest(key, oldValue); + } + + private void restoreSystemPropertyAfterTest(String key, @Nullable String oldValue) { + toRunAfter.push( + () -> { + if (oldValue == null) { + System.clearProperty(key); + } else { + System.setProperty(key, oldValue); + } + }); + } + @Override protected void after() { RuntimeException toThrow = null; From 65596ae3a9cdf420d91748e5ad1779ad92e14627 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Mon, 12 Jan 2026 05:20:15 -0800 Subject: [PATCH 515/591] core: Move 4 test cases from DnsNameResolverTest to DnsNameResolverProviderTest (#12605) These test cases make use of DnsNameResolverProvider but also cover some logic from DnsNameResolverProvider's ctor. They could reasonably live in either file but DnsNameResolverProviderTest already knows how to test the new RFC 3986 newNameResolver() overload. Leaves DnsNameResolverTest.java without any `java.net.URI` usage, which makes sense because DnsNameResolver's public API does not mention java.net.URI. Removed test helpers that asserted more than "a single conceptual fact" per https://abseil.io/resources/swe-book/html/ch12.html#shared_helpers_and_validation --- .../internal/DnsNameResolverProviderTest.java | 27 ++++++++++ .../io/grpc/internal/DnsNameResolverTest.java | 50 +------------------ 2 files changed, 28 insertions(+), 49 deletions(-) diff --git a/core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java b/core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java index fabecea0bad..787272cf630 100644 --- a/core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java +++ b/core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -75,6 +76,7 @@ public void newNameResolver_acceptsHostAndPort() { assertThat(nameResolver).isNotNull(); assertThat(nameResolver.getClass()).isSameInstanceAs(DnsNameResolver.class); assertThat(nameResolver.getServiceAuthority()).isEqualTo("localhost:443"); + assertThat(((DnsNameResolver) nameResolver).getPort()).isEqualTo(443); } @Test @@ -92,6 +94,15 @@ public void newNameResolver_rejectsNonDnsScheme() { assertThat(nameResolver).isNull(); } + @Test + public void newNameResolver_validDnsNameWithoutPort_usesDefaultPort() { + DnsNameResolver nameResolver = + (DnsNameResolver) newNameResolver("dns:/foo.googleapis.com", args); + assertThat(nameResolver).isNotNull(); + assertThat(nameResolver.getServiceAuthority()).isEqualTo("foo.googleapis.com"); + assertThat(nameResolver.getPort()).isEqualTo(args.getDefaultPort()); + } + @Test public void newNameResolver_toleratesTrailingPathSegments() { NameResolver nameResolver = newNameResolver("dns:///foo.googleapis.com/ig/nor/ed", args); @@ -108,6 +119,22 @@ public void newNameResolver_toleratesAuthority() { assertThat(nameResolver.getServiceAuthority()).isEqualTo("foo.googleapis.com"); } + @Test + public void newNameResolver_validIpv6Host() { + NameResolver nameResolver = newNameResolver("dns:/%5B::1%5D", args); + assertThat(nameResolver).isNotNull(); + assertThat(nameResolver.getClass()).isSameInstanceAs(DnsNameResolver.class); + assertThat(nameResolver.getServiceAuthority()).isEqualTo("[::1]"); + } + + @Test + public void newNameResolver_invalidIpv6Host_throws() { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, () -> newNameResolver("dns:/%5Binvalid%5D", args)); + assertThat(e).hasMessageThat().contains("invalid"); + } + private NameResolver newNameResolver(String uriString, NameResolver.Args args) { return enableRfc3986UrisParam ? provider.newNameResolver(Uri.create(uriString), args) diff --git a/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java b/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java index b50dd5bcca3..c6067d305b5 100644 --- a/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java +++ b/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java @@ -64,7 +64,6 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.net.URI; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; @@ -112,7 +111,6 @@ public void uncaughtException(Thread t, Throwable e) { } }); - private final DnsNameResolverProvider provider = new DnsNameResolverProvider(); private final FakeClock fakeClock = new FakeClock(); private final FakeClock fakeExecutor = new FakeClock(); private static final FakeClock.TaskFilter NAME_RESOLVER_REFRESH_TASK_FILTER = @@ -139,15 +137,6 @@ public Executor create() { public void close(Executor instance) {} } - private final NameResolver.Args args = NameResolver.Args.newBuilder() - .setDefaultPort(DEFAULT_PORT) - .setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR) - .setSynchronizationContext(syncContext) - .setServiceConfigParser(mock(ServiceConfigParser.class)) - .setChannelLogger(mock(ChannelLogger.class)) - .setScheduledExecutorService(fakeExecutor.getScheduledExecutorService()) - .build(); - @Mock private NameResolver.Listener2 mockListener; @Captor @@ -167,6 +156,7 @@ private RetryingNameResolver newResolver(String name, int defaultPort, boolean i isAndroid); } + private RetryingNameResolver newResolver( String name, int defaultPort, @@ -216,28 +206,6 @@ public void setUp() { when(mockListener.onResult2(isA(ResolutionResult.class))).thenReturn(Status.OK); } - @Test - public void invalidDnsName() throws Exception { - testInvalidUri(new URI("dns", null, "/[invalid]", null)); - } - - @Test - public void validIpv6() throws Exception { - testValidUri(new URI("dns", null, "/[::1]", null), "[::1]", DEFAULT_PORT); - } - - @Test - public void validDnsNameWithoutPort() throws Exception { - testValidUri(new URI("dns", null, "/foo.googleapis.com", null), - "foo.googleapis.com", DEFAULT_PORT); - } - - @Test - public void validDnsNameWithPort() throws Exception { - testValidUri(new URI("dns", null, "/foo.googleapis.com:456", null), - "foo.googleapis.com:456", 456); - } - @Test public void nullDnsName() { try { @@ -1284,22 +1252,6 @@ public void parseServiceConfig_matches() { assertThat(result.getConfig()).isEqualTo(ImmutableMap.of()); } - private void testInvalidUri(URI uri) { - try { - provider.newNameResolver(uri, args); - fail("Should have failed"); - } catch (IllegalArgumentException e) { - // expected - } - } - - private void testValidUri(URI uri, String exportedAuthority, int expectedPort) { - DnsNameResolver resolver = (DnsNameResolver) provider.newNameResolver(uri, args); - assertNotNull(resolver); - assertEquals(expectedPort, resolver.getPort()); - assertEquals(exportedAuthority, resolver.getServiceAuthority()); - } - private byte lastByte = 0; private List createAddressList(int n) throws UnknownHostException { From c589bef18f33c1aead47382391adba70f8bd4655 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 13 Jan 2026 12:33:54 -0800 Subject: [PATCH 516/591] core: clarify dns javadoc/test about trailing path segments --- .../io/grpc/internal/DnsNameResolverProvider.java | 11 ++++++----- .../io/grpc/internal/DnsNameResolverProviderTest.java | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/DnsNameResolverProvider.java b/core/src/main/java/io/grpc/internal/DnsNameResolverProvider.java index 16edf767901..14b56f1a12a 100644 --- a/core/src/main/java/io/grpc/internal/DnsNameResolverProvider.java +++ b/core/src/main/java/io/grpc/internal/DnsNameResolverProvider.java @@ -33,11 +33,12 @@ * A provider for {@link DnsNameResolver}. * *

    It resolves a target URI whose scheme is {@code "dns"}. The (optional) authority of the target - * URI is reserved for the address of alternative DNS server (not implemented yet). The first path - * segment of the hierarchical target URI is interpreted as an RFC 2396 "server-based" authority and - * used as the "service authority" of the resulting {@link NameResolver}. The "host" part of this - * authority is the name to be resolved by DNS. The "port" part of this authority (if present) will - * become the port number for all {@link InetSocketAddress} produced by this resolver. For example: + * URI is reserved for the address of alternative DNS server (not implemented yet). The target URI + * must be hierarchical and have exactly one path segment which will be interpreted as an RFC 2396 + * "server-based" authority and used as the "service authority" of the resulting {@link + * NameResolver}. The "host" part of this authority is the name to be resolved by DNS. The "port" + * part of this authority (if present) will become the port number for all {@link InetSocketAddress} + * produced by this resolver. For example: * *