From bd3714f11ac4b03b60a7101a1c00c4cdd0e1b27b Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Sat, 4 Apr 2026 09:36:38 +0530 Subject: [PATCH 1/2] chore(spanner): Auto-enable location API and dynamic channel pool for experimental hosts, and merge default dynamic channel pool options with user-provided options When an experimental host is configured, automatically enable the location API (unless explicitly disabled via GOOGLE_SPANNER_EXPERIMENTAL_LOCATION_API=false) and dynamic channel pool (unless explicitly disabled via disableDynamicChannelPool()). Also fix an issue where setting custom GcpChannelPoolOptions without specifying dynamic scaling parameters (minRpcPerChannel, maxRpcPerChannel, scaleDownInterval) would reset them to zero, preventing channel scale-up. Custom options are now merged with Spanner defaults so unset fields retain their default values. --- .../google/cloud/spanner/SpannerOptions.java | 76 ++++++++++++++++--- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 2fa6d4fd2914..cbdb63bed8cd 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -141,7 +141,7 @@ public class SpannerOptions extends ServiceOptions { // Dynamic Channel Pool (DCP) default values and bounds /** Default max concurrent RPCs per channel before triggering scale up. */ - public static final int DEFAULT_DYNAMIC_POOL_MAX_RPC = 25; + public static final int DEFAULT_DYNAMIC_POOL_MAX_RPC = 90; /** Default min concurrent RPCs per channel for scale down check. */ public static final int DEFAULT_DYNAMIC_POOL_MIN_RPC = 15; @@ -150,13 +150,13 @@ public class SpannerOptions extends ServiceOptions { public static final Duration DEFAULT_DYNAMIC_POOL_SCALE_DOWN_INTERVAL = Duration.ofMinutes(3); /** Default initial number of channels for dynamic pool. */ - public static final int DEFAULT_DYNAMIC_POOL_INITIAL_SIZE = 4; + public static final int DEFAULT_DYNAMIC_POOL_INITIAL_SIZE = 1; /** Default max number of channels for dynamic pool. */ - public static final int DEFAULT_DYNAMIC_POOL_MAX_CHANNELS = 10; + public static final int DEFAULT_DYNAMIC_POOL_MAX_CHANNELS = 256; /** Default min number of channels for dynamic pool. */ - public static final int DEFAULT_DYNAMIC_POOL_MIN_CHANNELS = 2; + public static final int DEFAULT_DYNAMIC_POOL_MIN_CHANNELS = 1; /** * Default affinity key lifetime for dynamic channel pool. This is how long to keep an affinity @@ -204,6 +204,49 @@ public static GcpChannelPoolOptions createDefaultDynamicChannelPoolOptions() { .build(); } + /** + * Merges user-provided {@link GcpChannelPoolOptions} with Spanner-specific defaults. Any value + * that the user has not explicitly set (i.e. left at the builder's default of 0 or null) will be + * filled in from {@link #createDefaultDynamicChannelPoolOptions()}. + */ + static GcpChannelPoolOptions mergeWithDefaultChannelPoolOptions( + GcpChannelPoolOptions userOptions) { + GcpChannelPoolOptions defaults = createDefaultDynamicChannelPoolOptions(); + GcpChannelPoolOptions.Builder merged = GcpChannelPoolOptions.newBuilder(userOptions); + if (userOptions.getMaxSize() <= 0) { + merged.setMaxSize(defaults.getMaxSize()); + } + if (userOptions.getMinSize() <= 0) { + merged.setMinSize(defaults.getMinSize()); + } + if (userOptions.getInitSize() <= 0) { + merged.setInitSize(defaults.getInitSize()); + } + if (userOptions.getMinRpcPerChannel() <= 0 + || userOptions.getMaxRpcPerChannel() <= 0 + || userOptions.getScaleDownInterval() == null + || userOptions.getScaleDownInterval().isZero()) { + merged.setDynamicScaling( + userOptions.getMinRpcPerChannel() > 0 + ? userOptions.getMinRpcPerChannel() + : defaults.getMinRpcPerChannel(), + userOptions.getMaxRpcPerChannel() > 0 + ? userOptions.getMaxRpcPerChannel() + : defaults.getMaxRpcPerChannel(), + userOptions.getScaleDownInterval() != null && !userOptions.getScaleDownInterval().isZero() + ? userOptions.getScaleDownInterval() + : defaults.getScaleDownInterval()); + } + if (userOptions.getAffinityKeyLifetime() == null + || userOptions.getAffinityKeyLifetime().isZero()) { + merged.setAffinityKeyLifetime(defaults.getAffinityKeyLifetime()); + } + if (userOptions.getCleanupInterval() == null || userOptions.getCleanupInterval().isZero()) { + merged.setCleanupInterval(defaults.getCleanupInterval()); + } + return merged.build(); + } + private final TransportChannelProvider channelProvider; private final ChannelEndpointCacheFactory channelEndpointCacheFactory; @@ -881,21 +924,29 @@ protected SpannerOptions(Builder builder) { // Dynamic channel pooling is disabled by default. // It is only enabled when: - // 1. enableDynamicChannelPool() was explicitly called, AND + // 1. enableDynamicChannelPool() was explicitly called (or experimentalHost is set and DCP was + // not explicitly disabled), AND // 2. grpc-gcp extension is enabled, AND // 3. numChannels was not explicitly set - if (builder.dynamicChannelPoolEnabled != null && builder.dynamicChannelPoolEnabled) { - // DCP was explicitly enabled, but respect numChannels if set + boolean dcpRequested = + builder.dynamicChannelPoolEnabled != null + ? builder.dynamicChannelPoolEnabled + : builder.experimentalHost != null; + if (dcpRequested) { + // DCP was enabled (explicitly or via experimentalHost), but respect numChannels if set dynamicChannelPoolEnabled = grpcGcpExtensionEnabled && !builder.numChannelsExplicitlySet; } else { // DCP is disabled by default, or was explicitly disabled dynamicChannelPoolEnabled = false; } - // Use user-provided GcpChannelPoolOptions or create Spanner-specific defaults + // Use user-provided GcpChannelPoolOptions merged with Spanner-specific defaults, + // or create Spanner-specific defaults if no custom options are provided. + // This ensures that dynamic scaling parameters (minRpcPerChannel, maxRpcPerChannel, + // scaleDownInterval) retain their defaults even when the user only customizes pool sizing. gcpChannelPoolOptions = builder.gcpChannelPoolOptions != null - ? builder.gcpChannelPoolOptions + ? mergeWithDefaultChannelPoolOptions(builder.gcpChannelPoolOptions) : createDefaultDynamicChannelPoolOptions(); autoThrottleAdministrativeRequests = builder.autoThrottleAdministrativeRequests; @@ -930,7 +981,12 @@ protected SpannerOptions(Builder builder) { } else { enableBuiltInMetrics = builder.enableBuiltInMetrics; } - enableLocationApi = builder.enableLocationApi; + if (builder.experimentalHost != null + && System.getenv(EXPERIMENTAL_LOCATION_API_ENV_VAR) == null) { + enableLocationApi = true; + } else { + enableLocationApi = builder.enableLocationApi; + } enableEndToEndTracing = builder.enableEndToEndTracing; monitoringHost = builder.monitoringHost; defaultTransactionOptions = builder.defaultTransactionOptions; From d658bcdb960fbd40ac6af07f0aeb762f093a0520 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Sat, 4 Apr 2026 09:53:39 +0530 Subject: [PATCH 2/2] read env --- .../java/com/google/cloud/spanner/SpannerOptions.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index cbdb63bed8cd..fac703dbb09a 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -981,9 +981,11 @@ protected SpannerOptions(Builder builder) { } else { enableBuiltInMetrics = builder.enableBuiltInMetrics; } - if (builder.experimentalHost != null - && System.getenv(EXPERIMENTAL_LOCATION_API_ENV_VAR) == null) { - enableLocationApi = true; + // Enable location API when experimental host is set, unless explicitly disabled + // via GOOGLE_SPANNER_EXPERIMENTAL_LOCATION_API=false. + if (builder.experimentalHost != null) { + String locationApiEnvValue = System.getenv(EXPERIMENTAL_LOCATION_API_ENV_VAR); + enableLocationApi = locationApiEnvValue == null || Boolean.parseBoolean(locationApiEnvValue); } else { enableLocationApi = builder.enableLocationApi; }