4343import com .google .cloud .ServiceRpc ;
4444import com .google .cloud .TransportOptions ;
4545import com .google .cloud .grpc .GcpManagedChannelOptions ;
46+ import com .google .cloud .grpc .GcpManagedChannelOptions .GcpChannelPoolOptions ;
4647import com .google .cloud .grpc .GrpcTransportOptions ;
4748import com .google .cloud .spanner .Options .DirectedReadOption ;
4849import com .google .cloud .spanner .Options .QueryOption ;
@@ -134,6 +135,72 @@ public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {
134135 // is enabled, to make sure there are sufficient channels available to move the sessions to a
135136 // different channel if a network connection in a particular channel fails.
136137 @ VisibleForTesting static final int GRPC_GCP_ENABLED_DEFAULT_CHANNELS = 8 ;
138+
139+ // Dynamic Channel Pool (DCP) default values and bounds
140+ /** Default max concurrent RPCs per channel before triggering scale up. */
141+ public static final int DEFAULT_DYNAMIC_POOL_MAX_RPC = 25 ;
142+
143+ /** Default min concurrent RPCs per channel for scale down check. */
144+ public static final int DEFAULT_DYNAMIC_POOL_MIN_RPC = 15 ;
145+
146+ /** Default scale down check interval. */
147+ public static final Duration DEFAULT_DYNAMIC_POOL_SCALE_DOWN_INTERVAL = Duration .ofMinutes (3 );
148+
149+ /** Default initial number of channels for dynamic pool. */
150+ public static final int DEFAULT_DYNAMIC_POOL_INITIAL_SIZE = 4 ;
151+
152+ /** Default max number of channels for dynamic pool. */
153+ public static final int DEFAULT_DYNAMIC_POOL_MAX_CHANNELS = 10 ;
154+
155+ /** Default min number of channels for dynamic pool. */
156+ public static final int DEFAULT_DYNAMIC_POOL_MIN_CHANNELS = 2 ;
157+
158+ /**
159+ * Default affinity key lifetime for dynamic channel pool. This is how long to keep an affinity
160+ * key after its last use. Zero means keeping keys forever. Default is 10 minutes, which is
161+ * sufficient to ensure that requests within a single transaction use the same channel.
162+ */
163+ public static final Duration DEFAULT_DYNAMIC_POOL_AFFINITY_KEY_LIFETIME = Duration .ofMinutes (10 );
164+
165+ /**
166+ * Default cleanup interval for dynamic channel pool affinity keys. This is how frequently the
167+ * affinity key cleanup process runs. Default is 1 minute (1/10 of default affinity key lifetime).
168+ */
169+ public static final Duration DEFAULT_DYNAMIC_POOL_CLEANUP_INTERVAL = Duration .ofMinutes (1 );
170+
171+ /**
172+ * Creates a {@link GcpChannelPoolOptions} instance with Spanner-specific defaults for dynamic
173+ * channel pooling. These defaults are optimized for typical Spanner workloads.
174+ *
175+ * <p>Default values:
176+ *
177+ * <ul>
178+ * <li>Max size: {@value #DEFAULT_DYNAMIC_POOL_MAX_CHANNELS}
179+ * <li>Min size: {@value #DEFAULT_DYNAMIC_POOL_MIN_CHANNELS}
180+ * <li>Initial size: {@value #DEFAULT_DYNAMIC_POOL_INITIAL_SIZE}
181+ * <li>Max RPC per channel: {@value #DEFAULT_DYNAMIC_POOL_MAX_RPC}
182+ * <li>Min RPC per channel: {@value #DEFAULT_DYNAMIC_POOL_MIN_RPC}
183+ * <li>Scale down interval: 3 minutes
184+ * <li>Affinity key lifetime: 10 minutes
185+ * <li>Cleanup interval: 1 minute
186+ * </ul>
187+ *
188+ * @return a new {@link GcpChannelPoolOptions} instance with Spanner defaults
189+ */
190+ public static GcpChannelPoolOptions createDefaultDynamicChannelPoolOptions () {
191+ return GcpChannelPoolOptions .newBuilder ()
192+ .setMaxSize (DEFAULT_DYNAMIC_POOL_MAX_CHANNELS )
193+ .setMinSize (DEFAULT_DYNAMIC_POOL_MIN_CHANNELS )
194+ .setInitSize (DEFAULT_DYNAMIC_POOL_INITIAL_SIZE )
195+ .setDynamicScaling (
196+ DEFAULT_DYNAMIC_POOL_MIN_RPC ,
197+ DEFAULT_DYNAMIC_POOL_MAX_RPC ,
198+ DEFAULT_DYNAMIC_POOL_SCALE_DOWN_INTERVAL )
199+ .setAffinityKeyLifetime (DEFAULT_DYNAMIC_POOL_AFFINITY_KEY_LIFETIME )
200+ .setCleanupInterval (DEFAULT_DYNAMIC_POOL_CLEANUP_INTERVAL )
201+ .build ();
202+ }
203+
137204 private final TransportChannelProvider channelProvider ;
138205
139206 @ SuppressWarnings ("rawtypes" )
@@ -153,6 +220,8 @@ public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {
153220 private final Duration partitionedDmlTimeout ;
154221 private final boolean grpcGcpExtensionEnabled ;
155222 private final GcpManagedChannelOptions grpcGcpOptions ;
223+ private final boolean dynamicChannelPoolEnabled ;
224+ private final GcpChannelPoolOptions gcpChannelPoolOptions ;
156225 private final boolean autoThrottleAdministrativeRequests ;
157226 private final RetrySettings retryAdministrativeRequestsSettings ;
158227 private final boolean trackTransactionStarter ;
@@ -800,6 +869,26 @@ protected SpannerOptions(Builder builder) {
800869 partitionedDmlTimeout = builder .partitionedDmlTimeout ;
801870 grpcGcpExtensionEnabled = builder .grpcGcpExtensionEnabled ;
802871 grpcGcpOptions = builder .grpcGcpOptions ;
872+
873+ // Dynamic channel pooling is disabled by default.
874+ // It is only enabled when:
875+ // 1. enableDynamicChannelPool() was explicitly called, AND
876+ // 2. grpc-gcp extension is enabled, AND
877+ // 3. numChannels was not explicitly set
878+ if (builder .dynamicChannelPoolEnabled != null && builder .dynamicChannelPoolEnabled ) {
879+ // DCP was explicitly enabled, but respect numChannels if set
880+ dynamicChannelPoolEnabled = grpcGcpExtensionEnabled && !builder .numChannelsExplicitlySet ;
881+ } else {
882+ // DCP is disabled by default, or was explicitly disabled
883+ dynamicChannelPoolEnabled = false ;
884+ }
885+
886+ // Use user-provided GcpChannelPoolOptions or create Spanner-specific defaults
887+ gcpChannelPoolOptions =
888+ builder .gcpChannelPoolOptions != null
889+ ? builder .gcpChannelPoolOptions
890+ : createDefaultDynamicChannelPoolOptions ();
891+
803892 autoThrottleAdministrativeRequests = builder .autoThrottleAdministrativeRequests ;
804893 retryAdministrativeRequestsSettings = builder .retryAdministrativeRequestsSettings ;
805894 trackTransactionStarter = builder .trackTransactionStarter ;
@@ -1010,6 +1099,7 @@ public static class Builder
10101099 private GrpcInterceptorProvider interceptorProvider ;
10111100
10121101 private Integer numChannels ;
1102+ private boolean numChannelsExplicitlySet = false ;
10131103
10141104 private String transportChannelExecutorThreadNameFormat = "Cloud-Spanner-TransportChannel-%d" ;
10151105
@@ -1027,6 +1117,8 @@ public static class Builder
10271117 private Duration partitionedDmlTimeout = Duration .ofHours (2L );
10281118 private boolean grpcGcpExtensionEnabled = true ;
10291119 private GcpManagedChannelOptions grpcGcpOptions ;
1120+ private Boolean dynamicChannelPoolEnabled ;
1121+ private GcpChannelPoolOptions gcpChannelPoolOptions ;
10301122 private RetrySettings retryAdministrativeRequestsSettings =
10311123 DEFAULT_ADMIN_REQUESTS_LIMIT_EXCEEDED_RETRY_SETTINGS ;
10321124 private boolean autoThrottleAdministrativeRequests = false ;
@@ -1099,6 +1191,8 @@ protected Builder() {
10991191 this .partitionedDmlTimeout = options .partitionedDmlTimeout ;
11001192 this .grpcGcpExtensionEnabled = options .grpcGcpExtensionEnabled ;
11011193 this .grpcGcpOptions = options .grpcGcpOptions ;
1194+ this .dynamicChannelPoolEnabled = options .dynamicChannelPoolEnabled ;
1195+ this .gcpChannelPoolOptions = options .gcpChannelPoolOptions ;
11021196 this .autoThrottleAdministrativeRequests = options .autoThrottleAdministrativeRequests ;
11031197 this .retryAdministrativeRequestsSettings = options .retryAdministrativeRequestsSettings ;
11041198 this .trackTransactionStarter = options .trackTransactionStarter ;
@@ -1189,6 +1283,7 @@ public Builder setInterceptorProvider(GrpcInterceptorProvider interceptorProvide
11891283 */
11901284 public Builder setNumChannels (int numChannels ) {
11911285 this .numChannels = numChannels ;
1286+ this .numChannelsExplicitlySet = true ;
11921287 return this ;
11931288 }
11941289
@@ -1578,6 +1673,62 @@ public Builder disableGrpcGcpExtension() {
15781673 return this ;
15791674 }
15801675
1676+ /**
1677+ * Enables dynamic channel pooling. When enabled, the client will automatically scale the number
1678+ * of channels based on load. This requires the gRPC-GCP extension to be enabled.
1679+ *
1680+ * <p>Dynamic channel pooling is disabled by default. Use this method to explicitly enable it.
1681+ * Note that calling {@link #setNumChannels(int)} will disable dynamic channel pooling even if
1682+ * this method was called.
1683+ */
1684+ public Builder enableDynamicChannelPool () {
1685+ this .dynamicChannelPoolEnabled = true ;
1686+ return this ;
1687+ }
1688+
1689+ /**
1690+ * Disables dynamic channel pooling. When disabled, the client will use a static number of
1691+ * channels as configured by {@link #setNumChannels(int)}.
1692+ *
1693+ * <p>Dynamic channel pooling is disabled by default, so this method is typically not needed
1694+ * unless you want to explicitly disable it after enabling it.
1695+ */
1696+ public Builder disableDynamicChannelPool () {
1697+ this .dynamicChannelPoolEnabled = false ;
1698+ return this ;
1699+ }
1700+
1701+ /**
1702+ * Sets the channel pool options for dynamic channel pooling. Use this to configure the dynamic
1703+ * channel pool behavior when {@link #enableDynamicChannelPool()} is enabled.
1704+ *
1705+ * <p>If not set, Spanner-specific defaults will be used (see {@link
1706+ * #createDefaultDynamicChannelPoolOptions()}).
1707+ *
1708+ * <p>Example usage:
1709+ *
1710+ * <pre>{@code
1711+ * SpannerOptions options = SpannerOptions.newBuilder()
1712+ * .setProjectId("my-project")
1713+ * .enableDynamicChannelPool()
1714+ * .setGcpChannelPoolOptions(
1715+ * GcpChannelPoolOptions.newBuilder()
1716+ * .setMaxSize(15)
1717+ * .setMinSize(3)
1718+ * .setInitSize(5)
1719+ * .setDynamicScaling(10, 30, Duration.ofMinutes(5))
1720+ * .build())
1721+ * .build();
1722+ * }</pre>
1723+ *
1724+ * @param gcpChannelPoolOptions the channel pool options to use
1725+ * @return this builder for chaining
1726+ */
1727+ public Builder setGcpChannelPoolOptions (GcpChannelPoolOptions gcpChannelPoolOptions ) {
1728+ this .gcpChannelPoolOptions = Preconditions .checkNotNull (gcpChannelPoolOptions );
1729+ return this ;
1730+ }
1731+
15811732 /**
15821733 * Sets the host of an emulator to use. By default the value is read from an environment
15831734 * variable. If the environment variable is not set, this will be <code>null</code>.
@@ -1990,6 +2141,26 @@ public GcpManagedChannelOptions getGrpcGcpOptions() {
19902141 return grpcGcpOptions ;
19912142 }
19922143
2144+ /**
2145+ * Returns whether dynamic channel pooling is enabled. Dynamic channel pooling is disabled by
2146+ * default. Use {@link Builder#enableDynamicChannelPool()} to explicitly enable it. Note that
2147+ * calling {@link Builder#setNumChannels(int)} will disable dynamic channel pooling even if it was
2148+ * explicitly enabled.
2149+ */
2150+ public boolean isDynamicChannelPoolEnabled () {
2151+ return dynamicChannelPoolEnabled ;
2152+ }
2153+
2154+ /**
2155+ * Returns the channel pool options for dynamic channel pooling. If no options were explicitly
2156+ * set, returns the Spanner-specific defaults.
2157+ *
2158+ * @see #createDefaultDynamicChannelPoolOptions()
2159+ */
2160+ public GcpChannelPoolOptions getGcpChannelPoolOptions () {
2161+ return gcpChannelPoolOptions ;
2162+ }
2163+
19932164 public boolean isAutoThrottleAdministrativeRequests () {
19942165 return autoThrottleAdministrativeRequests ;
19952166 }
0 commit comments