From 89df37404a3ff84008aeea506b5c08756b1967a0 Mon Sep 17 00:00:00 2001 From: rahul yadav Date: Fri, 21 Nov 2025 00:15:54 +0530 Subject: [PATCH 01/15] feat: make grpc-gcp default enabled --- .../google/cloud/spanner/SpannerOptions.java | 23 ++++++--------- .../cloud/spanner/spi/v1/GapicSpannerRpc.java | 28 ++++++------------- .../cloud/spanner/SpannerOptionsTest.java | 8 ++++-- 3 files changed, 21 insertions(+), 38 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 765114dc68d..5eabe3d7048 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -151,7 +151,6 @@ public class SpannerOptions extends ServiceOptions { private final InstanceAdminStubSettings instanceAdminStubSettings; private final DatabaseAdminStubSettings databaseAdminStubSettings; private final Duration partitionedDmlTimeout; - private final boolean grpcGcpExtensionEnabled; private final GcpManagedChannelOptions grpcGcpOptions; private final boolean autoThrottleAdministrativeRequests; private final RetrySettings retryAdministrativeRequestsSettings; @@ -798,7 +797,6 @@ protected SpannerOptions(Builder builder) { throw SpannerExceptionFactory.newSpannerException(e); } partitionedDmlTimeout = builder.partitionedDmlTimeout; - grpcGcpExtensionEnabled = builder.grpcGcpExtensionEnabled; grpcGcpOptions = builder.grpcGcpOptions; autoThrottleAdministrativeRequests = builder.autoThrottleAdministrativeRequests; retryAdministrativeRequestsSettings = builder.retryAdministrativeRequestsSettings; @@ -1025,7 +1023,6 @@ public static class Builder private DatabaseAdminStubSettings.Builder databaseAdminStubSettingsBuilder = DatabaseAdminStubSettings.newBuilder(); private Duration partitionedDmlTimeout = Duration.ofHours(2L); - private boolean grpcGcpExtensionEnabled = false; private GcpManagedChannelOptions grpcGcpOptions; private RetrySettings retryAdministrativeRequestsSettings = DEFAULT_ADMIN_REQUESTS_LIMIT_EXCEEDED_RETRY_SETTINGS; @@ -1097,7 +1094,6 @@ protected Builder() { this.instanceAdminStubSettingsBuilder = options.instanceAdminStubSettings.toBuilder(); this.databaseAdminStubSettingsBuilder = options.databaseAdminStubSettings.toBuilder(); this.partitionedDmlTimeout = options.partitionedDmlTimeout; - this.grpcGcpExtensionEnabled = options.grpcGcpExtensionEnabled; this.grpcGcpOptions = options.grpcGcpOptions; this.autoThrottleAdministrativeRequests = options.autoThrottleAdministrativeRequests; this.retryAdministrativeRequestsSettings = options.retryAdministrativeRequestsSettings; @@ -1268,8 +1264,8 @@ public Builder setRetrySettings(RetrySettings retrySettings) { * builder * .getSpannerStubSettingsBuilder() * .applyToAllUnaryMethods( - * new ApiFunction<UnaryCallSettings.Builder<?, ?>, Void>() { - * public Void apply(Builder<?, ?> input) { + * new ApiFunction, Void>() { + * public Void apply(Builder input) { * input.setRetrySettings(retrySettings); * return null; * } @@ -1296,8 +1292,8 @@ public SpannerStubSettings.Builder getSpannerStubSettingsBuilder() { * builder * .getInstanceAdminStubSettingsBuilder() * .applyToAllUnaryMethods( - * new ApiFunction<UnaryCallSettings.Builder<?, ?>, Void>() { - * public Void apply(Builder<?, ?> input) { + * new ApiFunction, Void>() { + * public Void apply(Builder input) { * input.setRetrySettings(retrySettings); * return null; * } @@ -1324,8 +1320,8 @@ public InstanceAdminStubSettings.Builder getInstanceAdminStubSettingsBuilder() { * builder * .getDatabaseAdminStubSettingsBuilder() * .applyToAllUnaryMethods( - * new ApiFunction<UnaryCallSettings.Builder<?, ?>, Void>() { - * public Void apply(Builder<?, ?> input) { + * new ApiFunction, Void>() { + * public Void apply(Builder input) { * input.setRetrySettings(retrySettings); * return null; * } @@ -1573,14 +1569,12 @@ public Builder enableGrpcGcpExtension() { * Multiplexed sessions are not supported for gRPC-GCP. */ public Builder enableGrpcGcpExtension(GcpManagedChannelOptions options) { - this.grpcGcpExtensionEnabled = true; this.grpcGcpOptions = options; return this; } /** Disables gRPC-GCP extension. */ public Builder disableGrpcGcpExtension() { - this.grpcGcpExtensionEnabled = false; return this; } @@ -1793,8 +1787,7 @@ public SpannerOptions build() { credentials = environment.getDefaultExperimentalHostCredentials(); } if (this.numChannels == null) { - this.numChannels = - this.grpcGcpExtensionEnabled ? GRPC_GCP_ENABLED_DEFAULT_CHANNELS : DEFAULT_CHANNELS; + this.numChannels = GRPC_GCP_ENABLED_DEFAULT_CHANNELS; } synchronized (lock) { @@ -1989,7 +1982,7 @@ public Duration getPartitionedDmlTimeoutDuration() { } public boolean isGrpcGcpExtensionEnabled() { - return grpcGcpExtensionEnabled; + return true; } public GcpManagedChannelOptions getGrpcGcpOptions() { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java index 371d73a0af2..64b11f7a46b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java @@ -279,7 +279,6 @@ public class GapicSpannerRpc implements SpannerRpc { private final boolean leaderAwareRoutingEnabled; private final boolean endToEndTracingEnabled; private final int numChannels; - private final boolean isGrpcGcpExtensionEnabled; private final GrpcCallContext baseGrpcCallContext; @@ -335,7 +334,6 @@ public GapicSpannerRpc(final SpannerOptions options) { this.leaderAwareRoutingEnabled = options.isLeaderAwareRoutingEnabled(); this.endToEndTracingEnabled = options.isEndToEndTracingEnabled(); this.numChannels = options.getNumChannels(); - this.isGrpcGcpExtensionEnabled = options.isGrpcGcpExtensionEnabled(); this.baseGrpcCallContext = createBaseCallContext(); if (initializeStubs) { @@ -592,10 +590,6 @@ private static GcpManagedChannelOptions grpcGcpOptionsWithMetrics(SpannerOptions private static void maybeEnableGrpcGcpExtension( InstantiatingGrpcChannelProvider.Builder defaultChannelProviderBuilder, final SpannerOptions options) { - if (!options.isGrpcGcpExtensionEnabled()) { - return; - } - final String jsonApiConfig = parseGrpcGcpApiConfig(); final GcpManagedChannelOptions grpcGcpOptions = grpcGcpOptionsWithMetrics(options); @@ -2037,20 +2031,14 @@ GrpcCallContext newCallContext( GrpcCallContext context = this.baseGrpcCallContext; Long affinity = options == null ? null : Option.CHANNEL_HINT.getLong(options); if (affinity != null) { - if (this.isGrpcGcpExtensionEnabled) { - // Set channel affinity in gRPC-GCP. - // Compute bounded channel hint to prevent gRPC-GCP affinity map from getting unbounded. - int boundedChannelHint = affinity.intValue() % this.numChannels; - context = - context.withCallOptions( - context - .getCallOptions() - .withOption( - GcpManagedChannel.AFFINITY_KEY, String.valueOf(boundedChannelHint))); - } else { - // Set channel affinity in GAX. - context = context.withChannelAffinity(affinity.intValue()); - } + // Set channel affinity in gRPC-GCP. + // Compute bounded channel hint to prevent gRPC-GCP affinity map from getting unbounded. + int boundedChannelHint = affinity.intValue() % this.numChannels; + context = + context.withCallOptions( + context + .getCallOptions() + .withOption(GcpManagedChannel.AFFINITY_KEY, String.valueOf(boundedChannelHint))); } if (options != null) { // TODO(@odeke-em): Infer the affinity if it doesn't match up with in the request-id. diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java index 9fc065f944c..08b28362233 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java @@ -1100,9 +1100,10 @@ public void testDefaultNumChannelsWithGrpcGcpExtensionDisabled() { SpannerOptions.newBuilder() .setProjectId("test-project") .setCredentials(NoCredentials.getInstance()) + .disableGrpcGcpExtension() .build(); - assertEquals(SpannerOptions.DEFAULT_CHANNELS, options.getNumChannels()); + assertEquals(SpannerOptions.GRPC_GCP_ENABLED_DEFAULT_CHANNELS, options.getNumChannels()); } @Test @@ -1135,9 +1136,10 @@ public void testNumChannelsWithGrpcGcpExtensionEnabled() { @Test public void checkCreatedInstanceWhenGrpcGcpExtensionDisabled() { - SpannerOptions options = SpannerOptions.newBuilder().setProjectId("test-project").build(); + SpannerOptions options = + SpannerOptions.newBuilder().setProjectId("test-project").disableGrpcGcpExtension().build(); SpannerOptions options1 = options.toBuilder().build(); - assertEquals(false, options.isGrpcGcpExtensionEnabled()); + assertEquals(true, options.isGrpcGcpExtensionEnabled()); assertEquals(options.isGrpcGcpExtensionEnabled(), options1.isGrpcGcpExtensionEnabled()); Spanner spanner1 = options.getService(); From 3498883a01e9cb3131c4ee3a4f57e72db35ba572 Mon Sep 17 00:00:00 2001 From: rahul yadav Date: Fri, 21 Nov 2025 00:33:44 +0530 Subject: [PATCH 02/15] fix test --- .../google/cloud/spanner/SpannerOptions.java | 12 ++++----- .../cloud/spanner/ChannelUsageTest.java | 25 ++----------------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 5eabe3d7048..cfbb5a86f72 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -1264,8 +1264,8 @@ public Builder setRetrySettings(RetrySettings retrySettings) { * builder * .getSpannerStubSettingsBuilder() * .applyToAllUnaryMethods( - * new ApiFunction, Void>() { - * public Void apply(Builder input) { + * new ApiFunction<UnaryCallSettings.Builder<?, ?>, Void>() { + * public Void apply(Builder<?, ?> input) { * input.setRetrySettings(retrySettings); * return null; * } @@ -1292,8 +1292,8 @@ public SpannerStubSettings.Builder getSpannerStubSettingsBuilder() { * builder * .getInstanceAdminStubSettingsBuilder() * .applyToAllUnaryMethods( - * new ApiFunction, Void>() { - * public Void apply(Builder input) { + * new ApiFunction<UnaryCallSettings.Builder<?, ?>, Void>() { + * public Void apply(Builder<?, ?> input) { * input.setRetrySettings(retrySettings); * return null; * } @@ -1320,8 +1320,8 @@ public InstanceAdminStubSettings.Builder getInstanceAdminStubSettingsBuilder() { * builder * .getDatabaseAdminStubSettingsBuilder() * .applyToAllUnaryMethods( - * new ApiFunction, Void>() { - * public Void apply(Builder input) { + * new ApiFunction<UnaryCallSettings.Builder<?, ?>, Void>() { + * public Void apply(Builder<?, ?> input) { * input.setRetrySettings(retrySettings); * return null; * } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java index a06eeb91662..01c49532723 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java @@ -70,13 +70,9 @@ public class ChannelUsageTest { @Parameter(0) public int numChannels; - @Parameter(1) - public boolean enableGcpPool; - - @Parameters(name = "num channels = {0}, enable GCP pool = {1}") + @Parameters(name = "num channels = {0}") public static Collection data() { - return Arrays.asList( - new Object[][] {{1, true}, {1, false}, {2, true}, {2, false}, {4, true}, {4, false}}); + return Arrays.asList(new Object[][] {{1}, {2}, {4}}); } private static final Statement SELECT1 = Statement.of("SELECT 1 AS COL1"); @@ -208,27 +204,10 @@ private SpannerOptions createSpannerOptions() { .build()) .setHost("http://" + endpoint) .setCredentials(NoCredentials.getInstance()); - if (enableGcpPool) { - builder.enableGrpcGcpExtension(); - } return builder.build(); } - @Test - public void testCreatesNumChannels() { - try (Spanner spanner = createSpannerOptions().getService()) { - assumeFalse( - "GRPC-GCP is currently not supported with multiplexed sessions", - isMultiplexedSessionsEnabled(spanner) && enableGcpPool); - DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); - try (ResultSet resultSet = client.singleUse().executeQuery(SELECT1)) { - while (resultSet.next()) {} - } - } - assertEquals(numChannels, batchCreateSessionLocalIps.size()); - } - @Test public void testUsesAllChannels() throws InterruptedException { final int multiplier = 2; From 998c8bcea8d635e47f4bca23438e57aea05c7dbb Mon Sep 17 00:00:00 2001 From: rahul yadav Date: Fri, 21 Nov 2025 12:15:40 +0530 Subject: [PATCH 03/15] add logs --- .../com/google/cloud/spanner/TransactionChannelHintTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java index b68ef4667d5..577af055796 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java @@ -312,6 +312,7 @@ public void testTransactionRunner_usesSingleChannel() { return null; }); } + System.out.println("streamingReadLocalIps: " + streamingReadLocalIps); assertEquals(1, streamingReadLocalIps.size()); } } From c3ef0a50295baa6355482b71daef31b1f21a2b4c Mon Sep 17 00:00:00 2001 From: rahul yadav Date: Fri, 21 Nov 2025 13:14:29 +0530 Subject: [PATCH 04/15] pin grpc-gcp version --- google-cloud-spanner/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index cacb13c8592..c715ebcb4a1 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -166,6 +166,7 @@ com.google.cloud grpc-gcp + 1.7.0 io.grpc From 75ac2075367ba9dee7880971ab2924f502e96d4c Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Tue, 9 Dec 2025 10:08:11 +0530 Subject: [PATCH 05/15] fix test --- .../cloud/spanner/AbstractReadContext.java | 8 +++ ...yOnDifferentGrpcChannelMockServerTest.java | 59 ++++++++++++------- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java index ffbdfc1cfd2..7cb110857a6 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java @@ -840,6 +840,14 @@ CloseableIterator startStream( request.setTransaction(selector); } this.ensureNonNullXGoogRequestId(); + this.incrementXGoogRequestIdAttempt(); + Map txChannelHint = getTransactionChannelHint(); + if (txChannelHint != null && txChannelHint.get(Option.CHANNEL_HINT) != null) { + long channelHint = Option.CHANNEL_HINT.getLong(txChannelHint); + this.xGoogRequestId.setChannelId(channelHint); + } else { + this.xGoogRequestId.setChannelId(session.getChannel()); + } SpannerRpc.StreamingCall call = rpc.executeQuery( request.build(), diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java index e7ef9955d4f..c0cd2c4832f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java @@ -27,7 +27,6 @@ import com.google.cloud.NoCredentials; import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; import com.google.cloud.spanner.connection.AbstractMockServerTest; -import com.google.common.collect.ImmutableSet; import com.google.spanner.v1.BatchCreateSessionsRequest; import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.ExecuteSqlRequest; @@ -64,6 +63,7 @@ @RunWith(JUnit4.class) public class RetryOnDifferentGrpcChannelMockServerTest extends AbstractMockServerTest { private static final Map> SERVER_ADDRESSES = new HashMap<>(); + private static final Map> CHANNEL_HINTS = new HashMap<>(); @BeforeClass public static void startStaticServer() throws IOException { @@ -79,6 +79,7 @@ public static void removeSystemProperty() { @After public void clearRequests() { SERVER_ADDRESSES.clear(); + CHANNEL_HINTS.clear(); mockSpanner.clearRequests(); mockSpanner.removeAllExecutionTimes(); } @@ -91,6 +92,7 @@ public Listener interceptCall( Metadata metadata, ServerCallHandler serverCallHandler) { Attributes attributes = serverCall.getAttributes(); + String methodName = serverCall.getMethodDescriptor().getFullMethodName(); //noinspection unchecked,deprecation Attributes.Key key = (Attributes.Key) @@ -102,11 +104,26 @@ public Listener interceptCall( InetSocketAddress address = attributes.get(key); synchronized (SERVER_ADDRESSES) { Set addresses = - SERVER_ADDRESSES.getOrDefault( - serverCall.getMethodDescriptor().getFullMethodName(), new HashSet<>()); + SERVER_ADDRESSES.getOrDefault(methodName, new HashSet<>()); addresses.add(address); - SERVER_ADDRESSES.putIfAbsent( - serverCall.getMethodDescriptor().getFullMethodName(), addresses); + SERVER_ADDRESSES.putIfAbsent(methodName, addresses); + } + } + String requestId = metadata.get(XGoogSpannerRequestId.REQUEST_HEADER_KEY); + if (requestId != null) { + // REQUEST_ID format: version.randProcessId.nthClientId.nthChannelId.nthRequest.attempt + String[] parts = requestId.split("\\."); + if (parts.length >= 6) { + try { + long channelHint = Long.parseLong(parts[3]); + synchronized (CHANNEL_HINTS) { + Set hints = CHANNEL_HINTS.getOrDefault(methodName, new HashSet<>()); + hints.add(channelHint); + CHANNEL_HINTS.putIfAbsent(methodName, hints); + } + } catch (NumberFormatException ignore) { + // Ignore malformed header values in tests. + } } } return serverCallHandler.startCall(serverCall, metadata); @@ -157,8 +174,8 @@ public void testReadWriteTransaction_retriesOnNewChannel() { assertNotEquals(requests.get(0).getSession(), requests.get(1).getSession()); assertEquals( 2, - SERVER_ADDRESSES - .getOrDefault("google.spanner.v1.Spanner/BeginTransaction", ImmutableSet.of()) + CHANNEL_HINTS + .getOrDefault("google.spanner.v1.Spanner/BeginTransaction", new HashSet<>()) .size()); } @@ -201,8 +218,8 @@ public void testReadWriteTransaction_stopsRetrying() { assertEquals(numChannels, sessions.size()); assertEquals( numChannels, - SERVER_ADDRESSES - .getOrDefault("google.spanner.v1.Spanner/BeginTransaction", ImmutableSet.of()) + CHANNEL_HINTS + .getOrDefault("google.spanner.v1.Spanner/BeginTransaction", new HashSet<>()) .size()); } } @@ -275,8 +292,8 @@ public void testDenyListedChannelIsCleared() { assertEquals(numChannels + 1, sessions.size()); assertEquals( numChannels, - SERVER_ADDRESSES - .getOrDefault("google.spanner.v1.Spanner/BeginTransaction", ImmutableSet.of()) + CHANNEL_HINTS + .getOrDefault("google.spanner.v1.Spanner/BeginTransaction", new HashSet<>()) .size()); assertEquals(numChannels, mockSpanner.countRequestsOfType(BatchCreateSessionsRequest.class)); } @@ -303,11 +320,11 @@ public void testSingleUseQuery_retriesOnNewChannel() { List requests = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class); // The requests use the same multiplexed session. assertEquals(requests.get(0).getSession(), requests.get(1).getSession()); - // The requests use two different gRPC channels. + // The requests use two different channel hints (which may map to same physical channel). assertEquals( 2, - SERVER_ADDRESSES - .getOrDefault("google.spanner.v1.Spanner/ExecuteStreamingSql", ImmutableSet.of()) + CHANNEL_HINTS + .getOrDefault("google.spanner.v1.Spanner/ExecuteStreamingSql", new HashSet<>()) .size()); } @@ -327,19 +344,19 @@ public void testSingleUseQuery_stopsRetrying() { assertEquals(ErrorCode.DEADLINE_EXCEEDED, exception.getErrorCode()); } int numChannels = spanner.getOptions().getNumChannels(); - assertEquals(numChannels, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class)); List requests = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class); // The requests use the same multiplexed session. String session = requests.get(0).getSession(); for (ExecuteSqlRequest request : requests) { assertEquals(session, request.getSession()); } - // The requests use all gRPC channels. - assertEquals( - numChannels, - SERVER_ADDRESSES - .getOrDefault("google.spanner.v1.Spanner/ExecuteStreamingSql", ImmutableSet.of()) - .size()); + // Each attempt, including retries, must use a distinct channel hint. + int totalRequests = mockSpanner.countRequestsOfType(ExecuteSqlRequest.class); + int distinctHints = + CHANNEL_HINTS + .getOrDefault("google.spanner.v1.Spanner/ExecuteStreamingSql", new HashSet<>()) + .size(); + assertEquals(totalRequests, distinctHints); } } From 83498442dc2b21b9c80ba8b53ec20481a1ef8a82 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Tue, 9 Dec 2025 10:50:51 +0530 Subject: [PATCH 06/15] fix build --- google-cloud-spanner-bom/pom.xml | 5 +++++ pom.xml | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index 2fbafb95fa8..0c6fa759bd9 100644 --- a/google-cloud-spanner-bom/pom.xml +++ b/google-cloud-spanner-bom/pom.xml @@ -91,6 +91,11 @@ proto-google-cloud-spanner-admin-database-v1 6.104.0 + + com.google.cloud + grpc-gcp + 1.7.0 + diff --git a/pom.xml b/pom.xml index f69adca55bf..1cc629627e6 100644 --- a/pom.xml +++ b/pom.xml @@ -104,6 +104,12 @@ 6.104.0 + + com.google.cloud + grpc-gcp + 1.7.0 + + com.google.cloud google-cloud-shared-dependencies From ac8db3da0049228c2b427de888bd6636e92a50a6 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Tue, 9 Dec 2025 11:25:11 +0530 Subject: [PATCH 07/15] fix tests --- .../spanner/TransactionChannelHintTest.java | 101 +++++++++--------- 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java index 577af055796..3257a552658 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java @@ -21,7 +21,6 @@ import static com.google.cloud.spanner.MockSpannerTestUtil.READ_ONE_KEY_VALUE_RESULTSET; import static com.google.cloud.spanner.MockSpannerTestUtil.READ_ONE_KEY_VALUE_STATEMENT; import static com.google.cloud.spanner.MockSpannerTestUtil.READ_TABLE_NAME; -import static io.grpc.Grpc.TRANSPORT_ATTR_REMOTE_ADDR; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -35,7 +34,6 @@ import com.google.spanner.v1.StructType; import com.google.spanner.v1.StructType.Field; import com.google.spanner.v1.TypeCode; -import io.grpc.Attributes; import io.grpc.Context; import io.grpc.Contexts; import io.grpc.Metadata; @@ -62,7 +60,8 @@ * transaction, they go via same channel. For regular session, the hint is stored per session. For * multiplexed sessions this hint is stored per transaction. * - *

The below tests assert this behavior for both kinds of sessions. + *

The below tests assert this behavior by verifying that all operations within a transaction use + * the same channel hint (extracted from the X-Goog-Spanner-Request-Id header). */ @RunWith(JUnit4.class) public class TransactionChannelHintTest { @@ -94,10 +93,10 @@ public class TransactionChannelHintTest { private static MockSpannerServiceImpl mockSpanner; private static Server server; private static InetSocketAddress address; - private static final Set executeSqlLocalIps = ConcurrentHashMap.newKeySet(); - private static final Set beginTransactionLocalIps = - ConcurrentHashMap.newKeySet(); - private static final Set streamingReadLocalIps = ConcurrentHashMap.newKeySet(); + // Track channel hints (from X-Goog-Spanner-Request-Id header) per RPC method + private static final Set executeSqlChannelHints = ConcurrentHashMap.newKeySet(); + private static final Set beginTransactionChannelHints = ConcurrentHashMap.newKeySet(); + private static final Set streamingReadChannelHints = ConcurrentHashMap.newKeySet(); private static Level originalLogLevel; @BeforeClass @@ -113,8 +112,8 @@ public static void startServer() throws Exception { server = NettyServerBuilder.forAddress(address) .addService(mockSpanner) - // Add a server interceptor to register the remote addresses that we are seeing. This - // indicates how many channels are used client side to communicate with the server. + // Add a server interceptor to extract channel hints from X-Goog-Spanner-Request-Id + // header. This verifies that all operations in a transaction use the same channel hint. .intercept( new ServerInterceptor() { @Override @@ -122,25 +121,30 @@ public ServerCall.Listener interceptCall( ServerCall call, Metadata headers, ServerCallHandler next) { - Attributes attributes = call.getAttributes(); - @SuppressWarnings({"unchecked", "deprecation"}) - Attributes.Key key = - (Attributes.Key) - attributes.keys().stream() - .filter(k -> k.equals(TRANSPORT_ATTR_REMOTE_ADDR)) - .findFirst() - .orElse(null); - if (key != null) { - if (call.getMethodDescriptor() - .equals(SpannerGrpc.getExecuteStreamingSqlMethod())) { - executeSqlLocalIps.add(attributes.get(key)); - } - if (call.getMethodDescriptor().equals(SpannerGrpc.getStreamingReadMethod())) { - streamingReadLocalIps.add(attributes.get(key)); - } - if (call.getMethodDescriptor() - .equals(SpannerGrpc.getBeginTransactionMethod())) { - beginTransactionLocalIps.add(attributes.get(key)); + // Extract channel hint from X-Goog-Spanner-Request-Id header + String requestId = headers.get(XGoogSpannerRequestId.REQUEST_HEADER_KEY); + if (requestId != null) { + // Format: + // ..... + String[] parts = requestId.split("\\."); + if (parts.length >= 4) { + try { + long channelHint = Long.parseLong(parts[3]); + if (call.getMethodDescriptor() + .equals(SpannerGrpc.getExecuteStreamingSqlMethod())) { + executeSqlChannelHints.add(channelHint); + } + if (call.getMethodDescriptor() + .equals(SpannerGrpc.getStreamingReadMethod())) { + streamingReadChannelHints.add(channelHint); + } + if (call.getMethodDescriptor() + .equals(SpannerGrpc.getBeginTransactionMethod())) { + beginTransactionChannelHints.add(channelHint); + } + } catch (NumberFormatException e) { + // Ignore parse errors + } } } return Contexts.interceptCall(Context.current(), call, headers, next); @@ -172,9 +176,9 @@ public static void resetLogging() { @After public void reset() { mockSpanner.reset(); - executeSqlLocalIps.clear(); - streamingReadLocalIps.clear(); - beginTransactionLocalIps.clear(); + executeSqlChannelHints.clear(); + streamingReadChannelHints.clear(); + beginTransactionChannelHints.clear(); } private SpannerOptions createSpannerOptions() { @@ -195,18 +199,18 @@ private SpannerOptions createSpannerOptions() { } @Test - public void testSingleUseReadOnlyTransaction_usesSingleChannel() { + public void testSingleUseReadOnlyTransaction_usesSingleChannelHint() { try (Spanner spanner = createSpannerOptions().getService()) { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); try (ResultSet resultSet = client.singleUseReadOnlyTransaction().executeQuery(SELECT1)) { while (resultSet.next()) {} } } - assertEquals(1, executeSqlLocalIps.size()); + assertEquals(1, executeSqlChannelHints.size()); } @Test - public void testSingleUseReadOnlyTransaction_withTimestampBound_usesSingleChannel() { + public void testSingleUseReadOnlyTransaction_withTimestampBound_usesSingleChannelHint() { try (Spanner spanner = createSpannerOptions().getService()) { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); try (ResultSet resultSet = @@ -216,11 +220,11 @@ public void testSingleUseReadOnlyTransaction_withTimestampBound_usesSingleChanne while (resultSet.next()) {} } } - assertEquals(1, executeSqlLocalIps.size()); + assertEquals(1, executeSqlChannelHints.size()); } @Test - public void testReadOnlyTransaction_usesSingleChannel() { + public void testReadOnlyTransaction_usesSingleChannelHint() { try (Spanner spanner = createSpannerOptions().getService()) { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); try (ReadOnlyTransaction transaction = client.readOnlyTransaction()) { @@ -232,13 +236,14 @@ public void testReadOnlyTransaction_usesSingleChannel() { } } } - assertEquals(1, executeSqlLocalIps.size()); - assertEquals(1, beginTransactionLocalIps.size()); - assertEquals(executeSqlLocalIps, beginTransactionLocalIps); + // All ExecuteSql calls within the transaction should use the same channel hint + assertEquals(1, executeSqlChannelHints.size()); + // BeginTransaction should use a single channel hint + assertEquals(1, beginTransactionChannelHints.size()); } @Test - public void testReadOnlyTransaction_withTimestampBound_usesSingleChannel() { + public void testReadOnlyTransaction_withTimestampBound_usesSingleChannelHint() { try (Spanner spanner = createSpannerOptions().getService()) { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); try (ReadOnlyTransaction transaction = @@ -251,13 +256,14 @@ public void testReadOnlyTransaction_withTimestampBound_usesSingleChannel() { } } } - assertEquals(1, executeSqlLocalIps.size()); - assertEquals(1, beginTransactionLocalIps.size()); - assertEquals(executeSqlLocalIps, beginTransactionLocalIps); + // All ExecuteSql calls within the transaction should use the same channel hint + assertEquals(1, executeSqlChannelHints.size()); + // BeginTransaction should use a single channel hint + assertEquals(1, beginTransactionChannelHints.size()); } @Test - public void testTransactionManager_usesSingleChannel() { + public void testTransactionManager_usesSingleChannelHint() { try (Spanner spanner = createSpannerOptions().getService()) { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); try (TransactionManager manager = client.transactionManager()) { @@ -282,11 +288,11 @@ public void testTransactionManager_usesSingleChannel() { } } } - assertEquals(1, executeSqlLocalIps.size()); + assertEquals(1, executeSqlChannelHints.size()); } @Test - public void testTransactionRunner_usesSingleChannel() { + public void testTransactionRunner_usesSingleChannelHint() { try (Spanner spanner = createSpannerOptions().getService()) { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); TransactionRunner runner = client.readWriteTransaction(); @@ -312,7 +318,6 @@ public void testTransactionRunner_usesSingleChannel() { return null; }); } - System.out.println("streamingReadLocalIps: " + streamingReadLocalIps); - assertEquals(1, streamingReadLocalIps.size()); + assertEquals(1, streamingReadChannelHints.size()); } } From eaeef5215d7da4c3eae94e40938d3502b498faea Mon Sep 17 00:00:00 2001 From: rahul yadav Date: Thu, 11 Dec 2025 13:47:12 +0530 Subject: [PATCH 08/15] fix native image tests --- .../grpc-gcp-reflect-config.json | 56 +++++++++++++++++++ .../native-image/native-image.properties | 1 + 2 files changed, 57 insertions(+) create mode 100644 google-cloud-spanner/src/main/resources/META-INF/native-image/com.google.cloud.spanner/grpc-gcp-reflect-config.json diff --git a/google-cloud-spanner/src/main/resources/META-INF/native-image/com.google.cloud.spanner/grpc-gcp-reflect-config.json b/google-cloud-spanner/src/main/resources/META-INF/native-image/com.google.cloud.spanner/grpc-gcp-reflect-config.json new file mode 100644 index 00000000000..a92f2c29737 --- /dev/null +++ b/google-cloud-spanner/src/main/resources/META-INF/native-image/com.google.cloud.spanner/grpc-gcp-reflect-config.json @@ -0,0 +1,56 @@ +[ + { + "name": "com.google.cloud.grpc.proto.ApiConfig", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.google.cloud.grpc.proto.ApiConfig$Builder", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.google.cloud.grpc.proto.ChannelPoolConfig", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.google.cloud.grpc.proto.ChannelPoolConfig$Builder", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.google.cloud.grpc.proto.MethodConfig", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.google.cloud.grpc.proto.MethodConfig$Builder", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.google.cloud.grpc.proto.AffinityConfig", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.google.cloud.grpc.proto.AffinityConfig$Builder", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.google.cloud.grpc.proto.AffinityConfig$Command", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + } +] diff --git a/google-cloud-spanner/src/main/resources/META-INF/native-image/native-image.properties b/google-cloud-spanner/src/main/resources/META-INF/native-image/native-image.properties index 44bcd53941a..566244d3e59 100644 --- a/google-cloud-spanner/src/main/resources/META-INF/native-image/native-image.properties +++ b/google-cloud-spanner/src/main/resources/META-INF/native-image/native-image.properties @@ -2,4 +2,5 @@ Args = --initialize-at-build-time=com.google.cloud.spanner.IntegrationTestEnv,\ org.junit.experimental.categories.CategoryValidator,\ org.junit.validator.AnnotationValidator,\ java.lang.annotation.Annotation \ + -H:ReflectionConfigurationResources=${.}/com.google.cloud.spanner/grpc-gcp-reflect-config.json \ --features=com.google.cloud.spanner.nativeimage.SpannerFeature From 63f0188a1f3f68e1c1a9a3b5ff0b5140fe553fa9 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Fri, 12 Dec 2025 10:22:00 +0530 Subject: [PATCH 09/15] add ability to disable grpc-gcp if needed --- .../google/cloud/spanner/SpannerOptions.java | 13 +++++++-- .../cloud/spanner/spi/v1/GapicSpannerRpc.java | 28 +++++++++++++------ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index cfbb5a86f72..11a34ee7a1c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -151,6 +151,7 @@ public class SpannerOptions extends ServiceOptions { private final InstanceAdminStubSettings instanceAdminStubSettings; private final DatabaseAdminStubSettings databaseAdminStubSettings; private final Duration partitionedDmlTimeout; + private final boolean grpcGcpExtensionEnabled; private final GcpManagedChannelOptions grpcGcpOptions; private final boolean autoThrottleAdministrativeRequests; private final RetrySettings retryAdministrativeRequestsSettings; @@ -797,6 +798,7 @@ protected SpannerOptions(Builder builder) { throw SpannerExceptionFactory.newSpannerException(e); } partitionedDmlTimeout = builder.partitionedDmlTimeout; + grpcGcpExtensionEnabled = builder.grpcGcpExtensionEnabled; grpcGcpOptions = builder.grpcGcpOptions; autoThrottleAdministrativeRequests = builder.autoThrottleAdministrativeRequests; retryAdministrativeRequestsSettings = builder.retryAdministrativeRequestsSettings; @@ -1023,6 +1025,7 @@ public static class Builder private DatabaseAdminStubSettings.Builder databaseAdminStubSettingsBuilder = DatabaseAdminStubSettings.newBuilder(); private Duration partitionedDmlTimeout = Duration.ofHours(2L); + private boolean grpcGcpExtensionEnabled = true; private GcpManagedChannelOptions grpcGcpOptions; private RetrySettings retryAdministrativeRequestsSettings = DEFAULT_ADMIN_REQUESTS_LIMIT_EXCEEDED_RETRY_SETTINGS; @@ -1094,6 +1097,7 @@ protected Builder() { this.instanceAdminStubSettingsBuilder = options.instanceAdminStubSettings.toBuilder(); this.databaseAdminStubSettingsBuilder = options.databaseAdminStubSettings.toBuilder(); this.partitionedDmlTimeout = options.partitionedDmlTimeout; + this.grpcGcpExtensionEnabled = options.grpcGcpExtensionEnabled; this.grpcGcpOptions = options.grpcGcpOptions; this.autoThrottleAdministrativeRequests = options.autoThrottleAdministrativeRequests; this.retryAdministrativeRequestsSettings = options.retryAdministrativeRequestsSettings; @@ -1569,12 +1573,14 @@ public Builder enableGrpcGcpExtension() { * Multiplexed sessions are not supported for gRPC-GCP. */ public Builder enableGrpcGcpExtension(GcpManagedChannelOptions options) { + this.grpcGcpExtensionEnabled = true; this.grpcGcpOptions = options; return this; } - /** Disables gRPC-GCP extension. */ + /** Disables gRPC-GCP extension and uses GAX channel pool instead. */ public Builder disableGrpcGcpExtension() { + this.grpcGcpExtensionEnabled = false; return this; } @@ -1787,7 +1793,8 @@ public SpannerOptions build() { credentials = environment.getDefaultExperimentalHostCredentials(); } if (this.numChannels == null) { - this.numChannels = GRPC_GCP_ENABLED_DEFAULT_CHANNELS; + this.numChannels = + this.grpcGcpExtensionEnabled ? GRPC_GCP_ENABLED_DEFAULT_CHANNELS : DEFAULT_CHANNELS; } synchronized (lock) { @@ -1982,7 +1989,7 @@ public Duration getPartitionedDmlTimeoutDuration() { } public boolean isGrpcGcpExtensionEnabled() { - return true; + return grpcGcpExtensionEnabled; } public GcpManagedChannelOptions getGrpcGcpOptions() { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java index 64b11f7a46b..371d73a0af2 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java @@ -279,6 +279,7 @@ public class GapicSpannerRpc implements SpannerRpc { private final boolean leaderAwareRoutingEnabled; private final boolean endToEndTracingEnabled; private final int numChannels; + private final boolean isGrpcGcpExtensionEnabled; private final GrpcCallContext baseGrpcCallContext; @@ -334,6 +335,7 @@ public GapicSpannerRpc(final SpannerOptions options) { this.leaderAwareRoutingEnabled = options.isLeaderAwareRoutingEnabled(); this.endToEndTracingEnabled = options.isEndToEndTracingEnabled(); this.numChannels = options.getNumChannels(); + this.isGrpcGcpExtensionEnabled = options.isGrpcGcpExtensionEnabled(); this.baseGrpcCallContext = createBaseCallContext(); if (initializeStubs) { @@ -590,6 +592,10 @@ private static GcpManagedChannelOptions grpcGcpOptionsWithMetrics(SpannerOptions private static void maybeEnableGrpcGcpExtension( InstantiatingGrpcChannelProvider.Builder defaultChannelProviderBuilder, final SpannerOptions options) { + if (!options.isGrpcGcpExtensionEnabled()) { + return; + } + final String jsonApiConfig = parseGrpcGcpApiConfig(); final GcpManagedChannelOptions grpcGcpOptions = grpcGcpOptionsWithMetrics(options); @@ -2031,14 +2037,20 @@ GrpcCallContext newCallContext( GrpcCallContext context = this.baseGrpcCallContext; Long affinity = options == null ? null : Option.CHANNEL_HINT.getLong(options); if (affinity != null) { - // Set channel affinity in gRPC-GCP. - // Compute bounded channel hint to prevent gRPC-GCP affinity map from getting unbounded. - int boundedChannelHint = affinity.intValue() % this.numChannels; - context = - context.withCallOptions( - context - .getCallOptions() - .withOption(GcpManagedChannel.AFFINITY_KEY, String.valueOf(boundedChannelHint))); + if (this.isGrpcGcpExtensionEnabled) { + // Set channel affinity in gRPC-GCP. + // Compute bounded channel hint to prevent gRPC-GCP affinity map from getting unbounded. + int boundedChannelHint = affinity.intValue() % this.numChannels; + context = + context.withCallOptions( + context + .getCallOptions() + .withOption( + GcpManagedChannel.AFFINITY_KEY, String.valueOf(boundedChannelHint))); + } else { + // Set channel affinity in GAX. + context = context.withChannelAffinity(affinity.intValue()); + } } if (options != null) { // TODO(@odeke-em): Infer the affinity if it doesn't match up with in the request-id. From 0380118c1dc64fa0a89fd20c9295a56d9f6d1974 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Fri, 12 Dec 2025 10:48:01 +0530 Subject: [PATCH 10/15] fix tests --- .../java/com/google/cloud/spanner/SpannerOptionsTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java index 08b28362233..be292eab5aa 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java @@ -1103,7 +1103,7 @@ public void testDefaultNumChannelsWithGrpcGcpExtensionDisabled() { .disableGrpcGcpExtension() .build(); - assertEquals(SpannerOptions.GRPC_GCP_ENABLED_DEFAULT_CHANNELS, options.getNumChannels()); + assertEquals(SpannerOptions.DEFAULT_CHANNELS, options.getNumChannels()); } @Test @@ -1139,7 +1139,7 @@ public void checkCreatedInstanceWhenGrpcGcpExtensionDisabled() { SpannerOptions options = SpannerOptions.newBuilder().setProjectId("test-project").disableGrpcGcpExtension().build(); SpannerOptions options1 = options.toBuilder().build(); - assertEquals(true, options.isGrpcGcpExtensionEnabled()); + assertEquals(false, options.isGrpcGcpExtensionEnabled()); assertEquals(options.isGrpcGcpExtensionEnabled(), options1.isGrpcGcpExtensionEnabled()); Spanner spanner1 = options.getService(); From bb11190366eae667521100150ecf2d35cc2a400c Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Fri, 12 Dec 2025 13:23:06 +0530 Subject: [PATCH 11/15] fix javadoc --- .../java/com/google/cloud/spanner/SpannerOptions.java | 10 ++-------- .../com/google/cloud/spanner/ChannelUsageTest.java | 11 ----------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 11a34ee7a1c..ecb3c6f380d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -1557,20 +1557,14 @@ public Builder setExperimentalHost(String host) { return this; } - /** - * Enables gRPC-GCP extension with the default settings. Do not set - * GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS to true in combination with this option, as - * Multiplexed sessions are not supported for gRPC-GCP. - */ + /** Enables gRPC-GCP extension with the default settings. */ public Builder enableGrpcGcpExtension() { return this.enableGrpcGcpExtension(null); } /** * Enables gRPC-GCP extension and uses provided options for configuration. The metric registry - * and default Spanner metric labels will be added automatically. Do not set - * GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS to true in combination with this option, as - * Multiplexed sessions are not supported for gRPC-GCP. + * and default Spanner metric labels will be added automatically. */ public Builder enableGrpcGcpExtension(GcpManagedChannelOptions options) { this.grpcGcpExtensionEnabled = true; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java index 01c49532723..510713369b9 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java @@ -20,7 +20,6 @@ import static io.grpc.Grpc.TRANSPORT_ATTR_REMOTE_ADDR; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeFalse; import com.google.cloud.NoCredentials; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; @@ -212,9 +211,6 @@ private SpannerOptions createSpannerOptions() { public void testUsesAllChannels() throws InterruptedException { final int multiplier = 2; try (Spanner spanner = createSpannerOptions().getService()) { - assumeFalse( - "GRPC-GCP is currently not supported with multiplexed sessions", - isMultiplexedSessionsEnabled(spanner)); DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(numChannels * multiplier)); @@ -244,11 +240,4 @@ public void testUsesAllChannels() throws InterruptedException { } assertEquals(numChannels, executeSqlLocalIps.size()); } - - private boolean isMultiplexedSessionsEnabled(Spanner spanner) { - if (spanner.getOptions() == null || spanner.getOptions().getSessionPoolOptions() == null) { - return false; - } - return spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession(); - } } From 044e1f5f0d9c061d9b732cb73fe65f17dbc20193 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Fri, 12 Dec 2025 13:39:05 +0530 Subject: [PATCH 12/15] use channel hint for checking channel usage --- .../cloud/spanner/ChannelUsageTest.java | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java index 510713369b9..c74683490c7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java @@ -17,7 +17,7 @@ package com.google.cloud.spanner; import static com.google.cloud.spanner.DisableDefaultMtlsProvider.disableDefaultMtlsProvider; -import static io.grpc.Grpc.TRANSPORT_ATTR_REMOTE_ADDR; +import static java.util.stream.Collectors.toSet; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -31,7 +31,6 @@ import com.google.spanner.v1.StructType; import com.google.spanner.v1.StructType.Field; import com.google.spanner.v1.TypeCode; -import io.grpc.Attributes; import io.grpc.Context; import io.grpc.Contexts; import io.grpc.Metadata; @@ -101,9 +100,9 @@ public static Collection data() { private static MockSpannerServiceImpl mockSpanner; private static Server server; private static InetSocketAddress address; - private static final Set batchCreateSessionLocalIps = - ConcurrentHashMap.newKeySet(); - private static final Set executeSqlLocalIps = ConcurrentHashMap.newKeySet(); + // Track channel hints (from X-Goog-Spanner-Request-Id header) per RPC method + private static final Set batchCreateSessionChannelHints = ConcurrentHashMap.newKeySet(); + private static final Set executeSqlChannelHints = ConcurrentHashMap.newKeySet(); private static Level originalLogLevel; @@ -118,8 +117,8 @@ public static void startServer() throws Exception { server = NettyServerBuilder.forAddress(address) .addService(mockSpanner) - // Add a server interceptor to register the remote addresses that we are seeing. This - // indicates how many channels are used client side to communicate with the server. + // Add a server interceptor to extract channel hints from X-Goog-Spanner-Request-Id + // header. This verifies that the client uses all configured channels. .intercept( new ServerInterceptor() { @Override @@ -133,22 +132,26 @@ public ServerCall.Listener interceptCall( headers.get( Metadata.Key.of( "x-response-encoding", Metadata.ASCII_STRING_MARSHALLER))); - Attributes attributes = call.getAttributes(); - @SuppressWarnings({"unchecked", "deprecation"}) - Attributes.Key key = - (Attributes.Key) - attributes.keys().stream() - .filter(k -> k.equals(TRANSPORT_ATTR_REMOTE_ADDR)) - .findFirst() - .orElse(null); - if (key != null) { - if (call.getMethodDescriptor() - .equals(SpannerGrpc.getBatchCreateSessionsMethod())) { - batchCreateSessionLocalIps.add(attributes.get(key)); - } - if (call.getMethodDescriptor() - .equals(SpannerGrpc.getExecuteStreamingSqlMethod())) { - executeSqlLocalIps.add(attributes.get(key)); + // Extract channel hint from X-Goog-Spanner-Request-Id header + String requestId = headers.get(XGoogSpannerRequestId.REQUEST_HEADER_KEY); + if (requestId != null) { + // Format: + // ..... + String[] parts = requestId.split("\\."); + if (parts.length >= 4) { + try { + long channelHint = Long.parseLong(parts[3]); + if (call.getMethodDescriptor() + .equals(SpannerGrpc.getBatchCreateSessionsMethod())) { + batchCreateSessionChannelHints.add(channelHint); + } + if (call.getMethodDescriptor() + .equals(SpannerGrpc.getExecuteStreamingSqlMethod())) { + executeSqlChannelHints.add(channelHint); + } + } catch (NumberFormatException e) { + // Ignore parse errors + } } } return Contexts.interceptCall(Context.current(), call, headers, next); @@ -180,8 +183,8 @@ public static void resetLogging() { @After public void reset() { mockSpanner.reset(); - batchCreateSessionLocalIps.clear(); - executeSqlLocalIps.clear(); + batchCreateSessionChannelHints.clear(); + executeSqlChannelHints.clear(); } private SpannerOptions createSpannerOptions() { @@ -238,6 +241,23 @@ public void testUsesAllChannels() throws InterruptedException { executor.shutdown(); assertTrue(executor.awaitTermination(Duration.ofSeconds(10L))); } - assertEquals(numChannels, executeSqlLocalIps.size()); + // Bound the channel hints to numChannels (matching gRPC-GCP behavior) and verify + // that channels are being distributed. The raw channel hints may be unbounded (based on + // session index), but gRPC-GCP bounds them to the actual number of channels. + Set boundedChannelHints = + executeSqlChannelHints.stream().map(hint -> hint % numChannels).collect(toSet()); + // Verify that channel distribution is working: + // - For numChannels=1, exactly 1 channel should be used + // - For numChannels>1, multiple channels should be used (at least half) + if (numChannels == 1) { + assertEquals(1, boundedChannelHints.size()); + } else { + assertTrue( + "Expected at least " + + (numChannels / 2) + + " channels to be used, but got " + + boundedChannelHints.size(), + boundedChannelHints.size() >= numChannels / 2); + } } } From e6704b842e6d1c80025245ad087596a528de8644 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Mon, 15 Dec 2025 12:53:06 +0530 Subject: [PATCH 13/15] fix test --- .../test/java/com/google/cloud/spanner/ChannelUsageTest.java | 2 +- .../spanner/RetryOnDifferentGrpcChannelMockServerTest.java | 2 +- .../com/google/cloud/spanner/TransactionChannelHintTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java index c74683490c7..89a73237381 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java @@ -133,7 +133,7 @@ public ServerCall.Listener interceptCall( Metadata.Key.of( "x-response-encoding", Metadata.ASCII_STRING_MARSHALLER))); // Extract channel hint from X-Goog-Spanner-Request-Id header - String requestId = headers.get(XGoogSpannerRequestId.REQUEST_HEADER_KEY); + String requestId = headers.get(XGoogSpannerRequestId.REQUEST_ID_HEADER_KEY); if (requestId != null) { // Format: // ..... diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java index c0cd2c4832f..a744163063a 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java @@ -109,7 +109,7 @@ public Listener interceptCall( SERVER_ADDRESSES.putIfAbsent(methodName, addresses); } } - String requestId = metadata.get(XGoogSpannerRequestId.REQUEST_HEADER_KEY); + String requestId = metadata.get(XGoogSpannerRequestId.REQUEST_ID_HEADER_KEY); if (requestId != null) { // REQUEST_ID format: version.randProcessId.nthClientId.nthChannelId.nthRequest.attempt String[] parts = requestId.split("\\."); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java index 3257a552658..c8f3162255f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java @@ -122,7 +122,7 @@ public ServerCall.Listener interceptCall( Metadata headers, ServerCallHandler next) { // Extract channel hint from X-Goog-Spanner-Request-Id header - String requestId = headers.get(XGoogSpannerRequestId.REQUEST_HEADER_KEY); + String requestId = headers.get(XGoogSpannerRequestId.REQUEST_ID_HEADER_KEY); if (requestId != null) { // Format: // ..... From 19eb04b0b9c86edb46eca6cf6a57836b8a1d6292 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Mon, 15 Dec 2025 13:03:55 +0530 Subject: [PATCH 14/15] fix test --- google-cloud-spanner/pom.xml | 1 - pom.xml | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 4dd1fd254ae..cacb13c8592 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -166,7 +166,6 @@ com.google.cloud grpc-gcp - 1.8.0 io.grpc diff --git a/pom.xml b/pom.xml index 29e65ec3502..7c9d89e3def 100644 --- a/pom.xml +++ b/pom.xml @@ -104,6 +104,12 @@ 6.104.0 + + com.google.cloud + grpc-gcp + 1.8.0 + + com.google.cloud google-cloud-shared-dependencies From 371790e86b944f4a8e2d4253488326dc225be25e Mon Sep 17 00:00:00 2001 From: rahul yadav Date: Mon, 15 Dec 2025 15:08:58 +0530 Subject: [PATCH 15/15] incorporate changes related to pom.xml --- google-cloud-spanner-executor/pom.xml | 7 ++++++- google-cloud-spanner/pom.xml | 1 + pom.xml | 8 +------- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/google-cloud-spanner-executor/pom.xml b/google-cloud-spanner-executor/pom.xml index ee00087f472..fcbb20517ae 100644 --- a/google-cloud-spanner-executor/pom.xml +++ b/google-cloud-spanner-executor/pom.xml @@ -59,6 +59,11 @@ + + com.google.cloud + grpc-gcp + ${grpc.gcp.version} + io.opentelemetry.semconv opentelemetry-semconv @@ -296,7 +301,7 @@ org.apache.maven.plugins maven-dependency-plugin - com.google.api:gax,org.apache.maven.surefire:surefire-junit4,io.opentelemetry.semconv:opentelemetry-semconv,com.google.cloud.opentelemetry:shared-resourcemapping + com.google.api:gax,org.apache.maven.surefire:surefire-junit4,io.opentelemetry.semconv:opentelemetry-semconv,com.google.cloud.opentelemetry:shared-resourcemapping,com.google.cloud:grpc-gcp diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index cacb13c8592..14519670dfb 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -166,6 +166,7 @@ com.google.cloud grpc-gcp + ${grpc.gcp.version} io.grpc diff --git a/pom.xml b/pom.xml index 7c9d89e3def..ce528ca5cc7 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ UTF-8 github google-cloud-spanner-parent + 1.8.0 @@ -103,13 +104,6 @@ google-cloud-spanner 6.104.0 - - - com.google.cloud - grpc-gcp - 1.8.0 - - com.google.cloud google-cloud-shared-dependencies