Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.

Commit 90f82b1

Browse files
committed
feat: add support of dynamic channel pooling
1 parent bb82f9e commit 90f82b1

5 files changed

Lines changed: 920 additions & 14 deletions

File tree

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

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)