Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.

Commit ce43d4a

Browse files
committed
make dynamic channel pool default disabled
1 parent 482c344 commit ce43d4a

5 files changed

Lines changed: 165 additions & 90 deletions

File tree

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -850,15 +850,17 @@ protected SpannerOptions(Builder builder) {
850850
grpcGcpExtensionEnabled = builder.grpcGcpExtensionEnabled;
851851
grpcGcpOptions = builder.grpcGcpOptions;
852852

853-
// Dynamic channel pooling is enabled by default when:
854-
// 1. grpc-gcp extension is enabled, AND
855-
// 2. numChannels is not explicitly set, AND
856-
// 3. dynamicChannelPoolEnabled is not explicitly set to false
857-
if (builder.dynamicChannelPoolEnabled != null) {
858-
dynamicChannelPoolEnabled = builder.dynamicChannelPoolEnabled && grpcGcpExtensionEnabled;
859-
} else {
860-
// Enable DCP by default only if grpc-gcp is enabled and numChannels was not explicitly set
853+
// Dynamic channel pooling is disabled by default.
854+
// It is only enabled when:
855+
// 1. enableDynamicChannelPool() was explicitly called, AND
856+
// 2. grpc-gcp extension is enabled, AND
857+
// 3. numChannels was not explicitly set
858+
if (builder.dynamicChannelPoolEnabled != null && builder.dynamicChannelPoolEnabled) {
859+
// DCP was explicitly enabled, but respect numChannels if set
861860
dynamicChannelPoolEnabled = grpcGcpExtensionEnabled && !builder.numChannelsExplicitlySet;
861+
} else {
862+
// DCP is disabled by default, or was explicitly disabled
863+
dynamicChannelPoolEnabled = false;
862864
}
863865

864866
// Use defaults with proper bounds checking for DCP configuration
@@ -1722,12 +1724,25 @@ public Builder disableGrpcGcpExtension() {
17221724
return this;
17231725
}
17241726

1727+
/**
1728+
* Enables dynamic channel pooling. When enabled, the client will automatically scale the number
1729+
* of channels based on load. This requires the gRPC-GCP extension to be enabled.
1730+
*
1731+
* <p>Dynamic channel pooling is disabled by default. Use this method to explicitly enable it.
1732+
* Note that calling {@link #setNumChannels(int)} will disable dynamic channel pooling even if
1733+
* this method was called.
1734+
*/
1735+
public Builder enableDynamicChannelPool() {
1736+
this.dynamicChannelPoolEnabled = true;
1737+
return this;
1738+
}
1739+
17251740
/**
17261741
* Disables dynamic channel pooling. When disabled, the client will use a static number of
17271742
* channels as configured by {@link #setNumChannels(int)}.
17281743
*
1729-
* <p>Dynamic channel pooling is enabled by default unless {@link #setNumChannels(int)} is
1730-
* called or this method is used to disable it.
1744+
* <p>Dynamic channel pooling is disabled by default, so this method is typically not needed
1745+
* unless you want to explicitly disable it after enabling it.
17311746
*/
17321747
public Builder disableDynamicChannelPool() {
17331748
this.dynamicChannelPoolEnabled = false;
@@ -2273,9 +2288,10 @@ public GcpManagedChannelOptions getGrpcGcpOptions() {
22732288
}
22742289

22752290
/**
2276-
* Returns whether dynamic channel pooling is enabled. Dynamic channel pooling is enabled by
2277-
* default unless {@link Builder#setNumChannels(int)} is called or {@link
2278-
* Builder#disableDynamicChannelPool()} is used.
2291+
* Returns whether dynamic channel pooling is enabled. Dynamic channel pooling is disabled by
2292+
* default. Use {@link Builder#enableDynamicChannelPool()} to explicitly enable it. Note that
2293+
* calling {@link Builder#setNumChannels(int)} will disable dynamic channel pooling even if it was
2294+
* explicitly enabled.
22792295
*/
22802296
public boolean isDynamicChannelPoolEnabled() {
22812297
return dynamicChannelPoolEnabled;

google-cloud-spanner/src/test/java/com/google/cloud/spanner/RequestIdMockServerTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ public void testPartitionedDml() {
533533
XGoogSpannerRequestId.of(getClientId(), -1, 1, 1),
534534
XGoogSpannerRequestId.of(getClientId(), -1, 2, 1)),
535535
actual);
536-
verifySameChannelId(actual);
536+
// Channel ID is determined by grpc-gcp, so we don't verify same channel ID here.
537537
}
538538

539539
@Test
@@ -559,7 +559,7 @@ public void testPartitionedDmlError() {
559559
XGoogSpannerRequestId.of(getClientId(), -1, 1, 1),
560560
XGoogSpannerRequestId.of(getClientId(), -1, 2, 1)),
561561
actual);
562-
verifySameChannelId(actual);
562+
// Channel ID is determined by grpc-gcp, so we don't verify same channel ID here.
563563
assertEquals(XGoogSpannerRequestId.of(exception.getRequestId()), actual.get(1));
564564
}
565565

