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 ;
@@ -1100,6 +1192,8 @@ protected Builder() {
11001192 this .partitionedDmlTimeout = options .partitionedDmlTimeout ;
11011193 this .grpcGcpExtensionEnabled = options .grpcGcpExtensionEnabled ;
11021194 this .grpcGcpOptions = options .grpcGcpOptions ;
1195+ this .dynamicChannelPoolEnabled = options .dynamicChannelPoolEnabled ;
1196+ this .gcpChannelPoolOptions = options .gcpChannelPoolOptions ;
11031197 this .autoThrottleAdministrativeRequests = options .autoThrottleAdministrativeRequests ;
11041198 this .retryAdministrativeRequestsSettings = options .retryAdministrativeRequestsSettings ;
11051199 this .trackTransactionStarter = options .trackTransactionStarter ;
@@ -1190,6 +1284,7 @@ public Builder setInterceptorProvider(GrpcInterceptorProvider interceptorProvide
11901284 */
11911285 public Builder setNumChannels (int numChannels ) {
11921286 this .numChannels = numChannels ;
1287+ this .numChannelsExplicitlySet = true ;
11931288 return this ;
11941289 }
11951290
@@ -1587,6 +1682,62 @@ public Builder disableGrpcGcpExtension() {
15871682 return this ;
15881683 }
15891684
1685+ /**
1686+ * Enables dynamic channel pooling. When enabled, the client will automatically scale the number
1687+ * of channels based on load. This requires the gRPC-GCP extension to be enabled.
1688+ *
1689+ * <p>Dynamic channel pooling is disabled by default. Use this method to explicitly enable it.
1690+ * Note that calling {@link #setNumChannels(int)} will disable dynamic channel pooling even if
1691+ * this method was called.
1692+ */
1693+ public Builder enableDynamicChannelPool () {
1694+ this .dynamicChannelPoolEnabled = true ;
1695+ return this ;
1696+ }
1697+
1698+ /**
1699+ * Disables dynamic channel pooling. When disabled, the client will use a static number of
1700+ * channels as configured by {@link #setNumChannels(int)}.
1701+ *
1702+ * <p>Dynamic channel pooling is disabled by default, so this method is typically not needed
1703+ * unless you want to explicitly disable it after enabling it.
1704+ */
1705+ public Builder disableDynamicChannelPool () {
1706+ this .dynamicChannelPoolEnabled = false ;
1707+ return this ;
1708+ }
1709+
1710+ /**
1711+ * Sets the channel pool options for dynamic channel pooling. Use this to configure the dynamic
1712+ * channel pool behavior when {@link #enableDynamicChannelPool()} is enabled.
1713+ *
1714+ * <p>If not set, Spanner-specific defaults will be used (see {@link
1715+ * #createDefaultDynamicChannelPoolOptions()}).
1716+ *
1717+ * <p>Example usage:
1718+ *
1719+ * <pre>{@code
1720+ * SpannerOptions options = SpannerOptions.newBuilder()
1721+ * .setProjectId("my-project")
1722+ * .enableDynamicChannelPool()
1723+ * .setGcpChannelPoolOptions(
1724+ * GcpChannelPoolOptions.newBuilder()
1725+ * .setMaxSize(15)
1726+ * .setMinSize(3)
1727+ * .setInitSize(5)
1728+ * .setDynamicScaling(10, 30, Duration.ofMinutes(5))
1729+ * .build())
1730+ * .build();
1731+ * }</pre>
1732+ *
1733+ * @param gcpChannelPoolOptions the channel pool options to use
1734+ * @return this builder for chaining
1735+ */
1736+ public Builder setGcpChannelPoolOptions (GcpChannelPoolOptions gcpChannelPoolOptions ) {
1737+ this .gcpChannelPoolOptions = Preconditions .checkNotNull (gcpChannelPoolOptions );
1738+ return this ;
1739+ }
1740+
15901741 /**
15911742 * Sets the host of an emulator to use. By default the value is read from an environment
15921743 * variable. If the environment variable is not set, this will be <code>null</code>.
@@ -2016,6 +2167,26 @@ public GcpManagedChannelOptions getGrpcGcpOptions() {
20162167 return grpcGcpOptions ;
20172168 }
20182169
2170+ /**
2171+ * Returns whether dynamic channel pooling is enabled. Dynamic channel pooling is disabled by
2172+ * default. Use {@link Builder#enableDynamicChannelPool()} to explicitly enable it. Note that
2173+ * calling {@link Builder#setNumChannels(int)} will disable dynamic channel pooling even if it was
2174+ * explicitly enabled.
2175+ */
2176+ public boolean isDynamicChannelPoolEnabled () {
2177+ return dynamicChannelPoolEnabled ;
2178+ }
2179+
2180+ /**
2181+ * Returns the channel pool options for dynamic channel pooling. If no options were explicitly
2182+ * set, returns the Spanner-specific defaults.
2183+ *
2184+ * @see #createDefaultDynamicChannelPoolOptions()
2185+ */
2186+ public GcpChannelPoolOptions getGcpChannelPoolOptions () {
2187+ return gcpChannelPoolOptions ;
2188+ }
2189+
20192190 public boolean isAutoThrottleAdministrativeRequests () {
20202191 return autoThrottleAdministrativeRequests ;
20212192 }
0 commit comments