Skip to content

Commit bba03a4

Browse files
authored
feat: Add Dynamic Channel Pooling (DCP) support to Connection API (#4299)
* feat: Add Dynamic Channel Pooling (DCP) support to Connection API * incorporate changes
1 parent 3ad105a commit bba03a4

File tree

5 files changed

+394
-0
lines changed

5 files changed

+394
-0
lines changed

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: 47 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,45 @@ 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+
+ "The default is currently false (disabled), but this may change to true in a "
461+
+ "future version. Set this property explicitly to ensure consistent behavior.",
462+
DEFAULT_ENABLE_DYNAMIC_CHANNEL_POOL,
463+
BOOLEANS,
464+
BooleanConverter.INSTANCE,
465+
Context.STARTUP);
466+
static final ConnectionProperty<Integer> DCP_MIN_CHANNELS =
467+
create(
468+
DCP_MIN_CHANNELS_PROPERTY_NAME,
469+
"The minimum number of channels in the dynamic channel pool. Only used when "
470+
+ "enableDynamicChannelPool is true. The default is "
471+
+ "SpannerOptions.DEFAULT_DYNAMIC_POOL_MIN_CHANNELS (2).",
472+
DEFAULT_DCP_MIN_CHANNELS,
473+
NonNegativeIntegerConverter.INSTANCE,
474+
Context.STARTUP);
475+
static final ConnectionProperty<Integer> DCP_MAX_CHANNELS =
476+
create(
477+
DCP_MAX_CHANNELS_PROPERTY_NAME,
478+
"The maximum number of channels in the dynamic channel pool. Only used when "
479+
+ "enableDynamicChannelPool is true. The default is "
480+
+ "SpannerOptions.DEFAULT_DYNAMIC_POOL_MAX_CHANNELS (10).",
481+
DEFAULT_DCP_MAX_CHANNELS,
482+
NonNegativeIntegerConverter.INSTANCE,
483+
Context.STARTUP);
484+
static final ConnectionProperty<Integer> DCP_INITIAL_CHANNELS =
485+
create(
486+
DCP_INITIAL_CHANNELS_PROPERTY_NAME,
487+
"The initial number of channels in the dynamic channel pool. Only used when "
488+
+ "enableDynamicChannelPool is true. The default is "
489+
+ "SpannerOptions.DEFAULT_DYNAMIC_POOL_INITIAL_SIZE (4).",
490+
DEFAULT_DCP_INITIAL_CHANNELS,
491+
NonNegativeIntegerConverter.INSTANCE,
492+
Context.STARTUP);
446493
static final ConnectionProperty<String> CHANNEL_PROVIDER =
447494
create(
448495
CHANNEL_PROVIDER_PROPERTY_NAME,

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

Lines changed: 62 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,50 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
403421
if (key.numChannels != null) {
404422
builder.setNumChannels(key.numChannels);
405423
}
424+
// Configure Dynamic Channel Pooling (DCP) based on explicit user setting.
425+
// Note: Setting numChannels disables DCP even if enableDynamicChannelPool is true.
426+
if (key.enableDynamicChannelPool != null && key.numChannels == null) {
427+
if (Boolean.TRUE.equals(key.enableDynamicChannelPool)) {
428+
builder.enableDynamicChannelPool();
429+
// Build custom GcpChannelPoolOptions if any DCP-specific options are set.
430+
if (key.dcpMinChannels != null
431+
|| key.dcpMaxChannels != null
432+
|| key.dcpInitialChannels != null) {
433+
// Build GcpChannelPoolOptions from scratch with custom values or Spanner defaults.
434+
// Note: GcpChannelPoolOptions does not have a toBuilder() method, so we must
435+
// construct from scratch using SpannerOptions defaults for unspecified values.
436+
int minChannels =
437+
key.dcpMinChannels != null
438+
? key.dcpMinChannels
439+
: SpannerOptions.DEFAULT_DYNAMIC_POOL_MIN_CHANNELS;
440+
int maxChannels =
441+
key.dcpMaxChannels != null
442+
? key.dcpMaxChannels
443+
: SpannerOptions.DEFAULT_DYNAMIC_POOL_MAX_CHANNELS;
444+
int initChannels =
445+
key.dcpInitialChannels != null
446+
? key.dcpInitialChannels
447+
: SpannerOptions.DEFAULT_DYNAMIC_POOL_INITIAL_SIZE;
448+
GcpChannelPoolOptions poolOptions =
449+
GcpChannelPoolOptions.newBuilder()
450+
.setMinSize(minChannels)
451+
.setMaxSize(maxChannels)
452+
.setInitSize(initChannels)
453+
.setDynamicScaling(
454+
SpannerOptions.DEFAULT_DYNAMIC_POOL_MIN_RPC,
455+
SpannerOptions.DEFAULT_DYNAMIC_POOL_MAX_RPC,
456+
SpannerOptions.DEFAULT_DYNAMIC_POOL_SCALE_DOWN_INTERVAL)
457+
.setAffinityKeyLifetime(SpannerOptions.DEFAULT_DYNAMIC_POOL_AFFINITY_KEY_LIFETIME)
458+
.setCleanupInterval(SpannerOptions.DEFAULT_DYNAMIC_POOL_CLEANUP_INTERVAL)
459+
.build();
460+
builder.setGcpChannelPoolOptions(poolOptions);
461+
}
462+
} else {
463+
// Explicitly disable DCP when enableDynamicChannelPool=false.
464+
// This ensures consistent behavior even if the default changes in the future.
465+
builder.disableDynamicChannelPool();
466+
}
467+
}
406468
if (options.getChannelProvider() != null) {
407469
builder.setChannelProvider(options.getChannelProvider());
408470
}

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

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,4 +1418,113 @@ 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+
}
1441+
1442+
@Test
1443+
public void testDisableDynamicChannelPool() {
1444+
assertFalse(
1445+
ConnectionOptions.newBuilder()
1446+
.setUri(
1447+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?enableDynamicChannelPool=false")
1448+
.setCredentials(NoCredentials.getInstance())
1449+
.build()
1450+
.isEnableDynamicChannelPool());
1451+
}
1452+
1453+
@Test
1454+
public void testDcpMinChannels() {
1455+
// Default value
1456+
assertNull(
1457+
ConnectionOptions.newBuilder()
1458+
.setUri(
1459+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database")
1460+
.setCredentials(NoCredentials.getInstance())
1461+
.build()
1462+
.getDcpMinChannels());
1463+
// Custom value
1464+
assertEquals(
1465+
Integer.valueOf(3),
1466+
ConnectionOptions.newBuilder()
1467+
.setUri(
1468+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?dcpMinChannels=3")
1469+
.setCredentials(NoCredentials.getInstance())
1470+
.build()
1471+
.getDcpMinChannels());
1472+
}
1473+
1474+
@Test
1475+
public void testDcpMaxChannels() {
1476+
// Default value
1477+
assertNull(
1478+
ConnectionOptions.newBuilder()
1479+
.setUri(
1480+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database")
1481+
.setCredentials(NoCredentials.getInstance())
1482+
.build()
1483+
.getDcpMaxChannels());
1484+
// Custom value
1485+
assertEquals(
1486+
Integer.valueOf(15),
1487+
ConnectionOptions.newBuilder()
1488+
.setUri(
1489+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?dcpMaxChannels=15")
1490+
.setCredentials(NoCredentials.getInstance())
1491+
.build()
1492+
.getDcpMaxChannels());
1493+
}
1494+
1495+
@Test
1496+
public void testDcpInitialChannels() {
1497+
// Default value
1498+
assertNull(
1499+
ConnectionOptions.newBuilder()
1500+
.setUri(
1501+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database")
1502+
.setCredentials(NoCredentials.getInstance())
1503+
.build()
1504+
.getDcpInitialChannels());
1505+
// Custom value
1506+
assertEquals(
1507+
Integer.valueOf(5),
1508+
ConnectionOptions.newBuilder()
1509+
.setUri(
1510+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?dcpInitialChannels=5")
1511+
.setCredentials(NoCredentials.getInstance())
1512+
.build()
1513+
.getDcpInitialChannels());
1514+
}
1515+
1516+
@Test
1517+
public void testDcpWithAllOptions() {
1518+
ConnectionOptions options =
1519+
ConnectionOptions.newBuilder()
1520+
.setUri(
1521+
"cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"
1522+
+ "?enableDynamicChannelPool=true;dcpMinChannels=3;dcpMaxChannels=15;dcpInitialChannels=5")
1523+
.setCredentials(NoCredentials.getInstance())
1524+
.build();
1525+
assertTrue(options.isEnableDynamicChannelPool());
1526+
assertEquals(Integer.valueOf(3), options.getDcpMinChannels());
1527+
assertEquals(Integer.valueOf(15), options.getDcpMaxChannels());
1528+
assertEquals(Integer.valueOf(5), options.getDcpInitialChannels());
1529+
}
14211530
}

0 commit comments

Comments
 (0)