@@ -586,7 +586,7 @@ public void testPartitionedDmlAborted() {
586586
XGoogSpannerRequestId.of(getClientId(), -1, 3, 1),
587587
XGoogSpannerRequestId.of(getClientId(), -1, 4, 1)),
588588
actual);
589-
verifySameChannelId(actual);
589+
// Channel ID is determined by grpc-gcp, so we don't verify same channel ID here.
590590
}
591591

592592
@Test
@@ -612,7 +612,7 @@ public void testPartitionedDmlUnavailable() {
612612
XGoogSpannerRequestId.of(getClientId(), -1, 3, 1),
613613
XGoogSpannerRequestId.of(getClientId(), -1, 4, 1)),
614614
actual);
615-
verifySameChannelId(actual);
615+
// Channel ID is determined by grpc-gcp, so we don't verify same channel ID here.
616616
}
617617

618618
@Test
@@ -648,7 +648,7 @@ public void testPartitionedDmlUnavailableWithResumeToken() {
648648
XGoogSpannerRequestId.of(getClientId(), -1, 2, 1),
649649
XGoogSpannerRequestId.of(getClientId(), -1, 2, 2)),
650650
actual);
651-
verifySameChannelId(actual);
651+
// Channel ID is determined by grpc-gcp, so we don't verify same channel ID here.
652652
}
653653

654654
@Test

google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,12 +1203,22 @@ public void testExperimentalHostOptions() {
12031203
}
12041204

