From a5ce9d7ed3f3c31fcb0c70badcc9e9e86cf6292e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 23 Apr 2026 10:49:20 +0200 Subject: [PATCH 1/2] feat(spanner): add connection property for enabling/disabling grpc-gcp Adds a connection property for enabling/disabling the grpc-gcp channel pool. This channel pool is the default, but can be disabled by setting this property to false. The connection will use Gax for channel pooling if grpc-gcp is disabled. --- .../spanner/connection/ConnectionOptions.java | 10 +++ .../connection/ConnectionProperties.java | 11 +++ .../cloud/spanner/connection/SpannerPool.java | 15 +++- .../connection/GrpcGcpMockServerTest.java | 83 +++++++++++++++++++ .../spanner/connection/SpannerPoolTest.java | 43 ++++++++++ 5 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/GrpcGcpMockServerTest.java diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java index 44faecee7046..771cbfc24dad 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java @@ -35,6 +35,7 @@ import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_DYNAMIC_CHANNEL_POOL; import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_END_TO_END_TRACING; import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_EXTENDED_TRACING; +import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_GRPC_GCP; import static com.google.cloud.spanner.connection.ConnectionProperties.ENCODED_CREDENTIALS; import static com.google.cloud.spanner.connection.ConnectionProperties.ENDPOINT; import static com.google.cloud.spanner.connection.ConnectionProperties.GRPC_INTERCEPTOR_PROVIDER; @@ -160,6 +161,7 @@ public class ConnectionOptions { static final Integer DEFAULT_MAX_SESSIONS = null; static final Integer DEFAULT_NUM_CHANNELS = null; static final Boolean DEFAULT_ENABLE_DYNAMIC_CHANNEL_POOL = null; + static final Boolean DEFAULT_ENABLE_GRPC_GCP = null; static final Integer DEFAULT_DCP_MIN_CHANNELS = null; static final Integer DEFAULT_DCP_MAX_CHANNELS = null; static final Integer DEFAULT_DCP_INITIAL_CHANNELS = null; @@ -263,6 +265,9 @@ public class ConnectionOptions { /** Name of the 'enableDynamicChannelPool' connection property. */ public static final String ENABLE_DYNAMIC_CHANNEL_POOL_PROPERTY_NAME = "enableDynamicChannelPool"; + /** Name of the 'enableGrpcGcp' connection property. */ + public static final String ENABLE_GRPC_GCP_PROPERTY_NAME = "enableGrpcGcp"; + /** Name of the 'dcpMinChannels' connection property. */ public static final String DCP_MIN_CHANNELS_PROPERTY_NAME = "dcpMinChannels"; @@ -1016,6 +1021,11 @@ public Boolean isEnableDynamicChannelPool() { return getInitialConnectionPropertyValue(ENABLE_DYNAMIC_CHANNEL_POOL); } + /** Whether grpc-gcp is enabled for this connection. */ + public Boolean isEnableGrpcGcp() { + return getInitialConnectionPropertyValue(ENABLE_GRPC_GCP); + } + /** The minimum number of channels in the dynamic channel pool. */ public Integer getDcpMinChannels() { return getInitialConnectionPropertyValue(DCP_MIN_CHANNELS); diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java index 5fa678afef5e..82514367d00a 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java @@ -55,6 +55,7 @@ import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_DYNAMIC_CHANNEL_POOL; import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_END_TO_END_TRACING; import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_EXTENDED_TRACING; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_GRPC_GCP; import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENDPOINT; import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_IS_EXPERIMENTAL_HOST; import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_KEEP_TRANSACTION_ALIVE; @@ -85,6 +86,7 @@ import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_DYNAMIC_CHANNEL_POOL_PROPERTY_NAME; import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_END_TO_END_TRACING_PROPERTY_NAME; import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_EXTENDED_TRACING_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_GRPC_GCP_PROPERTY_NAME; import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_GRPC_INTERCEPTOR_PROVIDER_SYSTEM_PROPERTY; import static com.google.cloud.spanner.connection.ConnectionOptions.ENCODED_CREDENTIALS_PROPERTY_NAME; import static com.google.cloud.spanner.connection.ConnectionOptions.ENDPOINT_PROPERTY_NAME; @@ -463,6 +465,15 @@ public class ConnectionProperties { BOOLEANS, BooleanConverter.INSTANCE, Context.STARTUP); + static final ConnectionProperty ENABLE_GRPC_GCP = + create( + ENABLE_GRPC_GCP_PROPERTY_NAME, + "Enable or disable grpc-gcp channel pool (true/false). " + + "Setting this to false will disable grpc-gcp and use the Gax gRPC channel pool.", + DEFAULT_ENABLE_GRPC_GCP, + BOOLEANS, + BooleanConverter.INSTANCE, + Context.STARTUP); static final ConnectionProperty DCP_MIN_CHANNELS = create( DCP_MIN_CHANNELS_PROPERTY_NAME, diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java index e4912b8e4f26..0a40fb859574 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java @@ -155,6 +155,7 @@ static class SpannerPoolKey { private final SessionPoolOptions sessionPoolOptions; private final Integer numChannels; private final Boolean enableDynamicChannelPool; + private final Boolean enableGrpcGcp; private final Integer dcpMinChannels; private final Integer dcpMaxChannels; private final Integer dcpInitialChannels; @@ -197,6 +198,7 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException { : options.getSessionPoolOptions(); this.numChannels = options.getNumChannels(); this.enableDynamicChannelPool = options.isEnableDynamicChannelPool(); + this.enableGrpcGcp = options.isEnableGrpcGcp(); this.dcpMinChannels = options.getDcpMinChannels(); this.dcpMaxChannels = options.getDcpMaxChannels(); this.dcpInitialChannels = options.getDcpInitialChannels(); @@ -228,6 +230,7 @@ public boolean equals(Object o) { && Objects.equals(this.sessionPoolOptions, other.sessionPoolOptions) && Objects.equals(this.numChannels, other.numChannels) && Objects.equals(this.enableDynamicChannelPool, other.enableDynamicChannelPool) + && Objects.equals(this.enableGrpcGcp, other.enableGrpcGcp) && Objects.equals(this.dcpMinChannels, other.dcpMinChannels) && Objects.equals(this.dcpMaxChannels, other.dcpMaxChannels) && Objects.equals(this.dcpInitialChannels, other.dcpInitialChannels) @@ -258,6 +261,7 @@ public int hashCode() { this.sessionPoolOptions, this.numChannels, this.enableDynamicChannelPool, + this.enableGrpcGcp, this.dcpMinChannels, this.dcpMaxChannels, this.dcpInitialChannels, @@ -421,9 +425,18 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) { if (key.numChannels != null) { builder.setNumChannels(key.numChannels); } + if (key.enableGrpcGcp != null) { + if (Boolean.TRUE.equals(key.enableGrpcGcp)) { + builder.enableGrpcGcpExtension(); + } else { + builder.disableGrpcGcpExtension(); + } + } // Configure Dynamic Channel Pooling (DCP) based on explicit user setting. // Note: Setting numChannels disables DCP even if enableDynamicChannelPool is true. - if (key.enableDynamicChannelPool != null && key.numChannels == null) { + if (key.enableDynamicChannelPool != null + && key.numChannels == null + && !Boolean.FALSE.equals(key.enableGrpcGcp)) { if (Boolean.TRUE.equals(key.enableDynamicChannelPool)) { builder.enableDynamicChannelPool(); // Build custom GcpChannelPoolOptions if any DCP-specific options are set. diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/GrpcGcpMockServerTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/GrpcGcpMockServerTest.java new file mode 100644 index 000000000000..f6a23b5024b6 --- /dev/null +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/GrpcGcpMockServerTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2026 Google LLC + * + * 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 com.google.cloud.spanner.connection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.connection.StatementResult.ResultType; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class GrpcGcpMockServerTest extends AbstractMockServerTest { + + @Before + public void setup() { + SpannerPool.closeSpannerPool(); + } + + @After + public void teardown() { + SpannerPool.closeSpannerPool(); + } + + private void verifyConnectionWorks(Connection connection) { + StatementResult result = connection.execute(Statement.of("SELECT 1")); + assertEquals(ResultType.RESULT_SET, result.getResultType()); + try (ResultSet rs = result.getResultSet()) { + assertTrue(rs.next()); + assertEquals(1L, rs.getLong(0)); + assertFalse(rs.next()); + } + } + + @Test + public void testDisableGrpcGcp() { + try (Connection connection = createConnection(";enableGrpcGcp=false")) { + Spanner spanner = ((ConnectionImpl) connection).getSpanner(); + assertFalse(spanner.getOptions().isGrpcGcpExtensionEnabled()); + verifyConnectionWorks(connection); + } + } + + @Test + public void testEnableGrpcGcp() { + try (Connection connection = createConnection(";enableGrpcGcp=true")) { + Spanner spanner = ((ConnectionImpl) connection).getSpanner(); + assertTrue(spanner.getOptions().isGrpcGcpExtensionEnabled()); + verifyConnectionWorks(connection); + } + } + + @Test + public void testDefaultGrpcGcp() { + try (Connection connection = createConnection()) { + Spanner spanner = ((ConnectionImpl) connection).getSpanner(); + // Default should be true in SpannerOptions + assertTrue(spanner.getOptions().isGrpcGcpExtensionEnabled()); + verifyConnectionWorks(connection); + } + } +} diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java index b03288354b24..d18f3568e281 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java @@ -771,4 +771,47 @@ public void testExplicitlyDisabledDynamicChannelPool() { .setCredentials(NoCredentials.getInstance()) .build())); } + + @Test + public void testGrpcGcpSettings() { + SpannerPoolKey keyWithoutGrpcGcp = + SpannerPoolKey.of( + ConnectionOptions.newBuilder() + .setUri("cloudspanner:/projects/p/instances/i/databases/d") + .setCredentials(NoCredentials.getInstance()) + .build()); + SpannerPoolKey keyWithGrpcGcpEnabled = + SpannerPoolKey.of( + ConnectionOptions.newBuilder() + .setUri("cloudspanner:/projects/p/instances/i/databases/d?enableGrpcGcp=true") + .setCredentials(NoCredentials.getInstance()) + .build()); + SpannerPoolKey keyWithGrpcGcpDisabled = + SpannerPoolKey.of( + ConnectionOptions.newBuilder() + .setUri("cloudspanner:/projects/p/instances/i/databases/d?enableGrpcGcp=false") + .setCredentials(NoCredentials.getInstance()) + .build()); + + // grpcGcp settings should affect the SpannerPoolKey + assertNotEquals(keyWithoutGrpcGcp, keyWithGrpcGcpEnabled); + assertNotEquals(keyWithoutGrpcGcp, keyWithGrpcGcpDisabled); + assertNotEquals(keyWithGrpcGcpEnabled, keyWithGrpcGcpDisabled); + + // Same configuration should create equal keys + assertEquals( + keyWithGrpcGcpEnabled, + SpannerPoolKey.of( + ConnectionOptions.newBuilder() + .setUri("cloudspanner:/projects/p/instances/i/databases/d?enableGrpcGcp=true") + .setCredentials(NoCredentials.getInstance()) + .build())); + assertEquals( + keyWithGrpcGcpDisabled, + SpannerPoolKey.of( + ConnectionOptions.newBuilder() + .setUri("cloudspanner:/projects/p/instances/i/databases/d?enableGrpcGcp=false") + .setCredentials(NoCredentials.getInstance()) + .build())); + } } From 9f8a7744744d3654278a2dd22b4a09a9c7a9cdc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 23 Apr 2026 11:28:02 +0200 Subject: [PATCH 2/2] docs(spanner): clarify relationship between grpc-gcp and dcp --- .../cloud/spanner/connection/ConnectionProperties.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java index 82514367d00a..836fa504d30e 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java @@ -469,7 +469,10 @@ public class ConnectionProperties { create( ENABLE_GRPC_GCP_PROPERTY_NAME, "Enable or disable grpc-gcp channel pool (true/false). " - + "Setting this to false will disable grpc-gcp and use the Gax gRPC channel pool.", + + "Setting this to false will disable grpc-gcp and use the Gax gRPC channel pool. " + + "Disabling grpc-gcp also automatically disables dynamic channel pooling, regardless " + + "of the value of enableDynamicChannelPool, as Spanner only supports dynamic channel " + + "pooling in combination with grpc-gcp.", DEFAULT_ENABLE_GRPC_GCP, BOOLEANS, BooleanConverter.INSTANCE,