diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index d789dcaa78..71cf6fdacc 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -151,6 +151,16 @@ public class SpannerOptions extends ServiceOptions { private final Duration partitionedDmlTimeout; private final boolean grpcGcpExtensionEnabled; private final GcpManagedChannelOptions grpcGcpOptions; + // Whether dynamic channel pooling is enabled (via automatic gRPC-GCP enablement) by default. + // This is derived from the builder flag at build time. + private final boolean dynamicChannelPoolEnabled; + // Dynamic Channel Pool parameters + private final Integer dcpMaxRpcPerChannel; + private final Integer dcpMinRpcPerChannel; + private final Duration dcpScaleDownInterval; + private final Integer dcpInitialSize; + private final Integer dcpMaxChannels; + private final Integer dcpMinChannels; private final boolean autoThrottleAdministrativeRequests; private final RetrySettings retryAdministrativeRequestsSettings; private final boolean trackTransactionStarter; @@ -788,6 +798,13 @@ protected SpannerOptions(Builder builder) { partitionedDmlTimeout = builder.partitionedDmlTimeout; grpcGcpExtensionEnabled = builder.grpcGcpExtensionEnabled; grpcGcpOptions = builder.grpcGcpOptions; + dynamicChannelPoolEnabled = builder.dynamicChannelPoolEnabled; + dcpMaxRpcPerChannel = builder.dcpMaxRpcPerChannel; + dcpMinRpcPerChannel = builder.dcpMinRpcPerChannel; + dcpScaleDownInterval = builder.dcpScaleDownInterval; + dcpInitialSize = builder.dcpInitialSize; + dcpMaxChannels = builder.dcpMaxChannels; + dcpMinChannels = builder.dcpMinChannels; autoThrottleAdministrativeRequests = builder.autoThrottleAdministrativeRequests; retryAdministrativeRequestsSettings = builder.retryAdministrativeRequestsSettings; trackTransactionStarter = builder.trackTransactionStarter; @@ -1002,6 +1019,10 @@ public static class Builder private Duration partitionedDmlTimeout = Duration.ofHours(2L); private boolean grpcGcpExtensionEnabled = false; private GcpManagedChannelOptions grpcGcpOptions; + // Tracks whether enable/disableGrpcGcpExtension has been explicitly called by the user. + private boolean grpcGcpExtensionExplicitlySet = false; + // Dynamic Channel Pool (DCP) toggle. Default: enabled. + private boolean dynamicChannelPoolEnabled = true; private RetrySettings retryAdministrativeRequestsSettings = DEFAULT_ADMIN_REQUESTS_LIMIT_EXCEEDED_RETRY_SETTINGS; private boolean autoThrottleAdministrativeRequests = false; @@ -1025,6 +1046,14 @@ public static class Builder private boolean isExperimentalHost = false; private TransactionOptions defaultTransactionOptions = TransactionOptions.getDefaultInstance(); + // Dynamic Channel Pool configuration (defaults per dynamic_cahnnel_pooling.md) + private Integer dcpMaxRpcPerChannel = 25; + private Integer dcpMinRpcPerChannel = 15; + private Duration dcpScaleDownInterval = Duration.ofMinutes(3); + private Integer dcpInitialSize = 4; + private Integer dcpMaxChannels = 10; + private Integer dcpMinChannels = 2; + private static String createCustomClientLibToken(String token) { return token + " " + ServiceOptions.getGoogApiClientLibName(); } @@ -1532,30 +1561,83 @@ public Builder setExperimentalHost(String host) { return this; } - /** - * Enables gRPC-GCP extension with the default settings. Do not set - * GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS to true in combination with this option, as - * Multiplexed sessions are not supported for gRPC-GCP. - */ + /** Enables gRPC-GCP extension with the default settings. */ public Builder enableGrpcGcpExtension() { return this.enableGrpcGcpExtension(null); } /** * Enables gRPC-GCP extension and uses provided options for configuration. The metric registry - * and default Spanner metric labels will be added automatically. Do not set - * GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS to true in combination with this option, as - * Multiplexed sessions are not supported for gRPC-GCP. + * and default Spanner metric labels will be added automatically. */ public Builder enableGrpcGcpExtension(GcpManagedChannelOptions options) { this.grpcGcpExtensionEnabled = true; this.grpcGcpOptions = options; + this.grpcGcpExtensionExplicitlySet = true; return this; } /** Disables gRPC-GCP extension. */ public Builder disableGrpcGcpExtension() { this.grpcGcpExtensionEnabled = false; + this.grpcGcpExtensionExplicitlySet = true; + return this; + } + + /** + * Enables or disables dynamic channel pooling. When enabled and no explicit number of channels + * has been configured and no custom {@link TransportChannelProvider} has been set, the client + * will automatically enable the gRPC-GCP channel pool. If multiplexed sessions are enabled, + * dynamic channel pooling will not be enabled. + */ + public Builder setDynamicChannelPoolEnabled(boolean enabled) { + this.dynamicChannelPoolEnabled = enabled; + return this; + } + + // Granular DCP configuration setters with validation bounds + public Builder setDynamicPoolMaxRpc(int maxRpcPerChannel) { + Preconditions.checkArgument( + maxRpcPerChannel >= 1 && maxRpcPerChannel <= 100, "maxRpcPerChannel must be in [1, 100]"); + this.dcpMaxRpcPerChannel = maxRpcPerChannel; + return this; + } + + public Builder setDynamicPoolMinRpc(int minRpcPerChannel) { + Preconditions.checkArgument(minRpcPerChannel >= 1, "minRpcPerChannel must be >= 1"); + this.dcpMinRpcPerChannel = minRpcPerChannel; + return this; + } + + public Builder setDynamicPoolScaleDownInterval(Duration interval) { + Preconditions.checkNotNull(interval, "interval cannot be null"); + Preconditions.checkArgument( + !interval.isNegative() && !interval.isZero(), "interval must be > 0"); + Preconditions.checkArgument( + interval.compareTo(Duration.ofSeconds(30)) >= 0, "interval must be >= 30 seconds"); + Preconditions.checkArgument( + interval.compareTo(Duration.ofMinutes(60)) <= 0, "interval must be <= 60 minutes"); + this.dcpScaleDownInterval = interval; + return this; + } + + public Builder setDynamicPoolInitialSize(int initialSize) { + Preconditions.checkArgument( + initialSize >= 1 && initialSize <= 256, "initialSize must be in [1, 256]"); + this.dcpInitialSize = initialSize; + return this; + } + + public Builder setDynamicPoolMaxChannels(int maxChannels) { + Preconditions.checkArgument( + maxChannels >= 1 && maxChannels <= 256, "maxChannels must be in [1, 256]"); + this.dcpMaxChannels = maxChannels; + return this; + } + + public Builder setDynamicPoolMinChannels(int minChannels) { + Preconditions.checkArgument(minChannels >= 1, "minChannels must be >= 1"); + this.dcpMinChannels = minChannels; return this; } @@ -1756,6 +1838,15 @@ public SpannerOptions build() { } else if (isExperimentalHost && credentials == null) { credentials = environment.getDefaultExperimentalHostCredentials(); } + // Auto-enable gRPC-GCP (dynamic channel pool) if allowed and not explicitly overridden. + if (!grpcGcpExtensionExplicitlySet && dynamicChannelPoolEnabled) { + boolean hasCustomChannelProvider = this.channelProvider != null; + boolean hasStaticNumChannels = this.numChannels != null; + if (!hasCustomChannelProvider && !hasStaticNumChannels) { + this.grpcGcpExtensionEnabled = true; + } + } + if (this.numChannels == null) { this.numChannels = this.grpcGcpExtensionEnabled ? GRPC_GCP_ENABLED_DEFAULT_CHANNELS : DEFAULT_CHANNELS; @@ -1960,6 +2051,36 @@ public GcpManagedChannelOptions getGrpcGcpOptions() { return grpcGcpOptions; } + /** Returns whether dynamic channel pooling is enabled by default. */ + public boolean isDynamicChannelPoolEnabled() { + return dynamicChannelPoolEnabled; + } + + // Dynamic Channel Pool getters used by channel setup + public Integer getDcpMaxRpcPerChannel() { + return dcpMaxRpcPerChannel; + } + + public Integer getDcpMinRpcPerChannel() { + return dcpMinRpcPerChannel; + } + + public Duration getDcpScaleDownInterval() { + return dcpScaleDownInterval; + } + + public Integer getDcpInitialSize() { + return dcpInitialSize; + } + + public Integer getDcpMaxChannels() { + return dcpMaxChannels; + } + + public Integer getDcpMinChannels() { + return dcpMinChannels; + } + public boolean isAutoThrottleAdministrativeRequests() { return autoThrottleAdministrativeRequests; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java index 60a99a8bfa..4e68d3cf40 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java @@ -580,8 +580,34 @@ private static GcpManagedChannelOptions grpcGcpOptionsWithMetrics(SpannerOptions if (metricsOptions.getNamePrefix().equals("")) { metricsOptionsBuilder.withNamePrefix("cloud.google.com/java/spanner/gcp-channel-pool/"); } + + // Build channel pool options from SpannerOptions DCP settings + GcpManagedChannelOptions.GcpChannelPoolOptions.Builder poolBuilder = + GcpManagedChannelOptions.GcpChannelPoolOptions.newBuilder( + grpcGcpOptions.getChannelPoolOptions()); + Integer maxChannels = options.getDcpMaxChannels(); + Integer minChannels = options.getDcpMinChannels(); + Integer initSize = options.getDcpInitialSize(); + Integer minRpc = options.getDcpMinRpcPerChannel(); + Integer maxRpc = options.getDcpMaxRpcPerChannel(); + java.time.Duration scaleDown = options.getDcpScaleDownInterval(); + + if (maxChannels != null) { + poolBuilder.setMaxSize(maxChannels); + } + if (minChannels != null) { + poolBuilder.setMinSize(minChannels); + } + if (initSize != null) { + poolBuilder.setInitSize(initSize); + } + if (minRpc != null && maxRpc != null && scaleDown != null) { + poolBuilder.setDynamicScaling(minRpc, maxRpc, scaleDown); + } + return GcpManagedChannelOptions.newBuilder(grpcGcpOptions) .withMetricsOptions(metricsOptionsBuilder.build()) + .withChannelPoolOptions(poolBuilder.build()) .build(); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java index e7ef9955d4..b3076e3d4f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java @@ -288,6 +288,9 @@ public void testSingleUseQuery_retriesOnNewChannel() { SpannerOptions.Builder builder = createSpannerOptionsBuilder(); builder.setSessionPoolOption( SessionPoolOptions.newBuilder().setUseMultiplexedSession(true).build()); + // Ensure retry happens on a different underlying channel by disabling grpc-gcp and limiting + // number of channels to 2 for this test. + builder.disableGrpcGcpExtension().setNumChannels(2); mockSpanner.setExecuteStreamingSqlExecutionTime( SimulatedExecutionTime.ofException(Status.DEADLINE_EXCEEDED.asRuntimeException())); @@ -317,6 +320,8 @@ public void testSingleUseQuery_stopsRetrying() { SpannerOptions.Builder builder = createSpannerOptionsBuilder(); builder.setSessionPoolOption( SessionPoolOptions.newBuilder().setUseMultiplexedSession(true).build()); + // Ensure a deterministic number of channels for this assertion. + builder.disableGrpcGcpExtension().setNumChannels(8); mockSpanner.setExecuteStreamingSqlExecutionTime( SimulatedExecutionTime.ofStickyException(Status.DEADLINE_EXCEEDED.asRuntimeException())); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java index 9fc065f944..be292eab5a 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java @@ -1100,6 +1100,7 @@ public void testDefaultNumChannelsWithGrpcGcpExtensionDisabled() { SpannerOptions.newBuilder() .setProjectId("test-project") .setCredentials(NoCredentials.getInstance()) + .disableGrpcGcpExtension() .build(); assertEquals(SpannerOptions.DEFAULT_CHANNELS, options.getNumChannels()); @@ -1135,7 +1136,8 @@ public void testNumChannelsWithGrpcGcpExtensionEnabled() { @Test public void checkCreatedInstanceWhenGrpcGcpExtensionDisabled() { - SpannerOptions options = SpannerOptions.newBuilder().setProjectId("test-project").build(); + SpannerOptions options = + SpannerOptions.newBuilder().setProjectId("test-project").disableGrpcGcpExtension().build(); SpannerOptions options1 = options.toBuilder().build(); assertEquals(false, options.isGrpcGcpExtensionEnabled()); assertEquals(options.isGrpcGcpExtensionEnabled(), options1.isGrpcGcpExtensionEnabled());