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

Commit 418c90e

Browse files
committed
feat: Add Dynamic Channel Pooling (DCP) support to Connection API
1 parent 3ad105a commit 418c90e

5 files changed

Lines changed: 351 additions & 0 deletions

File tree

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@
2626
import static com.google.cloud.spanner.connection.ConnectionProperties.CREDENTIALS_URL;
2727
import static com.google.cloud.spanner.connection.ConnectionProperties.DATABASE_ROLE;
2828
import static com.google.cloud.spanner.connection.ConnectionProperties.DATA_BOOST_ENABLED;
29+
import static com.google.cloud.spanner.connection.ConnectionProperties.DCP_INITIAL_CHANNELS;
30+
import static com.google.cloud.spanner.connection.ConnectionProperties.DCP_MAX_CHANNELS;
31+
import static com.google.cloud.spanner.connection.ConnectionProperties.DCP_MIN_CHANNELS;
2932
import static com.google.cloud.spanner.connection.ConnectionProperties.DIALECT;
3033
import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_API_TRACING;
3134
import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_DIRECT_ACCESS;
35+
import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_DYNAMIC_CHANNEL_POOL;
3236
import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_END_TO_END_TRACING;
3337
import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_EXTENDED_TRACING;
3438
import static com.google.cloud.spanner.connection.ConnectionProperties.ENCODED_CREDENTIALS;
@@ -155,6 +159,10 @@ public class ConnectionOptions {
155159
static final Integer DEFAULT_MIN_SESSIONS = null;
156160
static final Integer DEFAULT_MAX_SESSIONS = null;
157161
static final Integer DEFAULT_NUM_CHANNELS = null;
162+
static final Boolean DEFAULT_ENABLE_DYNAMIC_CHANNEL_POOL = null;
163+
static final Integer DEFAULT_DCP_MIN_CHANNELS = null;
164+
static final Integer DEFAULT_DCP_MAX_CHANNELS = null;
165+
static final Integer DEFAULT_DCP_INITIAL_CHANNELS = null;
158166
static final String DEFAULT_ENDPOINT = null;
159167
static final String DEFAULT_CHANNEL_PROVIDER = null;
160168
static final String DEFAULT_DATABASE_ROLE = null;
@@ -252,6 +260,18 @@ public class ConnectionOptions {
252260
/** Name of the 'numChannels' connection property. */
253261
public static final String NUM_CHANNELS_PROPERTY_NAME = "numChannels";
254262

263+
/** Name of the 'enableDynamicChannelPool' connection property. */
264+
public static final String ENABLE_DYNAMIC_CHANNEL_POOL_PROPERTY_NAME = "enableDynamicChannelPool";
265+
266+
/** Name of the 'dcpMinChannels' connection property. */
267+
public static final String DCP_MIN_CHANNELS_PROPERTY_NAME = "dcpMinChannels";
268+
269+
/** Name of the 'dcpMaxChannels' connection property. */
270+
public static final String DCP_MAX_CHANNELS_PROPERTY_NAME = "dcpMaxChannels";
271+
272+
/** Name of the 'dcpInitialChannels' connection property. */
273+
public static final String DCP_INITIAL_CHANNELS_PROPERTY_NAME = "dcpInitialChannels";
274+
255275
/** Name of the 'endpoint' connection property. */
256276
public static final String ENDPOINT_PROPERTY_NAME = "endpoint";
257277

@@ -991,6 +1011,26 @@ public Integer getNumChannels() {
9911011
return getInitialConnectionPropertyValue(NUM_CHANNELS);
9921012
}
9931013

1014+
/** Whether dynamic channel pooling is enabled for this connection. */
1015+
public Boolean isEnableDynamicChannelPool() {
1016+
return getInitialConnectionPropertyValue(ENABLE_DYNAMIC_CHANNEL_POOL);
1017+
}
1018+
1019+
/** The minimum number of channels in the dynamic channel pool. */
1020+
public Integer getDcpMinChannels() {
1021+
return getInitialConnectionPropertyValue(DCP_MIN_CHANNELS);
1022+
}
1023+
1024+
/** The maximum number of channels in the dynamic channel pool. */
1025+
public Integer getDcpMaxChannels() {
1026+
return getInitialConnectionPropertyValue(DCP_MAX_CHANNELS);
1027+
}
1028+
1029+
/** The initial number of channels in the dynamic channel pool. */
1030+
public Integer getDcpInitialChannels() {
1031+
return getInitialConnectionPropertyValue(DCP_INITIAL_CHANNELS);
1032+
}
1033+
9941034
/** Calls the getChannelProvider() method from the supplied class. */
9951035
public TransportChannelProvider getChannelProvider() {
9961036
String channelProvider = getInitialConnectionPropertyValue(CHANNEL_PROVIDER);

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
import static com.google.cloud.spanner.connection.ConnectionOptions.CREDENTIALS_PROVIDER_PROPERTY_NAME;
3030
import static com.google.cloud.spanner.connection.ConnectionOptions.DATABASE_ROLE_PROPERTY_NAME;
3131
import static com.google.cloud.spanner.connection.ConnectionOptions.DATA_BOOST_ENABLED_PROPERTY_NAME;
32+
import static com.google.cloud.spanner.connection.ConnectionOptions.DCP_INITIAL_CHANNELS_PROPERTY_NAME;
33+
import static com.google.cloud.spanner.connection.ConnectionOptions.DCP_MAX_CHANNELS_PROPERTY_NAME;
34+
import static com.google.cloud.spanner.connection.ConnectionOptions.DCP_MIN_CHANNELS_PROPERTY_NAME;
3235
import static com.google.cloud.spanner.connection.ConnectionOptions.DDL_IN_TRANSACTION_MODE_PROPERTY_NAME;
3336
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTOCOMMIT;
3437
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTO_BATCH_DML;
@@ -42,10 +45,14 @@
4245
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CREDENTIALS;
4346
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATABASE_ROLE;
4447
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATA_BOOST_ENABLED;
48+
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DCP_INITIAL_CHANNELS;
49+
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DCP_MAX_CHANNELS;
50+
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DCP_MIN_CHANNELS;
4551
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DDL_IN_TRANSACTION_MODE;
4652
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DEFAULT_SEQUENCE_KIND;
4753
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE;
4854
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_API_TRACING;
55+
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_DYNAMIC_CHANNEL_POOL;
4956
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_END_TO_END_TRACING;
5057
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_EXTENDED_TRACING;
5158
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENDPOINT;
@@ -75,6 +82,7 @@
7582
import static com.google.cloud.spanner.connection.ConnectionOptions.DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE_NAME;
7683
import static com.google.cloud.spanner.connection.ConnectionOptions.DIALECT_PROPERTY_NAME;
7784
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_API_TRACING_PROPERTY_NAME;
85+
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_DYNAMIC_CHANNEL_POOL_PROPERTY_NAME;
7886
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_END_TO_END_TRACING_PROPERTY_NAME;
7987
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_EXTENDED_TRACING_PROPERTY_NAME;
8088
import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_GRPC_INTERCEPTOR_PROVIDER_SYSTEM_PROPERTY;
@@ -443,6 +451,43 @@ public class ConnectionProperties {
443451
DEFAULT_NUM_CHANNELS,
444452
NonNegativeIntegerConverter.INSTANCE,
445453
Context.STARTUP);
454+
static final ConnectionProperty<Boolean> ENABLE_DYNAMIC_CHANNEL_POOL =
455+
create(
456+
ENABLE_DYNAMIC_CHANNEL_POOL_PROPERTY_NAME,
457+
"Enable dynamic channel pooling for automatic gRPC channel scaling. When enabled, the "
458+
+ "client will automatically scale the number of channels based on load. Setting "
459+
+ "numChannels will disable dynamic channel pooling even if this is set to true.",
460+
DEFAULT_ENABLE_DYNAMIC_CHANNEL_POOL,
461+
BOOLEANS,
462+
BooleanConverter.INSTANCE,
463+
Context.STARTUP);
464+
static final ConnectionProperty<Integer> DCP_MIN_CHANNELS =
465+
create(
466+
DCP_MIN_CHANNELS_PROPERTY_NAME,
467+
"The minimum number of channels in the dynamic channel pool. Only used when "
468+
+ "enableDynamicChannelPool is true. The default is "
469+
+ "SpannerOptions.DEFAULT_DYNAMIC_POOL_MIN_CHANNELS (2).",
470+
DEFAULT_DCP_MIN_CHANNELS,
471+
NonNegativeIntegerConverter.INSTANCE,
472+
Context.STARTUP);
473+
static final ConnectionProperty<Integer> DCP_MAX_CHANNELS =
474+
create(
475+
DCP_MAX_CHANNELS_PROPERTY_NAME,
476+
"The maximum number of channels in the dynamic channel pool. Only used when "
477+
+ "enableDynamicChannelPool is true. The default is "
478+
+ "SpannerOptions.DEFAULT_DYNAMIC_POOL_MAX_CHANNELS (10).",
479+
DEFAULT_DCP_MAX_CHANNELS,
480+
NonNegativeIntegerConverter.INSTANCE,
481+
Context.STARTUP);
482+
static final ConnectionProperty<Integer> DCP_INITIAL_CHANNELS =
483+
create(
484+
DCP_INITIAL_CHANNELS_PROPERTY_NAME,
485+
"The initial number of channels in the dynamic channel pool. Only used when "
486+
+ "enableDynamicChannelPool is true. The default is "
487+
+ "SpannerOptions.DEFAULT_DYNAMIC_POOL_INITIAL_SIZE (4).",
488+
DEFAULT_DCP_INITIAL_CHANNELS,
489+
NonNegativeIntegerConverter.INSTANCE,
490+
Context.STARTUP);
446491
static final ConnectionProperty<String> CHANNEL_PROVIDER =
447492
create(
448493
CHANNEL_PROVIDER_PROPERTY_NAME,

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
package com.google.cloud.spanner.connection;
1818

1919
import com.google.cloud.NoCredentials;
20+
import com.google.cloud.grpc.GcpManagedChannelOptions.GcpChannelPoolOptions;
2021
import com.google.cloud.spanner.DecodeMode;
2122
import com.google.cloud.spanner.ErrorCode;
2223
import com.google.cloud.spanner.SessionPoolOptions;
2324
import com.google.cloud.spanner.Spanner;
2425
import com.google.cloud.spanner.SpannerException;
2526
import com.google.cloud.spanner.SpannerExceptionFactory;
27+
import com.google.cloud.spanner.SpannerOptions;
2628
import com.google.common.annotations.VisibleForTesting;
2729
import com.google.common.base.MoreObjects;
2830
import com.google.common.base.Preconditions;
@@ -152,6 +154,10 @@ static class SpannerPoolKey {
152154
private final CredentialsKey credentialsKey;
153155
private final SessionPoolOptions sessionPoolOptions;
154156
private final Integer numChannels;
157+
private final Boolean enableDynamicChannelPool;
158+
private final Integer dcpMinChannels;
159+
private final Integer dcpMaxChannels;
160+
private final Integer dcpInitialChannels;
155161
private final boolean usePlainText;
156162
private final String userAgent;
157163
private final String databaseRole;
@@ -190,6 +196,10 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException {
190196
? SessionPoolOptions.newBuilder().build()
191197
: options.getSessionPoolOptions();
192198
this.numChannels = options.getNumChannels();
199+
this.enableDynamicChannelPool = options.isEnableDynamicChannelPool();
200+
this.dcpMinChannels = options.getDcpMinChannels();
201+
this.dcpMaxChannels = options.getDcpMaxChannels();
202+
this.dcpInitialChannels = options.getDcpInitialChannels();
193203
this.usePlainText = options.isUsePlainText();
194204
this.userAgent = options.getUserAgent();
195205
this.routeToLeader = options.isRouteToLeader();
@@ -217,6 +227,10 @@ public boolean equals(Object o) {
217227
&& Objects.equals(this.credentialsKey, other.credentialsKey)
218228
&& Objects.equals(this.sessionPoolOptions, other.sessionPoolOptions)
219229
&& Objects.equals(this.numChannels, other.numChannels)
230+
&& Objects.equals(this.enableDynamicChannelPool, other.enableDynamicChannelPool)
231+
&& Objects.equals(this.dcpMinChannels, other.dcpMinChannels)
232+
&& Objects.equals(this.dcpMaxChannels, other.dcpMaxChannels)
233+
&& Objects.equals(this.dcpInitialChannels, other.dcpInitialChannels)
220234
&& Objects.equals(this.databaseRole, other.databaseRole)
221235
&& Objects.equals(this.usePlainText, other.usePlainText)
222236
&& Objects.equals(this.userAgent, other.userAgent)
@@ -243,6 +257,10 @@ public int hashCode() {
243257
this.credentialsKey,
244258
this.sessionPoolOptions,
245259
this.numChannels,
260+
this.enableDynamicChannelPool,
261+
this.dcpMinChannels,
262+
this.dcpMaxChannels,
263+
this.dcpInitialChannels,
246264
this.usePlainText,
247265
this.databaseRole,
248266
this.userAgent,
@@ -403,6 +421,42 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
403421
if (key.numChannels != null) {
404422
builder.setNumChannels(key.numChannels);
405423
}
424+
// Configure Dynamic Channel Pooling (DCP) if enabled.
425+
// Note: Setting numChannels disables DCP even if enableDynamicChannelPool is true.
426+
if (Boolean.TRUE.equals(key.enableDynamicChannelPool) && key.numChannels == null) {
427+
builder.enableDynamicChannelPool();
428+
// Build custom GcpChannelPoolOptions if any DCP-specific options are set.
429+
if (key.dcpMinChannels != null
430+
|| key.dcpMaxChannels != null
431+
|| key.dcpInitialChannels != null) {
432+
// Build GcpChannelPoolOptions from scratch with custom values or Spanner defaults.
433+
int minChannels =
434+
key.dcpMinChannels != null
435+
? key.dcpMinChannels
436+
: SpannerOptions.DEFAULT_DYNAMIC_POOL_MIN_CHANNELS;
437+
int maxChannels =
438+
key.dcpMaxChannels != null
439+
? key.dcpMaxChannels
440+
: SpannerOptions.DEFAULT_DYNAMIC_POOL_MAX_CHANNELS;
441+
int initChannels =
442+
key.dcpInitialChannels != null
443+
? key.dcpInitialChannels
444+
: SpannerOptions.DEFAULT_DYNAMIC_POOL_INITIAL_SIZE;
445+
GcpChannelPoolOptions poolOptions =
446+
GcpChannelPoolOptions.newBuilder()
447+
.setMinSize(minChannels)
448+
.setMaxSize(maxChannels)
449+
.setInitSize(initChannels)
450+
.setDynamicScaling(
451+
SpannerOptions.DEFAULT_DYNAMIC_POOL_MIN_RPC,
452+
SpannerOptions.DEFAULT_DYNAMIC_POOL_MAX_RPC,
453+
SpannerOptions.DEFAULT_DYNAMIC_POOL_SCALE_DOWN_INTERVAL)
454+
.setAffinityKeyLifetime(SpannerOptions.DEFAULT_DYNAMIC_POOL_AFFINITY_KEY_LIFETIME)
455+
.setCleanupInterval(SpannerOptions.DEFAULT_DYNAMIC_POOL_CLEANUP_INTERVAL)
456+
.build();
457+
builder.setGcpChannelPoolOptions(poolOptions);
458+
}
459+
}
406460
if (options.getChannelProvider() != null) {
407461
builder.setChannelProvider(options.getChannelProvider());
408462
}

google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,4 +1418,110 @@ public void testUniverseDomain() {
14181418

14191419
connection.close();
14201420
}
1421+
1422+
@Test
1423+
public void testEnableDynamicChannelPool() {
1424+
// Default value
1425+
assertNull(
1426+
ConnectionOptions.newBuilder()
1427+
.setUri(
1428+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database")
1429+
.setCredentials(NoCredentials.getInstance())
1430+
.build()
1431+
.isEnableDynamicChannelPool());
1432+
// Enabled
1433+
assertTrue(
1434+
ConnectionOptions.newBuilder()
1435+
.setUri(
1436+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?enableDynamicChannelPool=true")
1437+
.setCredentials(NoCredentials.getInstance())
1438+
.build()
1439+
.isEnableDynamicChannelPool());
1440+
// Disabled
1441+
assertFalse(
1442+
ConnectionOptions.newBuilder()
1443+
.setUri(
1444+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?enableDynamicChannelPool=false")
1445+
.setCredentials(NoCredentials.getInstance())
1446+
.build()
1447+
.isEnableDynamicChannelPool());
1448+
}
1449+
1450+
@Test
1451+
public void testDcpMinChannels() {
1452+
// Default value
1453+
assertNull(
1454+
ConnectionOptions.newBuilder()
1455+
.setUri(
1456+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database")
1457+
.setCredentials(NoCredentials.getInstance())
1458+
.build()
1459+
.getDcpMinChannels());
1460+
// Custom value
1461+
assertEquals(
1462+
Integer.valueOf(3),
1463+
ConnectionOptions.newBuilder()
1464+
.setUri(
1465+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?dcpMinChannels=3")
1466+
.setCredentials(NoCredentials.getInstance())
1467+
.build()
1468+
.getDcpMinChannels());
1469+
}
1470+
1471+
@Test
1472+
public void testDcpMaxChannels() {
1473+
// Default value
1474+
assertNull(
1475+
ConnectionOptions.newBuilder()
1476+
.setUri(
1477+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database")
1478+
.setCredentials(NoCredentials.getInstance())
1479+
.build()
1480+
.getDcpMaxChannels());
1481+
// Custom value
1482+
assertEquals(
1483+
Integer.valueOf(15),
1484+
ConnectionOptions.newBuilder()
1485+
.setUri(
1486+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?dcpMaxChannels=15")
1487+
.setCredentials(NoCredentials.getInstance())
1488+
.build()
1489+
.getDcpMaxChannels());
1490+
}
1491+
1492+
@Test
1493+
public void testDcpInitialChannels() {
1494+
// Default value
1495+
assertNull(
1496+
ConnectionOptions.newBuilder()
1497+
.setUri(
1498+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database")
1499+
.setCredentials(NoCredentials.getInstance())
1500+
.build()
1501+
.getDcpInitialChannels());
1502+
// Custom value
1503+
assertEquals(
1504+
Integer.valueOf(5),
1505+
ConnectionOptions.newBuilder()
1506+
.setUri(
1507+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?dcpInitialChannels=5")
1508+
.setCredentials(NoCredentials.getInstance())
1509+
.build()
1510+
.getDcpInitialChannels());
1511+
}
1512+
1513+
@Test
1514+
public void testDcpWithAllOptions() {
1515+
ConnectionOptions options =
1516+
ConnectionOptions.newBuilder()
1517+
.setUri(
1518+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"
1519+
+ "?enableDynamicChannelPool=true;dcpMinChannels=3;dcpMaxChannels=15;dcpInitialChannels=5")
1520+
.setCredentials(NoCredentials.getInstance())
1521+
.build();
1522+
assertTrue(options.isEnableDynamicChannelPool());
1523+
assertEquals(Integer.valueOf(3), options.getDcpMinChannels());
1524+
assertEquals(Integer.valueOf(15), options.getDcpMaxChannels());
1525+
assertEquals(Integer.valueOf(5), options.getDcpInitialChannels());
1526+
}
14211527
}

0 commit comments

Comments
 (0)