|
64 | 64 | import com.google.cloud.grpc.GcpManagedChannel; |
65 | 65 | import com.google.cloud.grpc.GcpManagedChannelBuilder; |
66 | 66 | import com.google.cloud.grpc.GcpManagedChannelOptions; |
| 67 | +import com.google.cloud.grpc.GcpManagedChannelOptions.GcpChannelPoolOptions; |
67 | 68 | import com.google.cloud.grpc.GcpManagedChannelOptions.GcpMetricsOptions; |
68 | 69 | import com.google.cloud.grpc.GrpcTransportOptions; |
69 | 70 | import com.google.cloud.grpc.fallback.GcpFallbackChannel; |
@@ -301,6 +302,7 @@ public class GapicSpannerRpc implements SpannerRpc { |
301 | 302 | private final boolean isGrpcGcpExtensionEnabled; |
302 | 303 | private final boolean isDynamicChannelPoolEnabled; |
303 | 304 | @Nullable private final KeyAwareChannel keyAwareChannel; |
| 305 | + @Nullable private final GcpManagedChannel grpcGcpChannel; |
304 | 306 |
|
305 | 307 | private final GrpcCallContext baseGrpcCallContext; |
306 | 308 |
|
@@ -420,6 +422,7 @@ public GapicSpannerRpc(final SpannerOptions options) { |
420 | 422 | .build(); |
421 | 423 | ClientContext clientContext = ClientContext.create(spannerStubSettings); |
422 | 424 | this.keyAwareChannel = extractKeyAwareChannel(clientContext.getTransportChannel()); |
| 425 | + this.grpcGcpChannel = extractGrpcGcpChannel(clientContext.getTransportChannel()); |
423 | 426 | this.spannerStub = |
424 | 427 | GrpcSpannerStubWithStubSettingsAndClientContext.create( |
425 | 428 | spannerStubSettings, clientContext); |
@@ -540,6 +543,7 @@ public <RequestT, ResponseT> UnaryCallable<RequestT, ResponseT> createUnaryCalla |
540 | 543 | } |
541 | 544 | } else { |
542 | 545 | this.keyAwareChannel = null; |
| 546 | + this.grpcGcpChannel = null; |
543 | 547 | this.databaseAdminStub = null; |
544 | 548 | this.instanceAdminStub = null; |
545 | 549 | this.spannerStub = null; |
@@ -589,13 +593,51 @@ private static KeyAwareChannel extractKeyAwareChannel(TransportChannel transport |
589 | 593 | return null; |
590 | 594 | } |
591 | 595 |
|
| 596 | + @Nullable |
| 597 | + private static GcpManagedChannel extractGrpcGcpChannel(TransportChannel transportChannel) { |
| 598 | + if (!(transportChannel instanceof GrpcTransportChannel)) { |
| 599 | + return null; |
| 600 | + } |
| 601 | + Channel channel = ((GrpcTransportChannel) transportChannel).getChannel(); |
| 602 | + if (channel instanceof GcpManagedChannel) { |
| 603 | + return (GcpManagedChannel) channel; |
| 604 | + } |
| 605 | + return null; |
| 606 | + } |
| 607 | + |
592 | 608 | @Override |
593 | 609 | public void clearTransactionAffinity(ByteString transactionId) { |
594 | 610 | if (keyAwareChannel != null) { |
595 | 611 | keyAwareChannel.clearTransactionAffinity(transactionId); |
596 | 612 | } |
597 | 613 | } |
598 | 614 |
|
| 615 | + @Override |
| 616 | + public void clearTransactionAndChannelAffinity( |
| 617 | + ByteString transactionId, @Nullable Long channelHint) { |
| 618 | + if (keyAwareChannel != null) { |
| 619 | + keyAwareChannel.clearTransactionAndChannelAffinity(transactionId, channelHint); |
| 620 | + return; |
| 621 | + } |
| 622 | + clearTransactionAffinity(transactionId); |
| 623 | + clearChannelHintAffinity(grpcGcpChannel, channelHint); |
| 624 | + } |
| 625 | + |
| 626 | + @VisibleForTesting |
| 627 | + static void clearChannelHintAffinity( |
| 628 | + @Nullable ManagedChannel channel, @Nullable Long channelHint) { |
| 629 | + if (!(channel instanceof GcpManagedChannel) || channelHint == null) { |
| 630 | + return; |
| 631 | + } |
| 632 | + ClientCall<ExecuteSqlRequest, ResultSet> call = |
| 633 | + channel.newCall( |
| 634 | + SpannerGrpc.getExecuteSqlMethod(), |
| 635 | + CallOptions.DEFAULT |
| 636 | + .withOption(GcpManagedChannel.AFFINITY_KEY, String.valueOf(channelHint)) |
| 637 | + .withOption(GcpManagedChannel.UNBIND_AFFINITY_KEY, true)); |
| 638 | + call.cancel("Cloud Spanner transaction closed", null); |
| 639 | + } |
| 640 | + |
599 | 641 | private static String parseGrpcGcpApiConfig() { |
600 | 642 | try { |
601 | 643 | return Resources.toString( |
@@ -772,16 +814,30 @@ private static GcpManagedChannelOptions grpcGcpOptionsWithMetricsAndDcp(SpannerO |
772 | 814 | } |
773 | 815 | optionsBuilder.withMetricsOptions(metricsOptionsBuilder.build()); |
774 | 816 |
|
775 | | - // Configure dynamic channel pool options if enabled. |
776 | | - // Uses the GcpChannelPoolOptions from SpannerOptions, which contains Spanner-specific defaults |
777 | | - // or user-provided configuration. |
778 | | - if (options.isDynamicChannelPoolEnabled()) { |
779 | | - optionsBuilder.withChannelPoolOptions(options.getGcpChannelPoolOptions()); |
| 817 | + // Always pass channel-pool options when grpc-gcp is enabled so affinity cleanup settings are |
| 818 | + // applied regardless of whether dynamic channel pool is enabled. In the non-DCP path, only |
| 819 | + // propagate the affinity cleanup configuration to avoid implicitly turning on dynamic scaling. |
| 820 | + if (options.isGrpcGcpExtensionEnabled()) { |
| 821 | + optionsBuilder.withChannelPoolOptions(getGrpcGcpChannelPoolOptions(options)); |
780 | 822 | } |
781 | 823 |
|
782 | 824 | return optionsBuilder.build(); |
783 | 825 | } |
784 | 826 |
|
| 827 | + @VisibleForTesting |
| 828 | + static GcpChannelPoolOptions getGrpcGcpChannelPoolOptions(SpannerOptions options) { |
| 829 | + GcpChannelPoolOptions channelPoolOptions = options.getGcpChannelPoolOptions(); |
| 830 | + if (options.isDynamicChannelPoolEnabled()) { |
| 831 | + return channelPoolOptions; |
| 832 | + } |
| 833 | + |
| 834 | + return GcpChannelPoolOptions.newBuilder() |
| 835 | + .disableDynamicScaling() |
| 836 | + .setAffinityKeyLifetime(channelPoolOptions.getAffinityKeyLifetime()) |
| 837 | + .setCleanupInterval(channelPoolOptions.getCleanupInterval()) |
| 838 | + .build(); |
| 839 | + } |
| 840 | + |
785 | 841 | @SuppressWarnings("rawtypes") |
786 | 842 | private static void maybeEnableGrpcGcpExtension( |
787 | 843 | InstantiatingGrpcChannelProvider.Builder defaultChannelProviderBuilder, |
@@ -2275,20 +2331,9 @@ <ReqT, RespT> GrpcCallContext newCallContext( |
2275 | 2331 | Long affinity = options == null ? null : Option.CHANNEL_HINT.getLong(options); |
2276 | 2332 | if (affinity != null) { |
2277 | 2333 | if (this.isGrpcGcpExtensionEnabled) { |
2278 | | - // Set channel affinity in gRPC-GCP. |
2279 | | - String affinityKey; |
2280 | | - if (this.isDynamicChannelPoolEnabled) { |
2281 | | - // When dynamic channel pooling is enabled, we use the raw affinity value as the key. |
2282 | | - // This allows grpc-gcp to use round-robin for new keys, enabling new channels |
2283 | | - // (created during scale-up) to receive requests. The affinity key lifetime setting |
2284 | | - // ensures the affinity map doesn't grow unbounded. |
2285 | | - affinityKey = String.valueOf(affinity); |
2286 | | - } else { |
2287 | | - // When DCP is disabled, compute bounded channel hint to prevent |
2288 | | - // gRPC-GCP affinity map from getting unbounded. |
2289 | | - int boundedChannelHint = affinity.intValue() % this.numChannels; |
2290 | | - affinityKey = String.valueOf(boundedChannelHint); |
2291 | | - } |
| 2334 | + // Set channel affinity in gRPC-GCP. Always use the raw affinity value as the key. |
| 2335 | + // Cleanup is handled explicitly by unbind on terminal/single-use operations. |
| 2336 | + String affinityKey = String.valueOf(affinity); |
2292 | 2337 | context = |
2293 | 2338 | context.withCallOptions( |
2294 | 2339 | context.getCallOptions().withOption(GcpManagedChannel.AFFINITY_KEY, affinityKey)); |
|
0 commit comments