From 55aefd5b8e76a15861c5988d1d8ced0a58160303 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Thu, 25 Sep 2025 11:03:54 +0530 Subject: [PATCH 001/294] 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 002/294] 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 003/294] 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 004/294] 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 005/294] 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 006/294] 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 007/294] 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 008/294] 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 009/294] 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 010/294] 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 011/294] 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 012/294] 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 013/294] 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 014/294] 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 015/294] 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 016/294] 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 017/294] 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 018/294] 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 019/294] 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 020/294] 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 021/294] 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 022/294] 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 023/294] 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 024/294] 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 025/294] 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 026/294] 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 027/294] 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 028/294] 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 029/294] 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 030/294] 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 031/294] 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 032/294] 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 033/294] 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 034/294] 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 035/294] 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 036/294] 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 037/294] 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 038/294] 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 039/294] 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 040/294] 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 041/294] 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 042/294] 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 043/294] 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 044/294] 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 045/294] 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 046/294] 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 047/294] 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 048/294] 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 049/294] 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 050/294] 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 051/294] 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 052/294] 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 053/294] 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 054/294] 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 055/294] 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 056/294] 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 057/294] 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 058/294] 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 059/294] 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 060/294] 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 061/294] 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 062/294] 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 063/294] 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 064/294] 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 065/294] 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 066/294] 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 067/294] 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 068/294] 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 069/294] 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 070/294] 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 071/294] 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 072/294] 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 073/294] 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 074/294] 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 075/294] 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 076/294] 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 077/294] 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 078/294] 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 079/294] 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 080/294] 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 081/294] 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 082/294] 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 083/294] 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 084/294] 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 085/294] 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 086/294] 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 087/294] 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 088/294] 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 089/294] 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 090/294] 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 091/294] 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 092/294] 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 093/294] 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 094/294] 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 095/294] 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 096/294] 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 097/294] 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 098/294] 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 099/294] 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 100/294] 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 101/294] 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 102/294] 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 103/294] 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 104/294] 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 105/294] 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 106/294] 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 107/294] 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 108/294] 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 109/294] 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 110/294] 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 111/294] 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 112/294] 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 113/294] 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 114/294] 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 115/294] 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 116/294] 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 117/294] 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 118/294] 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 119/294] 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 120/294] 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 121/294] 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 122/294] 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 123/294] 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 124/294] 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 125/294] 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: * *