Skip to content

Commit 2b4f463

Browse files
committed
feat(spanner): enable dynamic channel pooling by default
1 parent 6886eb5 commit 2b4f463

File tree

4 files changed

+150
-9
lines changed

4 files changed

+150
-9
lines changed

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

Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@ public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {
151151
private final Duration partitionedDmlTimeout;
152152
private final boolean grpcGcpExtensionEnabled;
153153
private final GcpManagedChannelOptions grpcGcpOptions;
154+
// Whether dynamic channel pooling is enabled (via automatic gRPC-GCP enablement) by default.
155+
// This is derived from the builder flag at build time.
156+
private final boolean dynamicChannelPoolEnabled;
157+
// Dynamic Channel Pool parameters
158+
private final Integer dcpMaxRpcPerChannel;
159+
private final Integer dcpMinRpcPerChannel;
160+
private final Duration dcpScaleDownInterval;
161+
private final Integer dcpInitialSize;
162+
private final Integer dcpMaxChannels;
163+
private final Integer dcpMinChannels;
154164
private final boolean autoThrottleAdministrativeRequests;
155165
private final RetrySettings retryAdministrativeRequestsSettings;
156166
private final boolean trackTransactionStarter;
@@ -788,6 +798,13 @@ protected SpannerOptions(Builder builder) {
788798
partitionedDmlTimeout = builder.partitionedDmlTimeout;
789799
grpcGcpExtensionEnabled = builder.grpcGcpExtensionEnabled;
790800
grpcGcpOptions = builder.grpcGcpOptions;
801+
dynamicChannelPoolEnabled = builder.dynamicChannelPoolEnabled;
802+
dcpMaxRpcPerChannel = builder.dcpMaxRpcPerChannel;
803+
dcpMinRpcPerChannel = builder.dcpMinRpcPerChannel;
804+
dcpScaleDownInterval = builder.dcpScaleDownInterval;
805+
dcpInitialSize = builder.dcpInitialSize;
806+
dcpMaxChannels = builder.dcpMaxChannels;
807+
dcpMinChannels = builder.dcpMinChannels;
791808
autoThrottleAdministrativeRequests = builder.autoThrottleAdministrativeRequests;
792809
retryAdministrativeRequestsSettings = builder.retryAdministrativeRequestsSettings;
793810
trackTransactionStarter = builder.trackTransactionStarter;
@@ -1002,6 +1019,10 @@ public static class Builder
10021019
private Duration partitionedDmlTimeout = Duration.ofHours(2L);
10031020
private boolean grpcGcpExtensionEnabled = false;
10041021
private GcpManagedChannelOptions grpcGcpOptions;
1022+
// Tracks whether enable/disableGrpcGcpExtension has been explicitly called by the user.
1023+
private boolean grpcGcpExtensionExplicitlySet = false;
1024+
// Dynamic Channel Pool (DCP) toggle. Default: enabled.
1025+
private boolean dynamicChannelPoolEnabled = true;
10051026
private RetrySettings retryAdministrativeRequestsSettings =
10061027
DEFAULT_ADMIN_REQUESTS_LIMIT_EXCEEDED_RETRY_SETTINGS;
10071028
private boolean autoThrottleAdministrativeRequests = false;
@@ -1025,6 +1046,14 @@ public static class Builder
10251046
private boolean isExperimentalHost = false;
10261047
private TransactionOptions defaultTransactionOptions = TransactionOptions.getDefaultInstance();
10271048

1049+
// Dynamic Channel Pool configuration (defaults per dynamic_cahnnel_pooling.md)
1050+
private Integer dcpMaxRpcPerChannel = 25;
1051+
private Integer dcpMinRpcPerChannel = 15;
1052+
private Duration dcpScaleDownInterval = Duration.ofMinutes(3);
1053+
private Integer dcpInitialSize = 4;
1054+
private Integer dcpMaxChannels = 10;
1055+
private Integer dcpMinChannels = 2;
1056+
10281057
private static String createCustomClientLibToken(String token) {
10291058
return token + " " + ServiceOptions.getGoogApiClientLibName();
10301059
}
@@ -1532,30 +1561,87 @@ public Builder setExperimentalHost(String host) {
15321561
return this;
15331562
}
15341563

1535-
/**
1536-
* Enables gRPC-GCP extension with the default settings. Do not set
1537-
* GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS to true in combination with this option, as
1538-
* Multiplexed sessions are not supported for gRPC-GCP.
1539-
*/
1564+
/** Enables gRPC-GCP extension with the default settings. */
15401565
public Builder enableGrpcGcpExtension() {
15411566
return this.enableGrpcGcpExtension(null);
15421567
}
15431568

15441569
/**
15451570
* Enables gRPC-GCP extension and uses provided options for configuration. The metric registry
1546-
* and default Spanner metric labels will be added automatically. Do not set
1547-
* GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS to true in combination with this option, as
1548-
* Multiplexed sessions are not supported for gRPC-GCP.
1571+
* and default Spanner metric labels will be added automatically.
15491572
*/
15501573
public Builder enableGrpcGcpExtension(GcpManagedChannelOptions options) {
15511574
this.grpcGcpExtensionEnabled = true;
15521575
this.grpcGcpOptions = options;
1576+
this.grpcGcpExtensionExplicitlySet = true;
15531577
return this;
15541578
}
15551579

15561580
/** Disables gRPC-GCP extension. */
15571581
public Builder disableGrpcGcpExtension() {
15581582
this.grpcGcpExtensionEnabled = false;
1583+
this.grpcGcpExtensionExplicitlySet = true;
1584+
return this;
1585+
}
1586+
1587+
/**
1588+
* Enables or disables dynamic channel pooling. When enabled and no explicit number of channels
1589+
* has been configured and no custom {@link TransportChannelProvider} has been set, the client
1590+
* will automatically enable the gRPC-GCP channel pool. If multiplexed sessions are enabled,
1591+
* dynamic channel pooling will not be enabled.
1592+
*/
1593+
public Builder setDynamicChannelPoolEnabled(boolean enabled) {
1594+
this.dynamicChannelPoolEnabled = enabled;
1595+
return this;
1596+
}
1597+
1598+
// Granular DCP configuration setters with validation bounds
1599+
public Builder setDynamicPoolMaxRpc(int maxRpcPerChannel) {
1600+
Preconditions.checkArgument(maxRpcPerChannel >= 1 && maxRpcPerChannel <= 100,
1601+
"maxRpcPerChannel must be in [1, 100]");
1602+
this.dcpMaxRpcPerChannel = maxRpcPerChannel;
1603+
return this;
1604+
}
1605+
1606+
public Builder setDynamicPoolMinRpc(int minRpcPerChannel) {
1607+
Preconditions.checkArgument(minRpcPerChannel >= 1,
1608+
"minRpcPerChannel must be >= 1");
1609+
this.dcpMinRpcPerChannel = minRpcPerChannel;
1610+
return this;
1611+
}
1612+
1613+
public Builder setDynamicPoolScaleDownInterval(Duration interval) {
1614+
Preconditions.checkNotNull(interval, "interval cannot be null");
1615+
Preconditions.checkArgument(!interval.isNegative() && !interval.isZero(),
1616+
"interval must be > 0");
1617+
Preconditions.checkArgument(
1618+
interval.compareTo(Duration.ofSeconds(30)) >= 0,
1619+
"interval must be >= 30 seconds");
1620+
Preconditions.checkArgument(
1621+
interval.compareTo(Duration.ofMinutes(60)) <= 0,
1622+
"interval must be <= 60 minutes");
1623+
this.dcpScaleDownInterval = interval;
1624+
return this;
1625+
}
1626+
1627+
public Builder setDynamicPoolInitialSize(int initialSize) {
1628+
Preconditions.checkArgument(initialSize >= 1 && initialSize <= 256,
1629+
"initialSize must be in [1, 256]");
1630+
this.dcpInitialSize = initialSize;
1631+
return this;
1632+
}
1633+
1634+
public Builder setDynamicPoolMaxChannels(int maxChannels) {
1635+
Preconditions.checkArgument(maxChannels >= 1 && maxChannels <= 256,
1636+
"maxChannels must be in [1, 256]");
1637+
this.dcpMaxChannels = maxChannels;
1638+
return this;
1639+
}
1640+
1641+
public Builder setDynamicPoolMinChannels(int minChannels) {
1642+
Preconditions.checkArgument(minChannels >= 1,
1643+
"minChannels must be >= 1");
1644+
this.dcpMinChannels = minChannels;
15591645
return this;
15601646
}
15611647

@@ -1756,6 +1842,15 @@ public SpannerOptions build() {
17561842
} else if (isExperimentalHost && credentials == null) {
17571843
credentials = environment.getDefaultExperimentalHostCredentials();
17581844
}
1845+
// Auto-enable gRPC-GCP (dynamic channel pool) if allowed and not explicitly overridden.
1846+
if (!grpcGcpExtensionExplicitlySet && dynamicChannelPoolEnabled) {
1847+
boolean hasCustomChannelProvider = this.channelProvider != null;
1848+
boolean hasStaticNumChannels = this.numChannels != null;
1849+
if (!hasCustomChannelProvider && !hasStaticNumChannels) {
1850+
this.grpcGcpExtensionEnabled = true;
1851+
}
1852+
}
1853+
17591854
if (this.numChannels == null) {
17601855
this.numChannels =
17611856
this.grpcGcpExtensionEnabled ? GRPC_GCP_ENABLED_DEFAULT_CHANNELS : DEFAULT_CHANNELS;
@@ -1960,6 +2055,19 @@ public GcpManagedChannelOptions getGrpcGcpOptions() {
19602055
return grpcGcpOptions;
19612056
}
19622057

2058+
/** Returns whether dynamic channel pooling is enabled by default. */
2059+
public boolean isDynamicChannelPoolEnabled() {
2060+
return dynamicChannelPoolEnabled;
2061+
}
2062+
2063+
// Dynamic Channel Pool getters used by channel setup
2064+
public Integer getDcpMaxRpcPerChannel() { return dcpMaxRpcPerChannel; }
2065+
public Integer getDcpMinRpcPerChannel() { return dcpMinRpcPerChannel; }
2066+
public Duration getDcpScaleDownInterval() { return dcpScaleDownInterval; }
2067+
public Integer getDcpInitialSize() { return dcpInitialSize; }
2068+
public Integer getDcpMaxChannels() { return dcpMaxChannels; }
2069+
public Integer getDcpMinChannels() { return dcpMinChannels; }
2070+
19632071
public boolean isAutoThrottleAdministrativeRequests() {
19642072
return autoThrottleAdministrativeRequests;
19652073
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,8 +580,34 @@ private static GcpManagedChannelOptions grpcGcpOptionsWithMetrics(SpannerOptions
580580
if (metricsOptions.getNamePrefix().equals("")) {
581581
metricsOptionsBuilder.withNamePrefix("cloud.google.com/java/spanner/gcp-channel-pool/");
582582
}
583+
584+
// Build channel pool options from SpannerOptions DCP settings
585+
GcpManagedChannelOptions.GcpChannelPoolOptions.Builder poolBuilder =
586+
GcpManagedChannelOptions.GcpChannelPoolOptions.newBuilder(
587+
grpcGcpOptions.getChannelPoolOptions());
588+
Integer maxChannels = options.getDcpMaxChannels();
589+
Integer minChannels = options.getDcpMinChannels();
590+
Integer initSize = options.getDcpInitialSize();
591+
Integer minRpc = options.getDcpMinRpcPerChannel();
592+
Integer maxRpc = options.getDcpMaxRpcPerChannel();
593+
java.time.Duration scaleDown = options.getDcpScaleDownInterval();
594+
595+
if (maxChannels != null) {
596+
poolBuilder.setMaxSize(maxChannels);
597+
}
598+
if (minChannels != null) {
599+
poolBuilder.setMinSize(minChannels);
600+
}
601+
if (initSize != null) {
602+
poolBuilder.setInitSize(initSize);
603+
}
604+
if (minRpc != null && maxRpc != null && scaleDown != null) {
605+
poolBuilder.setDynamicScaling(minRpc, maxRpc, scaleDown);
606+
}
607+
583608
return GcpManagedChannelOptions.newBuilder(grpcGcpOptions)
584609
.withMetricsOptions(metricsOptionsBuilder.build())
610+
.withChannelPoolOptions(poolBuilder.build())
585611
.build();
586612
}
587613

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@ public void testSingleUseQuery_retriesOnNewChannel() {
288288
SpannerOptions.Builder builder = createSpannerOptionsBuilder();
289289
builder.setSessionPoolOption(
290290
SessionPoolOptions.newBuilder().setUseMultiplexedSession(true).build());
291+
// Ensure retry happens on a different underlying channel by disabling grpc-gcp and limiting
292+
// number of channels to 2 for this test.
293+
builder.disableGrpcGcpExtension().setNumChannels(2);
291294
mockSpanner.setExecuteStreamingSqlExecutionTime(
292295
SimulatedExecutionTime.ofException(Status.DEADLINE_EXCEEDED.asRuntimeException()));
293296

@@ -317,6 +320,8 @@ public void testSingleUseQuery_stopsRetrying() {
317320
SpannerOptions.Builder builder = createSpannerOptionsBuilder();
318321
builder.setSessionPoolOption(
319322
SessionPoolOptions.newBuilder().setUseMultiplexedSession(true).build());
323+
// Ensure a deterministic number of channels for this assertion.
324+
builder.disableGrpcGcpExtension().setNumChannels(8);
320325
mockSpanner.setExecuteStreamingSqlExecutionTime(
321326
SimulatedExecutionTime.ofStickyException(Status.DEADLINE_EXCEEDED.asRuntimeException()));
322327

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,7 @@ public void testDefaultNumChannelsWithGrpcGcpExtensionDisabled() {
11001100
SpannerOptions.newBuilder()
11011101
.setProjectId("test-project")
11021102
.setCredentials(NoCredentials.getInstance())
1103+
.disableGrpcGcpExtension()
11031104
.build();
11041105

11051106
assertEquals(SpannerOptions.DEFAULT_CHANNELS, options.getNumChannels());
@@ -1135,7 +1136,8 @@ public void testNumChannelsWithGrpcGcpExtensionEnabled() {
11351136

11361137
@Test
11371138
public void checkCreatedInstanceWhenGrpcGcpExtensionDisabled() {
1138-
SpannerOptions options = SpannerOptions.newBuilder().setProjectId("test-project").build();
1139+
SpannerOptions options =
1140+
SpannerOptions.newBuilder().setProjectId("test-project").disableGrpcGcpExtension().build();
11391141
SpannerOptions options1 = options.toBuilder().build();
11401142
assertEquals(false, options.isGrpcGcpExtensionEnabled());
11411143
assertEquals(options.isGrpcGcpExtensionEnabled(), options1.isGrpcGcpExtensionEnabled());

0 commit comments

Comments
 (0)