12051205
@Test
1206-
public void testDynamicChannelPoolingEnabledByDefault() {
1206+
public void testDynamicChannelPoolingDisabledByDefault() {
12071207
SpannerOptions options =
12081208
SpannerOptions.newBuilder()
12091209
.setProjectId("test-project")
12101210
.setCredentials(NoCredentials.getInstance())
1211-
.enableGrpcGcpExtension()
1211+
.build();
1212+
assertFalse(options.isDynamicChannelPoolEnabled());
1213+
}
1214+
1215+
@Test
1216+
public void testDynamicChannelPoolingEnabledExplicitly() {
1217+
SpannerOptions options =
1218+
SpannerOptions.newBuilder()
1219+
.setProjectId("test-project")
1220+
.setCredentials(NoCredentials.getInstance())
1221+
.enableDynamicChannelPool()
12121222
.build();
12131223
assertTrue(options.isDynamicChannelPoolEnabled());
12141224
assertEquals(
@@ -1230,7 +1240,7 @@ public void testDynamicChannelPoolingDisabledWhenNumChannelsSet() {
12301240
SpannerOptions.newBuilder()
12311241
.setProjectId("test-project")
12321242
.setCredentials(NoCredentials.getInstance())
1233-
.enableGrpcGcpExtension()
1243+
.enableDynamicChannelPool()
12341244
.setNumChannels(5) // Explicitly setting numChannels should disable DCP.
12351245
.build();
12361246
assertFalse(options.isDynamicChannelPoolEnabled());
@@ -1243,7 +1253,7 @@ public void testDynamicChannelPoolingDisabledExplicitly() {
12431253
SpannerOptions.newBuilder()
12441254
.setProjectId("test-project")
12451255
.setCredentials(NoCredentials.getInstance())
1246-
.enableGrpcGcpExtension()
1256+
.enableDynamicChannelPool()
12471257
.disableDynamicChannelPool()
12481258
.build();
12491259
assertFalse(options.isDynamicChannelPoolEnabled());
@@ -1256,7 +1266,7 @@ public void testDynamicChannelPoolingCustomSettings() {
12561266
SpannerOptions.newBuilder()
12571267
.setProjectId("test-project")
12581268
.setCredentials(NoCredentials.getInstance())
1259-
.enableGrpcGcpExtension()
1269+
.enableDynamicChannelPool()
12601270
.setDynamicPoolInitialSize(6)
12611271
.setDynamicPoolMaxChannels(15)
12621272
.setDynamicPoolMinChannels(3)

google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java

Lines changed: 66 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,25 @@
2424
import static org.junit.Assert.assertEquals;
2525
import static org.junit.Assert.assertNotNull;
2626

27+
import com.google.api.gax.grpc.GrpcInterceptorProvider;
2728
import com.google.cloud.NoCredentials;
29+
import com.google.cloud.grpc.GcpManagedChannel;
2830
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
2931
import com.google.cloud.spanner.Options.RpcPriority;
3032
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
33+
import com.google.common.collect.ImmutableList;
3134
import com.google.protobuf.ListValue;
3235
import com.google.spanner.v1.ResultSetMetadata;
3336
import com.google.spanner.v1.SpannerGrpc;
3437
import com.google.spanner.v1.StructType;
3538
import com.google.spanner.v1.StructType.Field;
3639
import com.google.spanner.v1.TypeCode;
37-
import io.grpc.Context;
38-
import io.grpc.Contexts;
39-
import io.grpc.Metadata;
40+
import io.grpc.CallOptions;
41+
import io.grpc.Channel;
42+
import io.grpc.ClientCall;
43+
import io.grpc.ClientInterceptor;
44+
import io.grpc.MethodDescriptor;
4045
import io.grpc.Server;
41-
import io.grpc.ServerCall;
42-
import io.grpc.ServerCallHandler;
43-
import io.grpc.ServerInterceptor;
4446
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
4547
import java.net.InetSocketAddress;
4648
import java.util.Set;
@@ -93,10 +95,11 @@ public class TransactionChannelHintTest {
9395
private static MockSpannerServiceImpl mockSpanner;
9496
private static Server server;
9597
private static InetSocketAddress address;
96-
// Track channel hints (from X-Goog-Spanner-Request-Id header) per RPC method
97-
private static final Set<Long> executeSqlChannelHints = ConcurrentHashMap.newKeySet();
98-
private static final Set<Long> beginTransactionChannelHints = ConcurrentHashMap.newKeySet();
99-
private static final Set<Long> streamingReadChannelHints = ConcurrentHashMap.newKeySet();
98+
// Track logical affinity keys (before grpc-gcp routing) per RPC method.
99+
// These are captured by a client interceptor to verify channel affinity consistency.
100+
private static final Set<String> executeSqlAffinityKeys = ConcurrentHashMap.newKeySet();
101+
private static final Set<String> beginTransactionAffinityKeys = ConcurrentHashMap.newKeySet();
102+
private static final Set<String> streamingReadAffinityKeys = ConcurrentHashMap.newKeySet();
100103
private static Level originalLogLevel;
101104

102105
@BeforeClass
@@ -109,49 +112,40 @@ public static void startServer() throws Exception {
109112
StatementResult.query(READ_ONE_KEY_VALUE_STATEMENT, READ_ONE_KEY_VALUE_RESULTSET));
110113

111114
address = new InetSocketAddress("localhost", 0);
112-
server =
113-
NettyServerBuilder.forAddress(address)
114-
.addService(mockSpanner)
115-
// Add a server interceptor to extract channel hints from X-Goog-Spanner-Request-Id
116-
// header. This verifies that all operations in a transaction use the same channel hint.
117-
.intercept(
118-
new ServerInterceptor() {
119-
@Override
120-
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
121-
ServerCall<ReqT, RespT> call,
122-
Metadata headers,
123-
ServerCallHandler<ReqT, RespT> next) {
124-
// Extract channel hint from X-Goog-Spanner-Request-Id header
125-
String requestId = headers.get(XGoogSpannerRequestId.REQUEST_ID_HEADER_KEY);
126-
if (requestId != null) {
127-
// Format:
128-
// <version>.<randProcessId>.<nthClientId>.<nthChannelId>.<nthRequest>.<attempt>
129-
String[] parts = requestId.split("\\.");
130-
if (parts.length >= 4) {
131-
try {
132-
long channelHint = Long.parseLong(parts[3]);
133-
if (call.getMethodDescriptor()
134-
.equals(SpannerGrpc.getExecuteStreamingSqlMethod())) {
135-
executeSqlChannelHints.add(channelHint);
136-
}
137-
if (call.getMethodDescriptor()
138-
.equals(SpannerGrpc.getStreamingReadMethod())) {
139-
streamingReadChannelHints.add(channelHint);
140-
}
141-
if (call.getMethodDescriptor()
142-
.equals(SpannerGrpc.getBeginTransactionMethod())) {
143-
beginTransactionChannelHints.add(channelHint);
144-
}
145-
} catch (NumberFormatException e) {
146-
// Ignore parse errors
147-
}
148-
}
149-
}
150-
return Contexts.interceptCall(Context.current(), call, headers, next);
115+
server = NettyServerBuilder.forAddress(address).addService(mockSpanner).build().start();
116+
}
117+
118+
/**
119+
* Creates a client interceptor that captures the logical affinity key before grpc-gcp routes the
120+
* request. This allows us to verify that all operations within a transaction use the same logical
121+
* channel affinity, even though the physical channel ID may vary.
122+
*/
123+
private static GrpcInterceptorProvider createAffinityKeyInterceptorProvider() {
124+
return () ->
125+
ImmutableList.of(
126+
new ClientInterceptor() {
127+
@Override
128+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
129+
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
130+
// Capture the AFFINITY_KEY before grpc-gcp processes it
131+
String affinityKey = callOptions.getOption(GcpManagedChannel.AFFINITY_KEY);
132+
if (affinityKey != null) {
133+
String methodName = method.getFullMethodName();
134+
if (methodName.equals(
135+
SpannerGrpc.getExecuteStreamingSqlMethod().getFullMethodName())) {
136+
executeSqlAffinityKeys.add(affinityKey);
137+
}
138+
if (methodName.equals(SpannerGrpc.getStreamingReadMethod().getFullMethodName())) {
139+
streamingReadAffinityKeys.add(affinityKey);
140+
}
141+
if (methodName.equals(
142+
SpannerGrpc.getBeginTransactionMethod().getFullMethodName())) {
143+
beginTransactionAffinityKeys.add(affinityKey);
151144
}
152-
})
153-
.build()
154-
.start();
145+
}
146+
return next.newCall(method, callOptions);
147+
}
148+
});
155149
}
156150

157151
@AfterClass
@@ -176,9 +170,9 @@ public static void resetLogging() {
176170
@After
177171
public void reset() {
178172
mockSpanner.reset();
179-
executeSqlChannelHints.clear();
180-
streamingReadChannelHints.clear();
181-
beginTransactionChannelHints.clear();
173+
executeSqlAffinityKeys.clear();
174+
streamingReadAffinityKeys.clear();
175+
beginTransactionAffinityKeys.clear();
182176
}
183177

184178
private SpannerOptions createSpannerOptions() {
@@ -193,6 +187,7 @@ private SpannerOptions createSpannerOptions() {
193187
.setCompressorName("gzip")
194188
.setHost("http://" + endpoint)
195189
.setCredentials(NoCredentials.getInstance())
190+
.setInterceptorProvider(createAffinityKeyInterceptorProvider())
196191
.setSessionPoolOption(
197192
SessionPoolOptions.newBuilder().setSkipVerifyingBeginTransactionForMuxRW(true).build())
198193
.build();
@@ -206,7 +201,8 @@ public void testSingleUseReadOnlyTransaction_usesSingleChannelHint() {
206201
while (resultSet.next()) {}
207202
}
208203
}
209-
assertEquals(1, executeSqlChannelHints.size());
204+
// All ExecuteSql calls should use the same logical affinity key
205+
assertEquals(1, executeSqlAffinityKeys.size());
210206
}
211207

212208
@Test
@@ -220,7 +216,8 @@ public void testSingleUseReadOnlyTransaction_withTimestampBound_usesSingleChanne
220216
while (resultSet.next()) {}
221217
}
222218
}
223-
assertEquals(1, executeSqlChannelHints.size());
219+
// All ExecuteSql calls should use the same logical affinity key
220+
assertEquals(1, executeSqlAffinityKeys.size());
224221
}
225222

226223
@Test
@@ -236,10 +233,10 @@ public void testReadOnlyTransaction_usesSingleChannelHint() {
236233
}
237234
}
238235
}
239-
// All ExecuteSql calls within the transaction should use the same channel hint
240-
assertEquals(1, executeSqlChannelHints.size());
241-
// BeginTransaction should use a single channel hint
242-
assertEquals(1, beginTransactionChannelHints.size());
236+
// All ExecuteSql calls within the transaction should use the same logical affinity key
237+
assertEquals(1, executeSqlAffinityKeys.size());
238+
// BeginTransaction should use a single logical affinity key
239+
assertEquals(1, beginTransactionAffinityKeys.size());
243240
}
244241

