@@ -134,6 +134,46 @@ public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {
134134 // is enabled, to make sure there are sufficient channels available to move the sessions to a
135135 // different channel if a network connection in a particular channel fails.
136136 @ VisibleForTesting static final int GRPC_GCP_ENABLED_DEFAULT_CHANNELS = 8 ;
137+
138+ // Dynamic Channel Pool (DCP) default values and bounds
139+ /** Default max concurrent RPCs per channel before triggering scale up. */
140+ public static final int DEFAULT_DYNAMIC_POOL_MAX_RPC = 25 ;
141+
142+ /** Default min concurrent RPCs per channel for scale down check. */
143+ public static final int DEFAULT_DYNAMIC_POOL_MIN_RPC = 15 ;
144+
145+ /** Default scale down check interval. */
146+ public static final Duration DEFAULT_DYNAMIC_POOL_SCALE_DOWN_INTERVAL = Duration .ofMinutes (3 );
147+
148+ /** Default initial number of channels for dynamic pool. */
149+ public static final int DEFAULT_DYNAMIC_POOL_INITIAL_SIZE = 4 ;
150+
151+ /** Default max number of channels for dynamic pool. */
152+ public static final int DEFAULT_DYNAMIC_POOL_MAX_CHANNELS = 10 ;
153+
154+ /** Default min number of channels for dynamic pool. */
155+ public static final int DEFAULT_DYNAMIC_POOL_MIN_CHANNELS = 2 ;
156+
157+ // DCP bounds
158+ static final int MAX_DYNAMIC_POOL_MAX_RPC = 100 ;
159+ static final int MAX_DYNAMIC_POOL_MAX_CHANNELS = 20 ;
160+ static final int MAX_DYNAMIC_POOL_INITIAL_SIZE = 256 ;
161+ static final Duration MIN_DYNAMIC_POOL_SCALE_DOWN_INTERVAL = Duration .ofSeconds (30 );
162+ static final Duration MAX_DYNAMIC_POOL_SCALE_DOWN_INTERVAL = Duration .ofHours (1 );
163+
164+ /**
165+ * Default affinity key lifetime for dynamic channel pool. This is how long to keep an affinity
166+ * key after its last use. Zero means keeping keys forever. Default is 1 hour.
167+ */
168+ public static final Duration DEFAULT_DYNAMIC_POOL_AFFINITY_KEY_LIFETIME = Duration .ofHours (1 );
169+
170+ /**
171+ * Default cleanup interval for dynamic channel pool affinity keys. This is how frequently the
172+ * affinity key cleanup process runs. Default is 6 minutes (1/10 of default affinity key
173+ * lifetime).
174+ */
175+ public static final Duration DEFAULT_DYNAMIC_POOL_CLEANUP_INTERVAL = Duration .ofMinutes (6 );
176+
137177 private final TransportChannelProvider channelProvider ;
138178
139179 @ SuppressWarnings ("rawtypes" )
@@ -153,6 +193,15 @@ public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {
153193 private final Duration partitionedDmlTimeout ;
154194 private final boolean grpcGcpExtensionEnabled ;
155195 private final GcpManagedChannelOptions grpcGcpOptions ;
196+ private final boolean dynamicChannelPoolEnabled ;
197+ private final int dynamicPoolMaxRpc ;
198+ private final int dynamicPoolMinRpc ;
199+ private final Duration dynamicPoolScaleDownInterval ;
200+ private final int dynamicPoolInitialSize ;
201+ private final int dynamicPoolMaxChannels ;
202+ private final int dynamicPoolMinChannels ;
203+ private final Duration dynamicPoolAffinityKeyLifetime ;
204+ private final Duration dynamicPoolCleanupInterval ;
156205 private final boolean autoThrottleAdministrativeRequests ;
157206 private final RetrySettings retryAdministrativeRequestsSettings ;
158207 private final boolean trackTransactionStarter ;
@@ -800,6 +849,72 @@ protected SpannerOptions(Builder builder) {
800849 partitionedDmlTimeout = builder .partitionedDmlTimeout ;
801850 grpcGcpExtensionEnabled = builder .grpcGcpExtensionEnabled ;
802851 grpcGcpOptions = builder .grpcGcpOptions ;
852+
853+ // Dynamic channel pooling is enabled by default when:
854+ // 1. grpc-gcp extension is enabled, AND
855+ // 2. numChannels is not explicitly set, AND
856+ // 3. dynamicChannelPoolEnabled is not explicitly set to false
857+ if (builder .dynamicChannelPoolEnabled != null ) {
858+ dynamicChannelPoolEnabled = builder .dynamicChannelPoolEnabled && grpcGcpExtensionEnabled ;
859+ } else {
860+ // Enable DCP by default only if grpc-gcp is enabled and numChannels was not explicitly set
861+ dynamicChannelPoolEnabled = grpcGcpExtensionEnabled && !builder .numChannelsExplicitlySet ;
862+ }
863+
864+ // Use defaults with proper bounds checking for DCP configuration
865+ int effectiveMaxRpc =
866+ builder .dynamicPoolMaxRpc != null
867+ ? builder .dynamicPoolMaxRpc
868+ : DEFAULT_DYNAMIC_POOL_MAX_RPC ;
869+ dynamicPoolMaxRpc = effectiveMaxRpc ;
870+
871+ int effectiveMinRpc =
872+ builder .dynamicPoolMinRpc != null
873+ ? builder .dynamicPoolMinRpc
874+ : DEFAULT_DYNAMIC_POOL_MIN_RPC ;
875+ // Ensure minRpc does not exceed maxRpc
876+ dynamicPoolMinRpc = Math .min (effectiveMinRpc , effectiveMaxRpc );
877+
878+ dynamicPoolScaleDownInterval =
879+ builder .dynamicPoolScaleDownInterval != null
880+ ? builder .dynamicPoolScaleDownInterval
881+ : DEFAULT_DYNAMIC_POOL_SCALE_DOWN_INTERVAL ;
882+
883+ int effectiveMaxChannels =
884+ builder .dynamicPoolMaxChannels != null
885+ ? builder .dynamicPoolMaxChannels
886+ : DEFAULT_DYNAMIC_POOL_MAX_CHANNELS ;
887+ dynamicPoolMaxChannels = effectiveMaxChannels ;
888+
889+ int effectiveMinChannels =
890+ builder .dynamicPoolMinChannels != null
891+ ? builder .dynamicPoolMinChannels
892+ : DEFAULT_DYNAMIC_POOL_MIN_CHANNELS ;
893+ // Ensure minChannels does not exceed maxChannels
894+ dynamicPoolMinChannels = Math .min (effectiveMinChannels , effectiveMaxChannels );
895+
896+ int effectiveInitialSize =
897+ builder .dynamicPoolInitialSize != null
898+ ? builder .dynamicPoolInitialSize
899+ : DEFAULT_DYNAMIC_POOL_INITIAL_SIZE ;
900+ // Ensure initialSize is within [minChannels, maxChannels]
901+ dynamicPoolInitialSize =
902+ Math .max (dynamicPoolMinChannels , Math .min (effectiveInitialSize , dynamicPoolMaxChannels ));
903+
904+ dynamicPoolAffinityKeyLifetime =
905+ builder .dynamicPoolAffinityKeyLifetime != null
906+ ? builder .dynamicPoolAffinityKeyLifetime
907+ : DEFAULT_DYNAMIC_POOL_AFFINITY_KEY_LIFETIME ;
908+
909+ // If cleanup interval is not set but affinity key lifetime is set, default to 1/10 of lifetime
910+ if (builder .dynamicPoolCleanupInterval != null ) {
911+ dynamicPoolCleanupInterval = builder .dynamicPoolCleanupInterval ;
912+ } else if (!dynamicPoolAffinityKeyLifetime .isZero ()) {
913+ dynamicPoolCleanupInterval = dynamicPoolAffinityKeyLifetime .dividedBy (10 );
914+ } else {
915+ dynamicPoolCleanupInterval = DEFAULT_DYNAMIC_POOL_CLEANUP_INTERVAL ;
916+ }
917+
803918 autoThrottleAdministrativeRequests = builder .autoThrottleAdministrativeRequests ;
804919 retryAdministrativeRequestsSettings = builder .retryAdministrativeRequestsSettings ;
805920 trackTransactionStarter = builder .trackTransactionStarter ;
@@ -1010,6 +1125,7 @@ public static class Builder
10101125 private GrpcInterceptorProvider interceptorProvider ;
10111126
10121127 private Integer numChannels ;
1128+ private boolean numChannelsExplicitlySet = false ;
10131129
10141130 private String transportChannelExecutorThreadNameFormat = "Cloud-Spanner-TransportChannel-%d" ;
10151131
@@ -1027,6 +1143,15 @@ public static class Builder
10271143 private Duration partitionedDmlTimeout = Duration .ofHours (2L );
10281144 private boolean grpcGcpExtensionEnabled = true ;
10291145 private GcpManagedChannelOptions grpcGcpOptions ;
1146+ private Boolean dynamicChannelPoolEnabled ;
1147+ private Integer dynamicPoolMaxRpc ;
1148+ private Integer dynamicPoolMinRpc ;
1149+ private Duration dynamicPoolScaleDownInterval ;
1150+ private Integer dynamicPoolInitialSize ;
1151+ private Integer dynamicPoolMaxChannels ;
1152+ private Integer dynamicPoolMinChannels ;
1153+ private Duration dynamicPoolAffinityKeyLifetime ;
1154+ private Duration dynamicPoolCleanupInterval ;
10301155 private RetrySettings retryAdministrativeRequestsSettings =
10311156 DEFAULT_ADMIN_REQUESTS_LIMIT_EXCEEDED_RETRY_SETTINGS ;
10321157 private boolean autoThrottleAdministrativeRequests = false ;
@@ -1099,6 +1224,15 @@ protected Builder() {
10991224 this .partitionedDmlTimeout = options .partitionedDmlTimeout ;
11001225 this .grpcGcpExtensionEnabled = options .grpcGcpExtensionEnabled ;
11011226 this .grpcGcpOptions = options .grpcGcpOptions ;
1227+ this .dynamicChannelPoolEnabled = options .dynamicChannelPoolEnabled ;
1228+ this .dynamicPoolMaxRpc = options .dynamicPoolMaxRpc ;
1229+ this .dynamicPoolMinRpc = options .dynamicPoolMinRpc ;
1230+ this .dynamicPoolScaleDownInterval = options .dynamicPoolScaleDownInterval ;
1231+ this .dynamicPoolInitialSize = options .dynamicPoolInitialSize ;
1232+ this .dynamicPoolMaxChannels = options .dynamicPoolMaxChannels ;
1233+ this .dynamicPoolMinChannels = options .dynamicPoolMinChannels ;
1234+ this .dynamicPoolAffinityKeyLifetime = options .dynamicPoolAffinityKeyLifetime ;
1235+ this .dynamicPoolCleanupInterval = options .dynamicPoolCleanupInterval ;
11021236 this .autoThrottleAdministrativeRequests = options .autoThrottleAdministrativeRequests ;
11031237 this .retryAdministrativeRequestsSettings = options .retryAdministrativeRequestsSettings ;
11041238 this .trackTransactionStarter = options .trackTransactionStarter ;
@@ -1189,6 +1323,7 @@ public Builder setInterceptorProvider(GrpcInterceptorProvider interceptorProvide
11891323 */
11901324 public Builder setNumChannels (int numChannels ) {
11911325 this .numChannels = numChannels ;
1326+ this .numChannelsExplicitlySet = true ;
11921327 return this ;
11931328 }
11941329
@@ -1578,6 +1713,144 @@ public Builder disableGrpcGcpExtension() {
15781713 return this ;
15791714 }
15801715
1716+ /**
1717+ * Disables dynamic channel pooling. When disabled, the client will use a static number of
1718+ * channels as configured by {@link #setNumChannels(int)}.
1719+ *
1720+ * <p>Dynamic channel pooling is enabled by default unless {@link #setNumChannels(int)} is
1721+ * called or this method is used to disable it.
1722+ */
1723+ public Builder disableDynamicChannelPool () {
1724+ this .dynamicChannelPoolEnabled = false ;
1725+ return this ;
1726+ }
1727+
1728+ /**
1729+ * Sets the maximum number of concurrent RPCs per channel before triggering a scale up of the
1730+ * channel pool. Default is 25. Must be between 1 and 100.
1731+ *
1732+ * <p>This setting is only effective when dynamic channel pooling is enabled.
1733+ */
1734+ public Builder setDynamicPoolMaxRpc (int maxRpc ) {
1735+ Preconditions .checkArgument (
1736+ maxRpc >= 1 && maxRpc <= MAX_DYNAMIC_POOL_MAX_RPC ,
1737+ "Dynamic pool max RPC must be between 1 and %s, got: %s" ,
1738+ MAX_DYNAMIC_POOL_MAX_RPC ,
1739+ maxRpc );
1740+ this .dynamicPoolMaxRpc = maxRpc ;
1741+ return this ;
1742+ }
1743+
1744+ /**
1745+ * Sets the minimum number of concurrent RPCs per channel used for scale down checks. When the
1746+ * average concurrent RPCs per channel falls below this value, the pool may scale down. Default
1747+ * is 15. Must be between 1 and the configured max RPC value.
1748+ *
1749+ * <p>This setting is only effective when dynamic channel pooling is enabled.
1750+ */
1751+ public Builder setDynamicPoolMinRpc (int minRpc ) {
1752+ Preconditions .checkArgument (
1753+ minRpc >= 1 , "Dynamic pool min RPC must be at least 1, got: %s" , minRpc );
1754+ this .dynamicPoolMinRpc = minRpc ;
1755+ return this ;
1756+ }
1757+
1758+ /**
1759+ * Sets the interval at which the channel pool checks whether it can scale down. Default is 3
1760+ * minutes. Must be between 30 seconds and 1 hour.
1761+ *
1762+ * <p>This setting is only effective when dynamic channel pooling is enabled.
1763+ */
1764+ public Builder setDynamicPoolScaleDownInterval (Duration interval ) {
1765+ Preconditions .checkNotNull (interval , "Scale down interval cannot be null" );
1766+ Preconditions .checkArgument (
1767+ interval .compareTo (MIN_DYNAMIC_POOL_SCALE_DOWN_INTERVAL ) >= 0
1768+ && interval .compareTo (MAX_DYNAMIC_POOL_SCALE_DOWN_INTERVAL ) <= 0 ,
1769+ "Scale down interval must be between %s and %s, got: %s" ,
1770+ MIN_DYNAMIC_POOL_SCALE_DOWN_INTERVAL ,
1771+ MAX_DYNAMIC_POOL_SCALE_DOWN_INTERVAL ,
1772+ interval );
1773+ this .dynamicPoolScaleDownInterval = interval ;
1774+ return this ;
1775+ }
1776+
1777+ /**
1778+ * Sets the initial number of channels to create for the dynamic channel pool. Default is 4.
1779+ * Must be between 1 and 256.
1780+ *
1781+ * <p>This setting is only effective when dynamic channel pooling is enabled.
1782+ */
1783+ public Builder setDynamicPoolInitialSize (int initialSize ) {
1784+ Preconditions .checkArgument (
1785+ initialSize >= 1 && initialSize <= MAX_DYNAMIC_POOL_INITIAL_SIZE ,
1786+ "Dynamic pool initial size must be between 1 and %s, got: %s" ,
1787+ MAX_DYNAMIC_POOL_INITIAL_SIZE ,
1788+ initialSize );
1789+ this .dynamicPoolInitialSize = initialSize ;
1790+ return this ;
1791+ }
1792+
1793+ /**
1794+ * Sets the maximum number of channels for the dynamic channel pool. Default is 10. Must be
1795+ * between 1 and 20.
1796+ *
1797+ * <p>This setting is only effective when dynamic channel pooling is enabled.
1798+ */
1799+ public Builder setDynamicPoolMaxChannels (int maxChannels ) {
1800+ Preconditions .checkArgument (
1801+ maxChannels >= 1 && maxChannels <= MAX_DYNAMIC_POOL_MAX_CHANNELS ,
1802+ "Dynamic pool max channels must be between 1 and %s, got: %s" ,
1803+ MAX_DYNAMIC_POOL_MAX_CHANNELS ,
1804+ maxChannels );
1805+ this .dynamicPoolMaxChannels = maxChannels ;
1806+ return this ;
1807+ }
1808+
1809+ /**
1810+ * Sets the minimum number of channels for the dynamic channel pool. The pool will not scale
1811+ * down below this number. Default is 2. Must be between 1 and the configured max channels.
1812+ *
1813+ * <p>This setting is only effective when dynamic channel pooling is enabled.
1814+ */
1815+ public Builder setDynamicPoolMinChannels (int minChannels ) {
1816+ Preconditions .checkArgument (
1817+ minChannels >= 1 , "Dynamic pool min channels must be at least 1, got: %s" , minChannels );
1818+ this .dynamicPoolMinChannels = minChannels ;
1819+ return this ;
1820+ }
1821+
1822+ /**
1823+ * Sets the affinity key lifetime for the dynamic channel pool. This determines how long to keep
1824+ * an affinity key after its last use. Setting this to a non-zero value enables automatic
1825+ * cleanup of stale affinity keys, which is important for long-running applications. Default is
1826+ * 1 hour. Use {@link Duration#ZERO} to keep keys forever (not recommended for long-running
1827+ * applications).
1828+ *
1829+ * <p>This setting is only effective when dynamic channel pooling is enabled.
1830+ */
1831+ public Builder setDynamicPoolAffinityKeyLifetime (Duration lifetime ) {
1832+ Preconditions .checkNotNull (lifetime , "Affinity key lifetime cannot be null" );
1833+ Preconditions .checkArgument (
1834+ !lifetime .isNegative (), "Affinity key lifetime must not be negative, got: %s" , lifetime );
1835+ this .dynamicPoolAffinityKeyLifetime = lifetime ;
1836+ return this ;
1837+ }
1838+
1839+ /**
1840+ * Sets the cleanup interval for the dynamic channel pool affinity keys. This determines how
1841+ * frequently the affinity key cleanup process runs. Default is 6 minutes. Must be positive if
1842+ * affinity key lifetime is set.
1843+ *
1844+ * <p>This setting is only effective when dynamic channel pooling is enabled.
1845+ */
1846+ public Builder setDynamicPoolCleanupInterval (Duration interval ) {
1847+ Preconditions .checkNotNull (interval , "Cleanup interval cannot be null" );
1848+ Preconditions .checkArgument (
1849+ !interval .isNegative (), "Cleanup interval must not be negative, got: %s" , interval );
1850+ this .dynamicPoolCleanupInterval = interval ;
1851+ return this ;
1852+ }
1853+
15811854 /**
15821855 * Sets the host of an emulator to use. By default the value is read from an environment
15831856 * variable. If the environment variable is not set, this will be <code>null</code>.
@@ -1990,6 +2263,64 @@ public GcpManagedChannelOptions getGrpcGcpOptions() {
19902263 return grpcGcpOptions ;
19912264 }
19922265
2266+ /**
2267+ * Returns whether dynamic channel pooling is enabled. Dynamic channel pooling is enabled by
2268+ * default unless {@link Builder#setNumChannels(int)} is called or {@link
2269+ * Builder#disableDynamicChannelPool()} is used.
2270+ */
2271+ public boolean isDynamicChannelPoolEnabled () {
2272+ return dynamicChannelPoolEnabled ;
2273+ }
2274+
2275+ /**
2276+ * Returns the maximum number of concurrent RPCs per channel before triggering a scale up. Default
2277+ * is 25.
2278+ */
2279+ public int getDynamicPoolMaxRpc () {
2280+ return dynamicPoolMaxRpc ;
2281+ }
2282+
2283+ /**
2284+ * Returns the minimum number of concurrent RPCs per channel used for scale down checks. Default
2285+ * is 15.
2286+ */
2287+ public int getDynamicPoolMinRpc () {
2288+ return dynamicPoolMinRpc ;
2289+ }
2290+
2291+ /** Returns the scale down check interval. Default is 3 minutes. */
2292+ public Duration getDynamicPoolScaleDownInterval () {
2293+ return dynamicPoolScaleDownInterval ;
2294+ }
2295+
2296+ /** Returns the initial number of channels for the dynamic pool. Default is 4. */
2297+ public int getDynamicPoolInitialSize () {
2298+ return dynamicPoolInitialSize ;
2299+ }
2300+
2301+ /** Returns the maximum number of channels for the dynamic pool. Default is 10. */
2302+ public int getDynamicPoolMaxChannels () {
2303+ return dynamicPoolMaxChannels ;
2304+ }
2305+
2306+ /** Returns the minimum number of channels for the dynamic pool. Default is 2. */
2307+ public int getDynamicPoolMinChannels () {
2308+ return dynamicPoolMinChannels ;
2309+ }
2310+
2311+ /**
2312+ * Returns the affinity key lifetime for the dynamic pool. This is how long to keep an affinity
2313+ * key after its last use. Default is 1 hour.
2314+ */
2315+ public Duration getDynamicPoolAffinityKeyLifetime () {
2316+ return dynamicPoolAffinityKeyLifetime ;
2317+ }
2318+
2319+ /** Returns the cleanup interval for dynamic pool affinity keys. Default is 6 minutes. */
2320+ public Duration getDynamicPoolCleanupInterval () {
2321+ return dynamicPoolCleanupInterval ;
2322+ }
2323+
19932324 public boolean isAutoThrottleAdministrativeRequests () {
19942325 return autoThrottleAdministrativeRequests ;
19952326 }
0 commit comments