|
51 | 51 | import com.google.api.gax.rpc.StatusCode; |
52 | 52 | import com.google.api.gax.rpc.StatusCode.Code; |
53 | 53 | import com.google.api.gax.rpc.StreamController; |
| 54 | +import com.google.api.gax.rpc.TransportChannel; |
54 | 55 | import com.google.api.gax.rpc.TransportChannelProvider; |
55 | 56 | import com.google.api.gax.rpc.UnaryCallSettings; |
56 | 57 | import com.google.api.gax.rpc.UnaryCallable; |
57 | 58 | import com.google.api.gax.rpc.UnavailableException; |
58 | 59 | import com.google.api.gax.rpc.WatchdogProvider; |
59 | 60 | import com.google.api.pathtemplate.PathTemplate; |
| 61 | +import com.google.auth.Credentials; |
60 | 62 | import com.google.cloud.RetryHelper; |
61 | 63 | import com.google.cloud.RetryHelper.RetryHelperException; |
62 | 64 | import com.google.cloud.grpc.GcpManagedChannel; |
63 | 65 | import com.google.cloud.grpc.GcpManagedChannelBuilder; |
64 | 66 | import com.google.cloud.grpc.GcpManagedChannelOptions; |
65 | 67 | import com.google.cloud.grpc.GcpManagedChannelOptions.GcpMetricsOptions; |
66 | 68 | import com.google.cloud.grpc.GrpcTransportOptions; |
| 69 | +import com.google.cloud.grpc.fallback.GcpFallbackChannel; |
| 70 | +import com.google.cloud.grpc.fallback.GcpFallbackChannelOptions; |
| 71 | +import com.google.cloud.grpc.fallback.GcpFallbackOpenTelemetry; |
67 | 72 | import com.google.cloud.spanner.AdminRequestsPerMinuteExceededException; |
68 | 73 | import com.google.cloud.spanner.BackupId; |
69 | 74 | import com.google.cloud.spanner.ErrorCode; |
|
185 | 190 | import com.google.spanner.v1.SpannerGrpc; |
186 | 191 | import com.google.spanner.v1.Transaction; |
187 | 192 | import io.grpc.CallCredentials; |
| 193 | +import io.grpc.CallOptions; |
| 194 | +import io.grpc.Channel; |
| 195 | +import io.grpc.ClientCall; |
| 196 | +import io.grpc.ClientInterceptor; |
188 | 197 | import io.grpc.Context; |
| 198 | +import io.grpc.ForwardingChannelBuilder2; |
| 199 | +import io.grpc.ManagedChannel; |
189 | 200 | import io.grpc.ManagedChannelBuilder; |
190 | 201 | import io.grpc.MethodDescriptor; |
| 202 | +import io.grpc.NameResolver; |
| 203 | +import io.grpc.StatusRuntimeException; |
| 204 | +import io.grpc.auth.MoreCallCredentials; |
| 205 | +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; |
| 206 | +import io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoopGroup; |
| 207 | +import io.grpc.netty.shaded.io.netty.channel.socket.nio.NioSocketChannel; |
191 | 208 | import java.io.IOException; |
192 | 209 | import java.io.UnsupportedEncodingException; |
193 | 210 | import java.net.URLDecoder; |
|
214 | 231 | import java.util.concurrent.Future; |
215 | 232 | import java.util.concurrent.ScheduledExecutorService; |
216 | 233 | import java.util.concurrent.TimeUnit; |
| 234 | +import java.util.concurrent.atomic.AtomicReference; |
217 | 235 | import java.util.stream.Collectors; |
218 | 236 | import java.util.stream.Stream; |
219 | 237 | import javax.annotation.Nullable; |
@@ -341,65 +359,89 @@ public GapicSpannerRpc(final SpannerOptions options) { |
341 | 359 | this.isDynamicChannelPoolEnabled = options.isDynamicChannelPoolEnabled(); |
342 | 360 | this.baseGrpcCallContext = createBaseCallContext(); |
343 | 361 |
|
| 362 | + boolean isEnableDirectAccess = options.isEnableDirectAccess(); |
| 363 | + |
344 | 364 | if (initializeStubs) { |
345 | | - // First check if SpannerOptions provides a TransportChannelProvider. Create one |
346 | | - // with information gathered from SpannerOptions if none is provided |
347 | | - InstantiatingGrpcChannelProvider.Builder defaultChannelProviderBuilder = |
348 | | - InstantiatingGrpcChannelProvider.newBuilder() |
349 | | - .setChannelConfigurator(options.getChannelConfigurator()) |
350 | | - .setEndpoint(options.getEndpoint()) |
351 | | - .setMaxInboundMessageSize(MAX_MESSAGE_SIZE) |
352 | | - .setMaxInboundMetadataSize(MAX_METADATA_SIZE) |
353 | | - .setPoolSize(options.getNumChannels()) |
354 | | - |
355 | | - // Set a keepalive time of 120 seconds to help long running |
356 | | - // commit GRPC calls succeed |
357 | | - .setKeepAliveTimeDuration(Duration.ofSeconds(GRPC_KEEPALIVE_SECONDS)) |
358 | | - |
359 | | - // Then check if SpannerOptions provides an InterceptorProvider. Create a default |
360 | | - // SpannerInterceptorProvider if none is provided |
361 | | - .setInterceptorProvider( |
362 | | - SpannerInterceptorProvider.create( |
363 | | - MoreObjects.firstNonNull( |
364 | | - options.getInterceptorProvider(), |
365 | | - SpannerInterceptorProvider.createDefault(options.getOpenTelemetry()))) |
366 | | - // This sets the trace context headers. |
367 | | - .withTraceContext(endToEndTracingEnabled, options.getOpenTelemetry()) |
368 | | - // This sets the response compressor (Server -> Client). |
369 | | - .withEncoding(compressorName)) |
370 | | - .setHeaderProvider(headerProviderWithUserAgent) |
371 | | - .setAllowNonDefaultServiceAccount(true); |
372 | | - boolean isEnableDirectAccess = options.isEnableDirectAccess(); |
373 | | - if (isEnableDirectAccess) { |
374 | | - defaultChannelProviderBuilder.setAttemptDirectPath(true); |
375 | | - if (isEnableDirectPathBoundToken()) { |
376 | | - // This will let the credentials try to fetch a hard-bound access token if the runtime |
377 | | - // environment supports it. |
378 | | - defaultChannelProviderBuilder.setAllowHardBoundTokenTypes( |
379 | | - Collections.singletonList(InstantiatingGrpcChannelProvider.HardBoundTokenTypes.ALTS)); |
| 365 | + CredentialsProvider credentialsProvider = |
| 366 | + GrpcTransportOptions.setUpCredentialsProvider(options); |
| 367 | + |
| 368 | + InstantiatingGrpcChannelProvider.Builder defaultChannelProviderBuilder = getDefaultChannelProviderBuilder(options, headerProviderWithUserAgent, isEnableDirectAccess); |
| 369 | + |
| 370 | + if (options.getChannelProvider() == null && isEnableDirectAccess && isEnableGcpFallbackEnv()) { |
| 371 | + InstantiatingGrpcChannelProvider.Builder cloudPathProviderBuilder = getDefaultChannelProviderBuilder(options, headerProviderWithUserAgent, /*isEnableDirectAccess=*/false); |
| 372 | + cloudPathProviderBuilder.setAttemptDirectPath(false); |
| 373 | + |
| 374 | + final AtomicReference<ManagedChannelBuilder> cloudPathBuilderRef = new AtomicReference<>(); |
| 375 | + cloudPathProviderBuilder.setChannelConfigurator(builder -> { |
| 376 | + cloudPathBuilderRef.set(builder); |
| 377 | + return builder; |
| 378 | + }); |
| 379 | + |
| 380 | + try (TransportChannel ignored = cloudPathProviderBuilder.build().getTransportChannel()) { |
| 381 | + } catch (Exception e) { |
| 382 | + throw asSpannerException(e); |
380 | 383 | } |
381 | | - defaultChannelProviderBuilder.setAttemptDirectPathXds(); |
382 | | - } |
383 | 384 |
|
384 | | - options.enablegRPCMetrics(defaultChannelProviderBuilder); |
| 385 | + ManagedChannelBuilder cloudPathBuilder = cloudPathBuilderRef.get(); |
| 386 | + if (cloudPathBuilder == null) { |
| 387 | + throw new IllegalStateException("CloudPath builder was not captured."); |
| 388 | + } |
385 | 389 |
|
386 | | - if (options.isUseVirtualThreads()) { |
387 | | - ExecutorService executor = |
388 | | - tryCreateVirtualThreadPerTaskExecutor("spanner-virtual-grpc-executor"); |
389 | | - if (executor != null) { |
390 | | - defaultChannelProviderBuilder.setExecutor(executor); |
| 390 | + try { |
| 391 | + Credentials credentials = credentialsProvider.getCredentials(); |
| 392 | + if (credentials != null) { |
| 393 | + cloudPathBuilder.intercept(new ClientInterceptor() { |
| 394 | + @Override |
| 395 | + public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall( |
| 396 | + MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) { |
| 397 | + return next.newCall(method, callOptions.withCallCredentials(MoreCallCredentials.from(credentials))); |
| 398 | + } |
| 399 | + }); |
| 400 | + } |
| 401 | + } catch (Exception e) { |
| 402 | + throw asSpannerException(e); |
391 | 403 | } |
| 404 | + |
| 405 | + defaultChannelProviderBuilder.setChannelConfigurator(directPathBuilder -> { |
| 406 | + String jsonApiConfig = parseGrpcGcpApiConfig(); |
| 407 | + GcpManagedChannelOptions gcpOptions = options.getGrpcGcpOptions(); |
| 408 | + if (gcpOptions == null) { |
| 409 | + gcpOptions = GcpManagedChannelOptions.newBuilder().build(); |
| 410 | + } |
| 411 | + |
| 412 | + GcpManagedChannelBuilder primaryGcpBuilder = |
| 413 | + GcpManagedChannelBuilder.forDelegateBuilder(directPathBuilder) |
| 414 | + .withApiConfigJsonString(jsonApiConfig) |
| 415 | + .withOptions(gcpOptions); |
| 416 | + |
| 417 | + GcpManagedChannelBuilder fallbackGcpBuilder = |
| 418 | + GcpManagedChannelBuilder.forDelegateBuilder(cloudPathBuilder) |
| 419 | + .withApiConfigJsonString(jsonApiConfig) |
| 420 | + .withOptions(gcpOptions); |
| 421 | + |
| 422 | + GcpFallbackOpenTelemetry fallbackTelemetry = GcpFallbackOpenTelemetry.newBuilder() |
| 423 | + .withSdk(options.getOpenTelemetry()) |
| 424 | + .build(); |
| 425 | + |
| 426 | + return new FallbackChannelBuilder( |
| 427 | + primaryGcpBuilder, |
| 428 | + fallbackGcpBuilder, |
| 429 | + GcpFallbackChannelOptions.newBuilder() |
| 430 | + .setPrimaryChannelName("directpath") |
| 431 | + .setFallbackChannelName("cloudpath") |
| 432 | + .setMinFailedCalls(1) |
| 433 | + .setGcpFallbackOpenTelemetry(fallbackTelemetry) |
| 434 | + .build() |
| 435 | + ); |
| 436 | + }); |
392 | 437 | } |
393 | | - // If it is enabled in options uses the channel pool provided by the gRPC-GCP extension. |
394 | | - maybeEnableGrpcGcpExtension(defaultChannelProviderBuilder, options); |
395 | 438 |
|
| 439 | + // First check if SpannerOptions provides a TransportChannelProvider. Create one |
| 440 | + // with information gathered from SpannerOptions if none is provided |
396 | 441 | TransportChannelProvider channelProvider = |
397 | 442 | MoreObjects.firstNonNull( |
398 | 443 | options.getChannelProvider(), defaultChannelProviderBuilder.build()); |
399 | 444 |
|
400 | | - CredentialsProvider credentialsProvider = |
401 | | - GrpcTransportOptions.setUpCredentialsProvider(options); |
402 | | - |
403 | 445 | spannerWatchdog = |
404 | 446 | Executors.newSingleThreadScheduledExecutor( |
405 | 447 | new ThreadFactoryBuilder() |
@@ -572,6 +614,57 @@ private static String parseGrpcGcpApiConfig() { |
572 | 614 | } |
573 | 615 | } |
574 | 616 |
|
| 617 | + private InstantiatingGrpcChannelProvider.Builder getDefaultChannelProviderBuilder(final SpannerOptions options, final HeaderProvider headerProviderWithUserAgent, Boolean isEnableDirectAccess) { |
| 618 | + InstantiatingGrpcChannelProvider.Builder defaultChannelProviderBuilder = |
| 619 | + InstantiatingGrpcChannelProvider.newBuilder() |
| 620 | + .setChannelConfigurator(options.getChannelConfigurator()) |
| 621 | + .setEndpoint(options.getEndpoint()) |
| 622 | + .setMaxInboundMessageSize(MAX_MESSAGE_SIZE) |
| 623 | + .setMaxInboundMetadataSize(MAX_METADATA_SIZE) |
| 624 | + .setPoolSize(options.getNumChannels()) |
| 625 | + |
| 626 | + // Set a keepalive time of 120 seconds to help long running |
| 627 | + // commit GRPC calls succeed |
| 628 | + .setKeepAliveTimeDuration(Duration.ofSeconds(GRPC_KEEPALIVE_SECONDS)) |
| 629 | + |
| 630 | + // Then check if SpannerOptions provides an InterceptorProvider. Create a default |
| 631 | + // SpannerInterceptorProvider if none is provided |
| 632 | + .setInterceptorProvider( |
| 633 | + SpannerInterceptorProvider.create( |
| 634 | + MoreObjects.firstNonNull( |
| 635 | + options.getInterceptorProvider(), |
| 636 | + SpannerInterceptorProvider.createDefault(options.getOpenTelemetry()))) |
| 637 | + // This sets the trace context headers. |
| 638 | + .withTraceContext(endToEndTracingEnabled, options.getOpenTelemetry()) |
| 639 | + // This sets the response compressor (Server -> Client). |
| 640 | + .withEncoding(compressorName)) |
| 641 | + .setHeaderProvider(headerProviderWithUserAgent) |
| 642 | + .setAllowNonDefaultServiceAccount(true); |
| 643 | + if (isEnableDirectAccess) { |
| 644 | + defaultChannelProviderBuilder.setAttemptDirectPath(true); |
| 645 | + defaultChannelProviderBuilder.setAttemptDirectPathXds(); |
| 646 | + if (isEnableDirectPathBoundToken()) { |
| 647 | + // This will let the credentials try to fetch a hard-bound access token if the runtime |
| 648 | + // environment supports it. |
| 649 | + defaultChannelProviderBuilder.setAllowHardBoundTokenTypes( |
| 650 | + Collections.singletonList(InstantiatingGrpcChannelProvider.HardBoundTokenTypes.ALTS)); |
| 651 | + } |
| 652 | + } |
| 653 | + |
| 654 | + options.enablegRPCMetrics(defaultChannelProviderBuilder); |
| 655 | + |
| 656 | + if (options.isUseVirtualThreads()) { |
| 657 | + ExecutorService executor = |
| 658 | + tryCreateVirtualThreadPerTaskExecutor("spanner-virtual-grpc-executor"); |
| 659 | + if (executor != null) { |
| 660 | + defaultChannelProviderBuilder.setExecutor(executor); |
| 661 | + } |
| 662 | + } |
| 663 | + // If it is enabled in options uses the channel pool provided by the gRPC-GCP extension. |
| 664 | + maybeEnableGrpcGcpExtension(defaultChannelProviderBuilder, options); |
| 665 | + return defaultChannelProviderBuilder; |
| 666 | + } |
| 667 | + |
575 | 668 | // Enhance gRPC-GCP options with metrics and dynamic channel pool configuration. |
576 | 669 | private static GcpManagedChannelOptions grpcGcpOptionsWithMetricsAndDcp(SpannerOptions options) { |
577 | 670 | GcpManagedChannelOptions grpcGcpOptions = |
@@ -715,6 +808,10 @@ public static boolean isEnableDirectPathBoundToken() { |
715 | 808 | return !Boolean.parseBoolean(System.getenv("GOOGLE_SPANNER_DISABLE_DIRECT_ACCESS_BOUND_TOKEN")); |
716 | 809 | } |
717 | 810 |
|
| 811 | + public static boolean isEnableGcpFallbackEnv() { |
| 812 | + return Boolean.parseBoolean(System.getenv("GOOGLE_SPANNER_ENABLE_GCP_FALLBACK")); |
| 813 | + } |
| 814 | + |
718 | 815 | private static final RetrySettings ADMIN_REQUESTS_LIMIT_EXCEEDED_RETRY_SETTINGS = |
719 | 816 | RetrySettings.newBuilder() |
720 | 817 | .setInitialRetryDelayDuration(Duration.ofSeconds(5L)) |
@@ -2313,4 +2410,39 @@ private static Duration systemProperty(String name, int defaultValue) { |
2313 | 2410 | String stringValue = System.getProperty(name, ""); |
2314 | 2411 | return Duration.ofSeconds(stringValue.isEmpty() ? defaultValue : Integer.parseInt(stringValue)); |
2315 | 2412 | } |
| 2413 | + |
| 2414 | + // Wrapper class to build the GcpFallbackChannel using GAX's configuration |
| 2415 | + private static class FallbackChannelBuilder extends ForwardingChannelBuilder2<FallbackChannelBuilder> { |
| 2416 | + private final GcpFallbackChannelOptions options; |
| 2417 | + |
| 2418 | + private final GcpManagedChannelBuilder primaryGcpBuilder; |
| 2419 | + private final GcpManagedChannelBuilder fallbackGcpBuilder; |
| 2420 | + |
| 2421 | + private FallbackChannelBuilder( |
| 2422 | + GcpManagedChannelBuilder primary, |
| 2423 | + GcpManagedChannelBuilder fallback, |
| 2424 | + GcpFallbackChannelOptions options) { |
| 2425 | + this.primaryGcpBuilder = primary; |
| 2426 | + this.fallbackGcpBuilder = fallback; |
| 2427 | + this.options = options; |
| 2428 | + } |
| 2429 | + |
| 2430 | + /** |
| 2431 | + * Delegates all configuration calls (e.g., interceptors, userAgent) to the primary builder. |
| 2432 | + * This ensures the primary channel receives all of GAX's standard configuration. |
| 2433 | + */ |
| 2434 | + @Override |
| 2435 | + protected ManagedChannelBuilder<?> delegate() { |
| 2436 | + return primaryGcpBuilder; |
| 2437 | + } |
| 2438 | + |
| 2439 | + /** |
| 2440 | + * Overrides the build method to return our custom GcpFallbackChannel |
| 2441 | + * instead of a standard gRPC channel. |
| 2442 | + */ |
| 2443 | + @Override |
| 2444 | + public ManagedChannel build() { |
| 2445 | + return new GcpFallbackChannel(options, primaryGcpBuilder, fallbackGcpBuilder); |
| 2446 | + } |
| 2447 | + } |
2316 | 2448 | } |
0 commit comments