Skip to content

feat: enable dynamic channel pooling by default #4020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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();
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()));

Expand Down Expand Up @@ -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()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,7 @@ public void testDefaultNumChannelsWithGrpcGcpExtensionDisabled() {
SpannerOptions.newBuilder()
.setProjectId("test-project")
.setCredentials(NoCredentials.getInstance())
.disableGrpcGcpExtension()
.build();

assertEquals(SpannerOptions.DEFAULT_CHANNELS, options.getNumChannels());
Expand Down Expand Up @@ -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());
Expand Down
Loading