245242
@Test
@@ -256,10 +253,10 @@ public void testReadOnlyTransaction_withTimestampBound_usesSingleChannelHint() {
256253
}
257254
}
258255
}
259-
// All ExecuteSql calls within the transaction should use the same channel hint
260-
assertEquals(1, executeSqlChannelHints.size());
261-
// BeginTransaction should use a single channel hint
262-
assertEquals(1, beginTransactionChannelHints.size());
256+
// All ExecuteSql calls within the transaction should use the same logical affinity key
257+
assertEquals(1, executeSqlAffinityKeys.size());
258+
// BeginTransaction should use a single logical affinity key
259+
assertEquals(1, beginTransactionAffinityKeys.size());
263260
}
264261

265262
@Test
@@ -288,7 +285,8 @@ public void testTransactionManager_usesSingleChannelHint() {
288285
}
289286
}
290287
}
291-
assertEquals(1, executeSqlChannelHints.size());
288+
// All ExecuteSql calls within the transaction should use the same logical affinity key
289+
assertEquals(1, executeSqlAffinityKeys.size());
292290
}
293291

294292
@Test
@@ -318,6 +316,7 @@ public void testTransactionRunner_usesSingleChannelHint() {
318316
return null;
319317
});
320318
}
321-
assertEquals(1, streamingReadChannelHints.size());
319+
// All StreamingRead calls within the transaction should use the same logical affinity key
320+
assertEquals(1, streamingReadAffinityKeys.size());
322321
}
323322
}

0 commit comments

Comments
 (0)